From e297e494c20eb2768c1ac08df411f90b4ab397a8 Mon Sep 17 00:00:00 2001 From: mcarare Date: Wed, 22 Dec 2021 19:04:46 +0200 Subject: [PATCH] [fenix] For https://github.com/mozilla-mobile/fenix/issues/22383: Propagate ViewTreeLifecycleOwner to composable view holders. --- .../fenix/compose/ComposeViewHolder.kt | 50 +++++++++++++ .../home/pocket/PocketStoriesViewHolder.kt | 52 +++++++------- .../view/RecentBookmarksViewHolder.kt | 35 +++++----- .../recenttabs/view/RecentTabViewHolder.kt | 40 +++++------ .../view/RecentlyVisitedViewHolder.kt | 70 +++++++++---------- .../sessioncontrol/SessionControlAdapter.kt | 7 +- .../CustomizeHomeButtonViewHolder.kt | 37 +++++----- 7 files changed, 168 insertions(+), 123 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/compose/ComposeViewHolder.kt diff --git a/app/src/main/java/org/mozilla/fenix/compose/ComposeViewHolder.kt b/app/src/main/java/org/mozilla/fenix/compose/ComposeViewHolder.kt new file mode 100644 index 0000000000..6bc6769c90 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/ComposeViewHolder.kt @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.recyclerview.widget.RecyclerView +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.ViewTreeSavedStateRegistryOwner +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * [RecyclerView.ViewHolder] used for Jetpack Compose UI content . + * + * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. + * @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. + */ +abstract class ComposeViewHolder( + val composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner +) : RecyclerView.ViewHolder(composeView) { + + /** + * Composable that contains the content for a specific [ComposeViewHolder] implementation. + */ + @Composable + abstract fun Content() + + init { + composeView.setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + composeView.setContent { + FirefoxTheme { + Content() + } + } + + ViewTreeLifecycleOwner.set(composeView, viewLifecycleOwner) + ViewTreeSavedStateRegistryOwner.set( + composeView, + viewLifecycleOwner as SavedStateRegistryOwner + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt index f61c149150..1587f3849e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt @@ -17,16 +17,16 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.service.pocket.PocketRecommendedStory import org.mozilla.fenix.R +import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.SectionHeader import org.mozilla.fenix.home.HomeFragmentStore -import org.mozilla.fenix.theme.FirefoxTheme internal const val POCKET_STORIES_TO_SHOW_COUNT = 8 internal const val POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT = 8 @@ -40,35 +40,30 @@ internal const val POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT = 8 * @param interactor [PocketStoriesInteractor] callback for user interaction. */ class PocketStoriesViewHolder( - val composeView: ComposeView, + composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner, val store: HomeFragmentStore, val interactor: PocketStoriesInteractor -) : RecyclerView.ViewHolder(composeView) { - - init { - composeView.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed - ) - composeView.setContent { - FirefoxTheme { - PocketStories( - store, - interactor::onStoriesShown, - interactor::onStoryClicked, - interactor::onCategoryClicked, - interactor::onDiscoverMoreClicked, - interactor::onLearnMoreClicked, - with(composeView.resources) { - getDimensionPixelSize(R.dimen.home_item_horizontal_margin) / displayMetrics.density - } - ) - } - } - } +) : ComposeViewHolder(composeView, viewLifecycleOwner) { companion object { val LAYOUT_ID = View.generateViewId() } + + @Composable + override fun Content() { + PocketStories( + store, + interactor::onStoriesShown, + interactor::onStoryClicked, + interactor::onCategoryClicked, + interactor::onDiscoverMoreClicked, + interactor::onLearnMoreClicked, + with(composeView.resources) { + getDimensionPixelSize(R.dimen.home_item_horizontal_margin) / displayMetrics.density + } + ) + } } @Composable @@ -110,7 +105,12 @@ fun PocketStories( Spacer(Modifier.height(17.dp)) - PocketStories(stories ?: emptyList(), horizontalPadding.dp, onStoryClicked, onDiscoverMoreClicked) + PocketStories( + stories ?: emptyList(), + horizontalPadding.dp, + onStoryClicked, + onDiscoverMoreClicked + ) Spacer(Modifier.height(24.dp)) diff --git a/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksViewHolder.kt index 70dd7ccd6f..218b8b82de 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksViewHolder.kt @@ -5,42 +5,39 @@ package org.mozilla.fenix.home.recentbookmarks.view import android.view.View +import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.lifecycle.LifecycleOwner import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.home.HomeFragmentStore import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor -import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.utils.view.ViewHolder class RecentBookmarksViewHolder( - val composeView: ComposeView, + composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner, private val store: HomeFragmentStore, val interactor: RecentBookmarksInteractor, val metrics: MetricController -) : ViewHolder(composeView) { +) : ComposeViewHolder(composeView, viewLifecycleOwner) { init { metrics.track(Event.RecentBookmarksShown) - - composeView.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed - ) - composeView.setContent { - val recentBookmarks = store.observeAsComposableState { state -> state.recentBookmarks } - - FirefoxTheme { - RecentBookmarks( - bookmarks = recentBookmarks.value ?: emptyList(), - onRecentBookmarkClick = interactor::onRecentBookmarkClicked - ) - } - } } companion object { val LAYOUT_ID = View.generateViewId() } + + @Composable + override fun Content() { + val recentBookmarks = store.observeAsComposableState { state -> state.recentBookmarks } + + RecentBookmarks( + bookmarks = recentBookmarks.value ?: emptyList(), + onRecentBookmarkClick = interactor::onRecentBookmarkClicked + ) + } } diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt index b88c858ecb..efef0e61e4 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt @@ -5,14 +5,14 @@ package org.mozilla.fenix.home.recenttabs.view import android.view.View +import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.lifecycle.LifecycleOwner import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.R +import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.home.HomeFragmentStore import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor -import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.utils.view.ViewHolder /** * View holder for a recent tab item. @@ -22,32 +22,30 @@ import org.mozilla.fenix.utils.view.ViewHolder * @param interactor [RecentTabInteractor] which will have delegated to all user interactions. */ class RecentTabViewHolder( - val composeView: ComposeView, + composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner, private val store: HomeFragmentStore, private val interactor: RecentTabInteractor -) : ViewHolder(composeView) { +) : ComposeViewHolder(composeView, viewLifecycleOwner) { init { - val horizontalPadding = composeView.resources.getDimensionPixelSize(R.dimen.home_item_horizontal_margin) + val horizontalPadding = + composeView.resources.getDimensionPixelSize(R.dimen.home_item_horizontal_margin) composeView.setPadding(horizontalPadding, 0, horizontalPadding, 0) - - composeView.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed - ) - composeView.setContent { - val recentTabs = store.observeAsComposableState { state -> state.recentTabs } - - FirefoxTheme { - RecentTabs( - recentTabs = recentTabs.value ?: emptyList(), - onRecentTabClick = { interactor.onRecentTabClicked(it) }, - onRecentSearchGroupClicked = { interactor.onRecentSearchGroupClicked(it) } - ) - } - } } companion object { val LAYOUT_ID = View.generateViewId() } + + @Composable + override fun Content() { + val recentTabs = store.observeAsComposableState { state -> state.recentTabs } + + RecentTabs( + recentTabs = recentTabs.value ?: emptyList(), + onRecentTabClick = { interactor.onRecentTabClicked(it) }, + onRecentSearchGroupClicked = { interactor.onRecentSearchGroupClicked(it) } + ) + } } diff --git a/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt index c5e82a2374..cbb6209c7b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt @@ -5,20 +5,20 @@ package org.mozilla.fenix.home.recentvisits.view import android.view.View +import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource +import androidx.lifecycle.LifecycleOwner import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.home.HomeFragmentStore import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor -import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.utils.view.ViewHolder /** * View holder for [RecentlyVisitedItem]s. @@ -29,50 +29,50 @@ import org.mozilla.fenix.utils.view.ViewHolder * @property metrics [MetricController] that handles telemetry events. */ class RecentlyVisitedViewHolder( - val composeView: ComposeView, + composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner, private val store: HomeFragmentStore, private val interactor: RecentVisitsInteractor, private val metrics: MetricController -) : ViewHolder(composeView) { +) : ComposeViewHolder(composeView, viewLifecycleOwner) { init { - val horizontalPadding = composeView.resources.getDimensionPixelSize(R.dimen.home_item_horizontal_margin) + val horizontalPadding = + composeView.resources.getDimensionPixelSize(R.dimen.home_item_horizontal_margin) composeView.setPadding(horizontalPadding, 0, horizontalPadding, 0) + } - composeView.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed - ) - composeView.setContent { - val recentVisits = store.observeAsComposableState { state -> state.recentHistory } + @Composable + override fun Content() { + val recentVisits = store.observeAsComposableState { state -> state.recentHistory } - FirefoxTheme { - RecentlyVisited( - recentVisits = recentVisits.value ?: emptyList(), - menuItems = listOfNotNull( - RecentVisitMenuItem( - title = stringResource(R.string.recently_visited_menu_item_remove), - onClick = { visit -> - when (visit) { - is RecentHistoryGroup -> interactor.onRemoveRecentHistoryGroup(visit.title) - is RecentHistoryHighlight -> interactor.onRemoveRecentHistoryHighlight(visit.url) - } - } - ) - ), - onRecentVisitClick = { recentlyVisitedItem, pageNumber -> - when (recentlyVisitedItem) { - is RecentHistoryHighlight -> { - interactor.onRecentHistoryHighlightClicked(recentlyVisitedItem) - } - is RecentHistoryGroup -> { - metrics.track(Event.HistoryRecentSearchesTapped(pageNumber.toString())) - interactor.onRecentHistoryGroupClicked(recentlyVisitedItem) - } + RecentlyVisited( + recentVisits = recentVisits.value ?: emptyList(), + menuItems = listOfNotNull( + RecentVisitMenuItem( + title = stringResource(R.string.recently_visited_menu_item_remove), + onClick = { visit -> + when (visit) { + is RecentHistoryGroup -> interactor.onRemoveRecentHistoryGroup(visit.title) + is RecentHistoryHighlight -> interactor.onRemoveRecentHistoryHighlight( + visit.url + ) } } ) + ), + onRecentVisitClick = { recentlyVisitedItem, pageNumber -> + when (recentlyVisitedItem) { + is RecentHistoryHighlight -> { + interactor.onRecentHistoryHighlightClicked(recentlyVisitedItem) + } + is RecentHistoryGroup -> { + metrics.track(Event.HistoryRecentSearchesTapped(pageNumber.toString())) + interactor.onRecentHistoryGroupClicked(recentlyVisitedItem) + } + } } - } + ) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index b321e2cce9..98b4f761e3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -19,6 +19,7 @@ import mozilla.components.feature.top.sites.TopSite.Type.FRECENT import mozilla.components.ui.widgets.WidgetSiteItemView import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.tips.Tip +import org.mozilla.fenix.home.BottomSpacerViewHolder import org.mozilla.fenix.home.HomeFragmentStore import org.mozilla.fenix.home.TopPlaceholderViewHolder import org.mozilla.fenix.home.pocket.PocketStoriesViewHolder @@ -44,7 +45,6 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTh import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingToolbarPositionPickerViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder import org.mozilla.fenix.home.tips.ButtonTipViewHolder -import org.mozilla.fenix.home.BottomSpacerViewHolder import org.mozilla.fenix.home.topsites.TopSitePagerViewHolder import mozilla.components.feature.tab.collections.Tab as ComponentTab @@ -211,26 +211,31 @@ class SessionControlAdapter( when (viewType) { CustomizeHomeButtonViewHolder.LAYOUT_ID -> return CustomizeHomeButtonViewHolder( composeView = ComposeView(parent.context), + viewLifecycleOwner, interactor = interactor ) PocketStoriesViewHolder.LAYOUT_ID -> return PocketStoriesViewHolder( composeView = ComposeView(parent.context), + viewLifecycleOwner, store = store, interactor = interactor ) RecentBookmarksViewHolder.LAYOUT_ID -> return RecentBookmarksViewHolder( composeView = ComposeView(parent.context), + viewLifecycleOwner, store = store, interactor = interactor, metrics = components.analytics.metrics ) RecentTabViewHolder.LAYOUT_ID -> return RecentTabViewHolder( composeView = ComposeView(parent.context), + viewLifecycleOwner, store = store, interactor = interactor ) RecentlyVisitedViewHolder.LAYOUT_ID -> return RecentlyVisitedViewHolder( composeView = ComposeView(parent.context), + viewLifecycleOwner, store = store, interactor = interactor, metrics = components.analytics.metrics diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CustomizeHomeButtonViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CustomizeHomeButtonViewHolder.kt index 4f723448cb..15f96a6945 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CustomizeHomeButtonViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CustomizeHomeButtonViewHolder.kt @@ -18,44 +18,39 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.recyclerview.widget.RecyclerView +import androidx.lifecycle.LifecycleOwner import mozilla.components.ui.colors.PhotonColors import org.mozilla.fenix.R +import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.home.sessioncontrol.CustomizeHomeIteractor import org.mozilla.fenix.theme.FirefoxTheme class CustomizeHomeButtonViewHolder( - val composeView: ComposeView, + composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner, private val interactor: CustomizeHomeIteractor -) : RecyclerView.ViewHolder(composeView) { - - init { - composeView.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed - ) - composeView.setContent { - FirefoxTheme { - Column { - Spacer(modifier = Modifier.height(68.dp)) - - CustomizeHomeButton( - onButtonClick = { interactor.openCustomizeHomePage() } - ) - } - } - } - } +) : ComposeViewHolder(composeView, viewLifecycleOwner) { companion object { val LAYOUT_ID = View.generateViewId() } + + @Composable + override fun Content() { + Column { + Spacer(modifier = Modifier.height(68.dp)) + + CustomizeHomeButton( + onButtonClick = { interactor.openCustomizeHomePage() } + ) + } + } } /**