1
0
Fork 0

For #1994: Re-architect state handling code (#2382)

master
Colin Lee 2019-05-09 18:06:12 -05:00 committed by Jeff Boek
parent 27d8c09def
commit ccbc14a71f
29 changed files with 557 additions and 212 deletions

View File

@ -51,4 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1429 - Updated site permissions ui for MVP - #1429 - Updated site permissions ui for MVP
- #1599 - Fixed a crash creating a bookmark for a custom tab - #1599 - Fixed a crash creating a bookmark for a custom tab
- #1414 - Fixed site permissions settings getting reset in Android 6. - #1414 - Fixed site permissions settings getting reset in Android 6.
- #1994 - Made app state persist better when rotating the screen
### Removed ### Removed

View File

@ -337,6 +337,7 @@ dependencies {
implementation Deps.androidx_navigation_ui implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview implementation Deps.androidx_recyclerview
implementation Deps.androidx_lifecycle_viewmodel_ktx implementation Deps.androidx_lifecycle_viewmodel_ktx
implementation Deps.androidx_lifecycle_viewmodel_ss
implementation Deps.androidx_core implementation Deps.androidx_core
implementation Deps.androidx_transition implementation Deps.androidx_transition

View File

@ -132,6 +132,7 @@ 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), SearchState("", getSessionById()?.searchTerms ?: "", isEditing = false),
@ -156,6 +157,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope,
QuickActionComponent( QuickActionComponent(
view.nestedScrollQuickAction, view.nestedScrollQuickAction,
this,
ActionBusFactory.get(this), ActionBusFactory.get(this),
QuickActionState( QuickActionState(
readable = getSessionById()?.readerable ?: false, readable = getSessionById()?.readerable ?: false,

View File

@ -5,6 +5,10 @@ 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.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.Action import org.mozilla.fenix.mvi.Action
@ -12,6 +16,7 @@ import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.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.ViewState import org.mozilla.fenix.mvi.ViewState
sealed class SaveCollectionStep { sealed class SaveCollectionStep {
@ -51,34 +56,63 @@ 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() override var initialState: CollectionCreationState = CollectionCreationState()
) : 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)
) { ) {
override val reducer: Reducer<CollectionCreationState, CollectionCreationChange> =
{ state, change ->
when (change) {
is CollectionCreationChange.AddAllTabs -> state.copy(selectedTabs = state.tabs.toSet())
is CollectionCreationChange.TabListChange -> state.copy(tabs = change.tabs)
is CollectionCreationChange.TabAdded -> {
val selectedTabs = state.selectedTabs + setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.TabRemoved -> {
val selectedTabs = state.selectedTabs - setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.StepChanged -> {
state.copy(saveCollectionStep = change.saveCollectionStep)
}
}
}
override fun initView() = CollectionCreationUIView(container, actionEmitter, changesObservable) override fun initView() = CollectionCreationUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<CollectionCreationState> =
ViewModelProvider(owner, CollectionCreationViewModel.Factory(initialState, changesObservable)).get(
CollectionCreationViewModel::class.java
).render(uiView)
init { init {
render(reducer) render()
}
}
class CollectionCreationViewModel(
initialState: CollectionCreationState,
changesObservable: Observable<CollectionCreationChange>
) :
UIComponentViewModel<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: CollectionCreationState,
private val changesObservable: Observable<CollectionCreationChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
CollectionCreationViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<CollectionCreationState, CollectionCreationChange> =
{ state, change ->
when (change) {
is CollectionCreationChange.AddAllTabs -> state.copy(selectedTabs = state.tabs.toSet())
is CollectionCreationChange.TabListChange -> state.copy(tabs = change.tabs)
is CollectionCreationChange.TabAdded -> {
val selectedTabs = state.selectedTabs + setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.TabRemoved -> {
val selectedTabs = state.selectedTabs - setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.StepChanged -> {
state.copy(saveCollectionStep = change.saveCollectionStep)
}
}
}
} }
} }

View File

@ -50,6 +50,7 @@ class CreateCollectionFragment : DialogFragment() {
collectionCreationComponent = CollectionCreationComponent( collectionCreationComponent = CollectionCreationComponent(
view.create_collection_wrapper, view.create_collection_wrapper,
this,
ActionBusFactory.get(this), ActionBusFactory.get(this),
CollectionCreationState( CollectionCreationState(
tabs = tabs, tabs = tabs,

View File

@ -7,6 +7,11 @@ 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
@ -17,10 +22,12 @@ import org.mozilla.fenix.mvi.ActionBusFactory
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.ViewState import org.mozilla.fenix.mvi.ViewState
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,
@ -28,21 +35,13 @@ class ToolbarComponent(
private val engineIconView: ImageView? = null private val engineIconView: ImageView? = null
) : ) :
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)
) { ) {
fun getView(): BrowserToolbar = uiView.toolbar fun getView(): BrowserToolbar = uiView.toolbar
override val reducer: Reducer<SearchState, SearchChange> = { state, change ->
when (change) {
is SearchChange.ToolbarClearedFocus -> state.copy(focused = false)
is SearchChange.ToolbarRequestedFocus -> state.copy(focused = true)
is SearchChange.SearchShortcutEngineSelected ->
state.copy(engine = change.engine)
}
}
override fun initView() = ToolbarUIView( override fun initView() = ToolbarUIView(
sessionId, sessionId,
isPrivate, isPrivate,
@ -52,8 +51,12 @@ class ToolbarComponent(
engineIconView engineIconView
) )
override fun render(): Observable<SearchState> =
ViewModelProviders.of(owner, ToolbarViewModel.Factory(initialState, changesObservable))
.get(ToolbarViewModel::class.java).render(uiView)
init { init {
render(reducer) render()
applyTheme() applyTheme()
} }
@ -95,3 +98,27 @@ sealed class SearchChange : Change {
object ToolbarClearedFocus : SearchChange() object ToolbarClearedFocus : SearchChange()
data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchChange() data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchChange()
} }
class ToolbarViewModel(initialState: SearchState, changesObservable: Observable<SearchChange>) :
UIComponentViewModel<SearchState, SearchAction, SearchChange>(initialState, changesObservable, 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, changesObservable) as T
}
companion object {
val reducer: Reducer<SearchState, SearchChange> = { state, change ->
when (change) {
is SearchChange.ToolbarClearedFocus -> state.copy(focused = false)
is SearchChange.ToolbarRequestedFocus -> state.copy(focused = true)
is SearchChange.SearchShortcutEngineSelected ->
state.copy(engine = change.engine)
}
}
}
}

View File

@ -45,7 +45,7 @@ class ToolbarUIView(
init { init {
val sessionManager = view.context.components.core.sessionManager val sessionManager = view.context.components.core.sessionManager
val session = sessionId?.let { sessionManager.findSessionById(it) } val session = sessionId?.let { sessionManager.findSessionById(it) }
?: sessionManager.selectedSession ?: sessionManager.selectedSession
view.apply { view.apply {
elevation = resources.pxToDp(TOOLBAR_ELEVATION).toFloat() elevation = resources.pxToDp(TOOLBAR_ELEVATION).toFloat()

View File

@ -4,10 +4,16 @@
package org.mozilla.fenix.exceptions package org.mozilla.fenix.exceptions
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 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.Change
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.test.Mockable
@ -16,24 +22,24 @@ 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()) override var initialState: ExceptionsState = ExceptionsState(emptyList())
) : ) :
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)
) { ) {
override val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change -> override fun render(): Observable<ExceptionsState> =
when (change) { ViewModelProviders.of(owner, ExceptionsViewModel.Factory(initialState, changesObservable))
is ExceptionsChange.Change -> state.copy(items = change.list) .get(ExceptionsViewModel::class.java).render(uiView)
}
}
override fun initView() = ExceptionsUIView(container, actionEmitter, changesObservable) override fun initView() = ExceptionsUIView(container, actionEmitter, changesObservable)
init { init {
render(reducer) render()
} }
} }
@ -49,3 +55,28 @@ sealed class ExceptionsAction : Action {
sealed class ExceptionsChange : Change { sealed class ExceptionsChange : Change {
data class Change(val list: List<ExceptionsItem>) : ExceptionsChange() data class Change(val list: List<ExceptionsItem>) : ExceptionsChange()
} }
class ExceptionsViewModel(initialState: ExceptionsState, changesObservable: Observable<ExceptionsChange>) :
UIComponentViewModel<ExceptionsState, ExceptionsAction, ExceptionsChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: ExceptionsState,
private val changesObservable: Observable<ExceptionsChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
ExceptionsViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change ->
when (change) {
is ExceptionsChange.Change -> state.copy(items = change.list)
}
}
}
}

View File

@ -48,6 +48,7 @@ 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()) ExceptionsState(loadAndMapExceptions())
) )

