From ccbc14a71fd83ca4b2d8dc9bc60fd8d0e75df6cc Mon Sep 17 00:00:00 2001 From: Colin Lee Date: Thu, 9 May 2019 18:06:12 -0500 Subject: [PATCH] For #1994: Re-architect state handling code (#2382) --- CHANGELOG.md | 2 + app/build.gradle | 1 + .../mozilla/fenix/browser/BrowserFragment.kt | 2 + .../CollectionCreationComponent.kt | 74 +++++++++++---- .../collections/CreateCollectionFragment.kt | 1 + .../components/toolbar/ToolbarComponent.kt | 47 ++++++++-- .../fenix/components/toolbar/ToolbarUIView.kt | 2 +- .../fenix/exceptions/ExceptionsComponent.kt | 43 +++++++-- .../fenix/exceptions/ExceptionsFragment.kt | 1 + .../org/mozilla/fenix/home/HomeFragment.kt | 1 + .../sessioncontrol/SessionControlComponent.kt | 49 ++++++++-- .../library/bookmarks/BookmarkComponent.kt | 94 ++++++++++++------- .../library/bookmarks/BookmarkFragment.kt | 4 +- .../library/bookmarks/SignInComponent.kt | 49 ++++++++-- .../SelectBookmarkFolderFragment.kt | 2 +- .../fenix/library/history/HistoryComponent.kt | 85 +++++++++++------ .../fenix/library/history/HistoryFragment.kt | 2 +- .../quickactionsheet/QuickActionComponent.kt | 63 +++++++++---- .../mozilla/fenix/search/SearchFragment.kt | 3 +- .../search/awesomebar/AwesomeBarComponent.kt | 56 ++++++++--- .../quicksettings/QuickSettingsComponent.kt | 93 ++++++++++++------ .../QuickSettingsSheetDialogFragment.kt | 29 +++--- .../bookmarks/BookmarkComponentTest.kt | 4 +- .../library/history/HistoryComponentTest.kt | 4 +- architecture/build.gradle | 4 +- .../org/mozilla/fenix/mvi/ActionBusFactory.kt | 11 ++- .../java/org/mozilla/fenix/mvi/UIComponent.kt | 32 ++++++- .../main/java/org/mozilla/fenix/mvi/UIView.kt | 8 +- buildSrc/src/main/java/Dependencies.kt | 3 + 29 files changed, 557 insertions(+), 212 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9955b8f2a..34baa2ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,4 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #1429 - Updated site permissions ui for MVP - #1599 - Fixed a crash creating a bookmark for a custom tab - #1414 - Fixed site permissions settings getting reset in Android 6. +- #1994 - Made app state persist better when rotating the screen + ### Removed diff --git a/app/build.gradle b/app/build.gradle index 99abd693b..c5432dec4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -337,6 +337,7 @@ dependencies { implementation Deps.androidx_navigation_ui implementation Deps.androidx_recyclerview implementation Deps.androidx_lifecycle_viewmodel_ktx + implementation Deps.androidx_lifecycle_viewmodel_ss implementation Deps.androidx_core implementation Deps.androidx_transition diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index c234078ca..72d4424ee 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -132,6 +132,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope, toolbarComponent = ToolbarComponent( view.browserLayout, + this, ActionBusFactory.get(this), customTabSessionId, (activity as HomeActivity).browsingModeManager.isPrivate, SearchState("", getSessionById()?.searchTerms ?: "", isEditing = false), @@ -156,6 +157,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope, QuickActionComponent( view.nestedScrollQuickAction, + this, ActionBusFactory.get(this), QuickActionState( readable = getSessionById()?.readerable ?: false, diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt index a66dcc434..c80be417d 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt @@ -5,6 +5,10 @@ package org.mozilla.fenix.collections file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Observable import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.mvi.Action @@ -12,6 +16,7 @@ import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.ViewState sealed class SaveCollectionStep { @@ -51,34 +56,63 @@ sealed class CollectionCreationAction : Action { class CollectionCreationComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: CollectionCreationState = CollectionCreationState() ) : UIComponent( + owner, bus.getManagedEmitter(CollectionCreationAction::class.java), bus.getSafeManagedObservable(CollectionCreationChange::class.java) ) { - override val reducer: Reducer = - { state, change -> - when (change) { - is CollectionCreationChange.AddAllTabs -> state.copy(selectedTabs = state.tabs.toSet()) - is CollectionCreationChange.TabListChange -> state.copy(tabs = change.tabs) - is CollectionCreationChange.TabAdded -> { - val selectedTabs = state.selectedTabs + setOf(change.tab) - state.copy(selectedTabs = selectedTabs) - } - is CollectionCreationChange.TabRemoved -> { - val selectedTabs = state.selectedTabs - setOf(change.tab) - state.copy(selectedTabs = selectedTabs) - } - is CollectionCreationChange.StepChanged -> { - state.copy(saveCollectionStep = change.saveCollectionStep) - } - } - } - override fun initView() = CollectionCreationUIView(container, actionEmitter, changesObservable) + override fun render(): Observable = + ViewModelProvider(owner, CollectionCreationViewModel.Factory(initialState, changesObservable)).get( + CollectionCreationViewModel::class.java + ).render(uiView) + init { - render(reducer) + render() + } +} + +class CollectionCreationViewModel( + initialState: CollectionCreationState, + changesObservable: Observable +) : + UIComponentViewModel( + initialState, + changesObservable, + reducer + ) { + + class Factory( + private val initialState: CollectionCreationState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + CollectionCreationViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: Reducer = + { state, change -> + when (change) { + is CollectionCreationChange.AddAllTabs -> state.copy(selectedTabs = state.tabs.toSet()) + is CollectionCreationChange.TabListChange -> state.copy(tabs = change.tabs) + is CollectionCreationChange.TabAdded -> { + val selectedTabs = state.selectedTabs + setOf(change.tab) + state.copy(selectedTabs = selectedTabs) + } + is CollectionCreationChange.TabRemoved -> { + val selectedTabs = state.selectedTabs - setOf(change.tab) + state.copy(selectedTabs = selectedTabs) + } + is CollectionCreationChange.StepChanged -> { + state.copy(saveCollectionStep = change.saveCollectionStep) + } + } + } } } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt index 733f12d2d..8170a6c52 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt @@ -50,6 +50,7 @@ class CreateCollectionFragment : DialogFragment() { collectionCreationComponent = CollectionCreationComponent( view.create_collection_wrapper, + this, ActionBusFactory.get(this), CollectionCreationState( tabs = tabs, diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt index ac24a030b..3368d14df 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarComponent.kt @@ -7,6 +7,11 @@ package org.mozilla.fenix.components.toolbar import android.view.ViewGroup import android.widget.ImageView import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import io.reactivex.Observable import kotlinx.android.synthetic.main.component_search.* import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.toolbar.BrowserToolbar @@ -17,10 +22,12 @@ import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.ViewState class ToolbarComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, private val sessionId: String?, private val isPrivate: Boolean, @@ -28,21 +35,13 @@ class ToolbarComponent( private val engineIconView: ImageView? = null ) : UIComponent( + owner, bus.getManagedEmitter(SearchAction::class.java), bus.getSafeManagedObservable(SearchChange::class.java) ) { fun getView(): BrowserToolbar = uiView.toolbar - override val reducer: Reducer = { state, change -> - when (change) { - is SearchChange.ToolbarClearedFocus -> state.copy(focused = false) - is SearchChange.ToolbarRequestedFocus -> state.copy(focused = true) - is SearchChange.SearchShortcutEngineSelected -> - state.copy(engine = change.engine) - } - } - override fun initView() = ToolbarUIView( sessionId, isPrivate, @@ -52,8 +51,12 @@ class ToolbarComponent( engineIconView ) + override fun render(): Observable = + ViewModelProviders.of(owner, ToolbarViewModel.Factory(initialState, changesObservable)) + .get(ToolbarViewModel::class.java).render(uiView) + init { - render(reducer) + render() applyTheme() } @@ -95,3 +98,27 @@ sealed class SearchChange : Change { object ToolbarClearedFocus : SearchChange() data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchChange() } + +class ToolbarViewModel(initialState: SearchState, changesObservable: Observable) : + UIComponentViewModel(initialState, changesObservable, reducer) { + + class Factory( + private val initialState: SearchState, + val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + ToolbarViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: Reducer = { state, change -> + when (change) { + is SearchChange.ToolbarClearedFocus -> state.copy(focused = false) + is SearchChange.ToolbarRequestedFocus -> state.copy(focused = true) + is SearchChange.SearchShortcutEngineSelected -> + state.copy(engine = change.engine) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt index 90c0bdbb4..f6776a94e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarUIView.kt @@ -45,7 +45,7 @@ class ToolbarUIView( init { val sessionManager = view.context.components.core.sessionManager val session = sessionId?.let { sessionManager.findSessionById(it) } - ?: sessionManager.selectedSession + ?: sessionManager.selectedSession view.apply { elevation = resources.pxToDp(TOOLBAR_ELEVATION).toFloat() diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsComponent.kt b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsComponent.kt index b8feb03fd..aff226da9 100644 --- a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsComponent.kt @@ -4,10 +4,16 @@ package org.mozilla.fenix.exceptions import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import io.reactivex.Observable import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.test.Mockable @@ -16,24 +22,24 @@ data class ExceptionsItem(val url: String) @Mockable class ExceptionsComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: ExceptionsState = ExceptionsState(emptyList()) ) : UIComponent( + owner, bus.getManagedEmitter(ExceptionsAction::class.java), bus.getSafeManagedObservable(ExceptionsChange::class.java) ) { - override val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change -> - when (change) { - is ExceptionsChange.Change -> state.copy(items = change.list) - } - } + override fun render(): Observable = + ViewModelProviders.of(owner, ExceptionsViewModel.Factory(initialState, changesObservable)) + .get(ExceptionsViewModel::class.java).render(uiView) override fun initView() = ExceptionsUIView(container, actionEmitter, changesObservable) init { - render(reducer) + render() } } @@ -49,3 +55,28 @@ sealed class ExceptionsAction : Action { sealed class ExceptionsChange : Change { data class Change(val list: List) : ExceptionsChange() } + +class ExceptionsViewModel(initialState: ExceptionsState, changesObservable: Observable) : + UIComponentViewModel( + initialState, + changesObservable, + reducer + ) { + + class Factory( + private val initialState: ExceptionsState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + ExceptionsViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change -> + when (change) { + is ExceptionsChange.Change -> state.copy(items = change.list) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsFragment.kt index b24d6aadf..55dd750ad 100644 --- a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsFragment.kt @@ -48,6 +48,7 @@ class ExceptionsFragment : Fragment(), CoroutineScope { val view = inflater.inflate(R.layout.fragment_exceptions, container, false) exceptionsComponent = ExceptionsComponent( view.exceptions_layout, + this, ActionBusFactory.get(this), ExceptionsState(loadAndMapExceptions()) ) diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 76e61aae6..48c59fc67 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -89,6 +89,7 @@ class HomeFragment : Fragment(), CoroutineScope { sessionControlComponent = SessionControlComponent( view.homeLayout, + this, bus, SessionControlState(listOf(), listOf(), mode) ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt index 26219f72f..09f643532 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt @@ -6,38 +6,42 @@ package org.mozilla.fenix.home.sessioncontrol import android.graphics.Bitmap import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.RecyclerView +import io.reactivex.Observable import io.reactivex.Observer import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.ViewState class SessionControlComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: SessionControlState = SessionControlState(emptyList(), emptyList(), Mode.Normal) ) : UIComponent( + owner, bus.getManagedEmitter(SessionControlAction::class.java), bus.getSafeManagedObservable(SessionControlChange::class.java) ) { - override val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change -> - when (change) { - is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections) - is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs) - is SessionControlChange.ModeChange -> state.copy(mode = change.mode) - } - } - override fun initView() = SessionControlUIView(container, actionEmitter, changesObservable) val view: RecyclerView get() = uiView.view as RecyclerView + override fun render(): Observable = + ViewModelProviders.of(owner, SessionControlViewModel.Factory(initialState, changesObservable)) + .get(SessionControlViewModel::class.java).render(uiView) + init { - render(reducer) + render() } } @@ -109,3 +113,30 @@ sealed class SessionControlChange : Change { data class ModeChange(val mode: Mode) : SessionControlChange() data class CollectionsChange(val collections: List) : SessionControlChange() } + +class SessionControlViewModel(initialState: SessionControlState, changesObservable: Observable) : + UIComponentViewModel( + initialState, + changesObservable, + reducer + ) { + + class Factory( + private val initialState: SessionControlState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + SessionControlViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change -> + when (change) { + is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections) + is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs) + is SessionControlChange.ModeChange -> state.copy(mode = change.mode) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkComponent.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkComponent.kt index 0efa3a645..c920aa471 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkComponent.kt @@ -5,12 +5,17 @@ package org.mozilla.fenix.library.bookmarks import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Observable import mozilla.components.concept.storage.BookmarkNode import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.test.Mockable @@ -18,51 +23,28 @@ import org.mozilla.fenix.test.Mockable @Mockable class BookmarkComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: BookmarkState = BookmarkState(null, BookmarkState.Mode.Normal) ) : UIComponent( + owner, bus.getManagedEmitter(BookmarkAction::class.java), bus.getSafeManagedObservable(BookmarkChange::class.java) ) { - - override val reducer: Reducer = { state, change -> - when (change) { - is BookmarkChange.Change -> { - val mode = - if (state.mode is BookmarkState.Mode.Selecting) { - val items = state.mode.selectedItems.filter { - it in change.tree - }.toSet() - if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items) - } else state.mode - state.copy(tree = change.tree, mode = mode) - } - is BookmarkChange.IsSelected -> { - val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) { - state.mode.selectedItems + change.newlySelectedItem - } else setOf(change.newlySelectedItem) - state.copy(mode = BookmarkState.Mode.Selecting(selectedItems)) - } - is BookmarkChange.IsDeselected -> { - val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) { - state.mode.selectedItems - change.newlyDeselectedItem - } else setOf() - val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting( - selectedItems - ) - state.copy(mode = mode) - } - is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal) - } - } - override fun initView(): UIView = BookmarkUIView(container, actionEmitter, changesObservable) + override fun render(): Observable { + return ViewModelProvider( + owner, + BookmarkViewModel.Factory(initialState, changesObservable) + ).get(BookmarkViewModel::class.java).render(uiView) + } + init { - render(reducer) + render() } } @@ -98,3 +80,49 @@ sealed class BookmarkChange : Change { operator fun BookmarkNode.contains(item: BookmarkNode): Boolean { return children?.contains(item) ?: false } + +class BookmarkViewModel(initialState: BookmarkState, changesObservable: Observable) : + UIComponentViewModel(initialState, changesObservable, reducer) { + + class Factory( + private val initialState: BookmarkState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + BookmarkViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: Reducer = { state, change -> + when (change) { + is BookmarkChange.Change -> { + val mode = + if (state.mode is BookmarkState.Mode.Selecting) { + val items = state.mode.selectedItems.filter { + it in change.tree + }.toSet() + if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items) + } else state.mode + state.copy(tree = change.tree, mode = mode) + } + is BookmarkChange.IsSelected -> { + val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) { + state.mode.selectedItems + change.newlySelectedItem + } else setOf(change.newlySelectedItem) + state.copy(mode = BookmarkState.Mode.Selecting(selectedItems)) + } + is BookmarkChange.IsDeselected -> { + val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) { + state.mode.selectedItems - change.newlyDeselectedItem + } else setOf() + val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting( + selectedItems + ) + state.copy(mode = mode) + } + is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt index 824e7a1be..bd7370117 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -74,8 +74,8 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_bookmark, container, false) - bookmarkComponent = BookmarkComponent(view.bookmark_layout, ActionBusFactory.get(this)) - signInComponent = SignInComponent(view.bookmark_layout, ActionBusFactory.get(this)) + bookmarkComponent = BookmarkComponent(view.bookmark_layout, this, ActionBusFactory.get(this)) + signInComponent = SignInComponent(view.bookmark_layout, this, ActionBusFactory.get(this)) return view } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt index 88ab709d2..2ac3c8e46 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/SignInComponent.kt @@ -5,36 +5,41 @@ package org.mozilla.fenix.library.bookmarks import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Observable import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.ViewState class SignInComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: SignInState = SignInState(false) ) : UIComponent( + owner, bus.getManagedEmitter(SignInAction::class.java), bus.getSafeManagedObservable(SignInChange::class.java) ) { - - override val reducer: Reducer = { state, change -> - when (change) { - SignInChange.SignedIn -> state.copy(signedIn = true) - SignInChange.SignedOut -> state.copy(signedIn = false) - } - } - override fun initView(): UIView = SignInUIView(container, actionEmitter, changesObservable) + override fun render(): Observable = + ViewModelProvider( + owner, + SignInViewModel.Factory(initialState, changesObservable) + ).get(SignInViewModel::class.java).render(uiView) + init { - render(reducer) + render() } } @@ -48,3 +53,29 @@ sealed class SignInChange : Change { object SignedIn : SignInChange() object SignedOut : SignInChange() } + +class SignInViewModel(initialState: SignInState, changesObservable: Observable) : + UIComponentViewModel( + initialState, changesObservable, reducer + ) { + + class Factory( + private val initialState: SignInState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + SignInViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer = object : Reducer { + override fun invoke(state: SignInState, change: SignInChange): SignInState { + return when (change) { + SignInChange.SignedIn -> state.copy(signedIn = true) + SignInChange.SignedOut -> state.copy(signedIn = false) + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt index 69465fbbc..228522d5f 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderFragment.kt @@ -68,7 +68,7 @@ class SelectBookmarkFolderFragment : Fragment(), CoroutineScope, AccountObserver override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false) - signInComponent = SignInComponent(view.select_bookmark_layout, ActionBusFactory.get(this)) + signInComponent = SignInComponent(view.select_bookmark_layout, this, ActionBusFactory.get(this)) return view } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryComponent.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryComponent.kt index 6c4967c94..35ebef3a7 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryComponent.kt @@ -4,56 +4,42 @@ package org.mozilla.fenix.library.history import android.view.ViewGroup -import org.mozilla.fenix.test.Mockable +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Observable import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.ViewState +import org.mozilla.fenix.test.Mockable data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long) @Mockable class HistoryComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: HistoryState = HistoryState(emptyList(), HistoryState.Mode.Normal) ) : UIComponent( + owner, bus.getManagedEmitter(HistoryAction::class.java), bus.getSafeManagedObservable(HistoryChange::class.java) ) { - - override val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change -> - when (change) { - is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list) - is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item))) - is HistoryChange.AddItemForRemoval -> { - val mode = state.mode - if (mode is HistoryState.Mode.Editing) { - val items = mode.selectedItems + listOf(change.item) - state.copy(mode = mode.copy(selectedItems = items)) - } else { - state - } - } - is HistoryChange.RemoveItemForRemoval -> { - val mode = state.mode - if (mode is HistoryState.Mode.Editing) { - val items = mode.selectedItems.filter { it.id != change.item.id } - state.copy(mode = mode.copy(selectedItems = items)) - } else { - state - } - } - is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal) - } - } - override fun initView() = HistoryUIView(container, actionEmitter, changesObservable) + override fun render(): Observable = + ViewModelProvider( + owner, + HistoryViewModel.Factory(initialState, changesObservable) + ).get(HistoryViewModel::class.java).render(uiView) + init { - render(reducer) + render() } } @@ -85,3 +71,44 @@ sealed class HistoryChange : Change { data class AddItemForRemoval(val item: HistoryItem) : HistoryChange() data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange() } + +class HistoryViewModel(initialState: HistoryState, changesObservable: Observable) : + UIComponentViewModel(initialState, changesObservable, reducer) { + + class Factory( + private val initialState: HistoryState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + HistoryViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change -> + when (change) { + is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list) + is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item))) + is HistoryChange.AddItemForRemoval -> { + val mode = state.mode + if (mode is HistoryState.Mode.Editing) { + val items = mode.selectedItems + listOf(change.item) + state.copy(mode = mode.copy(selectedItems = items)) + } else { + state + } + } + is HistoryChange.RemoveItemForRemoval -> { + val mode = state.mode + if (mode is HistoryState.Mode.Editing) { + val items = mode.selectedItems.filter { it.id != change.item.id } + state.copy(mode = mode.copy(selectedItems = items)) + } else { + state + } + } + is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index ab4d027c9..a2f61cb00 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -49,7 +49,7 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler { savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_history, container, false) - historyComponent = HistoryComponent(view.history_layout, ActionBusFactory.get(this)) + historyComponent = HistoryComponent(view.history_layout, this, ActionBusFactory.get(this)) return view } diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt index 644666eb2..862f987b4 100644 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt @@ -5,16 +5,22 @@ package org.mozilla.fenix.quickactionsheet import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Observable import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.ViewState class QuickActionComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: QuickActionState = QuickActionState( readable = false, @@ -22,29 +28,21 @@ class QuickActionComponent( readerActive = false ) ) : UIComponent( + owner, bus.getManagedEmitter(QuickActionAction::class.java), bus.getSafeManagedObservable(QuickActionChange::class.java) ) { - - override val reducer: Reducer = { state, change -> - when (change) { - is QuickActionChange.BookmarkedStateChange -> { - state.copy(bookmarked = change.bookmarked) - } - is QuickActionChange.ReadableStateChange -> { - state.copy(readable = change.readable) - } - is QuickActionChange.ReaderActiveStateChange -> { - state.copy(readerActive = change.active) - } - } - } - override fun initView(): UIView = QuickActionUIView(container, actionEmitter, changesObservable) + override fun render(): Observable = + ViewModelProvider( + owner, + QuickActionViewModel.Factory(initialState, changesObservable) + ).get(QuickActionViewModel::class.java).render(uiView) + init { - render(reducer) + render() } } @@ -65,3 +63,36 @@ sealed class QuickActionChange : Change { data class ReadableStateChange(val readable: Boolean) : QuickActionChange() data class ReaderActiveStateChange(val active: Boolean) : QuickActionChange() } + +class QuickActionViewModel(initialState: QuickActionState, changesObservable: Observable) : + UIComponentViewModel( + initialState, + changesObservable, + reducer + ) { + + class Factory( + private val initialState: QuickActionState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + QuickActionViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: Reducer = { state, change -> + when (change) { + is QuickActionChange.BookmarkedStateChange -> { + state.copy(bookmarked = change.bookmarked) + } + is QuickActionChange.ReadableStateChange -> { + state.copy(readable = change.readable) + } + is QuickActionChange.ReaderActiveStateChange -> { + state.copy(readerActive = change.active) + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt index 80df4ba0f..dbce0088e 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt @@ -67,6 +67,7 @@ class SearchFragment : Fragment(), BackHandler { toolbarComponent = ToolbarComponent( view.toolbar_component_wrapper, + this, ActionBusFactory.get(this), sessionId, isPrivate, @@ -74,7 +75,7 @@ class SearchFragment : Fragment(), BackHandler { view.search_engine_icon ) - awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this)) + awesomeBarComponent = AwesomeBarComponent(view.search_layout, this, ActionBusFactory.get(this)) ActionBusFactory.get(this).logMergedObservables() return view } diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt index a24cf5da0..0bdd46a34 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarComponent.kt @@ -1,15 +1,23 @@ package org.mozilla.fenix.search.awesomebar + /* 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/. */ +import android.util.Log import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import io.reactivex.Observable import mozilla.components.browser.search.SearchEngine import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.ViewState data class AwesomeBarState( @@ -32,25 +40,51 @@ sealed class AwesomeBarChange : Change { class AwesomeBarComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: AwesomeBarState = AwesomeBarState("", false) ) : UIComponent( + owner, bus.getManagedEmitter(AwesomeBarAction::class.java), bus.getSafeManagedObservable(AwesomeBarChange::class.java) ) { - override val reducer: Reducer = { state, change -> - when (change) { - is AwesomeBarChange.SearchShortcutEngineSelected -> - state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false) - is AwesomeBarChange.SearchShortcutEnginePicker -> - state.copy(showShortcutEnginePicker = change.show) - is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query) - } - } - override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable) + override fun render(): Observable = + ViewModelProviders.of(owner, AwesomeBarViewModel.Factory(initialState, changesObservable)) + .get(AwesomeBarViewModel::class.java).render(uiView) + init { - render(reducer) + render() + } +} + +class AwesomeBarViewModel(initialState: AwesomeBarState, changesObservable: Observable) : + UIComponentViewModel( + initialState, + changesObservable, + reducer + ) { + + class Factory( + private val initialState: AwesomeBarState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + AwesomeBarViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: Reducer = { state, change -> + Log.d("IN_REDUCER", change.toString()) + when (change) { + is AwesomeBarChange.SearchShortcutEngineSelected -> + state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false) + is AwesomeBarChange.SearchShortcutEnginePicker -> + state.copy(showShortcutEnginePicker = change.show) + is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query) + } + } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsComponent.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsComponent.kt index b3a6b48a3..26d4eb2db 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsComponent.kt @@ -6,6 +6,10 @@ package org.mozilla.fenix.settings.quicksettings import android.content.Context import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Observable import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.support.ktx.kotlin.toUri import org.mozilla.fenix.ext.components @@ -13,6 +17,7 @@ import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.UIComponent +import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.settings.PhoneFeature @@ -22,48 +27,25 @@ import org.mozilla.fenix.utils.Settings class QuickSettingsComponent( private val container: ViewGroup, + owner: Fragment, bus: ActionBusFactory, override var initialState: QuickSettingsState ) : UIComponent( + owner, bus.getManagedEmitter(QuickSettingsAction::class.java), bus.getSafeManagedObservable(QuickSettingsChange::class.java) ) { - override val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change -> - when (change) { - is QuickSettingsChange.Change -> { - state.copy( - mode = QuickSettingsState.Mode.Normal( - change.url, - change.isSecured, - change.isTrackingProtectionOn, - change.sitePermissions - ) - ) - } - is QuickSettingsChange.PermissionGranted -> { - state.copy( - mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions) - ) - } - is QuickSettingsChange.PromptRestarted -> { - state.copy( - mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid(change.sitePermissions) - ) - } - is QuickSettingsChange.Stored -> { - state.copy( - mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions) - ) - } - } - } - override fun initView(): UIView { return QuickSettingsUIView(container, actionEmitter, changesObservable, container) } + override fun render(): Observable = + ViewModelProvider(owner, QuickSettingsViewModel.Factory(initialState, changesObservable)).get( + QuickSettingsViewModel::class.java + ).render(uiView) + init { - render(reducer) + render() } fun toggleSitePermission( @@ -137,3 +119,52 @@ sealed class QuickSettingsChange : Change { data class PromptRestarted(val sitePermissions: SitePermissions?) : QuickSettingsChange() data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange() } + +class QuickSettingsViewModel(initialState: QuickSettingsState, changesObservable: Observable) : + UIComponentViewModel( + initialState, + changesObservable, + reducer + ) { + + class Factory( + private val initialState: QuickSettingsState, + private val changesObservable: Observable + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + QuickSettingsViewModel(initialState, changesObservable) as T + } + + companion object { + val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change -> + when (change) { + is QuickSettingsChange.Change -> { + state.copy( + mode = QuickSettingsState.Mode.Normal( + change.url, + change.isSecured, + change.isTrackingProtectionOn, + change.sitePermissions + ) + ) + } + is QuickSettingsChange.PermissionGranted -> { + state.copy( + mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions) + ) + } + is QuickSettingsChange.PromptRestarted -> { + state.copy( + mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid(change.sitePermissions) + ) + } + is QuickSettingsChange.Stored -> { + state.copy( + mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions) + ) + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt index ca8f6eefb..fdcb3b70a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt @@ -73,7 +73,19 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco container: ViewGroup?, savedInstanceState: Bundle? ): View { - return inflateRootView(container) + val rootView = inflateRootView(container) + quickSettingsComponent = QuickSettingsComponent( + rootView as NestedScrollView, this, ActionBusFactory.get(this), + QuickSettingsState( + QuickSettingsState.Mode.Normal( + url, + isSecured, + isTrackingProtectionOn, + sitePermissions + ) + ) + ) + return rootView } private fun inflateRootView(container: ViewGroup? = null): View { @@ -115,21 +127,6 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco return this } - override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { - super.onViewCreated(rootView, savedInstanceState) - quickSettingsComponent = QuickSettingsComponent( - rootView as NestedScrollView, ActionBusFactory.get(this), - QuickSettingsState( - QuickSettingsState.Mode.Normal( - url, - isSecured, - isTrackingProtectionOn, - sitePermissions - ) - ) - ) - } - companion object { const val FRAGMENT_TAG = "QUICK_SETTINGS_FRAGMENT_TAG" diff --git a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkComponentTest.kt b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkComponentTest.kt index 04cc80061..199c10266 100644 --- a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkComponentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkComponentTest.kt @@ -35,7 +35,7 @@ class BookmarkComponentTest { TestBookmarkComponent(mockk(), TestUtils.bus), recordPrivateCalls = true ) - bookmarkObserver = bookmarkComponent.internalRender(bookmarkComponent.reducer).test() + bookmarkObserver = bookmarkComponent.render().test() emitter = TestUtils.owner.getManagedEmitter() } @@ -87,7 +87,7 @@ class BookmarkComponentTest { @Suppress("MemberVisibilityCanBePrivate") class TestBookmarkComponent(container: ViewGroup, bus: ActionBusFactory) : - BookmarkComponent(container, bus) { + BookmarkComponent(container, mockk(relaxed = true), bus) { override val uiView: UIView get() = mockk(relaxed = true) diff --git a/app/src/test/java/org/mozilla/fenix/library/history/HistoryComponentTest.kt b/app/src/test/java/org/mozilla/fenix/library/history/HistoryComponentTest.kt index c69f20d96..c1e1398ca 100644 --- a/app/src/test/java/org/mozilla/fenix/library/history/HistoryComponentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/history/HistoryComponentTest.kt @@ -34,7 +34,7 @@ class HistoryComponentTest { TestHistoryComponent(mockk(), bus), recordPrivateCalls = true ) - historyObserver = historyComponent.internalRender(historyComponent.reducer).test() + historyObserver = historyComponent.render().test() emitter = owner.getManagedEmitter() } @@ -82,7 +82,7 @@ class HistoryComponentTest { @Suppress("MemberVisibilityCanBePrivate") class TestHistoryComponent(container: ViewGroup, bus: ActionBusFactory) : - HistoryComponent(container, bus) { + HistoryComponent(container, mockk(relaxed = true), bus) { override val uiView: UIView get() = mockk(relaxed = true) diff --git a/architecture/build.gradle b/architecture/build.gradle index 9301e3831..9d3de49a7 100644 --- a/architecture/build.gradle +++ b/architecture/build.gradle @@ -32,8 +32,8 @@ dependencies { implementation Deps.kotlin_stdlib implementation Deps.androidx_annotation - implementation Deps.androidx_lifecycle_runtime - + implementation Deps.androidx_lifecycle_extensions + implementation Deps.androidx_lifecycle_viewmodel_ss implementation Deps.mozilla_support_base implementation Deps.rxAndroid diff --git a/architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt b/architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt index 440eb7786..3a0758c5a 100644 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt +++ b/architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt @@ -90,6 +90,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) { * @param clazz is the Event Class * @param event is the instance of the Event to be sent */ + @Suppress("UNCHECKED_CAST") fun emit(clazz: Class, event: T) { val subject = if (map[clazz] != null) map[clazz] else create(clazz) (subject as Subject).onNext(event) @@ -102,6 +103,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) { * * @param clazz is the class of the event type used by this observable */ + @Suppress("UNCHECKED_CAST") fun getSafeManagedObservable(clazz: Class): Observable { return if (map[clazz] != null) map[clazz] as Observable else create(clazz) } @@ -111,6 +113,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) { .`as`(autoDisposable(AndroidLifecycleScopeProvider.from(owner))) } + @Suppress("UNCHECKED_CAST") fun getManagedEmitter(clazz: Class): Observer { return if (map[clazz] != null) map[clazz] as Observer else create(clazz) } @@ -132,7 +135,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) { * Extension on [LifecycleOwner] used to emit an event. */ inline fun LifecycleOwner.emit(event: T) = - kotlin.with(ActionBusFactory.get(this)) { + with(ActionBusFactory.get(this)) { getSafeManagedObservable(T::class.java) emit(T::class.java, event) } @@ -152,10 +155,10 @@ inline fun LifecycleOwner.getManagedEmitter(): Observer /** * This method returns a destroy observable that can be passed to [org.mozilla.fenix.mvi.UIView]s as needed. */ -inline fun LifecycleOwner?.createDestroyObservable(): Observable { +fun LifecycleOwner?.createDestroyObservable(): Observable { return Observable.create { emitter -> if (this == null || this.lifecycle.currentState == Lifecycle.State.DESTROYED) { - emitter.onNext(kotlin.Unit) + emitter.onNext(Unit) emitter.onComplete() return@create } @@ -163,7 +166,7 @@ inline fun LifecycleOwner?.createDestroyObservable(): Observable { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun emitDestroy() { if (emitter.isDisposed) { - emitter.onNext(kotlin.Unit) + emitter.onNext(Unit) emitter.onComplete() } } diff --git a/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt b/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt index 452e2cafa..85eadf47c 100644 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt +++ b/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.mvi +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel import io.reactivex.Observable import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers @@ -11,28 +13,50 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers abstract class UIComponent( + protected val owner: Fragment, protected val actionEmitter: Observer, protected val changesObservable: Observable ) { abstract var initialState: S - abstract val reducer: Reducer open val uiView: UIView by lazy { initView() } abstract fun initView(): UIView open fun getContainerId() = uiView.containerId + abstract fun render(): Observable +} + +open class UIComponentViewModel( + private val initialState: S, + val changesObservable: Observable, + reducer: Reducer +) : ViewModel() { + + private val statesObservable: Observable = internalRender(reducer) + private var statesDisposable: Disposable? = null + /** * Render the ViewState to the View through the Reducer */ - fun render(reducer: Reducer): Disposable = - internalRender(reducer) + fun render(uiView: UIView): Observable { + statesDisposable = statesObservable .subscribe(uiView.updateView()) + return statesObservable + } - fun internalRender(reducer: Reducer): Observable = + @Suppress("MemberVisibilityCanBePrivate") + protected fun internalRender(reducer: Reducer): Observable = changesObservable .scan(initialState, reducer) .distinctUntilChanged() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) + .replay(1) + .autoConnect(0) + + override fun onCleared() { + super.onCleared() + statesDisposable?.dispose() + } } diff --git a/architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt b/architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt index f319ad511..0cb8eea55 100644 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt +++ b/architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt @@ -36,12 +36,16 @@ abstract class UIView( /** * Show the UIView */ - open fun show() { view.visibility = View.VISIBLE } + open fun show() { + view.visibility = View.VISIBLE + } /** * Hide the UIView */ - open fun hide() { view.visibility = View.GONE } + open fun hide() { + view.visibility = View.GONE + } /** * Update the view from the ViewState diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 715254718..390be4c83 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -23,6 +23,7 @@ private object Versions { const val androidx_fragment = "1.1.0-alpha08" const val androidx_navigation = "2.1.0-alpha03" const val androidx_recyclerview = "1.1.0-alpha05" + const val androidx_lifecycle_savedstate = "1.0.0-alpha01" const val androidx_testing = "1.1.0-alpha08" const val androidx_core = "1.2.0-alpha01" const val androidx_transition = "1.1.0-rc01" @@ -134,8 +135,10 @@ object Deps { const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}" const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}" const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}" + const val androidx_lifecycle_extensions = "androidx.lifecycle:lifecycle-extensions:${Versions.androidx_lifecycle}" const val androidx_lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}" const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle}" + const val androidx_lifecycle_viewmodel_ss = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.androidx_lifecycle_savedstate}" const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}" const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}" const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment:${Versions.androidx_navigation}"