Обмен технологиями

Kotlin Flow Study Guide (3) Последняя глава

2024-07-12

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

Предисловие

В первых двух статьях было описано, что такое Flow, как его использовать, а также связанные с ним усовершенствования операторов. Следующая статья в основном знакомит с использованием Flow в реальных проектах.

Жизненный цикл потока

Прежде чем представить реальные сценарии применения Flow, давайте сначала рассмотрим пример таймера, представленный в первой статье Flow. Мы определили поток данных timeFlow в ViewModel:

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

Затем в действии получите поток данных, определенный ранее.

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

Позвольте мне запустить его, чтобы увидеть реальный эффект:

поток1.gif

Вы заметили, что когда приложение переключается в фоновый режим, журнал все равно печатается. Это не пустая трата ресурсов. Давайте изменим код получения:

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

Давайте изменим метод запуска сопрограммы с запуска на launchWhenStarted и запустим его еще раз, чтобы увидеть эффект:

flow2.gif

Видим, что при нажатии кнопки ДОМОЙ и возврате в фоновый режим лог больше не печатается. Видно, что изменение вступило в силу, но поток отменили? Вернёмся к стойке регистрации, чтобы взять? взгляд:

flow3.gif

Переключившись на передний план, мы видим, что счетчик не начинается с 0, поэтому фактически он не отменяет прием, а просто приостанавливает прием данных в фоновом режиме. Конвейер Flow по-прежнему сохраняет предыдущие данные. Вместо этого более рекомендуется использовать Google RepeatOnLifecycle, и у него нет проблем с сохранением старых данных в конвейере.
Попробуем преобразовать соответствующий код:

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

Повторите запуск, чтобы увидеть эффект:

flow4.gif

Мы видим, что при переключении с фона на передний план данные снова начинаются с 0, а это значит, что при переключении на фон Flow отменяет работу и все исходные данные очищаются.

Мы используем Flow, и с помощью повторенияOnLifecycle мы можем лучше обеспечить безопасность нашей программы.

StateFlow заменяет LiveData

Все предыдущие введения представляют собой примеры холодного потока Flow. Далее мы представим некоторые распространенные сценарии применения горячего потока.
Продолжая использовать предыдущий пример с таймером, что произойдет, если экран переключится между горизонтальным и вертикальным экранами?

flow5.gif

Мы видим, что после переключения между горизонтальным и вертикальным экранами Activity создается заново. После пересоздания timeFlow будет пересобран, холодный поток будет пересобран и повторно выполнен, а затем запустится таймер. считая от 0. Много раз мы хотим переключаться между горизонтальным и вертикальным экранами. На этот раз мы надеемся, что статус страницы останется неизменным, по крайней мере, в течение определенного периода времени. Здесь мы изменяем холодный поток на. горячий поток и попробуйте:

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

Здесь мы сосредоточимся на трех параметрах в stateIn. Первый — это область действия сопрограммы, второй — максимальное эффективное время, в течение которого поток сохраняет свое рабочее состояние. Если он превысит поток, он перестанет работать. параметр — начальное значение.

Повторите запуск, чтобы увидеть эффект:

flow6.gif

Здесь мы видим распечатанный журнал после переключения между горизонтальным и вертикальным экраном. Таймер не запускается с 0.
Выше мы рассказали, как преобразовать холодный поток в горячий. Мы еще не рассказали, как stateFlow может заменить LiveData. Вот введение в то, как stateFlow заменяет LiveData:

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

Мы определяем горячий поток StateFlow, а затем меняем значение stateFlow с помощью метода startTimer(), аналогично LiveData setData. При нажатии кнопки начинаем изменять значение StateFlow и собирать значения соответствующего потока, аналогично методу. Метод LiveData Observe для отслеживания изменений данных.
Давайте посмотрим на реальный эффект от бега:

flow7.gif

На этом этапе мы познакомились с базовым использованием StateFlow, а теперь представим SharedFlow.

SharedFlow

Чтобы понять SharedFlow, мы сначала познакомимся с концепцией закрепленных событий. Буквально, когда наблюдатель подписывается на источник данных, и если источник данных уже имеет самые последние данные, данные будут немедленно отправлены наблюдателю. Судя по приведенному выше объяснению, LiveData соответствует этой важной характеристике. А как насчет StateFlow? Давайте напишем простую демонстрацию для проверки:


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

Сначала мы определяем clickCountFlow в MainViewModel, затем в Activity изменяем данные clickCountFlow, нажав кнопку, а затем получаем clickCountFlow и отображаем данные в тексте.
Давайте посмотрим на эффект от бега:

flow8.gif