View File

@ -89,6 +89,7 @@ class HomeFragment : Fragment(), CoroutineScope {
sessionControlComponent = SessionControlComponent( sessionControlComponent = SessionControlComponent(
view.homeLayout, view.homeLayout,
this,
bus, bus,
SessionControlState(listOf(), listOf(), mode) SessionControlState(listOf(), listOf(), mode)
) )

View File

@ -6,38 +6,42 @@ package org.mozilla.fenix.home.sessioncontrol
import android.graphics.Bitmap import android.graphics.Bitmap
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 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.Change
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
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) override var initialState: SessionControlState = SessionControlState(emptyList(), emptyList(), Mode.Normal)
) : ) :
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)
) { ) {
override val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) {
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
is SessionControlChange.ModeChange -> state.copy(mode = change.mode)
}
}
override fun initView() = SessionControlUIView(container, actionEmitter, changesObservable) 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> =
ViewModelProviders.of(owner, SessionControlViewModel.Factory(initialState, changesObservable))
.get(SessionControlViewModel::class.java).render(uiView)
init { init {
render(reducer) render()
} }
} }
@ -109,3 +113,30 @@ sealed class SessionControlChange : Change {
data class ModeChange(val mode: Mode) : SessionControlChange() data class ModeChange(val mode: Mode) : SessionControlChange()
data class CollectionsChange(val collections: List<TabCollection>) : SessionControlChange() data class CollectionsChange(val collections: List<TabCollection>) : SessionControlChange()
} }
class SessionControlViewModel(initialState: SessionControlState, changesObservable: Observable<SessionControlChange>) :
UIComponentViewModel<SessionControlState, SessionControlAction, SessionControlChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: SessionControlState,
private val changesObservable: Observable<SessionControlChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SessionControlViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) {
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
is SessionControlChange.ModeChange -> state.copy(mode = change.mode)
}
}
}
}

