Teknologian jakaminen

kotlin Flow Study Guide (3) Viimeinen luku

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Esipuhe

Kaksi ensimmäistä artikkelia esittelivät, mitä Flow on, miten sitä käytetään ja siihen liittyvät operaattorin edistysaskeleet. Seuraava artikkeli esittelee pääasiassa Flow:n käyttöä varsinaisissa projekteissa.

Virran elinkaari

Ennen kuin esittelemme Flow:n todelliset sovellusskenaariot, katsotaan ensin Flow'n ensimmäisessä artikkelissa esitelty ajastin. Määritimme TimeFlow-tietovirran ViewModelissa:

class MainViewModel : ViewModel() {

val timeFlow = flow {
    var time = 0
    while (true) {
        emit(time)
        delay(1000)
        time++
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Vastaanota sitten Aktiviteetissa aiemmin määritetty tietovirta.

lifecycleOwner.lifecycleScope.launch {
    viewModel.timeFlow.collect { time ->
        times = time
        Log.d("ddup", "update UI $times")
    }
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Anna minun suorittaa se nähdäkseni todellisen vaikutuksen:

flow1.gif

Oletko huomannut, että kun sovellus siirtyy taustalle, tämä ei ole resurssien tuhlausta.

lifecycleOwner.lifecycleScope.launchWhenStarted {
     viewModel.timeFlow.collect { time ->
         times = time
         Log.d("ddup", "update UI $times")
     }
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Muutetaan tapa aloittaa korutiini käynnistyksestä launchWhenStarted-tilaan ja suoritetaan se uudelleen nähdäksesi vaikutuksen:

flow2.gif

Näemme, että kun HOME-painiketta napsautetaan ja palataan taustalle, loki ei enää tulostu. Näemme, että muutos on tullut voimaan, mutta onko stream peruutettu katse:

flow3.gif

Vaihdettaessa etualalle voimme nähdä, että laskuri ei aloita nollasta, joten se ei itse asiassa peruuta vastaanottoa, se vain keskeyttää tietojen vastaanottamisen taustalla. Itse asiassa launchWhenStarted säilyy Sovellusliittymä on hylätty Google RepeatOnLifecycle -sovelluksen sijaan, eikä sillä ole ongelmaa säilyttää vanhoja tietoja.
Yritetään muuntaa vastaava koodi:

lifecycleOwner.lifecycleScope.launch {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.timeFlow.collect { time ->
            times = time
            Log.d("ddup", "update UI $times")
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Suorita uudelleen nähdäksesi vaikutuksen:

flow4.gif

Näemme, että vaihdettaessa taustalta etualalle data alkaa taas nollasta, mikä tarkoittaa, että taustalle vaihtaessa Flow peruuttaa työn ja kaikki alkuperäiset tiedot tyhjennetään.

Käytämme Flow'ta, ja repeatOnLifecyclen avulla voimme paremmin varmistaa ohjelmamme turvallisuuden.

StateFlow korvaa LiveDatan

Edelliset johdannot ovat kaikki esimerkkejä Flow-kylmävirtauksesta. Seuraavaksi esittelemme joitain yleisiä kuumavirtauksen sovellusskenaarioita.
Mitä tapahtuu, jos näyttö vaihdetaan vaaka- ja pystynäytön välillä, kun käytät edelleen edellistä ajastinesimerkkiä?

flow5.gif

Näemme, että vaaka- ja pystynäytön välillä vaihtamisen jälkeen toiminto luodaan uudelleen Uudelleenluomisen jälkeen timeFlow kerätään uudelleen, kylmävirtaus kerätään uudelleen ja suoritetaan uudelleen, ja sitten ajastin käynnistyy. laskenta nollasta. Usein haluamme vaihtaa vaaka- ja pystynäytön välillä. Tällä hetkellä toivomme, että sivun tila pysyy ennallaan, ainakin tietyn ajan kuluessa virtaa ja kokeile:

val hotFlow =
    timeFlow.stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000),
        0
    )
    
    ```
lifecycleOwner.lifecycleScope.launch {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.hotFlow.collect { time ->
            times = time
            Log.d("ddup", "update UI $times")
        }
    }
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

Tässä keskitymme kolmeen parametriin stateIn:ssä. Ensimmäinen on korutiinin laajuus, toinen on maksimi tehollinen aika, jolloin virtaus ylittää toimintatilan parametri on alkuarvo.

Suorita uudelleen nähdäksesi vaikutuksen:

flow6.gif

Tässä näemme tulostetun lokin vaaka- ja pystynäytön välillä vaihtamisen jälkeen. Ajastin ei käynnisty nollasta.
Olemme esitelleet edellä, kuinka kylmävirtaus voidaan muuttaa kuumaksi virtaukseksi. Emme ole vielä esittäneet, kuinka stateFlow voi korvata LiveDatan.

private val _stateFlow = MutableStateFlow(0)
val stateFlow = _stateFlow.asStateFlow()

fun startTimer() {
    val timer = Timer()
    timer.scheduleAtFixedRate(object :TimerTask() {
        override fun run() {
            _stateFlow.value += 1
        }

    },0,1000)
}

```

viewModel.startTimer()

lifecycleOwner.lifecycleScope.launch {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.stateFlow.collect { time ->
            times = time
            Log.d("ddup", "update UI $times")
        }
    }
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

Määrittelemme StateFlow-kuuman virran ja muutamme sitten stateFlow-arvoa startTimer()-menetelmällä, joka on samanlainen kuin LiveData setData. Kun painiketta napsautetaan, aloita StateFlow-arvon muuttaminen ja kerää vastaavan virtauksen arvot LiveData Tarkkaile menetelmää tietojen muutosten seuraamiseen.
Katsotaanpa todellista juoksuvaikutusta:

flow7.gif

Tässä vaiheessa olemme ottaneet käyttöön StateFlow'n peruskäytön, ja nyt esittelemme SharedFlow'n.

SharedFlow

Ymmärtääksemme SharedFlow'n, tunnemme ensin tahmeiden tapahtumien käsitteen. Kirjaimellisesti, kun tarkkailija tilaa tietolähteen, jos tietolähteellä on jo uusimmat tiedot, tiedot välitetään tarkkailijalle välittömästi. Yllä olevasta selityksestä päätellen LiveData noudattaa tätä tahmeaa ominaisuutta. Kirjoitetaan yksinkertainen esittely varmistaaksesi:


class MainViewModel : ViewModel() {

private val _clickCountFlow = MutableStateFlow(0)

val clickCountFlow = _clickCountFlow.asStateFlow()

fun increaseClickCount() {
    _clickCountFlow.value += 1
}
}
//MainActivity
```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {
    viewModel.increaseClickCount()
}

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.clickCountFlow.collect { time ->
            tv.text = time.toString()
            Log.d("ddup", "update UI $time")
        }
    }
}
```

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

Määrittelemme ensin clickCountFlow:n MainViewModelissa, sitten Aktiviteetissa muutamme clickCountFlow-tietoja napsauttamalla painiketta ja vastaanotamme sitten clickCountFlow-arvon ja näytämme tiedot tekstissä.
Katsotaanpa juoksevaa vaikutusta:

flow8.gif

Näemme, että kun vaihdetaan vaaka- ja pystynäytön välillä, Activity luodaan uudelleen ja clickCountFlow kerätään uudelleen. Tiedot alkavat edelleen edellisestä neljästä, mikä osoittaa, että StateFlow on tahmea. Tässä ei näytä olevan ongelmaa katso toista esimerkkiä. Simuloimme Kirjaudu sisään napsauttamalla -painiketta, kirjaudu sisään ja kirjaudu sisään:

//MainViewModel
    private val _loginFlow = MutableStateFlow("")
    val loginFlow = _loginFlow.asStateFlow()
    fun startLogin() {
        // Handle login logic here.
        _loginFlow.value = "Login Success"
    }
//MainActivity

```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {
    viewModel.startLogin()
}

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.loginFlow.collect {
            if (it.isNotBlank()) {
                Toast.makeText(this@MainActivity2, it, Toast.LENGTH_LONG).show()
            }
        }
    }
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

Yllä oleva koodi itse asiassa simuloi sisäänkirjautumista napsautuksella ja ilmoittaa sitten, että kirjautuminen on onnistunut. Katsotaanpa todellista toiminnan vaikutusta:

flow9.gif

Huomasitko, että onnistunut sisäänkirjautumiskehote ponnahtaa uudelleen näkyviin. Tämä on toistuvan tiedon vastaanottamisen ongelma. Muutetaan yllä oleva koodi SharedFlow ja kokeile sitä:

    private val _loginFlow = MutableSharedFlow<String>()

    val loginFlow = _loginFlow.asSharedFlow()
    fun startLogin() {
        // Handle login logic here.
        viewModelScope.launch {
            _loginFlow.emit("Login Success")
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Muutimme StateFlow'n SharedFlow'ksi. Voimme nähdä, että emit-menetelmä lisätään kirjautumispaikkaan tietojen lähettämistä varten. Suorita se uudelleen nähdäksesi vaikutuksen.

flow10.gif

Tässä näemme, että tätä tahmeusongelmaa ei esiinny SharedFlow:ta käytettäessä. Itse asiassa SharedFlow:ssa on monia parametreja, jotka voidaan määrittää:

    public fun <T> MutableSharedFlow(
        // 每个新的订阅者订阅时收到的回放的数目,默认0
        replay: Int = 0,

       // 除了replay数目之外,缓存的容量,默认0
        extraBufferCapacity: Int = 0,

      // 缓存区溢出时的策略,默认为挂起。只有当至少有一个订阅者时,onBufferOverflow才会生效。当无订阅者时,只有最近replay数目的值会保存,并且onBufferOverflow无效。
        onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

SharedFlow'lla on muitakin käyttötapoja, jotka odottavat kaikkien löytämistä, mutta en mene yksityiskohtiin tässä.

Muut yleiset sovellusskenaariot

Aiemmin esittelimme peruskylmän virtauksen kuumavirtaukseen sekä StateFlow:n ja SharedFlow:n yleisen käytön ja soveltuvat skenaariot. Seuraavaksi keskitymme useisiin käytännön esimerkkeihin tarkastellaksemme muita yleisiä virtauksen sovellusskenaarioita.

Käsittele monimutkaista, aikaa vievää logiikkaa

Teemme yleensä monimutkaista aikaa vievää logiikkaa, käsittelemme sen alasäikeessä ja siirrymme sitten pääsäikeeseen näyttääksesi käyttöliittymän Flow tukee myös säikeen vaihtoa, ja flowOn voi laittaa aiemmat toiminnot vastaavaan aliketjuun käsittelyä varten .
Toteutamme lukupaikallisenAssetshakemiston allaperson.jsontiedosto ja jäsentää se,jsonTiedoston sisältö:

{
  "name": "ddup",
  "age": 101,
  "interest": "earn money..."
}
  • 1
  • 2
  • 3
  • 4
  • 5

Jäsennä sitten tiedosto:

fun getAssetJsonInfo(context: Context, fileName: String): String {
    val strBuilder = StringBuilder()
    var input: InputStream? = null
    var inputReader: InputStreamReader? = null
    var reader: BufferedReader? = null
    try {
        input = context.assets.open(fileName, AssetManager.ACCESS_BUFFER)
        inputReader = InputStreamReader(input, StandardCharsets.UTF_8)
        reader = BufferedReader(inputReader)
        var line: String?
        while ((reader.readLine().also { line = it }) != null) {
            strBuilder.append(line)
        }
    } catch (ex: Exception) {
        ex.printStackTrace()
    } finally {
        try {
            input?.close()
            inputReader?.close()
            reader?.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    return strBuilder.toString()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

Flow lukee tiedostoja:

/**
 * 通过Flow方式,获取本地文件
 */
private fun getFileInfo() {
    lifecycleScope.launch {
        flow {
            //解析本地json文件,并生成对应字符串
            val configStr = getAssetJsonInfo(this@MainActivity2, "person.json")
            //最后将得到的实体类发送到下游
            emit(configStr)
        }
            .map { json ->
                Gson().fromJson(json, PersonModel::class.java) //通过Gson将字符串转为实体类
            }
            .flowOn(Dispatchers.IO) //在flowOn之上的所有操作都是在IO线程中进行的
            .onStart { Log.d("ddup", "onStart") }
            .filterNotNull()
            .onCompletion { Log.d("ddup", "onCompletion") }
            .catch { ex -> Log.d("ddup", "catch:${ex.message}") }
            .collect {
                Log.d("ddup", "collect parse result:$it")
            }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Lopullinen tulostusloki:

2024-07-09 22:00:34.006 12251-12251 ddup com.ddup.flowtest D onStart 2024-07-09 22:00:34.018 12251-12251 ddup com.ddup.flowtest D collect parse result:PersonModel(name=ddup, age=101, interest=earn money...) 2024-07-09 22:00:34.019 12251-12251 ddup com.ddup.flowtest D onCompletion

Liittymäpyynnöt riippuvuuksilla

Kohtaamme usein toisen pyynnön tuloksista riippuvia käyttöliittymäpyyntöjä, jotka ovat niin sanottuja sisäkkäisiä pyyntöjä. Jos sisäkkäisiä pyyntöjä on liikaa, tapahtuu takaisinsoitto.

lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        //将两个flow串联起来 先搜索目的地,然后到达目的地
        viewModel.getTokenFlows()
            .flatMapConcat {
                //第二个flow依赖第一个的结果
                viewModel.getUserFlows(it)
            }.collect {
                tv.text = it ?: "error"
            }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Yhdistä tiedot useista liitännöistä

Mikä on skenaario tietojen yhdistämisestä useista liitännöistä. Pyydämme esimerkiksi useita rajapintoja ja yhdistämme niiden tulokset näyttämään ne yhtenäisesti tai käytämme niitä pyyntöparametreina toiselle rajapinnalle.
Ensimmäinen on pyytää yksitellen ja sitten yhdistää ne;
Toinen tyyppi on pyytää samanaikaisesti ja sitten yhdistää kaikki pyynnöt.
Ilmeisesti toinen vaikutus on tehokkaampi. Katsotaanpa koodia:

//分别请求电费、水费、网费,Flow之间是并行关系
suspend fun requestElectricCost(): Flow<SpendModel> =
    flow {
        delay(500)
        emit(SpendModel("电费", 10f, 500))
    }.flowOn(Dispatchers.IO)

suspend fun requestWaterCost(): Flow<SpendModel> =
    flow {
        delay(1000)
        emit(SpendModel("水费", 20f, 1000))
    }.flowOn(Dispatchers.IO)

suspend fun requestInternetCost(): Flow<SpendModel> =
    flow {
        delay(2000)
        emit(SpendModel("网费", 30f, 2000))
    }.flowOn(Dispatchers.IO)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Ensin simuloimme ja määritimme useita verkkopyyntöjä ViewModelissa ja yhdistämme sitten pyynnöt:

lifecycleScope.launch {
    val electricFlow = viewModel.requestElectricCost()
    val waterFlow = viewModel.requestWaterCost()
    val internetFlow = viewModel.requestInternetCost()

    val builder = StringBuilder()
    var totalCost = 0f
    val startTime = System.currentTimeMillis()
    //NOTE:注意这里可以多个zip操作符来合并Flow,且多个Flow之间是并行关系
    electricFlow.zip(waterFlow) { electric, water ->
        totalCost = electric.cost + water.cost
        builder.append("${electric.info()},n").append("${water.info()},n")
    }.zip(internetFlow) { two, internet ->
        totalCost += internet.cost
        two.append(internet.info()).append(",nn总花费:$totalCost")
    }.collect {
        tv.text = it.append(",总耗时:${System.currentTimeMillis() - startTime} ms")
        Log.d(
            "ddup",
            "${it.append(",总耗时:${System.currentTimeMillis() - startTime} ms")}"
        )
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

operaation tulos:
flow11.png
Näemme, että käytetty kokonaisaika on periaatteessa sama kuin pisin pyyntöaika.

Flow'n käyttöä koskevat varotoimet

UseitaFlowei voi laittaa yhteenlifecycleScope.launchMene sisällecollect{}, koska sisääntulocollect{}Vastaa ääretöntä silmukkaa, seuraavaa koodiriviä ei koskaan suoriteta, jos haluat vain kirjoittaa alifecycleScope.launch{}Mene sisään, voit kytkeä sen uudelleen päälle sisältälaunch{}Alakorutiini suoritetaan.
Esimerkki virheestä:

lifecycleScope.launch {
    flow1
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect {}

   flow2
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Oikea tapa kirjoittaa:

lifecycleScope.launch {
    launch {
       flow1
            .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
            .collect {}
    }

    launch {
      flow2
            .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
            .collect {}
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Tee yhteenveto

Flow'n elinkaaresta lähtien esittelimme virtauksen oikean käyttöasennon resurssien tuhlaamisen välttämiseksi, tavallisen kylmän virtauksen muuntamiseen kuumaksi virtaukseksi, LiveDatan korvaavaan StateFlow'hun ja sen tarttuvuusongelmaan, ja sitten ratkaisimme tahmeusongelman SharedFlow ja sitten yleiset sovellusskenaariot ja lopuksi Flow'n käyttöä koskevat varotoimet kattavat periaatteessa useimmat Flow'n ominaisuudet ja sovellusskenaariot. Tämä on myös Flow-oppimisen viimeinen luku.
Sen luominen ei ole helppoa, mutta siitä on hankala tykätäTykkää, kerää ja kommentoi kannustaaksesi
Viiteartikkeli
Kotlin Flow reaktiivinen ohjelmointi, StateFlow ja SharedFlow

Kotlin |. Useita Flow-tietovirran käyttöskenaarioita