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.preference.PreferenceManager
import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.storage.SessionStorage
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 org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import java.util.concurrent.TimeUnit
/**
* Component group for all core browser functionality.

View File

@ -4,10 +4,8 @@
package org.mozilla.fenix.home.sessions
import android.annotation.SuppressLint
import android.view.ViewGroup
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.ActionBusFactory
import org.mozilla.fenix.mvi.Change
@ -16,10 +14,13 @@ import org.mozilla.fenix.mvi.ViewState
class SessionsComponent(
private val container: ViewGroup,
override val bus: ActionBusFactory,
bus: ActionBusFactory,
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 ->
when (change) {
@ -27,20 +28,10 @@ class SessionsComponent(
}
}
override fun initView() = SessionsUIView(container, bus)
override fun initView() = SessionsUIView(container, actionEmitter, changesObservable)
init {
setup()
}
@SuppressLint("CheckResult")
fun setup(): SessionsComponent {
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 androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView
class SessionsUIView(container: ViewGroup, bus: ActionBusFactory) :
UIView<SessionsState>(container, bus) {
class SessionsUIView(
container: ViewGroup,
actionEmitter: Observer<SessionsAction>,
changesObservable: Observable<SessionsChange>
) :
UIView<SessionsState, SessionsAction, SessionsChange>(container, actionEmitter, changesObservable) {
override val view: RecyclerView = LayoutInflater.from(container.context)
.inflate(R.layout.component_sessions, container, true)

View File

@ -27,6 +27,7 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.rxkotlin.merge
import io.reactivex.subjects.PublishSubject
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)
}
fun <T : Action> getManagedEmitter(clazz: Class<T>): Observer<T> {
return if (map[clazz] != null) map[clazz] as Observer<T> else create(clazz)
}
fun logMergedObservables() {
// TODO make this observe new items in the map and combine them
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> =
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 is deliberately scoped to the attached [LifecycleOwner]'s [Lifecycle.Event.ON_DESTROY]

View File

@ -5,29 +5,28 @@
package org.mozilla.fenix.mvi
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
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 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
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
*/
inline fun <reified C : Change> render(noinline reducer: Reducer<S, C>): Disposable =
getModelChangeEvents<C>()
fun render(reducer: Reducer<S, C>): Disposable =
changesObservable
.scan(initialState, reducer)
.distinctUntilChanged()
.subscribeOn(Schedulers.io())

View File

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

View File

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

View File

@ -5,6 +5,8 @@ package org.mozilla.fenix.search.awesomebar
import android.view.LayoutInflater
import android.view.ViewGroup
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
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 org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView
class AwesomeBarUIView(container: ViewGroup, bus: ActionBusFactory) :
UIView<AwesomeBarState>(container, bus) {
class AwesomeBarUIView(
container: ViewGroup,
actionEmitter: Observer<AwesomeBarAction>,
changesObservable: Observable<AwesomeBarChange>
) :
UIView<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(container, actionEmitter, changesObservable) {
override val view: BrowserAwesomeBar = LayoutInflater.from(container.context)
.inflate(R.layout.component_awesomebar, container, true)
.findViewById(R.id.awesomeBar)
@ -31,18 +36,19 @@ class AwesomeBarUIView(container: ViewGroup, bus: ActionBusFactory) :
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(
components.search.searchEngineManager.getDefaultSearchEngine(this),
components.useCases.searchUseCases.defaultSearch,
SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS)
)
view.setOnStopListener { bus.emit(AwesomeBarAction::class.java, AwesomeBarAction.ItemSelected) }
view.setOnStopListener { actionEmitter.onNext(AwesomeBarAction.ItemSelected) }
}
}
override fun updateView() = Consumer<AwesomeBarState> {
view.onInputChanged(it.query)
}
}
}

View File

@ -16,18 +16,21 @@ import org.mozilla.fenix.mvi.ViewState
class ToolbarComponent(
private val container: ViewGroup,
override val bus: ActionBusFactory,
bus: ActionBusFactory,
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 ->
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 {
render(reducer)
}
@ -36,12 +39,11 @@ class ToolbarComponent(
fun editMode() = getView().editMode()
}
data class SearchState(val query: String) : ViewState {
fun updateQuery(query: String) = SearchState(query)
}
data class SearchState(val query: String) : ViewState
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 {

View File

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