1
0
Fork 0

Enforce unidirectional arch better

master
Colin Lee 2019-01-31 00:49:41 -06:00 committed by Jeff Boek
parent 580fa1011f
commit 0120558fce
11 changed files with 100 additions and 80 deletions

View File

@ -8,7 +8,6 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.preference.PreferenceManager import android.preference.PreferenceManager
import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage
@ -19,7 +18,6 @@ import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.lib.crash.handler.CrashHandlerService import mozilla.components.lib.crash.handler.CrashHandlerService
import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings import org.mozilla.geckoview.GeckoRuntimeSettings
import java.util.concurrent.TimeUnit
/** /**
* Component group for all core browser functionality. * Component group for all core browser functionality.

View File

@ -4,10 +4,8 @@
package org.mozilla.fenix.home.sessions package org.mozilla.fenix.home.sessions
import android.annotation.SuppressLint
import android.view.ViewGroup import android.view.ViewGroup
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.support.base.log.logger.Logger
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
@ -16,10 +14,13 @@ import org.mozilla.fenix.mvi.ViewState
class SessionsComponent( class SessionsComponent(
private val container: ViewGroup, private val container: ViewGroup,
override val bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: SessionsState = SessionsState(emptyList()) override var initialState: SessionsState = SessionsState(emptyList())
) : ) :
UIComponent<SessionsState, SessionsAction, SessionsChange>(bus) { UIComponent<SessionsState, SessionsAction, SessionsChange>(
bus.getManagedEmitter(SessionsAction::class.java),
bus.getSafeManagedObservable(SessionsChange::class.java)
) {
override val reducer: (SessionsState, SessionsChange) -> SessionsState = { state, change -> override val reducer: (SessionsState, SessionsChange) -> SessionsState = { state, change ->
when (change) { when (change) {
@ -27,20 +28,10 @@ class SessionsComponent(
} }
} }
override fun initView() = SessionsUIView(container, bus) override fun initView() = SessionsUIView(container, actionEmitter, changesObservable)
init { init {
setup()
}
@SuppressLint("CheckResult")
fun setup(): SessionsComponent {
render(reducer) render(reducer)
getUserInteractionEvents<SessionsAction>()
.subscribe {
Logger("SessionsComponent").debug(it.toString())
}
return this
} }
} }

View File

@ -8,13 +8,18 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
class SessionsUIView(container: ViewGroup, bus: ActionBusFactory) : class SessionsUIView(
UIView<SessionsState>(container, bus) { container: ViewGroup,
actionEmitter: Observer<SessionsAction>,
changesObservable: Observable<SessionsChange>
) :
UIView<SessionsState, SessionsAction, SessionsChange>(container, actionEmitter, changesObservable) {
override val view: RecyclerView = LayoutInflater.from(container.context) override val view: RecyclerView = LayoutInflater.from(container.context)
.inflate(R.layout.component_sessions, container, true) .inflate(R.layout.component_sessions, container, true)

View File

@ -27,6 +27,7 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.rxkotlin.merge import io.reactivex.rxkotlin.merge
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject import io.reactivex.subjects.Subject
@ -102,6 +103,10 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
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)
} }
fun <T : Action> getManagedEmitter(clazz: Class<T>): Observer<T> {
return if (map[clazz] != null) map[clazz] as Observer<T> else create(clazz)
}
fun logMergedObservables() { fun logMergedObservables() {
// TODO make this observe new items in the map and combine them // TODO make this observe new items in the map and combine them
map.values.merge().compose(logState()).subscribe() map.values.merge().compose(logState()).subscribe()
@ -131,6 +136,9 @@ inline fun <reified T : Action> LifecycleOwner.emit(event: T) =
inline fun <reified T : Action> LifecycleOwner.getSafeManagedObservable(): Observable<T> = inline fun <reified T : Action> LifecycleOwner.getSafeManagedObservable(): Observable<T> =
ActionBusFactory.get(this).getSafeManagedObservable(T::class.java) ActionBusFactory.get(this).getSafeManagedObservable(T::class.java)
inline fun <reified T : Action> LifecycleOwner.getManagedEmitter(): Observer<T> =
ActionBusFactory.get(this).getManagedEmitter(T::class.java)
/** /**
* 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.
* This is deliberately scoped to the attached [LifecycleOwner]'s [Lifecycle.Event.ON_DESTROY] * This is deliberately scoped to the attached [LifecycleOwner]'s [Lifecycle.Event.ON_DESTROY]

View File

@ -5,29 +5,28 @@
package org.mozilla.fenix.mvi package org.mozilla.fenix.mvi
import io.reactivex.Observable import io.reactivex.Observable
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.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
abstract class UIComponent<S : ViewState, A : Action, C : Change>(open val bus: ActionBusFactory) { abstract class UIComponent<S : ViewState, A : Action, C : Change>(
protected val actionEmitter: Observer<A>,
protected val changesObservable: Observable<C>
) {
abstract var initialState: S abstract var initialState: S
abstract val reducer: Reducer<S, C> abstract val reducer: Reducer<S, C>
val uiView: UIView<S> by lazy { initView() } val uiView: UIView<S, A, C> by lazy { initView() }
abstract fun initView(): UIView<S> abstract fun initView(): UIView<S, A, C>
open fun getContainerId() = uiView.containerId open fun getContainerId() = uiView.containerId
inline fun <reified A : Action> getUserInteractionEvents():
Observable<A> = bus.getSafeManagedObservable(A::class.java)
inline fun <reified C : Change> getModelChangeEvents():
Observable<C> = bus.getSafeManagedObservable(C::class.java)
/** /**
* Render the ViewState to the View through the Reducer * Render the ViewState to the View through the Reducer
*/ */
inline fun <reified C : Change> render(noinline reducer: Reducer<S, C>): Disposable = fun render(reducer: Reducer<S, C>): Disposable =
getModelChangeEvents<C>() changesObservable
.scan(initialState, reducer) .scan(initialState, reducer)
.distinctUntilChanged() .distinctUntilChanged()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())

