diff --git a/app/src/main/java/org/mozilla/fenix/FenixViewModelProvider.kt b/app/src/main/java/org/mozilla/fenix/FenixViewModelProvider.kt new file mode 100644 index 000000000..2ce4cb3a0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/FenixViewModelProvider.kt @@ -0,0 +1,35 @@ +/* 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 + +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import org.mozilla.fenix.mvi.ViewState +import org.mozilla.fenix.mvi.Change +import org.mozilla.fenix.mvi.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider + +object FenixViewModelProvider { + fun > create( + fragment: Fragment, + modelClass: Class, + viewModelCreator: () -> T + ): UIComponentViewModelProvider { + val factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return viewModelCreator() as T + } + } + + return object : UIComponentViewModelProvider { + override fun fetchViewModel(): T { + return ViewModelProviders.of(fragment, factory).get(modelClass) + } + } + } +} 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 21ec8306f..2b9aded78 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -55,6 +55,7 @@ import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded import mozilla.components.support.ktx.kotlin.toUri import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.DefaultThemeManager +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.R @@ -70,6 +71,7 @@ import org.mozilla.fenix.components.toolbar.ToolbarComponent import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.components.toolbar.ToolbarUIView +import org.mozilla.fenix.components.toolbar.ToolbarViewModel import org.mozilla.fenix.customtabs.CustomTabsIntegration import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents @@ -84,6 +86,7 @@ import org.mozilla.fenix.quickactionsheet.QuickActionAction import org.mozilla.fenix.quickactionsheet.QuickActionChange import org.mozilla.fenix.quickactionsheet.QuickActionComponent import org.mozilla.fenix.quickactionsheet.QuickActionState +import org.mozilla.fenix.quickactionsheet.QuickActionViewModel import org.mozilla.fenix.utils.ItsNotBrokenSnack import org.mozilla.fenix.utils.Settings import kotlin.coroutines.CoroutineContext @@ -129,11 +132,17 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope { toolbarComponent = ToolbarComponent( view.browserLayout, - this, ActionBusFactory.get(this), customTabSessionId, (activity as HomeActivity).browsingModeManager.isPrivate, - SearchState("", getSessionById()?.searchTerms ?: "", isEditing = false), - search_engine_icon + search_engine_icon, + FenixViewModelProvider.create( + this, + ToolbarViewModel::class.java + ) { + ToolbarViewModel( + SearchState("", getSessionById()?.searchTerms ?: "", isEditing = false) + ) + } ) toolbarComponent.uiView.view.apply { @@ -154,14 +163,20 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope { QuickActionComponent( view.nestedScrollQuickAction, - this, ActionBusFactory.get(this), - QuickActionState( - readable = getSessionById()?.readerable ?: false, - bookmarked = findBookmarkedURL(getSessionById()), - readerActive = getSessionById()?.readerMode ?: false, - bounceNeeded = false + FenixViewModelProvider.create( + this, + QuickActionViewModel::class.java + ) { + QuickActionViewModel( + QuickActionState( + readable = getSessionById()?.readerable ?: false, + bookmarked = findBookmarkedURL(getSessionById()), + readerActive = getSessionById()?.readerMode ?: false, + bounceNeeded = false + ) ) + } ) val activity = activity as HomeActivity 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 7c590dd2a..988b113b5 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt @@ -5,19 +5,18 @@ 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.ViewState +import org.mozilla.fenix.mvi.Change 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 +import org.mozilla.fenix.mvi.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider sealed class SaveCollectionStep { object SelectTabs : SaveCollectionStep() @@ -56,30 +55,24 @@ sealed class CollectionCreationAction : Action { class CollectionCreationComponent( private val container: ViewGroup, - owner: Fragment, bus: ActionBusFactory, - override var initialState: CollectionCreationState = CollectionCreationState() + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(CollectionCreationAction::class.java), - bus.getSafeManagedObservable(CollectionCreationChange::class.java) + bus.getSafeManagedObservable(CollectionCreationChange::class.java), + viewModelProvider ) { override fun initView() = CollectionCreationUIView(container, actionEmitter, changesObservable) - override fun render(): Observable = - ViewModelProvider(owner, CollectionCreationViewModel.Factory(initialState)).get( - CollectionCreationViewModel::class.java - ).render(changesObservable, uiView) - init { - render() + bind() } } class CollectionCreationViewModel( initialState: CollectionCreationState ) : - UIComponentViewModel( + UIComponentViewModelBase( initialState, reducer ) { 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 1a2de4f3a..9f4763552 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProviders import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_create_collection.view.* +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.ext.getRootView @@ -50,13 +51,13 @@ class CreateCollectionFragment : DialogFragment() { collectionCreationComponent = CollectionCreationComponent( view.create_collection_wrapper, - this, ActionBusFactory.get(this), - CollectionCreationState( - tabs = tabs, - selectedTabs = selectedTabs, - saveCollectionStep = step - ) + FenixViewModelProvider.create( + this, + CollectionCreationViewModel::class.java + ) { + CollectionCreationViewModel(CollectionCreationState(tabs, selectedTabs, step)) + } ) return view } 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 8b2d376f5..63280a2a0 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,37 +7,32 @@ 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 import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.R +import org.mozilla.fenix.mvi.ViewState +import org.mozilla.fenix.mvi.Change 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 +import org.mozilla.fenix.mvi.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider class ToolbarComponent( private val container: ViewGroup, - owner: Fragment, bus: ActionBusFactory, private val sessionId: String?, private val isPrivate: Boolean, - override var initialState: SearchState = SearchState("", "", false), - private val engineIconView: ImageView? = null + private val engineIconView: ImageView? = null, + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(SearchAction::class.java), - bus.getSafeManagedObservable(SearchChange::class.java) + bus.getSafeManagedObservable(SearchChange::class.java), + viewModelProvider ) { fun getView(): BrowserToolbar = uiView.toolbar @@ -51,12 +46,8 @@ class ToolbarComponent( engineIconView ) - override fun render(): Observable = - ViewModelProviders.of(owner, ToolbarViewModel.Factory(initialState, changesObservable)) - .get(ToolbarViewModel::class.java).render(changesObservable, uiView) - init { - render() + bind() applyTheme() } @@ -100,16 +91,7 @@ sealed class SearchChange : Change { } class ToolbarViewModel(initialState: SearchState) : - UIComponentViewModel(initialState, reducer) { - - class Factory( - private val initialState: SearchState, - val changesObservable: Observable - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - ToolbarViewModel(initialState) as T - } + UIComponentViewModelBase(initialState, reducer) { companion object { val reducer: Reducer = { state, change -> 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 52ecd5702..277cbd0e7 100644 --- a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsComponent.kt @@ -4,17 +4,13 @@ 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.ViewState +import org.mozilla.fenix.mvi.Change 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.mvi.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider import org.mozilla.fenix.test.Mockable data class ExceptionsItem(val url: String) @@ -22,24 +18,19 @@ data class ExceptionsItem(val url: String) @Mockable class ExceptionsComponent( private val container: ViewGroup, - owner: Fragment, bus: ActionBusFactory, - override var initialState: ExceptionsState = ExceptionsState(emptyList()) + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(ExceptionsAction::class.java), - bus.getSafeManagedObservable(ExceptionsChange::class.java) + bus.getSafeManagedObservable(ExceptionsChange::class.java), + viewModelProvider ) { - override fun render(): Observable = - ViewModelProviders.of(owner, ExceptionsViewModel.Factory(initialState)) - .get(ExceptionsViewModel::class.java).render(changesObservable, uiView) - override fun initView() = ExceptionsUIView(container, actionEmitter, changesObservable) init { - render() + bind() } } @@ -56,20 +47,9 @@ sealed class ExceptionsChange : Change { data class Change(val list: List) : ExceptionsChange() } -class ExceptionsViewModel(initialState: ExceptionsState) : - UIComponentViewModel( - initialState, - reducer - ) { - - class Factory( - private val initialState: ExceptionsState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - ExceptionsViewModel(initialState) as T - } - +class ExceptionsViewModel( + initialState: ExceptionsState +) : UIComponentViewModelBase(initialState, reducer) { companion object { val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change -> when (change) { 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 55dd750ad..5c8e27842 100644 --- a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsFragment.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.R import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable @@ -48,9 +49,13 @@ 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()) + FenixViewModelProvider.create( + this, + ExceptionsViewModel::class.java + ) { + ExceptionsViewModel(ExceptionsState(loadAndMapExceptions())) + } ) return view } 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 62a8ffccd..a2dddf3c3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -33,6 +33,7 @@ import org.jetbrains.anko.constraint.layout.applyConstraintSet import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.DefaultThemeManager +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.collections.CreateCollectionViewModel @@ -48,6 +49,7 @@ import org.mozilla.fenix.home.sessioncontrol.SessionControlAction import org.mozilla.fenix.home.sessioncontrol.SessionControlChange import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent import org.mozilla.fenix.home.sessioncontrol.SessionControlState +import org.mozilla.fenix.home.sessioncontrol.SessionControlViewModel import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.home.sessioncontrol.TabAction import org.mozilla.fenix.home.sessioncontrol.TabCollection @@ -88,9 +90,13 @@ class HomeFragment : Fragment(), CoroutineScope { sessionControlComponent = SessionControlComponent( view.homeLayout, - this, bus, - SessionControlState(listOf(), listOf(), mode) + FenixViewModelProvider.create( + this, + SessionControlViewModel::class.java + ) { + SessionControlViewModel(SessionControlState(listOf(), listOf(), mode)) + } ) view.homeLayout.applyConstraintSet { @@ -450,6 +456,7 @@ class HomeFragment : Fragment(), CoroutineScope { private fun emitSessionChanges() { val sessionManager = requireComponents.core.sessionManager + getManagedEmitter().onNext( SessionControlChange.TabsChange( sessionManager.sessions 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 71fadacab..bc90274e1 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 @@ -7,48 +7,35 @@ package org.mozilla.fenix.home.sessioncontrol import android.graphics.Bitmap import android.os.Parcelable 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 kotlinx.android.parcel.Parcelize +import org.mozilla.fenix.mvi.ViewState +import org.mozilla.fenix.mvi.Change 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.mvi.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider class SessionControlComponent( private val container: ViewGroup, - owner: Fragment, bus: ActionBusFactory, - override var initialState: SessionControlState = SessionControlState(emptyList(), emptyList(), Mode.Normal) + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(SessionControlAction::class.java), - bus.getSafeManagedObservable(SessionControlChange::class.java) + bus.getSafeManagedObservable(SessionControlChange::class.java), + viewModelProvider ) { - var stateObservable: Observable - lateinit var viewModel: SessionControlViewModel - override fun initView() = SessionControlUIView(container, actionEmitter, changesObservable) + val view: RecyclerView get() = uiView.view as RecyclerView - override fun render(): Observable { - viewModel = ViewModelProviders.of(owner, SessionControlViewModel.Factory(initialState)) - .get(SessionControlViewModel::class.java) - return viewModel.render(changesObservable, uiView) - } - init { - stateObservable = render() + bind() } } @@ -124,19 +111,11 @@ sealed class SessionControlChange : Change { } class SessionControlViewModel(initialState: SessionControlState) : - UIComponentViewModel( + UIComponentViewModelBase( initialState, reducer ) { - class Factory( - private val initialState: SessionControlState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - SessionControlViewModel(initialState) as T - } - companion object { val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change -> when (change) { 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 3a929b71f..4dd0a593e 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,46 +5,34 @@ 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.ViewState +import org.mozilla.fenix.mvi.Change 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.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider import org.mozilla.fenix.mvi.UIView -import org.mozilla.fenix.mvi.ViewState 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) + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(BookmarkAction::class.java), - bus.getSafeManagedObservable(BookmarkChange::class.java) + bus.getSafeManagedObservable(BookmarkChange::class.java), + viewModelProvider ) { override fun initView(): UIView = BookmarkUIView(container, actionEmitter, changesObservable) - override fun render(): Observable { - return ViewModelProvider( - owner, - BookmarkViewModel.Factory(initialState) - ).get(BookmarkViewModel::class.java).render(changesObservable, uiView) - } - init { - render() + bind() } } @@ -82,17 +70,11 @@ operator fun BookmarkNode.contains(item: BookmarkNode): Boolean { } class BookmarkViewModel(initialState: BookmarkState) : - UIComponentViewModel(initialState, reducer) { - - class Factory( - private val initialState: BookmarkState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - BookmarkViewModel(initialState) as T - } + UIComponentViewModelBase(initialState, reducer) { companion object { + fun create() = BookmarkViewModel(BookmarkState(null, BookmarkState.Mode.Normal)) + val reducer: Reducer = { state, change -> when (change) { is BookmarkChange.Change -> { 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 aa7700262..3284267c0 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 @@ -37,6 +37,7 @@ import mozilla.components.concept.sync.Profile import mozilla.components.support.base.feature.BackHandler import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowsingModeManager +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.Components @@ -74,8 +75,25 @@ 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, this, ActionBusFactory.get(this)) - signInComponent = SignInComponent(view.bookmark_layout, this, ActionBusFactory.get(this)) + bookmarkComponent = BookmarkComponent( + view.bookmark_layout, + ActionBusFactory.get(this), + FenixViewModelProvider.create( + this, + BookmarkViewModel::class.java, + BookmarkViewModel.Companion::create + ) + ) + signInComponent = SignInComponent( + view.bookmark_layout, + ActionBusFactory.get(this), + FenixViewModelProvider.create( + this, + SignInViewModel::class.java + ) { + SignInViewModel(SignInState(false)) + } + ) 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 71b9b5aa3..e42ee24d2 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,41 +5,30 @@ 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.ViewState +import org.mozilla.fenix.mvi.Change 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.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider 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) + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(SignInAction::class.java), - bus.getSafeManagedObservable(SignInChange::class.java) + bus.getSafeManagedObservable(SignInChange::class.java), + viewModelProvider ) { override fun initView(): UIView = SignInUIView(container, actionEmitter, changesObservable) - override fun render(): Observable = - ViewModelProvider( - owner, - SignInViewModel.Factory(initialState) - ).get(SignInViewModel::class.java).render(changesObservable, uiView) - init { - render() + bind() } } @@ -54,19 +43,9 @@ sealed class SignInChange : Change { object SignedOut : SignInChange() } -class SignInViewModel(initialState: SignInState) : - UIComponentViewModel( - initialState, reducer - ) { - - class Factory( - private val initialState: SignInState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - SignInViewModel(initialState) as T - } - +class SignInViewModel( + initialState: SignInState +) : UIComponentViewModelBase(initialState, reducer) { companion object { val reducer = object : Reducer { override fun invoke(state: SignInState, change: SignInChange): SignInState { 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 228522d5f..138c7037c 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 @@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProviders import androidx.navigation.Navigation +import kotlinx.android.synthetic.main.fragment_bookmark.view.* import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.* import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.* import kotlinx.coroutines.CoroutineScope @@ -31,6 +32,7 @@ import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.Profile import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.ext.getColorFromAttr @@ -39,6 +41,8 @@ import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel import org.mozilla.fenix.library.bookmarks.SignInAction import org.mozilla.fenix.library.bookmarks.SignInChange import org.mozilla.fenix.library.bookmarks.SignInComponent +import org.mozilla.fenix.library.bookmarks.SignInState +import org.mozilla.fenix.library.bookmarks.SignInViewModel import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getManagedEmitter @@ -68,7 +72,16 @@ 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, this, ActionBusFactory.get(this)) + signInComponent = SignInComponent( + view.bookmark_layout, + ActionBusFactory.get(this), + FenixViewModelProvider.create( + this, + SignInViewModel::class.java + ) { + SignInViewModel(SignInState(false)) + } + ) 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 18680f8e3..f6de63d3d 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,16 +4,13 @@ package org.mozilla.fenix.library.history 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.ViewState +import org.mozilla.fenix.mvi.Change 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.mvi.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider import org.mozilla.fenix.test.Mockable data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long) @@ -21,26 +18,19 @@ data class HistoryItem(val id: Int, val title: String, val url: String, val visi @Mockable class HistoryComponent( private val container: ViewGroup, - owner: Fragment, bus: ActionBusFactory, - override var initialState: HistoryState = HistoryState(emptyList(), HistoryState.Mode.Normal) + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(HistoryAction::class.java), - bus.getSafeManagedObservable(HistoryChange::class.java) + bus.getSafeManagedObservable(HistoryChange::class.java), + viewModelProvider ) { override fun initView() = HistoryUIView(container, actionEmitter, changesObservable) - override fun render(): Observable = - ViewModelProvider( - owner, - HistoryViewModel.Factory(initialState) - ).get(HistoryViewModel::class.java).render(changesObservable, uiView) - init { - render() + bind() } } @@ -74,18 +64,11 @@ sealed class HistoryChange : Change { data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange() } -class HistoryViewModel(initialState: HistoryState) : - UIComponentViewModel(initialState, reducer) { - - class Factory( - private val initialState: HistoryState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - HistoryViewModel(initialState) as T - } - +class HistoryViewModel( + initialState: HistoryState +) : UIComponentViewModelBase(initialState, reducer) { companion object { + fun create() = HistoryViewModel(HistoryState(emptyList(), HistoryState.Mode.Normal)) val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change -> when (change) { is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list) 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 4d9fb1a02..6c97d3ee9 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 @@ -29,6 +29,7 @@ import mozilla.components.concept.storage.VisitType import mozilla.components.support.base.feature.BackHandler import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowsingModeManager +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.Components @@ -59,7 +60,15 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler { savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_history, container, false) - historyComponent = HistoryComponent(view.history_layout, this, ActionBusFactory.get(this)) + historyComponent = HistoryComponent( + view.history_layout, + ActionBusFactory.get(this), + FenixViewModelProvider.create( + this, + HistoryViewModel::class.java, + HistoryViewModel.Companion::create + ) + ) 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 3ec185fb3..3e5d8064a 100644 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionComponent.kt @@ -5,45 +5,30 @@ 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.UIComponentViewModelProvider +import org.mozilla.fenix.mvi.ViewState 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.UIComponentViewModelBase 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, - bookmarked = false, - readerActive = false, - bounceNeeded = false - ) + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(QuickActionAction::class.java), - bus.getSafeManagedObservable(QuickActionChange::class.java) + bus.getSafeManagedObservable(QuickActionChange::class.java), + viewModelProvider ) { override fun initView(): UIView = QuickActionUIView(container, actionEmitter, changesObservable) - override fun render(): Observable = - ViewModelProvider( - owner, - QuickActionViewModel.Factory(initialState) - ).get(QuickActionViewModel::class.java).render(changesObservable, uiView) - init { - render() + bind() } } @@ -71,20 +56,9 @@ sealed class QuickActionChange : Change { object BounceNeededChange : QuickActionChange() } -class QuickActionViewModel(initialState: QuickActionState) : - UIComponentViewModel( - initialState, - reducer - ) { - - class Factory( - private val initialState: QuickActionState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - QuickActionViewModel(initialState) as T - } - +class QuickActionViewModel( + initialState: QuickActionState +) : UIComponentViewModelBase(initialState, reducer) { companion object { val reducer: Reducer = { state, change -> when (change) { 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 dbce0088e..9f1899f35 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt @@ -26,6 +26,7 @@ import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.content.isPermissionGranted import mozilla.components.support.ktx.kotlin.isUrl import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event @@ -34,6 +35,7 @@ import org.mozilla.fenix.components.toolbar.SearchChange import org.mozilla.fenix.components.toolbar.SearchState import org.mozilla.fenix.components.toolbar.ToolbarComponent import org.mozilla.fenix.components.toolbar.ToolbarUIView +import org.mozilla.fenix.components.toolbar.ToolbarViewModel import org.mozilla.fenix.ext.getSpannable import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.mvi.ActionBusFactory @@ -42,7 +44,9 @@ import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.search.awesomebar.AwesomeBarAction import org.mozilla.fenix.search.awesomebar.AwesomeBarChange import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent +import org.mozilla.fenix.search.awesomebar.AwesomeBarState import org.mozilla.fenix.search.awesomebar.AwesomeBarUIView +import org.mozilla.fenix.search.awesomebar.AwesomeBarViewModel @Suppress("TooManyFunctions") class SearchFragment : Fragment(), BackHandler { @@ -67,15 +71,28 @@ class SearchFragment : Fragment(), BackHandler { toolbarComponent = ToolbarComponent( view.toolbar_component_wrapper, - this, ActionBusFactory.get(this), sessionId, isPrivate, - SearchState(url, session?.searchTerms ?: "", isEditing = true), - view.search_engine_icon + view.search_engine_icon, + FenixViewModelProvider.create( + this, + ToolbarViewModel::class.java + ) { + ToolbarViewModel(SearchState(url, session?.searchTerms ?: "", isEditing = true)) + } ) - awesomeBarComponent = AwesomeBarComponent(view.search_layout, this, ActionBusFactory.get(this)) + awesomeBarComponent = AwesomeBarComponent( + view.search_layout, + ActionBusFactory.get(this), + FenixViewModelProvider.create( + this, + AwesomeBarViewModel::class.java + ) { + AwesomeBarViewModel(AwesomeBarState("", false)) + } + ) 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 4c8d2bbcb..ddf4ba99d 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 @@ -5,20 +5,15 @@ package org.mozilla.fenix.search.awesomebar 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 androidx.lifecycle.ViewModelProviders -import io.reactivex.Observable import mozilla.components.browser.search.SearchEngine -import org.mozilla.fenix.ext.logDebug +import org.mozilla.fenix.mvi.ViewState +import org.mozilla.fenix.mvi.Change 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 +import org.mozilla.fenix.mvi.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider data class AwesomeBarState( val query: String, @@ -40,42 +35,25 @@ sealed class AwesomeBarChange : Change { class AwesomeBarComponent( private val container: ViewGroup, - owner: Fragment, bus: ActionBusFactory, - override var initialState: AwesomeBarState = AwesomeBarState("", false) + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(AwesomeBarAction::class.java), - bus.getSafeManagedObservable(AwesomeBarChange::class.java) + bus.getSafeManagedObservable(AwesomeBarChange::class.java), + viewModelProvider ) { override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable) - override fun render(): Observable = - ViewModelProviders.of(owner, AwesomeBarViewModel.Factory(initialState)) - .get(AwesomeBarViewModel::class.java).render(changesObservable, uiView) - init { - render() + bind() } } -class AwesomeBarViewModel(initialState: AwesomeBarState) : - UIComponentViewModel( - initialState, - reducer - ) { - - class Factory( - private val initialState: AwesomeBarState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - AwesomeBarViewModel(initialState) as T - } - +class AwesomeBarViewModel( + initialState: AwesomeBarState +) : UIComponentViewModelBase(initialState, reducer) { companion object { val reducer: Reducer = { state, change -> - logDebug("IN_REDUCER", change.toString()) when (change) { is AwesomeBarChange.SearchShortcutEngineSelected -> state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false) 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 c16a7c517..27aef5308 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,20 +6,17 @@ 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 +import org.mozilla.fenix.mvi.ViewState +import org.mozilla.fenix.mvi.Change 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.UIComponentViewModelBase +import org.mozilla.fenix.mvi.UIComponentViewModelProvider import org.mozilla.fenix.mvi.UIView -import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.toStatus import org.mozilla.fenix.settings.toggle @@ -27,25 +24,19 @@ import org.mozilla.fenix.utils.Settings class QuickSettingsComponent( private val container: ViewGroup, - owner: Fragment, bus: ActionBusFactory, - override var initialState: QuickSettingsState + viewModelProvider: UIComponentViewModelProvider ) : UIComponent( - owner, bus.getManagedEmitter(QuickSettingsAction::class.java), - bus.getSafeManagedObservable(QuickSettingsChange::class.java) + bus.getSafeManagedObservable(QuickSettingsChange::class.java), + viewModelProvider ) { override fun initView(): UIView { return QuickSettingsUIView(container, actionEmitter, changesObservable, container) } - override fun render(): Observable = - ViewModelProvider(owner, QuickSettingsViewModel.Factory(initialState)).get( - QuickSettingsViewModel::class.java - ).render(changesObservable, uiView) - init { - render() + bind() } fun toggleSitePermission( @@ -121,20 +112,9 @@ sealed class QuickSettingsChange : Change { data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange() } -class QuickSettingsViewModel(initialState: QuickSettingsState) : - UIComponentViewModel( - initialState, - reducer - ) { - - class Factory( - private val initialState: QuickSettingsState - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = - QuickSettingsViewModel(initialState) as T - } - +class QuickSettingsViewModel( + initialState: QuickSettingsState +) : UIComponentViewModelBase(initialState, reducer) { companion object { val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change -> when (change) { 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 7ef65584a..cba214dd0 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 @@ -26,6 +26,7 @@ import kotlinx.coroutines.launch import mozilla.components.browser.session.Session import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.support.ktx.kotlin.toUri +import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserFragment @@ -74,15 +75,23 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco val rootView = inflateRootView(container) requireComponents.core.sessionManager.findSessionById(sessionId)?.register(sessionObserver, view = rootView) quickSettingsComponent = QuickSettingsComponent( - rootView as NestedScrollView, this, ActionBusFactory.get(this), - QuickSettingsState( - QuickSettingsState.Mode.Normal( - url, - isSecured, - isTrackingProtectionOn, - sitePermissions + rootView as NestedScrollView, + ActionBusFactory.get(this), + FenixViewModelProvider.create( + this, + QuickSettingsViewModel::class.java + ) { + QuickSettingsViewModel( + QuickSettingsState( + QuickSettingsState.Mode.Normal( + url, + isSecured, + isTrackingProtectionOn, + sitePermissions + ) + ) ) - ) + } ) return rootView } diff --git a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkComponentTest.kt b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkViewModelTest.kt similarity index 80% rename from app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkComponentTest.kt rename to app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkViewModelTest.kt index 199c10266..a9bb22356 100644 --- a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkComponentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkViewModelTest.kt @@ -4,10 +4,7 @@ package org.mozilla.fenix.library.bookmarks -import android.view.ViewGroup import io.mockk.MockKAnnotations -import io.mockk.mockk -import io.mockk.spyk import io.reactivex.Observer import io.reactivex.observers.TestObserver import mozilla.appservices.places.BookmarkRoot @@ -16,13 +13,12 @@ import mozilla.components.concept.storage.BookmarkNodeType import org.junit.Before import org.junit.Test import org.mozilla.fenix.TestUtils -import org.mozilla.fenix.mvi.ActionBusFactory -import org.mozilla.fenix.mvi.UIView +import org.mozilla.fenix.TestUtils.bus import org.mozilla.fenix.mvi.getManagedEmitter -class BookmarkComponentTest { +class BookmarkViewModelTest { - private lateinit var bookmarkComponent: TestBookmarkComponent + private lateinit var bookmarkViewModel: BookmarkViewModel private lateinit var bookmarkObserver: TestObserver private lateinit var emitter: Observer @@ -31,11 +27,10 @@ class BookmarkComponentTest { MockKAnnotations.init(this) TestUtils.setRxSchedulers() - bookmarkComponent = spyk( - TestBookmarkComponent(mockk(), TestUtils.bus), - recordPrivateCalls = true - ) - bookmarkObserver = bookmarkComponent.render().test() + bookmarkViewModel = BookmarkViewModel.create() + bookmarkObserver = bookmarkViewModel.state.test() + bus.getSafeManagedObservable(BookmarkChange::class.java) + .subscribe(bookmarkViewModel.changes::onNext) emitter = TestUtils.owner.getManagedEmitter() } @@ -84,12 +79,4 @@ class BookmarkComponentTest { BookmarkState(tree - itemToSelect.guid, BookmarkState.Mode.Normal) ) } - - @Suppress("MemberVisibilityCanBePrivate") - class TestBookmarkComponent(container: ViewGroup, bus: ActionBusFactory) : - 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/HistoryViewModelTest.kt similarity index 84% rename from app/src/test/java/org/mozilla/fenix/library/history/HistoryComponentTest.kt rename to app/src/test/java/org/mozilla/fenix/library/history/HistoryViewModelTest.kt index f82647583..30eeb521c 100644 --- a/app/src/test/java/org/mozilla/fenix/library/history/HistoryComponentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/history/HistoryViewModelTest.kt @@ -4,10 +4,7 @@ package org.mozilla.fenix.library.history -import android.view.ViewGroup import io.mockk.MockKAnnotations -import io.mockk.mockk -import io.mockk.spyk import io.reactivex.Observer import io.reactivex.observers.TestObserver import org.junit.Before @@ -15,13 +12,11 @@ import org.junit.Test import org.mozilla.fenix.TestUtils.bus import org.mozilla.fenix.TestUtils.owner import org.mozilla.fenix.TestUtils.setRxSchedulers -import org.mozilla.fenix.mvi.ActionBusFactory -import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.getManagedEmitter -class HistoryComponentTest { +class HistoryViewModelTest { - private lateinit var historyComponent: TestHistoryComponent + private lateinit var historyViewModel: HistoryViewModel private lateinit var historyObserver: TestObserver private lateinit var emitter: Observer @@ -30,11 +25,11 @@ class HistoryComponentTest { MockKAnnotations.init(this) setRxSchedulers() - historyComponent = spyk( - TestHistoryComponent(mockk(), bus), - recordPrivateCalls = true - ) - historyObserver = historyComponent.render().test() + historyViewModel = HistoryViewModel.create() + historyObserver = historyViewModel.state.test() + bus.getSafeManagedObservable(HistoryChange::class.java) + .subscribe(historyViewModel.changes::onNext) + emitter = owner.getManagedEmitter() } @@ -97,12 +92,4 @@ class HistoryComponentTest { HistoryState(historyItems, HistoryState.Mode.Normal) ) } - - @Suppress("MemberVisibilityCanBePrivate") - class TestHistoryComponent(container: ViewGroup, bus: ActionBusFactory) : - HistoryComponent(container, mockk(relaxed = true), bus) { - - override val uiView: UIView - get() = mockk(relaxed = true) - } } \ No newline at end of file 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 60506b51c..56fdac31f 100644 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt +++ b/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt @@ -4,60 +4,65 @@ 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 -import io.reactivex.disposables.Disposable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.withLatestFrom import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject + +interface UIComponentViewModel { + val changes: Observer + val state: Observable +} + +interface UIComponentViewModelProvider { + fun fetchViewModel(): UIComponentViewModel +} abstract class UIComponent( - protected val owner: Fragment, protected val actionEmitter: Observer, - protected val changesObservable: Observable + protected val changesObservable: Observable, + private val viewModelProvider: UIComponentViewModelProvider ) { - - abstract var initialState: S - open val uiView: UIView by lazy { initView() } abstract fun initView(): UIView open fun getContainerId() = uiView.containerId - abstract fun render(): Observable + + fun bind(): CompositeDisposable { + val compositeDisposable = CompositeDisposable() + val viewModel = viewModelProvider.fetchViewModel() + + compositeDisposable.add(changesObservable.subscribe(viewModel.changes::onNext)) + compositeDisposable.add(viewModel.state.subscribe(uiView.updateView())) + + return compositeDisposable + } } -open class UIComponentViewModel( +abstract class UIComponentViewModelBase( initialState: S, - private val reducer: Reducer -) : ViewModel() { + reducer: Reducer +) : ViewModel(), UIComponentViewModel { - private var currentState: S = initialState - private var statesDisposable: Disposable? = null + final override val changes: Observer + private var _state: BehaviorSubject = BehaviorSubject.createDefault(initialState) + override val state: Observable + get() = _state - /** - * Render the ViewState to the View through the Reducer - */ - fun render(changesObservable: Observable, uiView: UIView): Observable { - val statesObservable = internalRender(changesObservable, reducer) - statesDisposable = statesObservable - .subscribe(uiView.updateView()) - return statesObservable - } + init { + changes = PublishSubject.create() - @Suppress("MemberVisibilityCanBePrivate") - protected fun internalRender(changesObservable: Observable, reducer: Reducer): Observable = - changesObservable - .scan(currentState, reducer) + changes + .withLatestFrom(_state) + .map { reducer(it.second, it.first) } .distinctUntilChanged() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .replay(1) - .autoConnect(0) - .doOnNext { currentState = it } - - override fun onCleared() { - super.onCleared() - statesDisposable?.dispose() + .subscribe(_state) } }