View File

@ -5,12 +5,17 @@
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.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.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.UIView import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.test.Mockable
@ -18,51 +23,28 @@ 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 = override var initialState: BookmarkState =
BookmarkState(null, BookmarkState.Mode.Normal) 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)
) { ) {
override val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) {
is BookmarkChange.Change -> {
val mode =
if (state.mode is BookmarkState.Mode.Selecting) {
val items = state.mode.selectedItems.filter {
it in change.tree
}.toSet()
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
} else state.mode
state.copy(tree = change.tree, mode = mode)
}
is BookmarkChange.IsSelected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems + change.newlySelectedItem
} else setOf(change.newlySelectedItem)
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
}
is BookmarkChange.IsDeselected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems - change.newlyDeselectedItem
} else setOf()
val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(
selectedItems
)
state.copy(mode = mode)
}
is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal)
}
}
override fun initView(): UIView<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, changesObservable)
).get(BookmarkViewModel::class.java).render(uiView)
}
init { init {
render(reducer) render()
} }
} }
@ -98,3 +80,49 @@ sealed class BookmarkChange : Change {
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean { operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
return children?.contains(item) ?: false return children?.contains(item) ?: false
} }
class BookmarkViewModel(initialState: BookmarkState, changesObservable: Observable<BookmarkChange>) :
UIComponentViewModel<BookmarkState, BookmarkAction, BookmarkChange>(initialState, changesObservable, reducer) {
class Factory(
private val initialState: BookmarkState,
private val changesObservable: Observable<BookmarkChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
BookmarkViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) {
is BookmarkChange.Change -> {
val mode =
if (state.mode is BookmarkState.Mode.Selecting) {
val items = state.mode.selectedItems.filter {
it in change.tree
}.toSet()
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
} else state.mode
state.copy(tree = change.tree, mode = mode)
}
is BookmarkChange.IsSelected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems + change.newlySelectedItem
} else setOf(change.newlySelectedItem)
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
}
is BookmarkChange.IsDeselected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems - change.newlyDeselectedItem
} else setOf()
val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(
selectedItems
)
state.copy(mode = mode)
}
is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal)
}
}
}
}

