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) println("Version code: " + variant.mergedFlavor.versionCode)
} }
androidExtensions {
experimental = true
}
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation Deps.kotlin_stdlib implementation Deps.kotlin_stdlib
implementation Deps.androidx_appcompat implementation Deps.androidx_appcompat
implementation Deps.androidx_constraintlayout 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_engine
implementation Deps.mozilla_concept_storage implementation Deps.mozilla_concept_storage
implementation Deps.mozilla_concept_toolbar implementation Deps.mozilla_concept_toolbar

View File

@ -9,15 +9,15 @@ import android.transition.TransitionInflater
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.navigation.fragment.FragmentNavigator import androidx.navigation.fragment.FragmentNavigator
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import org.mozilla.fenix.R 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() { class HomeFragment : Fragment() {
override fun onCreateView( override fun onCreateView(
@ -27,8 +27,6 @@ class HomeFragment : Fragment() {
return inflater.inflate(R.layout.fragment_home, container, false) return inflater.inflate(R.layout.fragment_home, container, false)
} }
private lateinit var sessionsAdapter: SessionsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -36,8 +34,6 @@ class HomeFragment : Fragment() {
menuButton.visibility = View.GONE menuButton.visibility = View.GONE
privateBrowsingButton.visibility = View.GONE privateBrowsingButton.visibility = View.GONE
sessionsAdapter = SessionsAdapter()
toolbar_wrapper.clipToOutline = false toolbar_wrapper.clipToOutline = false
toolbar.setOnClickListener { it -> toolbar.setOnClickListener { it ->
val extras = FragmentNavigator.Extras.Builder().addSharedElement( 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) Navigation.findNavController(it).navigate(R.id.action_homeFragment_to_searchFragment, null, null, extras)
} }
session_list.apply { SessionsComponent(homeLayout, ActionBusFactory.get(this)).setup()
adapter = sessionsAdapter layoutComponents(homeLayout)
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
}
} }
override fun onCreate(savedInstanceState: Bundle?) { 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" /> android:transitionName="firstTransitionName" />
</FrameLayout> </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> </androidx.constraintlayout.motion.widget.MotionLayout>

View File

@ -6,6 +6,9 @@ private object Versions {
const val kotlin = "1.3.11" const val kotlin = "1.3.11"
const val android_gradle_plugin = "3.2.1" const val android_gradle_plugin = "3.2.1"
const val geckoNightly = "66.0.20190128092811" 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_appcompat = "1.0.2"
const val androidx_constraint_layout = "2.0.0-alpha3" 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 tools_kotlingradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${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_arm = "org.mozilla.geckoview:geckoview-nightly-armeabi-v7a:${Versions.geckoNightly}"
const val geckoview_nightly_x86 = "org.mozilla.geckoview:geckoview-nightly-x86:${Versions.geckoNightly}" const val geckoview_nightly_x86 = "org.mozilla.geckoview:geckoview-nightly-x86:${Versions.geckoNightly}"