I have read the article A safer way to collect flows from Android UIs.
I know the following content.
A cold flow backed by a channel or using operators with buffers such as buffer, conflate, flowOn, or shareIn is not safe to collect with some of the existing APIs such as CoroutineScope.launch, Flow.launchIn, or LifecycleCoroutineScope.launchWhenX, unless you manually cancel the Job that started the coroutine when the activity goes to the background. These APIs will keep the underlying flow producer active while emitting items into the buffer in the background, and thus wasting resources.
The Code A is from the official sample project.
The viewModel.suggestedDestinations is a MutableStateFlow, it's a hot Flow.
I don't know if the operation collectAsState() of hot Flow is safe in @Composable UI.
1: Do I need to use the Code just like Code B or Code C replace Code A for a hot Flow?
2: Is the operation collectAsState() of cold Flow safe in @Composable UI.
Code A
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
val suggestedDestinations by viewModel.suggestedDestinations.collectAsState()
...
}
@HiltViewModel
class MainViewModel @Inject constructor(
...
) : ViewModel() {
...
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>>
}
Code B
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
...
}
}
}
}
Code C
@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {
val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val location by locationFlowLifecycleAware.collectAsState()
...
}
collectAsState (Code A) is safe for any kind of Flow (cold/hot it doesn't matter). If you look at how collectAsState is implemented then you will see that it uses a LaunchedEffect deep down (collectAsState -> produceState -> LaunchedEffect)
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onRemembered() {
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
job?.cancel()
job = null
}
override fun onAbandoned() {
job?.cancel()
job = null
}
}
which creates a coroutine scope and launches the task lambda once it enters the composition and cancels it automatically once it leaves the composition.
In Code A, viewModel.suggestedDestinations.collectAsState() (together with it's LaunchedEffect and it's coroutine scope) will be active as long as CraneHomeContent is being called by some other code. As soon as CraneHomeContent is stopped being called the LaunchedEffect inside of collectAsState() is canceled (and coroutine scope as well).
If it's called from multiple places then there will be multiple LaunchedEffects and thus multiple coroutine scopes.