View File

@ -74,8 +74,8 @@ 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, ActionBusFactory.get(this)) bookmarkComponent = BookmarkComponent(view.bookmark_layout, this, ActionBusFactory.get(this))
signInComponent = SignInComponent(view.bookmark_layout, ActionBusFactory.get(this)) signInComponent = SignInComponent(view.bookmark_layout, this, ActionBusFactory.get(this))
return view return view
} }

View File

@ -5,36 +5,41 @@
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 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.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.UIView import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState 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 = override var initialState: SignInState =
SignInState(false) 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)
) { ) {
override val reducer: Reducer<SignInState, SignInChange> = { state, change ->
when (change) {
SignInChange.SignedIn -> state.copy(signedIn = true)
SignInChange.SignedOut -> state.copy(signedIn = false)
}
}
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, changesObservable)
).get(SignInViewModel::class.java).render(uiView)
init { init {
render(reducer) render()
} }
} }
@ -48,3 +53,29 @@ sealed class SignInChange : Change {
object SignedIn : SignInChange() object SignedIn : SignInChange()
object SignedOut : SignInChange() object SignedOut : SignInChange()
} }
class SignInViewModel(initialState: SignInState, changesObservable: Observable<SignInChange>) :
UIComponentViewModel<SignInState, SignInAction, SignInChange>(
initialState, changesObservable, reducer
) {
class Factory(
private val initialState: SignInState,
private val changesObservable: Observable<SignInChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SignInViewModel(initialState, changesObservable) as T
}
companion object {
val reducer = object : Reducer<SignInState, SignInChange> {
override fun invoke(state: SignInState, change: SignInChange): SignInState {
return when (change) {
SignInChange.SignedIn -> state.copy(signedIn = true)
SignInChange.SignedOut -> state.copy(signedIn = false)
}
}
}
}
}

View File

@ -68,7 +68,7 @@ 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, ActionBusFactory.get(this)) signInComponent = SignInComponent(view.select_bookmark_layout, this, ActionBusFactory.get(this))
return view return view
} }

View File

@ -4,56 +4,42 @@
package org.mozilla.fenix.library.history package org.mozilla.fenix.library.history
import android.view.ViewGroup import android.view.ViewGroup
import org.mozilla.fenix.test.Mockable import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change 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.ViewState import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long) data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long)
@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) override var initialState: HistoryState = HistoryState(emptyList(), HistoryState.Mode.Normal)
) : ) :
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)
) { ) {
override val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
when (change) {
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list)
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
is HistoryChange.AddItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems + listOf(change.item)
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.RemoveItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != change.item.id }
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
}
}
override fun initView() = HistoryUIView(container, actionEmitter, changesObservable) override fun initView() = HistoryUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<HistoryState> =
ViewModelProvider(
owner,
HistoryViewModel.Factory(initialState, changesObservable)
).get(HistoryViewModel::class.java).render(uiView)
init { init {
render(reducer) render()
} }
} }
@ -85,3 +71,44 @@ sealed class HistoryChange : Change {
data class AddItemForRemoval(val item: HistoryItem) : HistoryChange() data class AddItemForRemoval(val item: HistoryItem) : HistoryChange()
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange() data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange()
} }
class HistoryViewModel(initialState: HistoryState, changesObservable: Observable<HistoryChange>) :
UIComponentViewModel<HistoryState, HistoryAction, HistoryChange>(initialState, changesObservable, reducer) {
class Factory(
private val initialState: HistoryState,
private val changesObservable: Observable<HistoryChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
HistoryViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
when (change) {
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list)
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
is HistoryChange.AddItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems + listOf(change.item)
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.RemoveItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != change.item.id }
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
}
}
}
}

