1
0
Fork 0

Teases apart ViewModel dependencies (#2499)

* No Issue - pulls render outside of the viewmodel

* No Issue - Properly subscribes to the changesObservable

* No Issue - Fixes ViewModel tests
master
Jeff Boek 2019-05-14 23:16:48 -07:00 committed by GitHub
parent 0143c54817
commit c5e5ef4b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 322 additions and 404 deletions

View File

@ -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 <S : ViewState, C : Change, T : UIComponentViewModelBase<S, C>> create(
fragment: Fragment,
modelClass: Class<T>,
viewModelCreator: () -> T
): UIComponentViewModelProvider<S, C> {
val factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return viewModelCreator() as T
}
}
return object : UIComponentViewModelProvider<S, C> {
override fun fetchViewModel(): T {
return ViewModelProviders.of(fragment, factory).get(modelClass)
}
}
}
}

View File

@ -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

View File

@ -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<CollectionCreationState, CollectionCreationChange>
) : UIComponent<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
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<CollectionCreationState> =
ViewModelProvider(owner, CollectionCreationViewModel.Factory(initialState)).get(
CollectionCreationViewModel::class.java
).render(changesObservable, uiView)
init {
render()
bind()
}
}
class CollectionCreationViewModel(
initialState: CollectionCreationState
) :
UIComponentViewModel<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
UIComponentViewModelBase<CollectionCreationState, CollectionCreationChange>(
initialState,
reducer
) {

View File

@ -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
}

View File