View File

@ -7,12 +7,15 @@ package org.mozilla.fenix.mvi
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.IdRes import androidx.annotation.IdRes
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
abstract class UIView<S : ViewState>( abstract class UIView<S : ViewState, A : Action, C : Change>(
private val container: ViewGroup, private val container: ViewGroup,
val bus: ActionBusFactory protected val actionEmitter: Observer<A>,
protected val changesObservable: Observable<C>
) : LayoutContainer { ) : LayoutContainer {
abstract val view: View abstract val view: View

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.search package org.mozilla.fenix.search
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -13,10 +14,13 @@ import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_search.view.* import kotlinx.android.synthetic.main.fragment_search.view.*
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.getSafeManagedObservable
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.toolbar.* import org.mozilla.fenix.search.toolbar.SearchAction
import org.mozilla.fenix.search.toolbar.ToolbarComponent
import org.mozilla.fenix.search.toolbar.ToolbarUIView
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
private lateinit var toolbarComponent: ToolbarComponent private lateinit var toolbarComponent: ToolbarComponent
@ -30,6 +34,7 @@ class SearchFragment : Fragment() {
val view = inflater.inflate(R.layout.fragment_search, container, false) val view = inflater.inflate(R.layout.fragment_search, container, false)
toolbarComponent = ToolbarComponent(view.toolbar_wrapper, ActionBusFactory.get(this)) toolbarComponent = ToolbarComponent(view.toolbar_wrapper, ActionBusFactory.get(this))
awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this)) awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this))
ActionBusFactory.get(this).logMergedObservables()
return view return view
} }
@ -38,6 +43,7 @@ class SearchFragment : Fragment() {
toolbarComponent.editMode() toolbarComponent.editMode()
} }
@SuppressLint("CheckResult")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -47,26 +53,18 @@ class SearchFragment : Fragment() {
view.toolbar_wrapper.clipToOutline = false view.toolbar_wrapper.clipToOutline = false
toolbarComponent getSafeManagedObservable<SearchAction>()
.getModelChangeEvents<SearchChange>()
.subscribe { .subscribe {
when (it) { when (it) {
is SearchChange.QueryChanged -> { is SearchAction.UrlCommitted -> transitionToBrowser()
ActionBusFactory.get(this).emit(AwesomeBarChange::class.java, AwesomeBarChange.UpdateQuery(it.query)) is SearchAction.TextChanged -> {
ActionBusFactory.get(this)
.emit(AwesomeBarChange::class.java, AwesomeBarChange.UpdateQuery(it.query))
} }
} }
} }
toolbarComponent getSafeManagedObservable<AwesomeBarAction>()
.getUserInteractionEvents<SearchAction>()
.subscribe {
when (it) {
is SearchAction.UrlCommitted -> transitionToBrowser()
}
}
awesomeBarComponent
.getUserInteractionEvents<AwesomeBarAction>()
.subscribe { .subscribe {
when (it) { when (it) {
is AwesomeBarAction.ItemSelected -> transitionToBrowser() is AwesomeBarAction.ItemSelected -> transitionToBrowser()

View File

@ -4,32 +4,38 @@ 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 org.mozilla.fenix.mvi.* import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.ViewState
data class AwesomeBarState(val query: String) : ViewState { data class AwesomeBarState(val query: String) : ViewState
fun updateQuery(query: String) = AwesomeBarState(query)
}
sealed class AwesomeBarAction: Action { sealed class AwesomeBarAction : Action {
object ItemSelected: AwesomeBarAction() object ItemSelected : AwesomeBarAction()
} }
sealed class AwesomeBarChange : Change { sealed class AwesomeBarChange : Change {
data class UpdateQuery(val query: String): AwesomeBarChange() data class UpdateQuery(val query: String) : AwesomeBarChange()
} }
class AwesomeBarComponent( class AwesomeBarComponent(
private val container: ViewGroup, private val container: ViewGroup,
override val bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: AwesomeBarState = AwesomeBarState("") override var initialState: AwesomeBarState = AwesomeBarState("")
) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(bus) { ) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
bus.getManagedEmitter(AwesomeBarAction::class.java),
bus.getSafeManagedObservable(AwesomeBarChange::class.java)
) {
override val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change -> override val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
when (change) { when (change) {
is AwesomeBarChange.UpdateQuery -> state.updateQuery(change.query) is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
} }
} }
override fun initView() = AwesomeBarUIView(container, bus) override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable)
init { init {
render(reducer) render(reducer)

View File

@ -5,6 +5,8 @@ package org.mozilla.fenix.search.awesomebar
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
import mozilla.components.browser.awesomebar.BrowserAwesomeBar import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
@ -13,11 +15,14 @@ import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.support.ktx.android.graphics.drawable.toBitmap import mozilla.components.support.ktx.android.graphics.drawable.toBitmap
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
class AwesomeBarUIView(container: ViewGroup, bus: ActionBusFactory) : class AwesomeBarUIView(
UIView<AwesomeBarState>(container, bus) { container: ViewGroup,
actionEmitter: Observer<AwesomeBarAction>,
changesObservable: Observable<AwesomeBarChange>
) :
UIView<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(container, actionEmitter, changesObservable) {
override val view: BrowserAwesomeBar = LayoutInflater.from(container.context) override val view: BrowserAwesomeBar = LayoutInflater.from(container.context)
.inflate(R.layout.component_awesomebar, container, true) .inflate(R.layout.component_awesomebar, container, true)
.findViewById(R.id.awesomeBar) .findViewById(R.id.awesomeBar)
@ -31,18 +36,19 @@ class AwesomeBarUIView(container: ViewGroup, bus: ActionBusFactory) :
getString(R.string.awesomebar_clipboard_title) getString(R.string.awesomebar_clipboard_title)
) )
) )
view.addProviders(SessionSuggestionProvider(components.core.sessionManager, components.useCases.tabsUseCases.selectTab)) view.addProviders(SessionSuggestionProvider(components.core.sessionManager,
components.useCases.tabsUseCases.selectTab))
view.addProviders(SearchSuggestionProvider( view.addProviders(SearchSuggestionProvider(
components.search.searchEngineManager.getDefaultSearchEngine(this), components.search.searchEngineManager.getDefaultSearchEngine(this),
components.useCases.searchUseCases.defaultSearch, components.useCases.searchUseCases.defaultSearch,
SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS) SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS)
) )
view.setOnStopListener { bus.emit(AwesomeBarAction::class.java, AwesomeBarAction.ItemSelected) } view.setOnStopListener { actionEmitter.onNext(AwesomeBarAction.ItemSelected) }
} }
} }
override fun updateView() = Consumer<AwesomeBarState> { override fun updateView() = Consumer<AwesomeBarState> {
view.onInputChanged(it.query) view.onInputChanged(it.query)
} }
} }