View File

@ -49,7 +49,7 @@ 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, ActionBusFactory.get(this)) historyComponent = HistoryComponent(view.history_layout, this, ActionBusFactory.get(this))
return view return view
} }

View File

@ -5,16 +5,22 @@
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.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.UIView import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState 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( override var initialState: QuickActionState = QuickActionState(
readable = false, readable = false,
@ -22,29 +28,21 @@ class QuickActionComponent(
readerActive = false readerActive = 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)
) { ) {
override val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (change) {
is QuickActionChange.BookmarkedStateChange -> {
state.copy(bookmarked = change.bookmarked)
}
is QuickActionChange.ReadableStateChange -> {
state.copy(readable = change.readable)
}
is QuickActionChange.ReaderActiveStateChange -> {
state.copy(readerActive = change.active)
}
}
}
override fun initView(): UIView<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, changesObservable)
).get(QuickActionViewModel::class.java).render(uiView)
init { init {
render(reducer) render()
} }
} }
@ -65,3 +63,36 @@ sealed class QuickActionChange : Change {
data class ReadableStateChange(val readable: Boolean) : QuickActionChange() data class ReadableStateChange(val readable: Boolean) : QuickActionChange()
data class ReaderActiveStateChange(val active: Boolean) : QuickActionChange() data class ReaderActiveStateChange(val active: Boolean) : QuickActionChange()
} }
class QuickActionViewModel(initialState: QuickActionState, changesObservable: Observable<QuickActionChange>) :
UIComponentViewModel<QuickActionState, QuickActionAction, QuickActionChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: QuickActionState,
private val changesObservable: Observable<QuickActionChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
QuickActionViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (change) {
is QuickActionChange.BookmarkedStateChange -> {
state.copy(bookmarked = change.bookmarked)
}
is QuickActionChange.ReadableStateChange -> {
state.copy(readable = change.readable)
}
is QuickActionChange.ReaderActiveStateChange -> {
state.copy(readerActive = change.active)
}
}
}
}
}

View File

@ -67,6 +67,7 @@ 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,
@ -74,7 +75,7 @@ class SearchFragment : Fragment(), BackHandler {
view.search_engine_icon view.search_engine_icon
) )
awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this)) awesomeBarComponent = AwesomeBarComponent(view.search_layout, this, ActionBusFactory.get(this))
ActionBusFactory.get(this).logMergedObservables() ActionBusFactory.get(this).logMergedObservables()
return view return view
} }

View File

@ -1,15 +1,23 @@
package org.mozilla.fenix.search.awesomebar package org.mozilla.fenix.search.awesomebar
/* This Source Code Form is subject to the terms of the Mozilla Public /* 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 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/. */ file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import android.util.Log
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.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.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.ViewState import org.mozilla.fenix.mvi.ViewState
data class AwesomeBarState( data class AwesomeBarState(
@ -32,25 +40,51 @@ 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) override var initialState: AwesomeBarState = AwesomeBarState("", false)
) : 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)
) { ) {
override val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)
is AwesomeBarChange.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = change.show)
is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
}
}
override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable) override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<AwesomeBarState> =
ViewModelProviders.of(owner, AwesomeBarViewModel.Factory(initialState, changesObservable))
.get(AwesomeBarViewModel::class.java).render(uiView)
init { init {
render(reducer) render()
}
}
class AwesomeBarViewModel(initialState: AwesomeBarState, changesObservable: Observable<AwesomeBarChange>) :
UIComponentViewModel<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: AwesomeBarState,
private val changesObservable: Observable<AwesomeBarChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
AwesomeBarViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
Log.d("IN_REDUCER", change.toString())
when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)
is AwesomeBarChange.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = change.show)
is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
}
}
} }
} }

View File

