내 연락처 정보
우편메소피아@프로톤메일.com
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
처음 두 기사에서는 Flow가 무엇인지, 어떻게 사용하는지, 관련 연산자 개선 사항을 소개했습니다. 다음 기사에서는 주로 실제 프로젝트에서 Flow를 사용하는 방법을 소개합니다.
Flow의 실제 응용 시나리오를 소개하기 전에 먼저 Flow의 첫 번째 기사에서 소개된 타이머 예제를 검토해 보겠습니다. ViewModel에서 timeFlow 데이터 흐름을 정의했습니다.
class MainViewModel : ViewModel() {
val timeFlow = flow {
var time = 0
while (true) {
emit(time)
delay(1000)
time++
}
}
그런 다음 활동에서 이전에 정의한 데이터 스트림을 수신합니다.
lifecycleOwner.lifecycleScope.launch {
viewModel.timeFlow.collect { time ->
times = time
Log.d("ddup", "update UI $times")
}
}
실제 효과를 확인하기 위해 실행해 보겠습니다.
앱이 백그라운드로 전환되어도 로그가 계속 인쇄된다는 사실을 알고 계셨나요? 이는 리소스 낭비가 아닙니다.
lifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.timeFlow.collect { time ->
times = time
Log.d("ddup", "update UI $times")
}
}
코루틴 시작 방법을 launch에서 launchWhenStarted로 변경하고 다시 실행하여 효과를 살펴보겠습니다.
HOME 버튼을 클릭하고 백그라운드로 돌아가면 로그가 더 이상 출력되지 않는 것을 볼 수 있습니다. 변경 사항이 적용된 것을 볼 수 있지만 스트림이 취소된 것을 확인하기 위해 다시 프런트로 전환해 볼까요? 봐:
포그라운드로 전환하면 카운터가 0부터 시작하지 않으므로 실제로 수신을 취소하지 않고 백그라운드에서 데이터 수신을 일시 중지하는 것을 볼 수 있습니다. 실제로는 LaunchWhenStarted가 여전히 이전 데이터를 유지합니다. API는 폐기되었습니다. 대신 Google RepeatOnLifecycle이 더 권장되며 파이프라인에 오래된 데이터를 유지하는 문제가 없습니다.
해당 코드를 변환해 보겠습니다.
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.timeFlow.collect { time ->
times = time
Log.d("ddup", "update UI $times")
}
}
}
효과를 보려면 다시 실행하세요.
백그라운드에서 포그라운드로 전환하면 데이터가 다시 0부터 시작되는 것을 알 수 있는데, 이는 백그라운드로 전환하면 Flow가 작업을 취소하고 원본 데이터가 모두 지워진다는 의미입니다.
우리는 Flow를 사용하고 있으며, RepeatOnLifecycle을 통해 프로그램의 보안을 더 잘 보장할 수 있습니다.
이전 소개는 모두 Flow Cold Flow의 예입니다. 다음으로 Hot Flow의 몇 가지 일반적인 응용 시나리오를 소개합니다.
이전 타이머 예제를 계속 사용하여 화면이 가로 화면과 세로 화면 간에 전환되면 어떻게 되나요?
가로 화면과 세로 화면을 전환한 후 Activity가 다시 생성되고, timeFlow가 다시 수집되고, Cold Flow가 다시 수집되어 다시 실행된 후 타이머가 시작되는 것을 볼 수 있습니다. 0부터 계산합니다. 이때 우리는 페이지의 상태가 적어도 일정 기간 동안 변경되지 않기를 바랍니다. 여기서는 cold flow를 a로 수정합니다. 뜨거운 흐름을 시도하고 다음을 시도하십시오.
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")
}
}
}
```
여기서는 stateIn의 세 가지 매개변수에 중점을 둡니다. 첫 번째는 코루틴의 범위이고, 두 번째는 흐름이 작업 상태를 유지하는 데 걸리는 최대 유효 시간입니다. 매개변수는 초기값입니다.
효과를 보려면 다시 실행하세요.
여기서는 가로 화면과 세로 화면을 전환한 후 인쇄된 로그를 볼 수 있습니다. 타이머는 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")
}
}
}
```
StateFlow 핫 플로우를 정의한 다음 LiveData setData와 유사하게 startTimer() 메소드를 통해 stateFlow 값을 변경합니다. 버튼을 클릭하면 StateFlow 값 변경을 시작하고 해당 플로우의 값을 수집합니다. LiveData Observe 방법을 사용하여 데이터 변경 사항을 모니터링합니다.
실제 실행 효과를 살펴보겠습니다.
지금까지 StateFlow의 기본적인 사용법을 소개했고, 이제 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")
}
}
}
```
먼저 MainViewModel에서 clickCountFlow를 정의한 다음 활동에서 Button을 클릭하여 clickCountFlow 데이터를 변경하고 clickCountFlow를 수신하여 텍스트에 데이터를 표시합니다.
실행 효과를 살펴보겠습니다.
가로 화면과 세로 화면 사이를 전환하면 Activity가 다시 생성되고 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()
}
}
}
}
```
위의 코드는 실제로 클릭 로그인을 시뮬레이션한 후 로그인이 성공했다는 메시지를 표시합니다. 실제 작업 효과를 살펴보겠습니다.
가로 화면과 세로 화면 전환 후 다시 로그인 성공 메시지가 나타나는 것을 확인하셨나요? 끈끈한 이벤트로 인한 반복적인 데이터 수신 문제입니다. SharedFlow를 사용해 보세요.
private val _loginFlow = MutableSharedFlow<String>()
val loginFlow = _loginFlow.asSharedFlow()
fun startLogin() {
// Handle login logic here.
viewModelScope.launch {
_loginFlow.emit("Login Success")
}
}
StateFlow를 SharedFlow로 변경했는데, SharedFlow에는 초기값이 필요하지 않은 것을 볼 수 있습니다. 데이터를 보내는 위치에 내보내기 메소드가 추가되었으며, 데이터가 수신되는 위치는 변경되지 않고 그대로 유지됩니다.
여기서는 SharedFlow를 사용해도 이러한 끈적임 문제가 발생하지 않는다는 것을 알 수 있습니다. 실제로 SharedFlow에는 구성할 수 있는 많은 매개변수가 있습니다.
public fun <T> MutableSharedFlow(
// 每个新的订阅者订阅时收到的回放的数目,默认0
replay: Int = 0,
// 除了replay数目之外,缓存的容量,默认0
extraBufferCapacity: Int = 0,
// 缓存区溢出时的策略,默认为挂起。只有当至少有一个订阅者时,onBufferOverflow才会生效。当无订阅者时,只有最近replay数目的值会保存,并且onBufferOverflow无效。
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)
모두가 발견하기를 기다리는 SharedFlow의 더 많은 용도가 있지만 여기서는 자세히 다루지 않겠습니다.
이전에는 Hot Flow에 대한 기본 Cold Flow와 StateFlow 및 SharedFlow의 일반적인 사용법 및 적용 가능한 시나리오를 소개했습니다. 다음으로 Flow의 다른 일반적인 응용 시나리오를 살펴보기 위해 몇 가지 실제 사례에 중점을 둘 것입니다.
우리는 일반적으로 시간이 많이 걸리는 복잡한 로직을 수행하고 이를 하위 스레드에서 처리한 다음 메인 스레드로 전환하여 UI를 표시합니다. Flow도 스레드 전환을 지원하며 flowOn은 처리를 위해 이전 작업을 해당 하위 스레드에 넣을 수 있습니다. .
우리는 로컬 읽기를 구현합니다Assets
디렉토리 아래person.json
파일을 작성하고 구문 분석하고,json
파일 내용:
{
"name": "ddup",
"age": 101,
"interest": "earn money..."
}
그런 다음 파일을 구문 분석합니다.
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()
}
흐름은 파일을 읽습니다.
/**
* 通过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")
}
}
}
최종 인쇄 로그:
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"
}
}
}
여러 인터페이스의 데이터를 결합하는 시나리오는 무엇입니까? 예를 들어, 여러 인터페이스를 요청한 다음 그 결과를 결합하여 이를 균일하게 표시하거나 이를 다른 인터페이스의 요청 매개변수로 사용합니다. 이를 구현하는 방법은 무엇입니까?
첫 번째는 하나씩 요청한 후 병합하는 것입니다.
두 번째 유형은 동시에 요청한 다음 모든 요청을 병합하는 것입니다.
분명히 두 번째 효과가 더 효율적입니다. 코드를 살펴보겠습니다.
//分别请求电费、水费、网费,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)
먼저 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")}"
)
}
}
작업 결과:
소요된 총 시간은 기본적으로 가장 긴 요청 시간과 동일하다는 것을 알 수 있습니다.
다수의Flow
한 곳에는 넣을 수 없다lifecycleScope.launch
안으로 들어가세요collect{}
, 진입하기 때문에collect{}
무한 루프와 동일하게, 다음 코드 줄은 실행되지 않습니다.lifecycleScope.launch{}
안으로 들어가세요. 안에서 다시 켤 수 있어요launch{}
서브 코루틴이 실행됩니다.
오류 예:
lifecycleScope.launch {
flow1
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
flow2
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
}
올바른 작성 방법:
lifecycleScope.launch {
launch {
flow1
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
}
launch {
flow2
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {}
}
}
Flow의 라이프 사이클부터 자원 낭비를 피하기 위한 올바른 Flow의 사용 자세, 일반 Cold Flow를 Hot Flow로 변환, LiveData를 대체하는 StateFlow 및 그 고착성 문제까지 소개한 후 다음을 통해 고착성 문제를 해결했습니다. SharedFlow에 이어 일반적인 응용 시나리오, 마지막으로 Flow 사용 시 주의사항까지 기본적으로 Flow의 대부분의 기능과 응용 시나리오를 다루고 있습니다. 이는 Flow 학습의 마지막 장이기도 합니다.
만들기는 쉽지 않은데 좋아하기는 귀찮다좋아요, 수집, 댓글로 격려하기。
참고 기사
Kotlin Flow 반응형 프로그래밍, StateFlow 및 SharedFlow