View File

@ -16,18 +16,21 @@ import org.mozilla.fenix.mvi.ViewState
class ToolbarComponent( class ToolbarComponent(
private val container: ViewGroup, private val container: ViewGroup,
override val bus: ActionBusFactory, bus: ActionBusFactory,
override var initialState: SearchState = SearchState("") override var initialState: SearchState = SearchState("")
) : ) :
UIComponent<SearchState, SearchAction, SearchChange>(bus) { UIComponent<SearchState, SearchAction, SearchChange>(
bus.getManagedEmitter(SearchAction::class.java),
bus.getSafeManagedObservable(SearchChange::class.java)
) {
override val reducer: Reducer<SearchState, SearchChange> = { state, change -> override val reducer: Reducer<SearchState, SearchChange> = { state, change ->
when (change) { when (change) {
is SearchChange.QueryChanged -> state.updateQuery(change.query) is SearchChange.QueryChanged -> state.copy(query = change.query)
} }
} }
override fun initView() = ToolbarUIView(container, bus) override fun initView() = ToolbarUIView(container, actionEmitter, changesObservable)
init { init {
render(reducer) render(reducer)
} }
@ -36,12 +39,11 @@ class ToolbarComponent(
fun editMode() = getView().editMode() fun editMode() = getView().editMode()
} }
data class SearchState(val query: String) : ViewState { data class SearchState(val query: String) : ViewState
fun updateQuery(query: String) = SearchState(query)
}
sealed class SearchAction : Action { sealed class SearchAction : Action {
data class UrlCommitted(val url: String): SearchAction() data class UrlCommitted(val url: String) : SearchAction()
data class TextChanged(val query: String) : SearchAction()
} }
sealed class SearchChange : Change { sealed class SearchChange : Change {

View File

@ -7,6 +7,8 @@ package org.mozilla.fenix.search.toolbar
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer import io.reactivex.functions.Consumer
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
@ -14,12 +16,14 @@ import mozilla.components.support.ktx.android.content.res.pxToDp
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
class ToolbarUIView(container: ViewGroup, bus: ActionBusFactory) : class ToolbarUIView(
UIView<SearchState>(container, bus) { container: ViewGroup,
actionEmitter: Observer<SearchAction>,
changesObservable: Observable<SearchChange>
) :
UIView<SearchState, SearchAction, SearchChange>(container, actionEmitter, changesObservable) {
val toolbarIntegration: ToolbarIntegration val toolbarIntegration: ToolbarIntegration
@ -33,7 +37,7 @@ class ToolbarUIView(container: ViewGroup, bus: ActionBusFactory) :
init { init {
view.apply { view.apply {
onUrlClicked = { false } onUrlClicked = { false }
setOnUrlCommitListener { bus.emit(SearchAction::class.java, SearchAction.UrlCommitted(it)) } setOnUrlCommitListener { actionEmitter.onNext(SearchAction.UrlCommitted(it)) }
browserActionMargin = resources.pxToDp(browserActionMarginDp) browserActionMargin = resources.pxToDp(browserActionMarginDp)
urlBoxView = urlBackground urlBoxView = urlBackground
@ -46,11 +50,11 @@ class ToolbarUIView(container: ViewGroup, bus: ActionBusFactory) :
setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener { setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onTextChanged(text: String) { override fun onTextChanged(text: String) {
bus.emit(SearchChange::class.java, SearchChange.QueryChanged(text)) actionEmitter.onNext(SearchAction.TextChanged(text))
} }
override fun onStopEditing() { override fun onStopEditing() {
bus.emit(SearchAction::class.java, SearchAction.UrlCommitted("foo")) actionEmitter.onNext(SearchAction.UrlCommitted("foo"))
} }
}) })
} }