android-components icon indicating copy to clipboard operation
android-components copied to clipboard

WebContent does not allow for tab switching via Target.Tab

Open AllanWang opened this issue 3 years ago • 1 comments

WebContent works fine for Target.SelectedTab, but if we want to change tabs ourselves via Target.Tab with different ids, it won't listen to changes.

https://github.com/mozilla-mobile/android-components/blob/ee178c72d615ff225025e12f9a6405f0b8d1e552/components/compose/engine/src/main/java/mozilla/components/compose/engine/WebContent.kt#L27-L38

This is because the portion above is only disposed on store and lifecycleOwner changes, and not also when the target changes. As a result, it always tests against the first target.tabId. Occasionally, editing compose will trigger a soft restart, which actually updates the value; this is unrelated to the issue.


Use case:

I have an app with:

  • One WebContent for the main screen, with tabs (TabLayout) to switch between sessions
  • One pop up screen, which is not always present, with its own WebContent

At any given moment, only one session is active. The one in the main screen with the corresponding tab, or the pop up screen. However, if I show the pop up screen, I don't want the main screen to also show that tab, which is why I'm using Target.Tab directly. Is this an appropriate use case, or is Target.Tab incorrect? Should I be using two browser stores instead?

┆Issue is synchronized with this Jira Task

AllanWang avatar Sep 16 '22 08:09 AllanWang

@Composable
fun WebContent(engine: Engine, store: BrowserStore, target: Target) {

  val selectedTab =
    target.observeAsComposableStateFrom2(
      store = store,
      observe = { tab ->
        // Render if the tab itself changed or when the state of the linked engine session changes
        arrayOf(
          tab?.id,
          tab?.engineState?.engineSession,
          tab?.engineState?.crashed,
          tab?.content?.firstContentfulPaint
        )
      }
    )

  // No changes here
}

@Composable
private fun <R> Target.observeAsComposableStateFrom2(
  store: BrowserStore,
  observe: (SessionState?) -> R
): State<SessionState?> {
  return store.observeAsComposableState(
    this,
    map = { state -> lookupIn(state) },
    observe = { state -> observe(lookupIn(state)) }
  )
}

@Composable
private fun <S : mozilla.components.lib.state.State, A : Action, K, O, R> Store<S, A>
  .observeAsComposableState(key: K, observe: (S) -> O, map: (S) -> R): State<R?> {
  val lifecycleOwner = LocalLifecycleOwner.current
  var lastValue = observe(state)
  val state = remember { mutableStateOf<R?>(map(state)) }

  DisposableEffect(this, key, lifecycleOwner) {
    val subscription =
      observe(lifecycleOwner) { browserState ->
        val newValue = observe(browserState)
        if (newValue != lastValue) {
          state.value = map(browserState)
          lastValue = newValue
        }
      }
    onDispose { subscription?.unsubscribe() }
  }

  return state
}

This is the easiest change I could think of. Add target as key to DisposableEffect in observeAsComposableState, given that changing tabs has no effect on the Store. I imagine this means this is not the correct approach, and that I should be using SelectedTab

AllanWang avatar Sep 16 '22 08:09 AllanWang

Moved to bugzilla: https://bugzilla.mozilla.org/show_bug.cgi?id=1794854

Change performed by the Move to Bugzilla add-on.

csadilek avatar Oct 12 '22 18:10 csadilek