Issue #19002: Use AbstractBinding from lib-state

This commit is contained in:
Jonathan Almeida 2021-05-10 16:08:08 -04:00 committed by Jonathan Almeida
parent 551031eee3
commit e66983d093
14 changed files with 93 additions and 246 deletions

View File

@ -489,6 +489,7 @@ dependencies {
implementation Deps.mozilla_lib_crash
implementation Deps.mozilla_lib_push_firebase
implementation Deps.mozilla_lib_state
implementation Deps.mozilla_lib_dataprotect
debugImplementation Deps.leakcanary

View File

@ -1,43 +0,0 @@
/* 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.components
import androidx.annotation.CallSuper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
/**
* Helper class for creating small binding classes that are responsible for reacting to state
* changes.
*
* Taken with from Focus.
*/
abstract class AbstractBinding<in S : State>(
private val store: Store<S, out Action>
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
@CallSuper
override fun start() {
scope = store.flowScoped { flow ->
onState(flow)
}
}
@CallSuper
override fun stop() {
scope?.cancel()
}
abstract suspend fun onState(flow: Flow<S>)
}

View File

@ -6,13 +6,11 @@ package org.mozilla.fenix.tabstray
import android.view.View
import android.widget.ImageButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
@ -24,38 +22,33 @@ import org.mozilla.fenix.utils.Settings
* This binding is coupled with [FloatingActionButtonBinding].
* When [FloatingActionButtonBinding] is visible this should not be visible
*/
@OptIn(ExperimentalCoroutinesApi::class)
class AccessibleNewTabButtonBinding(
private val store: TabsTrayStore,
private val settings: Settings,
private val newTabButton: ImageButton,
private val browserTrayInteractor: BrowserTrayInteractor
) : LifecycleAwareFeature {
) : AbstractBinding<TabsTrayState>(store) {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
if (!settings.accessibilityServicesEnabled) {
newTabButton.visibility = View.GONE
return
}
scope = store.flowScoped { flow ->
flow.map { it }
.ifAnyChanged { state ->
arrayOf(
state.selectedPage,
state.syncing
)
}
.collect { state ->
setAccessibleNewTabButton(state.selectedPage, state.syncing)
}
}
super.start()
}
override fun stop() {
scope?.cancel()
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it }
.ifAnyChanged { state ->
arrayOf(
state.selectedPage,
state.syncing
)
}
.collect { state ->
setAccessibleNewTabButton(state.selectedPage, state.syncing)
}
}
private fun setAccessibleNewTabButton(selectedPage: Page, syncing: Boolean) {

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.tabstray
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
@ -12,12 +13,13 @@ import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.components.AbstractBinding
/**
* A binding that closes the tabs tray when the last tab is closed.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class CloseOnLastTabBinding(
browserStore: BrowserStore,
private val tabsTrayStore: TabsTrayStore,

View File

@ -5,13 +5,11 @@
package org.mozilla.fenix.tabstray
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
@ -23,38 +21,33 @@ import org.mozilla.fenix.utils.Settings
* This binding is coupled with [AccessibleNewTabButtonBinding].
* When [AccessibleNewTabButtonBinding] is visible this should not be visible
*/
@OptIn(ExperimentalCoroutinesApi::class)
class FloatingActionButtonBinding(
private val store: TabsTrayStore,
private val settings: Settings,
private val actionButton: ExtendedFloatingActionButton,
private val browserTrayInteractor: BrowserTrayInteractor
) : LifecycleAwareFeature {
) : AbstractBinding<TabsTrayState>(store) {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
if (settings.accessibilityServicesEnabled) {
actionButton.hide()
return
}
scope = store.flowScoped { flow ->
flow.map { it }
.ifAnyChanged { state ->
arrayOf(
state.selectedPage,
state.syncing
)
}
.collect { state ->
setFab(state.selectedPage, state.syncing)
}
}
super.start()
}
override fun stop() {
scope?.cancel()
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it }
.ifAnyChanged { state ->
arrayOf(
state.selectedPage,
state.syncing
)
}
.collect { state ->
setFab(state.selectedPage, state.syncing)
}
}
private fun setFab(selectedPage: Page, syncing: Boolean) {

View File

@ -4,41 +4,31 @@
package org.mozilla.fenix.tabstray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounter
/**
* Updates the tab counter to the size of [BrowserState.normalTabs].
*/
@OptIn(ExperimentalCoroutinesApi::class)
class TabCounterBinding(
private val store: BrowserStore,
private val counter: TabCounter
) : LifecycleAwareFeature {
) : AbstractBinding<BrowserState>(store) {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
scope = store.flowScoped { flow ->
flow.map { it.normalTabs }
.ifChanged()
.collect {
counter.setCount(it.size)
}
}
}
override fun stop() {
scope?.cancel()
override suspend fun onState(flow: Flow<BrowserState>) {
flow.map { it.normalTabs }
.ifChanged()
.collect {
counter.setCount(it.size)
}
}
}

View File

@ -9,16 +9,15 @@ import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.infobanner.InfoBanner
@ -26,34 +25,27 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.utils.Settings
@OptIn(ExperimentalCoroutinesApi::class)
class TabsTrayInfoBannerBinding(
private val context: Context,
private val store: BrowserStore,
store: BrowserStore,
private val infoBannerView: ViewGroup,
private val settings: Settings,
private val navigationInteractor: NavigationInteractor,
private val metrics: MetricController?
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
) : AbstractBinding<BrowserState>(store) {
@VisibleForTesting
internal var banner: InfoBanner? = null
@ExperimentalCoroutinesApi
override fun start() {
scope = store.flowScoped { flow ->
flow.map { state -> max(state.normalTabs.size, state.privateTabs.size) }
.ifChanged()
.collect { tabCount ->
if (tabCount >= TAB_COUNT_SHOW_CFR) {
displayInfoBannerIfNeeded(settings)
}
override suspend fun onState(flow: Flow<BrowserState>) {
flow.map { state -> max(state.normalTabs.size, state.privateTabs.size) }
.ifChanged()
.collect { tabCount ->
if (tabCount >= TAB_COUNT_SHOW_CFR) {
displayInfoBannerIfNeeded(settings)
}
}
}
override fun stop() {
scope?.cancel()
}
}
private fun displayInfoBannerIfNeeded(settings: Settings) {

View File

@ -4,44 +4,36 @@
package org.mozilla.fenix.tabstray.browser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* Notifies the adapter when the selection mode changes.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class SelectedItemAdapterBinding(
val store: TabsTrayStore,
store: TabsTrayStore,
val adapter: BrowserTabsAdapter
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
) : AbstractBinding<TabsTrayState>(store) {
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
scope = store.flowScoped { flow ->
flow.map { it.mode }
// ignore initial mode update; the adapter is already in an updated state.
.drop(1)
.ifChanged()
.collect { mode ->
notifyAdapter(mode)
}
}
}
override fun stop() {
scope?.cancel()
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it.mode }
// ignore initial mode update; the adapter is already in an updated state.
.drop(1)
.ifChanged()
.collect { mode ->
notifyAdapter(mode)
}
}
private fun notifyAdapter(mode: Mode) = with(adapter) {

View File

@ -12,13 +12,14 @@ import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.component_tabstray2.view.exit_multi_select
import kotlinx.android.synthetic.main.component_tabstray2.view.multiselect_title
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AbstractBinding
import org.mozilla.fenix.tabstray.NavigationInteractor
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TabsTrayState
@ -41,6 +42,7 @@ import org.mozilla.fenix.tabstray.ext.showWithTheme
* @property showOnSelectViews A variable list of views that will be made visible when in select mode.
* @property showOnNormalViews A variable list of views that will be made visible when in normal mode.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("LongParameterList")
class SelectionBannerBinding(
private val context: Context,

View File

@ -10,13 +10,14 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AbstractBinding
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
import org.mozilla.fenix.tabstray.TabsTrayStore
@ -31,6 +32,7 @@ private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
* @property handle The "handle" of the Tabs Tray that is used to drag the tray open/close.
* @property containerLayout The [ConstraintLayout] that contains the "handle".
*/
@OptIn(ExperimentalCoroutinesApi::class)
class SelectionHandleBinding(
store: TabsTrayStore,
private val handle: View,

View File

@ -4,13 +4,11 @@
package org.mozilla.fenix.tabstray.browser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
@ -18,25 +16,18 @@ import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* Notifies whether a tab is accessible for using the swipe-to-delete gesture.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class SwipeToDeleteBinding(
private val store: TabsTrayStore
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
) : AbstractBinding<TabsTrayState>(store) {
var isSwipeable = false
private set
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
scope = store.flowScoped { flow ->
flow.map { it.mode }
.ifChanged()
.collect { mode ->
isSwipeable = mode == TabsTrayState.Mode.Normal
}
}
}
override fun stop() {
scope?.cancel()
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it.mode }
.ifChanged()
.collect { mode ->
isSwipeable = mode == TabsTrayState.Mode.Normal
}
}
}

View File

@ -4,12 +4,13 @@
package org.mozilla.fenix.tabstray.syncedtabs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.components.AbstractBinding
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
@ -19,6 +20,7 @@ import org.mozilla.fenix.tabstray.TabsTrayStore
*
* This binding is useful for connecting with [SyncedTabsView.Listener].
*/
@OptIn(ExperimentalCoroutinesApi::class)
class SyncButtonBinding(
tabsTrayStore: TabsTrayStore,
private val onSyncNow: () -> Unit

View File

@ -1,71 +0,0 @@
/* 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.components
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
class AbstractBindingTest {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
@Test
fun `WHEN started THEN onState flow is invoked`() {
val store = BrowserStore()
var invoked = false
val binding = TestBinding(store) {
invoked = true
}
binding.start()
store.waitUntilIdle()
assertTrue(invoked)
}
@Test
fun `WHEN actions are dispatched THEN onState flow is invoked`() {
val store = BrowserStore()
var invoked = false
val binding = TestBinding(store) {
if (store.state.tabs.isNotEmpty()) {
invoked = true
}
}
binding.start()
store.dispatch(TabListAction.AddTabAction(createTab("https://mozilla.org")))
store.waitUntilIdle()
assertTrue(invoked)
}
class TestBinding(
store: BrowserStore,
private val invoked: (BrowserState) -> Unit
) : AbstractBinding<BrowserState>(store) {
override suspend fun onState(flow: Flow<BrowserState>) {
flow.collect {
invoked(it)
}
}
}
}

View File

@ -145,6 +145,7 @@ object Deps {
const val mozilla_lib_crash = "org.mozilla.components:lib-crash:${Versions.mozilla_android_components}"
const val mozilla_lib_push_firebase = "org.mozilla.components:lib-push-firebase:${Versions.mozilla_android_components}"
const val mozilla_lib_dataprotect = "org.mozilla.components:lib-dataprotect:${Versions.mozilla_android_components}"
const val mozilla_lib_state = "org.mozilla.components:lib-state:${Versions.mozilla_android_components}"
const val mozilla_lib_publicsuffixlist = "org.mozilla.components:lib-publicsuffixlist:${Versions.mozilla_android_components}"