museYke
museYke

Reputation: 81

Jetpack Compose do on compose, but not on recomposition - track ContentViewed

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:

But what I wan't to achieve is the following:

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

Answers (1)

Ma3x
Ma3x

Reputation: 6589

Assuming the following are true

  1. your view model is scoped to the Fragment in which MyScreen enters composition
  2. your composables leave the composition when you navigate to an item screen and re-enter composition when you navigate back

then 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 ViewModels 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:

  • if at least recomposition happens when navigating back, replace LaunchedEffect with SideEffect { viewModel.trackContentViewed(key = "MyScreen") }
  • if your composables are not even recomposed then you will have to call viewModel.trackContentViewed also when navigating back.

Upvotes: 1

Related Questions