Мы видим, что при переключении между горизонтальным и вертикальным экранами активность создается заново, и данные clickCountFlow по-прежнему начинаются с предыдущих 4, что указывает на то, что StateFlow является липким. Кажется, здесь нет проблем, но давайте. посмотрим на другой пример. Мы моделируем сценарий входа в систему по щелчку мыши, нажмите кнопку входа в систему, чтобы войти и войти в систему:

//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

Приведенный выше код фактически имитирует вход в систему по щелчку, а затем сообщает, что вход в систему прошел успешно. Давайте посмотрим на фактический эффект операции:

flow9.gif

Вы заметили, что после переключения между горизонтальным и вертикальным экранами снова выскакивает приглашение на успешный вход? У нас не прошла процедура повторного входа в систему. Это проблема повторного приема данных, вызванная залипающими событиями. Давайте изменим приведенный выше код на. SharedFlow и попробуйте:

    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

Мы изменили StateFlow на SharedFlow. Мы видим, что SharedFlow не требует начального значения. Метод submit добавляется в место входа в систему для отправки данных, а место получения данных остается неизменным. Запустите его еще раз, чтобы увидеть эффект:

flow10.gif

Здесь мы видим, что использование SharedFlow не вызывает этой проблемы с закреплением. Фактически, SharedFlow имеет множество параметров, которые можно настроить:

    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, которые ждут всех, но я не буду здесь вдаваться в подробности.

Другие распространенные сценарии применения

Ранее мы познакомили вас с базовым холодным потоком и горячим потоком, а также с общим использованием и применимыми сценариями StateFlow и SharedFlow. Далее мы сосредоточимся на нескольких практических примерах, чтобы рассмотреть другие распространенные сценарии применения потока.

Обрабатывать сложную и трудоемкую логику

Обычно мы выполняем сложную трудоемкую логику, обрабатываем ее в подпотоке, а затем переключаемся на основной поток для отображения пользовательского интерфейса. Flow также поддерживает переключение потоков, а flowOn может помещать предыдущие операции в соответствующий подпоток для обработки. .
Мы реализуем локальное чтениеAssetsв каталогеperson.jsonфайл и проанализировать его,jsonСодержимое файла:

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

Затем проанализируйте файл:

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方式,获取本地文件
 */
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

Итоговый журнал печати:

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

Запросы интерфейса с зависимостями

Мы часто сталкиваемся с запросами интерфейса, которые зависят от результатов другого запроса, это так называемые вложенные запросы. Если вложенных запросов слишком много, произойдет ад обратного вызова. Для реализации аналогичного требования мы используем FLow:

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

Объединение данных из нескольких интерфейсов

Каков сценарий объединения данных из нескольких интерфейсов. Например, мы запрашиваем несколько интерфейсов, а затем объединяем их результаты, чтобы отображать их единообразно или использовать в качестве параметров запроса для другого интерфейса. Позвольте мне спросить, как это реализовать:
Первый — запросить один за другим, а затем объединить их;
Второй тип — это одновременный запрос с последующим объединением всех запросов.
Очевидно, что второй эффект более эффективен. Давайте посмотрим на код:

//分别请求电费、水费、网费,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

Сначала мы смоделировали и определили несколько сетевых запросов во ViewModel, а затем объединили запросы:

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

результат операции:
flow11.png
Мы видим, что общее затраченное время практически такое же, как и самое продолжительное время запроса.

Меры предосторожности при использовании Flow

несколькоFlowнельзя разместить в одномlifecycleScope.launchВойти в помещениеcollect{}, потому что входяcollect{}Эквивалентно бесконечному циклу: следующая строка кода никогда не будет выполнена, если вы захотите написать;lifecycleScope.launch{}Зайдите внутрь, вы можете включить его снова изнутри.launch{}Подпрограмма выполняется.
Пример ошибки:

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

Правильный способ написания:

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

Подведем итог

Начиная с жизненного цикла Flow, мы представили правильную позицию использования потока, чтобы избежать напрасной траты ресурсов, преобразование обычного холодного потока в горячий поток, замену StateFlow LiveData и его проблему липкости, а затем решили проблему липкости с помощью SharedFlow, а затем общие сценарии приложений и, наконец, меры предосторожности при использовании Flow, в основном охватывают большинство функций и сценариев приложений Flow. Это также последняя глава изучения Flow.
Творить непросто, но нравиться хлопотноСтавьте лайки, собирайте и комментируйте, чтобы поощрить
Справочная статья
Реактивное программирование Kotlin Flow, StateFlow и SharedFlow

Котлин | Несколько сценариев использования потока данных Flow