Fixes #127: Add architecture classes
parent
262df015b1
commit
69e9617272
|
@ -65,12 +65,23 @@ android.applicationVariants.all { variant ->
|
|||
println("Version code: " + variant.mergedFlavor.versionCode)
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation Deps.kotlin_stdlib
|
||||
implementation Deps.androidx_appcompat
|
||||
implementation Deps.androidx_constraintlayout
|
||||
|
||||
implementation Deps.rxAndroid
|
||||
implementation Deps.rxKotlin
|
||||
implementation Deps.anko_commons
|
||||
implementation Deps.anko_sdk
|
||||
implementation Deps.anko_appcompat
|
||||
implementation Deps.anko_constraintlayout
|
||||
|
||||
implementation Deps.mozilla_concept_engine
|
||||
implementation Deps.mozilla_concept_storage
|
||||
implementation Deps.mozilla_concept_toolbar
|
||||
|
|
|
@ -9,15 +9,15 @@ import android.transition.TransitionInflater
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.FragmentNavigator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.home.sessions.SessionsAdapter
|
||||
import org.mozilla.fenix.home.sessions.SessionsComponent
|
||||
import org.mozilla.fenix.home.sessions.layoutComponents
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
|
@ -27,8 +27,6 @@ class HomeFragment : Fragment() {
|
|||
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||
}
|
||||
|
||||
private lateinit var sessionsAdapter: SessionsAdapter
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
@ -36,8 +34,6 @@ class HomeFragment : Fragment() {
|
|||
menuButton.visibility = View.GONE
|
||||
privateBrowsingButton.visibility = View.GONE
|
||||
|
||||
sessionsAdapter = SessionsAdapter()
|
||||
|
||||
toolbar_wrapper.clipToOutline = false
|
||||
toolbar.setOnClickListener { it ->
|
||||
val extras = FragmentNavigator.Extras.Builder().addSharedElement(
|
||||
|
@ -46,11 +42,8 @@ class HomeFragment : Fragment() {
|
|||
Navigation.findNavController(it).navigate(R.id.action_homeFragment_to_searchFragment, null, null, extras)
|
||||
}
|
||||
|
||||
session_list.apply {
|
||||
adapter = sessionsAdapter
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
SessionsComponent(homeLayout, ActionBusFactory.get(this)).setup()
|
||||
layoutComponents(homeLayout)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.sessions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import mozilla.components.browser.session.Session
|
||||
import org.mozilla.fenix.mvi.*
|
||||
|
||||
class SessionsComponent(private val container: ViewGroup, override val bus: ActionBusFactory) :
|
||||
UIComponent<SessionsState, SessionsAction, SessionsChange>(bus) {
|
||||
|
||||
override var initialState: SessionsState = SessionsState(emptyList())
|
||||
|
||||
override val reducer : (SessionsState, SessionsChange) -> SessionsState = { state, change ->
|
||||
when (change) {
|
||||
is SessionsChange.SessionsChanged -> state // copy state with changes here
|
||||
}
|
||||
}
|
||||
|
||||
override fun initView() = SessionsUIView(container, bus)
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
fun setup(): SessionsComponent {
|
||||
render(reducer)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
data class SessionsState(val sessions: List<Session>) : ViewState
|
||||
|
||||
sealed class SessionsAction : Action {
|
||||
object Select : SessionsAction()
|
||||
}
|
||||
|
||||
sealed class SessionsChange : Change {
|
||||
object SessionsChanged : SessionsChange()
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.mozilla.fenix.home.sessions
|
||||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||
import kotlinx.android.synthetic.main.component_sessions.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.BOTTOM
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.TOP
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.START
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.END
|
||||
import org.jetbrains.anko.constraint.layout.applyConstraintSet
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
|
||||
fun HomeFragment.layoutComponents(layout: ConstraintLayout) {
|
||||
layout.applyConstraintSet {
|
||||
session_list {
|
||||
connect(
|
||||
BOTTOM to BOTTOM of PARENT_ID,
|
||||
START to START of PARENT_ID,
|
||||
END to END of PARENT_ID,
|
||||
TOP to BOTTOM of toolbar_wrapper
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.sessions
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.functions.Consumer
|
||||
import kotlinx.android.synthetic.main.component_sessions.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
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) {
|
||||
|
||||
val view: ConstraintLayout = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_sessions, container, true) as ConstraintLayout
|
||||
|
||||
private var sessionAdapter = SessionsAdapter()
|
||||
|
||||
init {
|
||||
session_list.apply {
|
||||
layoutManager = LinearLayoutManager(view.context)
|
||||
adapter = sessionAdapter
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateView() = Consumer<SessionsState> {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Netflix, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* Created by Juliano Moraes, Rohan Dhruva, Emmanuel Boudrant.
|
||||
*/
|
||||
package org.mozilla.fenix.mvi
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.rxkotlin.merge
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.reactivex.subjects.Subject
|
||||
|
||||
/**
|
||||
* It implements a Factory pattern generating Rx Subjects based on Event Types.
|
||||
* It maintain a map of Rx Subjects, one per type per instance of ActionBusFactory.
|
||||
*
|
||||
* @param owner is a LifecycleOwner used to auto dispose based on destroy observable
|
||||
*/
|
||||
class ActionBusFactory private constructor(val owner: LifecycleOwner) {
|
||||
|
||||
companion object {
|
||||
|
||||
private val buses = mutableMapOf<LifecycleOwner, ActionBusFactory>()
|
||||
|
||||
/**
|
||||
* Return the [ActionBusFactory] associated to the [LifecycleOwner]. It there is no bus it will create one.
|
||||
* If the [LifecycleOwner] used is a fragment it use [Fragment#getViewLifecycleOwner()]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun get(lifecycleOwner: LifecycleOwner): ActionBusFactory {
|
||||
return with(lifecycleOwner) {
|
||||
var bus = buses[lifecycleOwner]
|
||||
if (bus == null) {
|
||||
bus = ActionBusFactory(lifecycleOwner)
|
||||
buses[lifecycleOwner] = bus
|
||||
// LifecycleOwner
|
||||
lifecycleOwner.lifecycle.addObserver(bus.observer)
|
||||
}
|
||||
bus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
val map = HashMap<Class<*>, Subject<*>>()
|
||||
|
||||
internal val observer = object : LifecycleObserver {
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
fun onDestroy() {
|
||||
map.forEach { entry -> entry.value.onComplete() }
|
||||
buses.remove(owner)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> create(clazz: Class<T>): Subject<T> {
|
||||
val subject = PublishSubject.create<T>().toSerialized()
|
||||
map[clazz] = subject
|
||||
return subject
|
||||
}
|
||||
|
||||
/**
|
||||
* emit will create (if needed) or use the existing Rx Subject to send events.
|
||||
*
|
||||
* @param clazz is the Event Class
|
||||
* @param event is the instance of the Event to be sent
|
||||
*/
|
||||
fun <T : Action> emit(clazz: Class<T>, event: T) {
|
||||
val subject = if (map[clazz] != null) map[clazz] else create(clazz)
|
||||
(subject as Subject<T>).onNext(event)
|
||||
}
|
||||
|
||||
/**x
|
||||
* getSafeManagedObservable returns an Rx Observable which is
|
||||
* *Safe* against reentrant events as it is serialized and
|
||||
* *Managed* since it disposes itself based on the lifecycle
|
||||
*
|
||||
* @param clazz is the class of the event type used by this observable
|
||||
*/
|
||||
fun <T : Action> getSafeManagedObservable(clazz: Class<T>): Observable<T> {
|
||||
return if (map[clazz] != null) map[clazz] as Observable<T> else create(clazz)
|
||||
}
|
||||
|
||||
fun logMergedObservables() {
|
||||
// TODO make this observe new items in the map and combine them
|
||||
map.values.merge().compose(logState()).subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* getDestroyObservable observes to Lifecycle owner and fires when
|
||||
* lifecycle.currentState == Lifecycle.State.DESTROYED
|
||||
*/
|
||||
fun getDestroyObservable(): Observable<Unit> {
|
||||
return owner.createDestroyObservable()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension on [LifecycleOwner] used to emit an event.
|
||||
*/
|
||||
inline fun <reified T : Action> LifecycleOwner.emit(event: T) =
|
||||
kotlin.with(ActionBusFactory.get(this)) {
|
||||
getSafeManagedObservable(T::class.java)
|
||||
emit(T::class.java, event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension on [LifecycleOwner] used used to get the state observable.
|
||||
*/
|
||||
inline fun <reified T : Action> LifecycleOwner.getSafeManagedObservable(): Observable<T> =
|
||||
ActionBusFactory.get(this).getSafeManagedObservable(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]
|
||||
* because a viewholder can be reused across adapter destroys.
|
||||
*/
|
||||
inline fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> {
|
||||
return Observable.create { emitter ->
|
||||
if (this == null || this.lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
emitter.onNext(kotlin.Unit)
|
||||
emitter.onComplete()
|
||||
return@create
|
||||
}
|
||||
this.lifecycle.addObserver(object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
fun emitDestroy() {
|
||||
if (emitter.isDisposed) {
|
||||
emitter.onNext(kotlin.Unit)
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.mvi
|
||||
|
||||
import io.reactivex.ObservableTransformer
|
||||
import io.reactivex.subjects.Subject
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
|
||||
/**
|
||||
* An action is a command or intent the user performed
|
||||
*/
|
||||
interface Action
|
||||
|
||||
/**
|
||||
* A Change is a change to the view coming from the model
|
||||
* (Extending action so we can reuse the ActionBusFactory)
|
||||
*/
|
||||
interface Change : Action
|
||||
|
||||
/**
|
||||
* A ViewState is a model reflecting the current state of the view
|
||||
*/
|
||||
interface ViewState
|
||||
|
||||
/**
|
||||
* A Reducer applies changes to the ViewState
|
||||
*/
|
||||
typealias Reducer<S, C> = (S, C) -> S
|
||||
|
||||
/**
|
||||
* Simple logger for tracking ViewState changes
|
||||
*/
|
||||
fun <S> logState(): ObservableTransformer<S, S> = ObservableTransformer { observable ->
|
||||
observable.doOnNext {
|
||||
if (BuildConfig.DEBUG) Logger("State").debug(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For capturing state to a Subject for testing
|
||||
*/
|
||||
fun <S> captureState(subject: Subject<S>):
|
||||
ObservableTransformer<S, S> = ObservableTransformer { observable ->
|
||||
observable.doOnNext {
|
||||
subject.onNext(it)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.mvi
|
||||
|
||||
import io.reactivex.Observable
|
||||
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 var initialState: S
|
||||
abstract val reducer: Reducer<S, C>
|
||||
val uiView: UIView<S> by lazy { initView() }
|
||||
|
||||
abstract fun initView(): UIView<S>
|
||||
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 =
|
||||
bus.getSafeManagedObservable(C::class.java)
|
||||
.scan(initialState, reducer)
|
||||
.distinctUntilChanged()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(uiView.updateView())
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.mvi
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IdRes
|
||||
import io.reactivex.functions.Consumer
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
|
||||
abstract class UIView<S : ViewState>(
|
||||
private val container: ViewGroup, val bus: ActionBusFactory
|
||||
) : LayoutContainer {
|
||||
/**
|
||||
* Get the XML id for the UIView
|
||||
*/
|
||||
@get:IdRes
|
||||
val containerId: Int
|
||||
get() = container.id
|
||||
|
||||
/**
|
||||
* Provides container to empower Kotlin Android Extensions
|
||||
*/
|
||||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
/**
|
||||
* Show the UIView
|
||||
*/
|
||||
open fun show() { container.visibility = View.VISIBLE }
|
||||
|
||||
/**
|
||||
* Hide the UIView
|
||||
*/
|
||||
open fun hide() { container.visibility = View.GONE }
|
||||
|
||||
/**
|
||||
* Update the view from the ViewState
|
||||
*/
|
||||
abstract fun updateView(): Consumer<S>
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/session_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"/>
|
|
@ -71,14 +71,4 @@
|
|||
android:transitionName="firstTransitionName" />
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/session_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper" />
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
|
@ -6,6 +6,9 @@ private object Versions {
|
|||
const val kotlin = "1.3.11"
|
||||
const val android_gradle_plugin = "3.2.1"
|
||||
const val geckoNightly = "66.0.20190128092811"
|
||||
const val rxAndroid = "2.1.0"
|
||||
const val rxKotlin = "2.3.0"
|
||||
const val anko = "0.10.8"
|
||||
|
||||
const val androidx_appcompat = "1.0.2"
|
||||
const val androidx_constraint_layout = "2.0.0-alpha3"
|
||||
|
@ -26,6 +29,14 @@ object Deps {
|
|||
const val tools_kotlingradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
|
||||
const val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
|
||||
|
||||
const val rxKotlin = "io.reactivex.rxjava2:rxkotlin:${Versions.rxKotlin}"
|
||||
const val rxAndroid = "io.reactivex.rxjava2:rxandroid:${Versions.rxAndroid}"
|
||||
|
||||
const val anko_commons = "org.jetbrains.anko:anko-commons:${Versions.anko}"
|
||||
const val anko_sdk = "org.jetbrains.anko:anko-sdk25:${Versions.anko}"
|
||||
const val anko_appcompat = "org.jetbrains.anko:anko-appcompat-v7:${Versions.anko}"
|
||||
const val anko_constraintlayout = "org.jetbrains.anko:anko-constraint-layout:${Versions.anko}"
|
||||
|
||||
const val geckoview_nightly_arm = "org.mozilla.geckoview:geckoview-nightly-armeabi-v7a:${Versions.geckoNightly}"
|
||||
const val geckoview_nightly_x86 = "org.mozilla.geckoview:geckoview-nightly-x86:${Versions.geckoNightly}"
|
||||
|
||||
|
|
Loading…
Reference in New Issue