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 mozilla.components.support.ktx.kotlin.toUri
import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R 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.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.components.toolbar.ToolbarUIView import org.mozilla.fenix.components.toolbar.ToolbarUIView
import org.mozilla.fenix.components.toolbar.ToolbarViewModel
import org.mozilla.fenix.customtabs.CustomTabsIntegration import org.mozilla.fenix.customtabs.CustomTabsIntegration
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents 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.QuickActionChange
import org.mozilla.fenix.quickactionsheet.QuickActionComponent import org.mozilla.fenix.quickactionsheet.QuickActionComponent
import org.mozilla.fenix.quickactionsheet.QuickActionState import org.mozilla.fenix.quickactionsheet.QuickActionState
import org.mozilla.fenix.quickactionsheet.QuickActionViewModel
import org.mozilla.fenix.utils.ItsNotBrokenSnack import org.mozilla.fenix.utils.ItsNotBrokenSnack
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -129,11 +132,17 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope {
toolbarComponent = ToolbarComponent( toolbarComponent = ToolbarComponent(
view.browserLayout, view.browserLayout,
this,
ActionBusFactory.get(this), customTabSessionId, ActionBusFactory.get(this), customTabSessionId,
(activity as HomeActivity).browsingModeManager.isPrivate, (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 { toolbarComponent.uiView.view.apply {
@ -154,14 +163,20 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope {
QuickActionComponent( QuickActionComponent(
view.nestedScrollQuickAction, view.nestedScrollQuickAction,
this,
ActionBusFactory.get(this), ActionBusFactory.get(this),
QuickActionState( FenixViewModelProvider.create(
readable = getSessionById()?.readerable ?: false, this,
bookmarked = findBookmarkedURL(getSessionById()), QuickActionViewModel::class.java
readerActive = getSessionById()?.readerMode ?: false, ) {
bounceNeeded = false QuickActionViewModel(
QuickActionState(
readable = getSessionById()?.readerable ?: false,
bookmarked = findBookmarkedURL(getSessionById()),
readerActive = getSessionById()?.readerMode ?: false,
bounceNeeded = false
)
) )
}
) )
val activity = activity as HomeActivity 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/. */ file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.home.sessioncontrol.TabCollection 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.UIComponentViewModelProvider
sealed class SaveCollectionStep { sealed class SaveCollectionStep {
object SelectTabs : SaveCollectionStep() object SelectTabs : SaveCollectionStep()
@ -56,30 +55,24 @@ sealed class CollectionCreationAction : Action {
class CollectionCreationComponent( class CollectionCreationComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: CollectionCreationState = CollectionCreationState() viewModelProvider: UIComponentViewModelProvider<CollectionCreationState, CollectionCreationChange>
) : UIComponent<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>( ) : UIComponent<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
owner,
bus.getManagedEmitter(CollectionCreationAction::class.java), 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 initView() = CollectionCreationUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<CollectionCreationState> =
ViewModelProvider(owner, CollectionCreationViewModel.Factory(initialState)).get(
CollectionCreationViewModel::class.java
).render(changesObservable, uiView)
init { init {
render() bind()
} }
} }
class CollectionCreationViewModel( class CollectionCreationViewModel(
initialState: CollectionCreationState initialState: CollectionCreationState
) : ) :
UIComponentViewModel<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>( UIComponentViewModelBase<CollectionCreationState, CollectionCreationChange>(
initialState, initialState,
reducer reducer
) { ) {

View File

@ -14,6 +14,7 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_create_collection.view.* import kotlinx.android.synthetic.main.fragment_create_collection.view.*
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
@ -50,13 +51,13 @@ class CreateCollectionFragment : DialogFragment() {
collectionCreationComponent = CollectionCreationComponent( collectionCreationComponent = CollectionCreationComponent(
view.create_collection_wrapper, view.create_collection_wrapper,
this,
ActionBusFactory.get(this), ActionBusFactory.get(this),
CollectionCreationState( FenixViewModelProvider.create(
tabs = tabs, this,
selectedTabs = selectedTabs, CollectionCreationViewModel::class.java
saveCollectionStep = step ) {
) CollectionCreationViewModel(CollectionCreationState(tabs, selectedTabs, step))
}
) )
return view return view
} }

View File

@ -7,37 +7,32 @@ package org.mozilla.fenix.components.toolbar
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.ContextCompat 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 kotlinx.android.synthetic.main.component_search.*
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.UIComponentViewModelProvider
class ToolbarComponent( class ToolbarComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
private val sessionId: String?, private val sessionId: String?,
private val isPrivate: Boolean, 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>( UIComponent<SearchState, SearchAction, SearchChange>(
owner,
bus.getManagedEmitter(SearchAction::class.java), bus.getManagedEmitter(SearchAction::class.java),
bus.getSafeManagedObservable(SearchChange::class.java) bus.getSafeManagedObservable(SearchChange::class.java),
viewModelProvider
) { ) {
fun getView(): BrowserToolbar = uiView.toolbar fun getView(): BrowserToolbar = uiView.toolbar
@ -51,12 +46,8 @@ class ToolbarComponent(
engineIconView engineIconView
) )
override fun render(): Observable<SearchState> =
ViewModelProviders.of(owner, ToolbarViewModel.Factory(initialState, changesObservable))
.get(ToolbarViewModel::class.java).render(changesObservable, uiView)
init { init {
render() bind()
applyTheme() applyTheme()
} }
@ -100,16 +91,7 @@ sealed class SearchChange : Change {
} }
class ToolbarViewModel(initialState: SearchState) : class ToolbarViewModel(initialState: SearchState) :
UIComponentViewModel<SearchState, SearchAction, SearchChange>(initialState, reducer) { UIComponentViewModelBase<SearchState, 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
}
companion object { companion object {
val reducer: Reducer<SearchState, SearchChange> = { state, change -> val reducer: Reducer<SearchState, SearchChange> = { state, change ->

View File

@ -4,17 +4,13 @@
package org.mozilla.fenix.exceptions package org.mozilla.fenix.exceptions
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import org.mozilla.fenix.mvi.ViewState
import androidx.lifecycle.ViewModel import org.mozilla.fenix.mvi.Change
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.UIComponentViewModelProvider
import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.test.Mockable
data class ExceptionsItem(val url: String) data class ExceptionsItem(val url: String)
@ -22,24 +18,19 @@ data class ExceptionsItem(val url: String)
@Mockable @Mockable
class ExceptionsComponent( class ExceptionsComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: ExceptionsState = ExceptionsState(emptyList()) viewModelProvider: UIComponentViewModelProvider<ExceptionsState, ExceptionsChange>
) : ) :
UIComponent<ExceptionsState, ExceptionsAction, ExceptionsChange>( UIComponent<ExceptionsState, ExceptionsAction, ExceptionsChange>(
owner,
bus.getManagedEmitter(ExceptionsAction::class.java), 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) override fun initView() = ExceptionsUIView(container, actionEmitter, changesObservable)
init { init {
render() bind()
} }
} }
@ -56,20 +47,9 @@ sealed class ExceptionsChange : Change {
data class Change(val list: List<ExceptionsItem>) : ExceptionsChange() data class Change(val list: List<ExceptionsItem>) : ExceptionsChange()
} }
class ExceptionsViewModel(initialState: ExceptionsState) : class ExceptionsViewModel(
UIComponentViewModel<ExceptionsState, ExceptionsAction, ExceptionsChange>( initialState: ExceptionsState
initialState, ) : UIComponentViewModelBase<ExceptionsState, ExceptionsChange>(initialState, reducer) {
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
}
companion object { companion object {
val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change -> val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change ->
when (change) { when (change) {

View File

@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
@ -48,9 +49,13 @@ class ExceptionsFragment : Fragment(), CoroutineScope {
val view = inflater.inflate(R.layout.fragment_exceptions, container, false) val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
exceptionsComponent = ExceptionsComponent( exceptionsComponent = ExceptionsComponent(
view.exceptions_layout, view.exceptions_layout,
this,
ActionBusFactory.get(this), ActionBusFactory.get(this),
ExceptionsState(loadAndMapExceptions()) FenixViewModelProvider.create(
this,
ExceptionsViewModel::class.java
) {
ExceptionsViewModel(ExceptionsState(loadAndMapExceptions()))
}
) )
return view return view
} }

View File

@ -33,6 +33,7 @@ import org.jetbrains.anko.constraint.layout.applyConstraintSet
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.collections.CreateCollectionViewModel 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.SessionControlChange
import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent
import org.mozilla.fenix.home.sessioncontrol.SessionControlState 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.Tab
import org.mozilla.fenix.home.sessioncontrol.TabAction import org.mozilla.fenix.home.sessioncontrol.TabAction
import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.home.sessioncontrol.TabCollection
@ -88,9 +90,13 @@ class HomeFragment : Fragment(), CoroutineScope {
sessionControlComponent = SessionControlComponent( sessionControlComponent = SessionControlComponent(
view.homeLayout, view.homeLayout,
this,
bus, bus,
SessionControlState(listOf(), listOf(), mode) FenixViewModelProvider.create(
this,
SessionControlViewModel::class.java
) {
SessionControlViewModel(SessionControlState(listOf(), listOf(), mode))
}
) )
view.homeLayout.applyConstraintSet { view.homeLayout.applyConstraintSet {
@ -450,6 +456,7 @@ class HomeFragment : Fragment(), CoroutineScope {
private fun emitSessionChanges() { private fun emitSessionChanges() {
val sessionManager = requireComponents.core.sessionManager val sessionManager = requireComponents.core.sessionManager
getManagedEmitter<SessionControlChange>().onNext( getManagedEmitter<SessionControlChange>().onNext(
SessionControlChange.TabsChange( SessionControlChange.TabsChange(
sessionManager.sessions sessionManager.sessions

View File

@ -7,48 +7,35 @@ package org.mozilla.fenix.home.sessioncontrol
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Parcelable import android.os.Parcelable
import android.view.ViewGroup 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 androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer import io.reactivex.Observer
import kotlinx.android.parcel.Parcelize 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.UIComponentViewModelProvider
class SessionControlComponent( class SessionControlComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: SessionControlState = SessionControlState(emptyList(), emptyList(), Mode.Normal) viewModelProvider: UIComponentViewModelProvider<SessionControlState, SessionControlChange>
) : ) :
UIComponent<SessionControlState, SessionControlAction, SessionControlChange>( UIComponent<SessionControlState, SessionControlAction, SessionControlChange>(
owner,
bus.getManagedEmitter(SessionControlAction::class.java), 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) override fun initView() = SessionControlUIView(container, actionEmitter, changesObservable)
val view: RecyclerView val view: RecyclerView
get() = uiView.view as 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 { init {
stateObservable = render() bind()
} }
} }
@ -124,19 +111,11 @@ sealed class SessionControlChange : Change {
} }
class SessionControlViewModel(initialState: SessionControlState) : class SessionControlViewModel(initialState: SessionControlState) :
UIComponentViewModel<SessionControlState, SessionControlAction, SessionControlChange>( UIComponentViewModelBase<SessionControlState, SessionControlChange>(
initialState, initialState,
reducer 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 { companion object {
val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change -> val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) { when (change) {

View File

@ -5,46 +5,34 @@
package org.mozilla.fenix.library.bookmarks package org.mozilla.fenix.library.bookmarks
import android.view.ViewGroup 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 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent 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.UIView
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.test.Mockable
@Mockable @Mockable
class BookmarkComponent( class BookmarkComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: BookmarkState = viewModelProvider: UIComponentViewModelProvider<BookmarkState, BookmarkChange>
BookmarkState(null, BookmarkState.Mode.Normal)
) : ) :
UIComponent<BookmarkState, BookmarkAction, BookmarkChange>( UIComponent<BookmarkState, BookmarkAction, BookmarkChange>(
owner,
bus.getManagedEmitter(BookmarkAction::class.java), bus.getManagedEmitter(BookmarkAction::class.java),
bus.getSafeManagedObservable(BookmarkChange::class.java) bus.getSafeManagedObservable(BookmarkChange::class.java),
viewModelProvider
) { ) {
override fun initView(): UIView<BookmarkState, BookmarkAction, BookmarkChange> = override fun initView(): UIView<BookmarkState, BookmarkAction, BookmarkChange> =
BookmarkUIView(container, actionEmitter, changesObservable) BookmarkUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<BookmarkState> {
return ViewModelProvider(
owner,
BookmarkViewModel.Factory(initialState)
).get(BookmarkViewModel::class.java).render(changesObservable, uiView)
}
init { init {
render() bind()
} }
} }
@ -82,17 +70,11 @@ operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
} }
class BookmarkViewModel(initialState: BookmarkState) : class BookmarkViewModel(initialState: BookmarkState) :
UIComponentViewModel<BookmarkState, BookmarkAction, BookmarkChange>(initialState, reducer) { UIComponentViewModelBase<BookmarkState, 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
}
companion object { companion object {
fun create() = BookmarkViewModel(BookmarkState(null, BookmarkState.Mode.Normal))
val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change -> val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) { when (change) {
is BookmarkChange.Change -> { is BookmarkChange.Change -> {

View File

@ -37,6 +37,7 @@ import mozilla.components.concept.sync.Profile
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components 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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_bookmark, container, false) val view = inflater.inflate(R.layout.fragment_bookmark, container, false)
bookmarkComponent = BookmarkComponent(view.bookmark_layout, this, ActionBusFactory.get(this)) bookmarkComponent = BookmarkComponent(
signInComponent = SignInComponent(view.bookmark_layout, this, ActionBusFactory.get(this)) 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 return view
} }

View File

@ -5,41 +5,30 @@
package org.mozilla.fenix.library.bookmarks package org.mozilla.fenix.library.bookmarks
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import org.mozilla.fenix.mvi.ViewState
import androidx.lifecycle.ViewModel import org.mozilla.fenix.mvi.Change
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent 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.UIView
import org.mozilla.fenix.mvi.ViewState
class SignInComponent( class SignInComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: SignInState = viewModelProvider: UIComponentViewModelProvider<SignInState, SignInChange>
SignInState(false)
) : UIComponent<SignInState, SignInAction, SignInChange>( ) : UIComponent<SignInState, SignInAction, SignInChange>(
owner,
bus.getManagedEmitter(SignInAction::class.java), bus.getManagedEmitter(SignInAction::class.java),
bus.getSafeManagedObservable(SignInChange::class.java) bus.getSafeManagedObservable(SignInChange::class.java),
viewModelProvider
) { ) {
override fun initView(): UIView<SignInState, SignInAction, SignInChange> = override fun initView(): UIView<SignInState, SignInAction, SignInChange> =
SignInUIView(container, actionEmitter, changesObservable) SignInUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<SignInState> =
ViewModelProvider(
owner,
SignInViewModel.Factory(initialState)
).get(SignInViewModel::class.java).render(changesObservable, uiView)
init { init {
render() bind()
} }
} }
@ -54,19 +43,9 @@ sealed class SignInChange : Change {
object SignedOut : SignInChange() object SignedOut : SignInChange()
} }
class SignInViewModel(initialState: SignInState) : class SignInViewModel(
UIComponentViewModel<SignInState, SignInAction, SignInChange>( initialState: SignInState
initialState, reducer ) : UIComponentViewModelBase<SignInState, 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
}
companion object { companion object {
val reducer = object : Reducer<SignInState, SignInChange> { val reducer = object : Reducer<SignInState, SignInChange> {
override fun invoke(state: SignInState, change: SignInChange): SignInState { 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.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation 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.*
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.* import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.*
import kotlinx.coroutines.CoroutineScope 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.OAuthAccount
import mozilla.components.concept.sync.Profile import mozilla.components.concept.sync.Profile
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getColorFromAttr 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.SignInAction
import org.mozilla.fenix.library.bookmarks.SignInChange import org.mozilla.fenix.library.bookmarks.SignInChange
import org.mozilla.fenix.library.bookmarks.SignInComponent 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.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter 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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false) 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 return view
} }

View File

@ -4,16 +4,13 @@
package org.mozilla.fenix.library.history package org.mozilla.fenix.library.history
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import org.mozilla.fenix.mvi.ViewState
import androidx.lifecycle.ViewModel import org.mozilla.fenix.mvi.Change
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.UIComponentViewModelProvider
import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.test.Mockable
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long) 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 @Mockable
class HistoryComponent( class HistoryComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: HistoryState = HistoryState(emptyList(), HistoryState.Mode.Normal) viewModelProvider: UIComponentViewModelProvider<HistoryState, HistoryChange>
) : ) :
UIComponent<HistoryState, HistoryAction, HistoryChange>( UIComponent<HistoryState, HistoryAction, HistoryChange>(
owner,
bus.getManagedEmitter(HistoryAction::class.java), 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 initView() = HistoryUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<HistoryState> =
ViewModelProvider(
owner,
HistoryViewModel.Factory(initialState)
).get(HistoryViewModel::class.java).render(changesObservable, uiView)
init { init {
render() bind()
} }
} }
@ -74,18 +64,11 @@ sealed class HistoryChange : Change {
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange() data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange()
} }
class HistoryViewModel(initialState: HistoryState) : class HistoryViewModel(
UIComponentViewModel<HistoryState, HistoryAction, HistoryChange>(initialState, reducer) { initialState: HistoryState
) : UIComponentViewModelBase<HistoryState, 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
}
companion object { companion object {
fun create() = HistoryViewModel(HistoryState(emptyList(), HistoryState.Mode.Normal))
val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change -> val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
when (change) { when (change) {
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list) 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 mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
@ -59,7 +60,15 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
val view = inflater.inflate(R.layout.fragment_history, container, false) 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 return view
} }

View File

@ -5,45 +5,30 @@
package org.mozilla.fenix.quickactionsheet package org.mozilla.fenix.quickactionsheet
import android.view.ViewGroup 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory 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.Change
import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent 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.UIView
import org.mozilla.fenix.mvi.ViewState
class QuickActionComponent( class QuickActionComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: QuickActionState = QuickActionState( viewModelProvider: UIComponentViewModelProvider<QuickActionState, QuickActionChange>
readable = false,
bookmarked = false,
readerActive = false,
bounceNeeded = false
)
) : UIComponent<QuickActionState, QuickActionAction, QuickActionChange>( ) : UIComponent<QuickActionState, QuickActionAction, QuickActionChange>(
owner,
bus.getManagedEmitter(QuickActionAction::class.java), bus.getManagedEmitter(QuickActionAction::class.java),
bus.getSafeManagedObservable(QuickActionChange::class.java) bus.getSafeManagedObservable(QuickActionChange::class.java),
viewModelProvider
) { ) {
override fun initView(): UIView<QuickActionState, QuickActionAction, QuickActionChange> = override fun initView(): UIView<QuickActionState, QuickActionAction, QuickActionChange> =
QuickActionUIView(container, actionEmitter, changesObservable) QuickActionUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<QuickActionState> =
ViewModelProvider(
owner,
QuickActionViewModel.Factory(initialState)
).get(QuickActionViewModel::class.java).render(changesObservable, uiView)
init { init {
render() bind()
} }
} }
@ -71,20 +56,9 @@ sealed class QuickActionChange : Change {
object BounceNeededChange : QuickActionChange() object BounceNeededChange : QuickActionChange()
} }
class QuickActionViewModel(initialState: QuickActionState) : class QuickActionViewModel(
UIComponentViewModel<QuickActionState, QuickActionAction, QuickActionChange>( initialState: QuickActionState
initialState, ) : UIComponentViewModelBase<QuickActionState, QuickActionChange>(initialState, reducer) {
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
}
companion object { companion object {
val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change -> val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (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.android.content.isPermissionGranted
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event 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.SearchState
import org.mozilla.fenix.components.toolbar.ToolbarComponent import org.mozilla.fenix.components.toolbar.ToolbarComponent
import org.mozilla.fenix.components.toolbar.ToolbarUIView 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.getSpannable
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory 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.AwesomeBarAction
import org.mozilla.fenix.search.awesomebar.AwesomeBarChange import org.mozilla.fenix.search.awesomebar.AwesomeBarChange
import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent 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.AwesomeBarUIView
import org.mozilla.fenix.search.awesomebar.AwesomeBarViewModel
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class SearchFragment : Fragment(), BackHandler { class SearchFragment : Fragment(), BackHandler {
@ -67,15 +71,28 @@ class SearchFragment : Fragment(), BackHandler {
toolbarComponent = ToolbarComponent( toolbarComponent = ToolbarComponent(
view.toolbar_component_wrapper, view.toolbar_component_wrapper,
this,
ActionBusFactory.get(this), ActionBusFactory.get(this),
sessionId, sessionId,
isPrivate, 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() ActionBusFactory.get(this).logMergedObservables()
return view 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/. */ file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import android.view.ViewGroup 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 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.UIComponentViewModelProvider
data class AwesomeBarState( data class AwesomeBarState(
val query: String, val query: String,
@ -40,42 +35,25 @@ sealed class AwesomeBarChange : Change {
class AwesomeBarComponent( class AwesomeBarComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: AwesomeBarState = AwesomeBarState("", false) viewModelProvider: UIComponentViewModelProvider<AwesomeBarState, AwesomeBarChange>
) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>( ) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
owner,
bus.getManagedEmitter(AwesomeBarAction::class.java), 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 initView() = AwesomeBarUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<AwesomeBarState> =
ViewModelProviders.of(owner, AwesomeBarViewModel.Factory(initialState))
.get(AwesomeBarViewModel::class.java).render(changesObservable, uiView)
init { init {
render() bind()
} }
} }
class AwesomeBarViewModel(initialState: AwesomeBarState) : class AwesomeBarViewModel(
UIComponentViewModel<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>( initialState: AwesomeBarState
initialState, ) : UIComponentViewModelBase<AwesomeBarState, AwesomeBarChange>(initialState, reducer) {
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
}
companion object { companion object {
val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change -> val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
logDebug("IN_REDUCER", change.toString())
when (change) { when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected -> is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false) 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.content.Context
import android.view.ViewGroup 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.feature.sitepermissions.SitePermissions
import mozilla.components.support.ktx.kotlin.toUri import mozilla.components.support.ktx.kotlin.toUri
import org.mozilla.fenix.ext.components 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.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent 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.UIView
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.toStatus import org.mozilla.fenix.settings.toStatus
import org.mozilla.fenix.settings.toggle import org.mozilla.fenix.settings.toggle
@ -27,25 +24,19 @@ import org.mozilla.fenix.utils.Settings
class QuickSettingsComponent( class QuickSettingsComponent(
private val container: ViewGroup, private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: QuickSettingsState viewModelProvider: UIComponentViewModelProvider<QuickSettingsState, QuickSettingsChange>
) : UIComponent<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>( ) : UIComponent<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>(
owner,
bus.getManagedEmitter(QuickSettingsAction::class.java), bus.getManagedEmitter(QuickSettingsAction::class.java),
bus.getSafeManagedObservable(QuickSettingsChange::class.java) bus.getSafeManagedObservable(QuickSettingsChange::class.java),
viewModelProvider
) { ) {
override fun initView(): UIView<QuickSettingsState, QuickSettingsAction, QuickSettingsChange> { override fun initView(): UIView<QuickSettingsState, QuickSettingsAction, QuickSettingsChange> {
return QuickSettingsUIView(container, actionEmitter, changesObservable, container) return QuickSettingsUIView(container, actionEmitter, changesObservable, container)
} }
override fun render(): Observable<QuickSettingsState> =
ViewModelProvider(owner, QuickSettingsViewModel.Factory(initialState)).get(
QuickSettingsViewModel::class.java
).render(changesObservable, uiView)
init { init {
render() bind()
} }
fun toggleSitePermission( fun toggleSitePermission(
@ -121,20 +112,9 @@ sealed class QuickSettingsChange : Change {
data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange() data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange()
} }
class QuickSettingsViewModel(initialState: QuickSettingsState) : class QuickSettingsViewModel(
UIComponentViewModel<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>( initialState: QuickSettingsState
initialState, ) : UIComponentViewModelBase<QuickSettingsState, QuickSettingsChange>(initialState, reducer) {
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
}
companion object { companion object {
val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change -> val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change ->
when (change) { when (change) {

View File

@ -26,6 +26,7 @@ import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.support.ktx.kotlin.toUri import mozilla.components.support.ktx.kotlin.toUri
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.browser.BrowserFragment
@ -74,15 +75,23 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
val rootView = inflateRootView(container) val rootView = inflateRootView(container)
requireComponents.core.sessionManager.findSessionById(sessionId)?.register(sessionObserver, view = rootView) requireComponents.core.sessionManager.findSessionById(sessionId)?.register(sessionObserver, view = rootView)
quickSettingsComponent = QuickSettingsComponent( quickSettingsComponent = QuickSettingsComponent(
rootView as NestedScrollView, this, ActionBusFactory.get(this), rootView as NestedScrollView,
QuickSettingsState( ActionBusFactory.get(this),
QuickSettingsState.Mode.Normal( FenixViewModelProvider.create(
url, this,
isSecured, QuickSettingsViewModel::class.java
isTrackingProtectionOn, ) {
sitePermissions QuickSettingsViewModel(
QuickSettingsState(
QuickSettingsState.Mode.Normal(
url,
isSecured,
isTrackingProtectionOn,
sitePermissions
)
)
) )
) }
) )
return rootView return rootView
} }

View File

@ -4,10 +4,7 @@
package org.mozilla.fenix.library.bookmarks package org.mozilla.fenix.library.bookmarks
import android.view.ViewGroup
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.spyk
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.observers.TestObserver import io.reactivex.observers.TestObserver
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
@ -16,13 +13,12 @@ import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.TestUtils import org.mozilla.fenix.TestUtils
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.TestUtils.bus
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.getManagedEmitter 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 bookmarkObserver: TestObserver<BookmarkState>
private lateinit var emitter: Observer<BookmarkChange> private lateinit var emitter: Observer<BookmarkChange>
@ -31,11 +27,10 @@ class BookmarkComponentTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
TestUtils.setRxSchedulers() TestUtils.setRxSchedulers()
bookmarkComponent = spyk( bookmarkViewModel = BookmarkViewModel.create()
TestBookmarkComponent(mockk(), TestUtils.bus), bookmarkObserver = bookmarkViewModel.state.test()
recordPrivateCalls = true bus.getSafeManagedObservable(BookmarkChange::class.java)
) .subscribe(bookmarkViewModel.changes::onNext)
bookmarkObserver = bookmarkComponent.render().test()
emitter = TestUtils.owner.getManagedEmitter() emitter = TestUtils.owner.getManagedEmitter()
} }
@ -84,12 +79,4 @@ class BookmarkComponentTest {
BookmarkState(tree - itemToSelect.guid, BookmarkState.Mode.Normal) 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 package org.mozilla.fenix.library.history
import android.view.ViewGroup
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.spyk
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.observers.TestObserver import io.reactivex.observers.TestObserver
import org.junit.Before import org.junit.Before
@ -15,13 +12,11 @@ import org.junit.Test
import org.mozilla.fenix.TestUtils.bus import org.mozilla.fenix.TestUtils.bus
import org.mozilla.fenix.TestUtils.owner import org.mozilla.fenix.TestUtils.owner
import org.mozilla.fenix.TestUtils.setRxSchedulers import org.mozilla.fenix.TestUtils.setRxSchedulers
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.getManagedEmitter 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 historyObserver: TestObserver<HistoryState>
private lateinit var emitter: Observer<HistoryChange> private lateinit var emitter: Observer<HistoryChange>
@ -30,11 +25,11 @@ class HistoryComponentTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
setRxSchedulers() setRxSchedulers()
historyComponent = spyk( historyViewModel = HistoryViewModel.create()
TestHistoryComponent(mockk(), bus), historyObserver = historyViewModel.state.test()
recordPrivateCalls = true bus.getSafeManagedObservable(HistoryChange::class.java)
) .subscribe(historyViewModel.changes::onNext)
historyObserver = historyComponent.render().test()
emitter = owner.getManagedEmitter() emitter = owner.getManagedEmitter()
} }
@ -97,12 +92,4 @@ class HistoryComponentTest {
HistoryState(historyItems, HistoryState.Mode.Normal) 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 package org.mozilla.fenix.mvi
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Observer import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers 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.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>( abstract class UIComponent<S : ViewState, A : Action, C : Change>(
protected val owner: Fragment,
protected val actionEmitter: Observer<A>, 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() } open val uiView: UIView<S, A, C> by lazy { initView() }
abstract fun initView(): UIView<S, A, C> abstract fun initView(): UIView<S, A, C>
open fun getContainerId() = uiView.containerId 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, initialState: S,
private val reducer: Reducer<S, C> reducer: Reducer<S, C>
) : ViewModel() { ) : ViewModel(), UIComponentViewModel<S, C> {
private var currentState: S = initialState final override val changes: Observer<C>
private var statesDisposable: Disposable? = null private var _state: BehaviorSubject<S> = BehaviorSubject.createDefault(initialState)
override val state: Observable<S>
get() = _state
/** init {
* Render the ViewState to the View through the Reducer changes = PublishSubject.create<C>()
*/
fun render(changesObservable: Observable<C>, uiView: UIView<S, A, C>): Observable<S> {
val statesObservable = internalRender(changesObservable, reducer)
statesDisposable = statesObservable
.subscribe(uiView.updateView())
return statesObservable
}
@Suppress("MemberVisibilityCanBePrivate") changes
protected fun internalRender(changesObservable: Observable<C>, reducer: Reducer<S, C>): Observable<S> = .withLatestFrom(_state)
changesObservable .map { reducer(it.second, it.first) }
.scan(currentState, reducer)
.distinctUntilChanged() .distinctUntilChanged()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.replay(1) .subscribe(_state)
.autoConnect(0)
.doOnNext { currentState = it }
override fun onCleared() {
super.onCleared()
statesDisposable?.dispose()
} }
} }