@ -6,6 +6,10 @@ 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
@ -13,6 +17,7 @@ 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.Change
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.PhoneFeature
@ -22,48 +27,25 @@ 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 override var initialState: QuickSettingsState
) : 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)
) { ) {
override val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change ->
when (change) {
is QuickSettingsChange.Change -> {
state.copy(
mode = QuickSettingsState.Mode.Normal(
change.url,
change.isSecured,
change.isTrackingProtectionOn,
change.sitePermissions
)
)
}
is QuickSettingsChange.PermissionGranted -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
is QuickSettingsChange.PromptRestarted -> {
state.copy(
mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid(change.sitePermissions)
)
}
is QuickSettingsChange.Stored -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
}
}
override fun initView(): UIView<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, changesObservable)).get(
QuickSettingsViewModel::class.java
).render(uiView)
init { init {
render(reducer) render()
} }
fun toggleSitePermission( fun toggleSitePermission(
@ -137,3 +119,52 @@ sealed class QuickSettingsChange : Change {
data class PromptRestarted(val sitePermissions: SitePermissions?) : QuickSettingsChange() data class PromptRestarted(val sitePermissions: SitePermissions?) : QuickSettingsChange()
data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange() data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange()
} }
class QuickSettingsViewModel(initialState: QuickSettingsState, changesObservable: Observable<QuickSettingsChange>) :
UIComponentViewModel<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: QuickSettingsState,
private val changesObservable: Observable<QuickSettingsChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
QuickSettingsViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change ->
when (change) {
is QuickSettingsChange.Change -> {
state.copy(
mode = QuickSettingsState.Mode.Normal(
change.url,
change.isSecured,
change.isTrackingProtectionOn,
change.sitePermissions
)
)
}
is QuickSettingsChange.PermissionGranted -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
is QuickSettingsChange.PromptRestarted -> {
state.copy(
mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid(change.sitePermissions)
)
}
is QuickSettingsChange.Stored -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
}
}
}
}

View File

