informasi kontak saya
Surat[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Dua artikel pertama memperkenalkan apa itu Flow, cara menggunakannya, dan kemajuan operator terkait. Artikel berikutnya terutama memperkenalkan penggunaan Flow dalam proyek sebenarnya.
Sebelum memperkenalkan skenario aplikasi Flow yang sebenarnya, mari kita tinjau dulu contoh pengatur waktu yang diperkenalkan di artikel pertama Flow. Kita mendefinisikan aliran data timeFlow di ViewModel:
class MainViewModel : ViewModel() {
val timeFlow = flow {
var time = 0
while (true) {
emit(time)
delay(1000)
time++
}
}
Kemudian di Aktivitas, terima aliran data yang ditentukan sebelumnya.
lifecycleOwner.lifecycleScope.launch {
viewModel.timeFlow.collect { time ->
times = time
Log.d("ddup", "update UI $times")
}
}
Biarkan saya menjalankannya untuk melihat efek sebenarnya:
Pernahkah Anda memperhatikan bahwa ketika Aplikasi beralih ke latar belakang, log masih dicetak. Ini bukan pemborosan sumber daya. Mari kita ubah kode penerimanya:
lifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.timeFlow.collect { time ->
times = time
Log.d("ddup", "update UI $times")
}
}
Mari kita ubah metode memulai coroutine dari peluncuran menjadi launchWhenStarted, dan jalankan lagi untuk melihat efeknya:
Kita dapat melihat bahwa ketika tombol HOME diklik dan kembali ke latar belakang, log tidak lagi dicetak. Terlihat bahwa perubahan telah diterapkan, tetapi apakah streaming telah dibatalkan? lihat:
Beralih ke latar depan, kita dapat melihat bahwa penghitung tidak dimulai dari 0, jadi sebenarnya tidak membatalkan penerimaan, hanya menghentikan sementara penerimaan data di latar belakang. Pipa Flow masih menyimpan data sebelumnya API telah ditinggalkan. Google RepeatOnLifecycle lebih direkomendasikan, dan tidak ada masalah dalam menyimpan data lama dalam pipeline.
Mari kita coba mengubah kode yang sesuai:
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.timeFlow.collect { time ->
times = time
Log.d("ddup", "update UI $times")
}
}
}
Jalankan kembali untuk melihat efeknya:
Kita dapat melihat bahwa ketika beralih dari latar belakang ke latar depan, data dimulai dari 0 lagi, yang berarti bahwa ketika beralih ke latar belakang, Flow membatalkan pekerjaan dan semua data asli dihapus.
Kami menggunakan Flow, dan melalui repeatOnLifecycle, kami dapat memastikan keamanan program kami dengan lebih baik.
Perkenalan sebelumnya adalah contoh aliran aliran dingin. Selanjutnya, kami akan memperkenalkan beberapa skenario penerapan umum aliran panas.
Masih menggunakan contoh pengatur waktu sebelumnya, apa yang terjadi jika layar dialihkan antara layar horizontal dan vertikal?
Kita dapat melihat bahwa setelah beralih antara layar horizontal dan vertikal, Aktivitas dibuat ulang. Setelah pembuatan ulang, timeFlow akan dikumpulkan kembali, aliran dingin akan dikumpulkan kembali dan dijalankan kembali, lalu pengatur waktu akan dimulai. menghitung dari 0. Seringkali, kami ingin beralih antara layar horizontal dan vertikal. Saat ini, kami berharap status halaman tetap tidak berubah, setidaknya dalam jangka waktu tertentu aliran panas dan coba:
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")
}
}
}
```
Di sini kita fokus pada tiga parameter di stateIn. Yang pertama adalah cakupan coroutine, yang kedua adalah waktu efektif maksimum aliran untuk mempertahankan status kerjanya parameter adalah nilai awal.
Jalankan kembali untuk melihat efeknya:
Di sini kita dapat melihat log yang dicetak setelah beralih antara layar horizontal dan vertikal. Pengatur waktu tidak akan dimulai dari 0.
Kami telah memperkenalkan di atas cara mengubah aliran dingin menjadi aliran panas. Kami belum memperkenalkan bagaimana stateFlow dapat menggantikan LiveData. Berikut adalah pengenalan tentang bagaimana stateFlow menggantikan 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")
}
}
}
```
Kami mendefinisikan aliran panas StateFlow, dan kemudian mengubah nilai stateFlow melalui metode startTimer(), mirip dengan LiveData setData. Saat tombol diklik, mulailah mengubah nilai StateFlow dan mengumpulkan nilai aliran yang sesuai, mirip dengan Metode LiveData Observe untuk memantau perubahan data.
Mari kita lihat efek berjalan sebenarnya:
Pada titik ini, kami telah memperkenalkan penggunaan dasar StateFlow, dan sekarang kami akan memperkenalkan SharedFlow.
Untuk memahami SharedFlow, pertama-tama kita mengetahui konsep peristiwa lengket. Secara harfiah, ketika pengamat berlangganan sumber data, jika sumber data sudah memiliki data terbaru, maka data tersebut akan segera dikirim ke pengamat. Dilihat dari penjelasan di atas, LiveData sesuai dengan karakteristik lengket ini. Bagaimana dengan StateFlow? Mari kita tulis demo sederhana untuk memverifikasi:
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")
}
}
}
```
Pertama-tama kita mendefinisikan clickCountFlow di MainViewModel, lalu di Aktivitas, mengubah data clickCountFlow dengan mengklik Tombol, lalu menerima clickCountFlow dan menampilkan data pada teks.
Mari kita lihat efek yang dijalankan:
Kita dapat melihat bahwa ketika beralih antara layar horizontal dan vertikal, Aktivitas dibuat ulang dan clickCountFlow dikumpulkan kembali. Data masih dimulai dari 4 sebelumnya, menunjukkan bahwa StateFlow bersifat lengket. Sepertinya tidak ada masalah di sini, tapi mari kita lihat contoh lain. Kami mensimulasikan skenario klik untuk masuk, klik tombol masuk untuk masuk dan masuk:
//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()
}
}
}
}
```
Kode di atas sebenarnya mensimulasikan login klik, dan kemudian meminta login berhasil. Mari kita lihat efek operasi sebenarnya:
Apakah Anda melihat bahwa setelah beralih antara layar horizontal dan vertikal, prompt login berhasil muncul lagi? Kami tidak melalui proses login ulang. Ini adalah masalah penerimaan data berulang yang disebabkan oleh peristiwa lengket SharedFlow dan mencobanya:
private val _loginFlow = MutableSharedFlow<String>()
val loginFlow = _loginFlow.asSharedFlow()
fun startLogin() {
// Handle login logic here.
viewModelScope.launch {
_loginFlow.emit("Login Success")
}
}
Kami mengubah StateFlow menjadi SharedFlow. Kita dapat melihat bahwa SharedFlow tidak memerlukan nilai awal. Metode emit ditambahkan ke tempat login untuk mengirim data, dan tempat di mana data diterima tetap tidak berubah.
Di sini kita dapat melihat bahwa penggunaan SharedFlow tidak akan menyebabkan masalah lengket ini. Faktanya, SharedFlow memiliki banyak parameter yang dapat dikonfigurasi:
public fun <T> MutableSharedFlow(
// 每个新的订阅者订阅时收到的回放的数目,默认0
replay: Int = 0,
// 除了replay数目之外,缓存的容量,默认0
extraBufferCapacity: Int = 0,
// 缓存区溢出时的策略,默认为挂起。只有当至少有一个订阅者时,onBufferOverflow才会生效。当无订阅者时,只有最近replay数目的值会保存,并且onBufferOverflow无效。
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)
Ada lebih banyak kegunaan SharedFlow yang menunggu untuk ditemukan semua orang, tetapi saya tidak akan membahas detailnya di sini.
Sebelumnya, kami memperkenalkan aliran dingin dasar ke aliran panas, serta penggunaan umum dan skenario StateFlow dan SharedFlow yang dapat diterapkan. Selanjutnya, kami akan fokus pada beberapa contoh praktis untuk melihat skenario penerapan aliran umum lainnya.
Kami biasanya melakukan logika rumit yang memakan waktu, memprosesnya dalam sub-utas, lalu beralih ke utas utama untuk menampilkan UI. Flow juga mendukung peralihan utas, dan flowOn dapat memasukkan operasi sebelumnya ke dalam sub-utas yang sesuai untuk diproses .
Kami menerapkan baca lokalAssets
di bawah direktoriperson.json
file dan parsing,json
Isi filenya:
{
"name": "ddup",
"age": 101,
"interest": "earn money..."
}
Kemudian parsing file tersebut:
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()
}
Aliran membaca file:
/**
* 通过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")
}
}
}
Log pencetakan akhir:
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
Kami sering menemukan permintaan antarmuka yang bergantung pada hasil permintaan lain, yang disebut permintaan bersarang. Jika ada terlalu banyak permintaan bersarang, callback hell akan terjadi. Kami menggunakan FLow untuk mengimplementasikan persyaratan serupa:
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
//将两个flow串联起来 先搜索目的地,然后到达目的地
viewModel.getTokenFlows()
.flatMapConcat {
//第二个flow依赖第一个的结果
viewModel.getUserFlows(it)
}.collect {
tv.text = it ?: "error"
}
}
}
Bagaimana skenario menggabungkan data dari beberapa antarmuka? Misalnya, kita meminta beberapa antarmuka, lalu menggabungkan hasilnya untuk menampilkannya secara seragam atau menggunakannya sebagai parameter permintaan untuk antarmuka lain.
Yang pertama adalah meminta satu per satu dan kemudian menggabungkannya;
Tipe kedua adalah meminta secara bersamaan, lalu menggabungkan semua permintaan.
Jelasnya, efek kedua lebih efisien. Mari kita lihat kodenya:
//分别请求电费、水费、网费,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)
Pertama, kami menyimulasikan dan mendefinisikan beberapa permintaan jaringan di ViewModel, lalu menggabungkan permintaan tersebut:
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")}"
)
}
}
hasil operasi:
Kami melihat bahwa total waktu yang dihabiskan pada dasarnya sama dengan waktu permintaan terlama.
banyakFlow
tidak dapat ditempatkan dalam satulifecycleScope.launch
Masuk ke dalamcollect{}
, karena masukcollect{}
Setara dengan loop tak terbatas, baris kode berikutnya tidak akan pernah dieksekusi; jika Anda ingin menulis alifecycleScope.launch{}
Masuk ke dalam, Anda bisa menyalakannya lagi dari dalamlaunch{}
Sub-coroutine dijalankan.
Contoh kesalahan:
lifecycleScope.launch {
flow1
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
flow2
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
}
Cara menulis yang benar:
lifecycleScope.launch {
launch {
flow1
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
}
launch {
flow2
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
}
}
Dari siklus hidup Flow, kami memperkenalkan postur penggunaan aliran yang benar untuk menghindari pemborosan sumber daya, hingga konversi aliran dingin biasa menjadi aliran panas, hingga StateFlow menggantikan LiveData, dan masalah lengketnya, lalu menyelesaikan masalah lengket tersebut melalui SharedFlow, lalu ke skenario Aplikasi umum, dan terakhir tindakan pencegahan untuk menggunakan Flow, pada dasarnya mencakup sebagian besar fitur dan skenario aplikasi Flow. Ini juga merupakan bab terakhir dari pembelajaran Flow.
Memang tidak mudah untuk membuatnya, tapi sulit untuk menyukainyaSukai, kumpulkan, dan beri komentar untuk memberi semangat。
Artikel referensi
Pemrograman reaktif Kotlin Flow, StateFlow dan SharedFlow