@ -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<SearchState, SearchChange>
) :
UIComponent<SearchState, SearchAction, SearchChange>(
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<SearchState> =
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<SearchState, SearchAction, SearchChange>(initialState, reducer) {
class Factory(
private val initialState: SearchState,
val changesObservable: Observable<SearchChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
ToolbarViewModel(initialState) as T
}
UIComponentViewModelBase<SearchState, SearchChange>(initialState, reducer) {
companion object {
val reducer: Reducer<SearchState, SearchChange> = { state, change ->

View File

@ -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<ExceptionsState, ExceptionsChange>
) :
UIComponent<ExceptionsState, ExceptionsAction, ExceptionsChange>(
owner,
bus.getManagedEmitter(ExceptionsAction::class.java),
bus.getSafeManagedObservable(ExceptionsChange::class.java)
bus.getSafeManagedObservable(ExceptionsChange::class.java),
viewModelProvider
) {
override fun render(): Observable<ExceptionsState> =
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<ExceptionsItem>) : ExceptionsChange()
}
class ExceptionsViewModel(initialState: ExceptionsState) :
UIComponentViewModel<ExceptionsState, ExceptionsAction, ExceptionsChange>(
initialState,
reducer
) {
class Factory(
private val initialState: ExceptionsState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
ExceptionsViewModel(initialState) as T
}
class ExceptionsViewModel(
initialState: ExceptionsState
) : UIComponentViewModelBase<ExceptionsState, ExceptionsChange>(initialState, reducer) {
companion object {
val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change ->
when (change) {

View File

@ -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
}

View File

@ -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<SessionControlChange>().onNext(
SessionControlChange.TabsChange(
sessionManager.sessions

View File

@ -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<SessionControlState, SessionControlChange>
) :
UIComponent<SessionControlState, SessionControlAction, SessionControlChange>(
owner,
bus.getManagedEmitter(SessionControlAction::class.java),
bus.getSafeManagedObservable(SessionControlChange::class.java)
bus.getSafeManagedObservable(SessionControlChange::class.java),
viewModelProvider
) {
var stateObservable: Observable<SessionControlState>
lateinit var viewModel: SessionControlViewModel
override fun initView() = SessionControlUIView(container, actionEmitter, changesObservable)
val view: RecyclerView
get() = uiView.view as RecyclerView
override fun render(): Observable<SessionControlState> {
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<SessionControlState, SessionControlAction, SessionControlChange>(
UIComponentViewModelBase<SessionControlState, SessionControlChange>(
initialState,
reducer
) {
class Factory(
private val initialState: SessionControlState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SessionControlViewModel(initialState) as T
}
companion object {
val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) {

View File

@ -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<BookmarkState, BookmarkChange>
) :
UIComponent<BookmarkState, BookmarkAction, BookmarkChange>(
owner,
bus.getManagedEmitter(BookmarkAction::class.java),
bus.getSafeManagedObservable(BookmarkChange::class.java)
bus.getSafeManagedObservable(BookmarkChange::class.java),
viewModelProvider
) {
override fun initView(): UIView<BookmarkState, BookmarkAction, BookmarkChange> =
BookmarkUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<BookmarkState> {
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<BookmarkState, BookmarkAction, BookmarkChange>(initialState, reducer) {
class Factory(
private val initialState: BookmarkState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
BookmarkViewModel(initialState) as T
}
UIComponentViewModelBase<BookmarkState, BookmarkChange>(initialState, reducer) {
companion object {
fun create() = BookmarkViewModel(BookmarkState(null, BookmarkState.Mode.Normal))
val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) {
is BookmarkChange.Change -> {

View File

@ -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
}

View File

@ -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<SignInState, SignInChange>
) : UIComponent<SignInState, SignInAction, SignInChange>(
owner,
bus.getManagedEmitter(SignInAction::class.java),
bus.getSafeManagedObservable(SignInChange::class.java)
bus.getSafeManagedObservable(SignInChange::class.java),
viewModelProvider
) {
override fun initView(): UIView<SignInState, SignInAction, SignInChange> =
SignInUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<SignInState> =
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<SignInState, SignInAction, SignInChange>(
initialState, reducer
) {
class Factory(
private val initialState: SignInState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SignInViewModel(initialState) as T
}
class SignInViewModel(
initialState: SignInState
) : UIComponentViewModelBase<SignInState, SignInChange>(initialState, reducer) {
companion object {
val reducer = object : Reducer<SignInState, SignInChange> {
override fun invoke(state: SignInState, change: SignInChange): SignInState {

View File

@ -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
}

View File

@ -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<HistoryState, HistoryChange>
) :
UIComponent<HistoryState, HistoryAction, HistoryChange>(
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<HistoryState> =
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<HistoryState, HistoryAction, HistoryChange>(initialState, reducer) {
class Factory(
private val initialState: HistoryState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
HistoryViewModel(initialState) as T
}
class HistoryViewModel(
initialState: HistoryState
) : UIComponentViewModelBase<HistoryState, HistoryChange>(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)

View File

@ -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
}

View File

@ -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<QuickActionState, QuickActionChange>
) : UIComponent<QuickActionState, QuickActionAction, QuickActionChange>(
owner,
bus.getManagedEmitter(QuickActionAction::class.java),
bus.getSafeManagedObservable(QuickActionChange::class.java)
bus.getSafeManagedObservable(QuickActionChange::class.java),
viewModelProvider
) {
override fun initView(): UIView<QuickActionState, QuickActionAction, QuickActionChange> =
QuickActionUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<QuickActionState> =
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<QuickActionState, QuickActionAction, QuickActionChange>(
initialState,
reducer
) {
class Factory(
private val initialState: QuickActionState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
QuickActionViewModel(initialState) as T
}
class QuickActionViewModel(
initialState: QuickActionState
) : UIComponentViewModelBase<QuickActionState, QuickActionChange>(initialState, reducer) {
companion object {
val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (change) {

View File

@ -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
}

View File

@ -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<AwesomeBarState, AwesomeBarChange>
) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
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<AwesomeBarState> =
ViewModelProviders.of(owner, AwesomeBarViewModel.Factory(initialState))
.get(AwesomeBarViewModel::class.java).render(changesObservable, uiView)
init {
render()
bind()
}
}
class AwesomeBarViewModel(initialState: AwesomeBarState) :
UIComponentViewModel<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
initialState,
reducer
) {
class Factory(
private val initialState: AwesomeBarState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
AwesomeBarViewModel(initialState) as T
}
class AwesomeBarViewModel(
initialState: AwesomeBarState
) : UIComponentViewModelBase<AwesomeBarState, AwesomeBarChange>(initialState, reducer) {
companion object {
val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
logDebug("IN_REDUCER", change.toString())
when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)

View File

@ -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<QuickSettingsState, QuickSettingsChange>
) : UIComponent<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>(
owner,
bus.getManagedEmitter(QuickSettingsAction::class.java),
bus.getSafeManagedObservable(QuickSettingsChange::class.java)
bus.getSafeManagedObservable(QuickSettingsChange::class.java),
viewModelProvider
) {
override fun initView(): UIView<QuickSettingsState, QuickSettingsAction, QuickSettingsChange> {
return QuickSettingsUIView(container, actionEmitter, changesObservable, container)
}
override fun render(): Observable<QuickSettingsState> =
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<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>(
initialState,
reducer
) {
class Factory(
private val initialState: QuickSettingsState
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
QuickSettingsViewModel(initialState) as T
}
class QuickSettingsViewModel(
initialState: QuickSettingsState
) : UIComponentViewModelBase<QuickSettingsState, QuickSettingsChange>(initialState, reducer) {
companion object {
val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change ->
when (change) {

View File

@ -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
}

View File

@ -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<BookmarkState>
private lateinit var emitter: Observer<BookmarkChange>
@ -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<BookmarkState, BookmarkAction, BookmarkChange>
get() = mockk(relaxed = true)
}
}

View File

@ -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<HistoryState>
private lateinit var emitter: Observer<HistoryChange>
@ -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<HistoryState, HistoryAction, HistoryChange>
get() = mockk(relaxed = true)
}
}

View File

@ -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<S : ViewState, C : Change> {
val changes: Observer<C>
val state: Observable<S>
}
interface UIComponentViewModelProvider<S : ViewState, C : Change> {
fun fetchViewModel(): UIComponentViewModel<S, C>
}
abstract class UIComponent<S : ViewState, A : Action, C : Change>(
protected val owner: Fragment,
protected val actionEmitter: Observer<A>,
protected val changesObservable: Observable<C>
protected val changesObservable: Observable<C>,
private val viewModelProvider: UIComponentViewModelProvider<S, C>
) {
abstract var initialState: S
open val uiView: UIView<S, A, C> by lazy { initView() }
abstract fun initView(): UIView<S, A, C>
open fun getContainerId() = uiView.containerId
abstract fun render(): Observable<S>
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<S : ViewState, A : Action, C : Change>(
abstract class UIComponentViewModelBase<S : ViewState, C : Change>(
initialState: S,
private val reducer: Reducer<S, C>
) : ViewModel() {
reducer: Reducer<S, C>
) : ViewModel(), UIComponentViewModel<S, C> {
private var currentState: S = initialState
private var statesDisposable: Disposable? = null
final override val changes: Observer<C>
private var _state: BehaviorSubject<S> = BehaviorSubject.createDefault(initialState)
override val state: Observable<S>
get() = _state
/**
* Render the ViewState to the View through the Reducer
*/
fun render(changesObservable: Observable<C>, uiView: UIView<S, A, C>): Observable<S> {
val statesObservable = internalRender(changesObservable, reducer)
statesDisposable = statesObservable
.subscribe(uiView.updateView())
return statesObservable
}
init {
changes = PublishSubject.create<C>()
@Suppress("MemberVisibilityCanBePrivate")
protected fun internalRender(changesObservable: Observable<C>, reducer: Reducer<S, C>): Observable<S> =
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)
}
}