1
0
Fork 0

Fixes #127: Add architecture classes

master
Colin Lee 2019-01-28 10:46:39 -06:00
parent 262df015b1
commit 69e9617272
12 changed files with 420 additions and 22 deletions

View File

@ -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

View File

@ -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?) {

View File

@ -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()
}

View File

@ -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
)
}
}
}

View File

@ -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> {
}
}

View File

@ -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()
}
}
})
}
}

View File

@ -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)
}
}

View File

@ -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())
}

View File

@ -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>
}

View File

@ -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"/>

View File

@ -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>

View File

@ -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}"