Reputation: 81
I'm trying to implement some kind of LaunchedEffectOnce
as I want to track a ContentViewed event. So my requirement is that every time the user sees the content provided by the composable, an event should get tracked.
Here is some example code of my problem:
@Composable
fun MyScreen(viewModel: MyViewModel = get()){
val items by viewModel.itemsToDisplay.collectAsState(initial = emptyList())
ItemList(items)
// when the UI is displayed, the VM should track an event (only once)
LaunchedEffectOnce { viewModel.trackContentViewed() }
}
@Composable
private fun LaunchedEffectOnce(doOnce: () -> Unit) {
var wasExecuted by rememberSaveable { mutableStateOf(false) }
if (!wasExecuted) {
LaunchedEffect(key1 = rememberUpdatedState(newValue = executed)) {
doOnce()
wasExecuted = true
}
}
}
This code is doing do the following:
MyScreen
But what I wan't to achieve is the following:
MyScreen
My ViewModel looks like that:
class MyViewModel() : ViewModel() {
val itemsToDisplay: Flow<List<Item>> = GetItemsUseCase()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
val contentTracking: Flow<Tracking?> = GetTrackingUseCase()
.distinctUntilChanged { old, new -> old === new }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
fun trackContentViewed(){
// track last element in contentTracking
}
}
I really hope someone can help me and can explain what I'm doing wrong here. Thanks in advance!
Upvotes: 2
Views: 1021
Reputation: 6589
Assuming the following are true
MyScreen
enters compositionthen you can simply track inside the view model itself whether specific content was already viewed in this view model's scope. Then when you navigate to any of the items screens you reset that "tracking state".
If you need to track only a single element of content then just a Boolean
variable would be enough, but in case you need to track more than one element, you can use either a HashSet
or a mutableSetOf
(which returns a LinkedHashSet
instead). Then when you navigate to any of the item screen you reset that variable or clear the Set
.
Your VM code would then change to
class MyViewModel() : ViewModel() {
// ... you existing code remains unchanged
private var viewedContent = mutableSetOf<Any>()
fun trackContentViewed(key: Any){
if (viewedContent.add(key)) {
// track last element in contentTracking
Log.d("Example", "Key $key tracked for 'first time'")
} else {
// content already viewed for this key
Log.d("Example", "Key $key already tracked before")
}
}
fun clearTrackedContent() {
viewedContent.clear()
}
}
and the MyScreen
composable would change to
@Composable
fun MyScreen(viewModel: MyViewModel = get()){
// ... you existing code remains unchanged
// Every time this UI enters the composition (but not on recomposition)
// the VM will be notified
LaunchedEffect(Unit) {
viewModel.trackContentViewed(key = "MyScreen") // or some other key
}
}
Where you start the navigation to an item screen (probably in some onClick handler on items) you would call viewmodel.clearTrackedContent()
.
Since (1) is true when ViewModel
s are requested inside a Fragment/Activity and if (2) is also true in your case, then the VM instance will survive configuration changes (orientation change, language change...) and the Set
will take care of tracking.
If (2) is not true in your case, then you have two options:
LaunchedEffect
with SideEffect { viewModel.trackContentViewed(key = "MyScreen") }
viewModel.trackContentViewed
also when navigating back.Upvotes: 1