@ -73,7 +73,19 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
return inflateRootView(container) val rootView = inflateRootView(container)
quickSettingsComponent = QuickSettingsComponent(
rootView as NestedScrollView, this, ActionBusFactory.get(this),
QuickSettingsState(
QuickSettingsState.Mode.Normal(
url,
isSecured,
isTrackingProtectionOn,
sitePermissions
)
)
)
return rootView
} }
private fun inflateRootView(container: ViewGroup? = null): View { private fun inflateRootView(container: ViewGroup? = null): View {
@ -115,21 +127,6 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
return this return this
} }
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
super.onViewCreated(rootView, savedInstanceState)
quickSettingsComponent = QuickSettingsComponent(
rootView as NestedScrollView, ActionBusFactory.get(this),
QuickSettingsState(
QuickSettingsState.Mode.Normal(
url,
isSecured,
isTrackingProtectionOn,
sitePermissions
)
)
)
}
companion object { companion object {
const val FRAGMENT_TAG = "QUICK_SETTINGS_FRAGMENT_TAG" const val FRAGMENT_TAG = "QUICK_SETTINGS_FRAGMENT_TAG"

View File

@ -35,7 +35,7 @@ class BookmarkComponentTest {
TestBookmarkComponent(mockk(), TestUtils.bus), TestBookmarkComponent(mockk(), TestUtils.bus),
recordPrivateCalls = true recordPrivateCalls = true
) )
bookmarkObserver = bookmarkComponent.internalRender(bookmarkComponent.reducer).test() bookmarkObserver = bookmarkComponent.render().test()
emitter = TestUtils.owner.getManagedEmitter() emitter = TestUtils.owner.getManagedEmitter()
} }
@ -87,7 +87,7 @@ class BookmarkComponentTest {
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
class TestBookmarkComponent(container: ViewGroup, bus: ActionBusFactory) : class TestBookmarkComponent(container: ViewGroup, bus: ActionBusFactory) :
BookmarkComponent(container, bus) { BookmarkComponent(container, mockk(relaxed = true), bus) {
override val uiView: UIView<BookmarkState, BookmarkAction, BookmarkChange> override val uiView: UIView<BookmarkState, BookmarkAction, BookmarkChange>
get() = mockk(relaxed = true) get() = mockk(relaxed = true)

View File

@ -34,7 +34,7 @@ class HistoryComponentTest {
TestHistoryComponent(mockk(), bus), TestHistoryComponent(mockk(), bus),
recordPrivateCalls = true recordPrivateCalls = true
) )
historyObserver = historyComponent.internalRender(historyComponent.reducer).test() historyObserver = historyComponent.render().test()
emitter = owner.getManagedEmitter() emitter = owner.getManagedEmitter()
} }
@ -82,7 +82,7 @@ class HistoryComponentTest {
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
class TestHistoryComponent(container: ViewGroup, bus: ActionBusFactory) : class TestHistoryComponent(container: ViewGroup, bus: ActionBusFactory) :
HistoryComponent(container, bus) { HistoryComponent(container, mockk(relaxed = true), bus) {
override val uiView: UIView<HistoryState, HistoryAction, HistoryChange> override val uiView: UIView<HistoryState, HistoryAction, HistoryChange>
get() = mockk(relaxed = true) get() = mockk(relaxed = true)

View File

@ -32,8 +32,8 @@ dependencies {
implementation Deps.kotlin_stdlib implementation Deps.kotlin_stdlib
implementation Deps.androidx_annotation implementation Deps.androidx_annotation
implementation Deps.androidx_lifecycle_runtime implementation Deps.androidx_lifecycle_extensions
implementation Deps.androidx_lifecycle_viewmodel_ss
implementation Deps.mozilla_support_base implementation Deps.mozilla_support_base
implementation Deps.rxAndroid implementation Deps.rxAndroid

View File

@ -90,6 +90,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
* @param clazz is the Event Class * @param clazz is the Event Class
* @param event is the instance of the Event to be sent * @param event is the instance of the Event to be sent
*/ */
@Suppress("UNCHECKED_CAST")
fun <T : Action> emit(clazz: Class<T>, event: T) { fun <T : Action> emit(clazz: Class<T>, event: T) {
val subject = if (map[clazz] != null) map[clazz] else create(clazz) val subject = if (map[clazz] != null) map[clazz] else create(clazz)
(subject as Subject<T>).onNext(event) (subject as Subject<T>).onNext(event)
@ -102,6 +103,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
* *
* @param clazz is the class of the event type used by this observable * @param clazz is the class of the event type used by this observable
*/ */
@Suppress("UNCHECKED_CAST")
fun <T : Action> getSafeManagedObservable(clazz: Class<T>): Observable<T> { fun <T : Action> getSafeManagedObservable(clazz: Class<T>): Observable<T> {
return if (map[clazz] != null) map[clazz] as Observable<T> else create(clazz) return if (map[clazz] != null) map[clazz] as Observable<T> else create(clazz)
} }
@ -111,6 +113,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(owner))) .`as`(autoDisposable(AndroidLifecycleScopeProvider.from(owner)))
} }
@Suppress("UNCHECKED_CAST")
fun <T : Action> getManagedEmitter(clazz: Class<T>): Observer<T> { fun <T : Action> getManagedEmitter(clazz: Class<T>): Observer<T> {
return if (map[clazz] != null) map[clazz] as Observer<T> else create(clazz) return if (map[clazz] != null) map[clazz] as Observer<T> else create(clazz)
} }
@ -132,7 +135,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
* Extension on [LifecycleOwner] used to emit an event. * Extension on [LifecycleOwner] used to emit an event.
*/ */
inline fun <reified T : Action> LifecycleOwner.emit(event: T) = inline fun <reified T : Action> LifecycleOwner.emit(event: T) =
kotlin.with(ActionBusFactory.get(this)) { with(ActionBusFactory.get(this)) {
getSafeManagedObservable(T::class.java) getSafeManagedObservable(T::class.java)
emit(T::class.java, event) emit(T::class.java, event)
} }
@ -152,10 +155,10 @@ inline fun <reified T : Action> LifecycleOwner.getManagedEmitter(): Observer<T>
/** /**
* This method returns a destroy observable that can be passed to [org.mozilla.fenix.mvi.UIView]s as needed. * This method returns a destroy observable that can be passed to [org.mozilla.fenix.mvi.UIView]s as needed.
*/ */
inline fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> { fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> {
return Observable.create { emitter -> return Observable.create { emitter ->
if (this == null || this.lifecycle.currentState == Lifecycle.State.DESTROYED) { if (this == null || this.lifecycle.currentState == Lifecycle.State.DESTROYED) {
emitter.onNext(kotlin.Unit) emitter.onNext(Unit)
emitter.onComplete() emitter.onComplete()
return@create return@create
} }
@ -163,7 +166,7 @@ inline fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun emitDestroy() { fun emitDestroy() {
if (emitter.isDisposed) { if (emitter.isDisposed) {
emitter.onNext(kotlin.Unit) emitter.onNext(Unit)
emitter.onComplete() emitter.onComplete()
} }
} }

View File

@ -4,6 +4,8 @@
package org.mozilla.fenix.mvi package org.mozilla.fenix.mvi
import androidx.fragment.app.Fragment
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
@ -11,28 +13,50 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
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>
) { ) {
abstract var initialState: S abstract var initialState: S
abstract val reducer: Reducer<S, C>
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>
}
open class UIComponentViewModel<S : ViewState, A : Action, C : Change>(
private val initialState: S,
val changesObservable: Observable<C>,
reducer: Reducer<S, C>
) : ViewModel() {
private val statesObservable: Observable<S> = internalRender(reducer)
private var statesDisposable: Disposable? = null
/** /**
* Render the ViewState to the View through the Reducer * Render the ViewState to the View through the Reducer
*/ */
fun render(reducer: Reducer<S, C>): Disposable = fun render(uiView: UIView<S, A, C>): Observable<S> {
internalRender(reducer) statesDisposable = statesObservable
.subscribe(uiView.updateView()) .subscribe(uiView.updateView())
return statesObservable
}
fun internalRender(reducer: Reducer<S, C>): Observable<S> = @Suppress("MemberVisibilityCanBePrivate")
protected fun internalRender(reducer: Reducer<S, C>): Observable<S> =
changesObservable changesObservable
.scan(initialState, reducer) .scan(initialState, reducer)
.distinctUntilChanged() .distinctUntilChanged()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
override fun onCleared() {
super.onCleared()
statesDisposable?.dispose()
}
} }

View File

@ -36,12 +36,16 @@ abstract class UIView<S : ViewState, A : Action, C : Change>(
/** /**
* Show the UIView * Show the UIView
*/ */
open fun show() { view.visibility = View.VISIBLE } open fun show() {
view.visibility = View.VISIBLE
}
/** /**
* Hide the UIView * Hide the UIView
*/ */
open fun hide() { view.visibility = View.GONE } open fun hide() {
view.visibility = View.GONE
}
/** /**
* Update the view from the ViewState * Update the view from the ViewState

View File

@ -23,6 +23,7 @@ private object Versions {
const val androidx_fragment = "1.1.0-alpha08" const val androidx_fragment = "1.1.0-alpha08"
const val androidx_navigation = "2.1.0-alpha03" const val androidx_navigation = "2.1.0-alpha03"
const val androidx_recyclerview = "1.1.0-alpha05" const val androidx_recyclerview = "1.1.0-alpha05"
const val androidx_lifecycle_savedstate = "1.0.0-alpha01"
const val androidx_testing = "1.1.0-alpha08" const val androidx_testing = "1.1.0-alpha08"
const val androidx_core = "1.2.0-alpha01" const val androidx_core = "1.2.0-alpha01"
const val androidx_transition = "1.1.0-rc01" const val androidx_transition = "1.1.0-rc01"
@ -134,8 +135,10 @@ object Deps {
const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}" const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}" const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}"
const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}" const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}"
const val androidx_lifecycle_extensions = "androidx.lifecycle:lifecycle-extensions:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}" const val androidx_lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle}" const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_viewmodel_ss = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.androidx_lifecycle_savedstate}"
const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}" const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}"
const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}" const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}"
const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment:${Versions.androidx_navigation}" const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment:${Versions.androidx_navigation}"