Flatten hierarchy in presentation module

This commit is contained in:
Moez Bhatti
2018-03-22 01:43:09 -04:00
parent 2e4d96059a
commit 7212983ffe
95 changed files with 51 additions and 65 deletions
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package presentation.common.base
import android.support.annotation.CallSuper
import android.support.v7.widget.RecyclerView
import io.reactivex.Flowable
import io.reactivex.disposables.Disposable
/**
* Base RecyclerView.Adapter that provides some convenience when creating a new Adapter, such as
* data list handing and item animations
*/
abstract class FlowableAdapter<T> : QkAdapter<T>() {
var flowable: Flowable<List<T>>? = null
set(value) {
if (field == value) return
field = value
// Stop listening for updates on the old flowable
dispose()
// If we're attached to any RecyclerViews, then subscribe to updates
if (recyclerViews.isNotEmpty()) {
subscribe()
}
}
private val recyclerViews = ArrayList<RecyclerView>()
private var disposable: Disposable? = null
@CallSuper
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
// If this is the first RecyclerView to be attached, then start listening to updates
if (recyclerViews.isEmpty() || disposable == null) {
subscribe()
}
recyclerViews.add(recyclerView)
}
@CallSuper
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
recyclerViews.remove(recyclerView)
// If no more RecyclerViews are attached, stop listening to updates
if (recyclerViews.isEmpty()) {
dispose()
}
}
private fun subscribe() {
disposable = flowable?.subscribe { data = it }
}
private fun dispose() {
disposable?.dispose()
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package presentation.common.base
import android.annotation.SuppressLint
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.toolbar.*
import presentation.common.Navigator
import kotlin.reflect.KClass
abstract class QkActivity<VM : QkViewModel<*, *>> : AppCompatActivity() {
protected abstract val viewModelClass: KClass<VM>
protected val viewModel: VM by lazy {
ViewModelProviders.of(this, Navigator.ViewModelFactory(intent))[viewModelClass.java]
}
protected val menu: Subject<Menu> = BehaviorSubject.create()
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onNewIntent(intent)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
setSupportActionBar(toolbar)
title = title // The title may have been set before layout inflation
}
override fun setTitle(title: CharSequence?) {
super.setTitle(title)
toolbarTitle?.text = title
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val result = super.onCreateOptionsMenu(menu)
if (menu != null) {
this.menu.onNext(menu)
}
return result
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package presentation.common.base
import android.support.v7.util.DiffUtil
import android.support.v7.widget.RecyclerView
/**
* Base RecyclerView.Adapter that provides some convenience when creating a new Adapter, such as
* data list handing and item animations
*/
abstract class QkAdapter<T> : RecyclerView.Adapter<QkViewHolder>() {
var data: List<T> = ArrayList()
set(value) {
val diff = DiffUtil.calculateDiff(getDiffUtilCallback(field, value))
field = value
diff.dispatchUpdatesTo(this)
onDatasetChanged()
}
fun getItem(position: Int): T {
return data[position]
}
override fun getItemCount(): Int {
return data.size
}
open fun onDatasetChanged() {}
/**
* Allows the adapter implementation to provide a custom DiffUtil.Callback
* If not, then the abstract implementation will be used
*/
private fun getDiffUtilCallback(oldData: List<T>, newData: List<T>): DiffUtil.Callback {
return object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
areItemsTheSame(oldData[oldItemPosition], newData[newItemPosition])
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
areContentsTheSame(oldData[oldItemPosition], newData[newItemPosition])
override fun getOldListSize() = oldData.size
override fun getNewListSize() = newData.size
}
}
open protected fun areItemsTheSame(old: T, new: T): Boolean {
return old == new
}
open protected fun areContentsTheSame(old: T, new: T): Boolean {
return old == new
}
}
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package presentation.common.base
import android.annotation.SuppressLint
import android.arch.lifecycle.Lifecycle
import android.os.Bundle
import com.moez.QKSMS.R
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.kotlin.autoDisposable
import common.util.Colors
import io.reactivex.rxkotlin.Observables
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.toolbar.*
import javax.inject.Inject
/**
* Base activity that automatically applies any necessary theme theme settings and colors
*
* In most cases, this should be used instead of the base QkActivity, except for when
* an activity does not depend on the theme
*/
abstract class QkThemedActivity<VM : QkViewModel<*, *>> : QkActivity<VM>() {
@Inject lateinit var colors: Colors
/**
* In case the activity should be themed for a specific conversation, the selected conversation
* can be changed by pushing the threadId to this subject
*/
protected val threadId: Subject<Long> = BehaviorSubject.createDefault(0)
/**
* Switch the theme if the threadId changes
*/
protected val theme = threadId
.distinctUntilChanged()
.switchMap { threadId -> colors.themeForConversation(threadId) }
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
colors.appThemeResources
.autoDisposable(scope())
.subscribe { res -> setTheme(res) }
colors.statusBarIcons
.autoDisposable(scope())
.subscribe { systemUiVisibility -> window.decorView.systemUiVisibility = systemUiVisibility }
colors.statusBar
.autoDisposable(scope())
.subscribe { color -> window.statusBarColor = color }
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// Update the colours of the menu items
Observables.combineLatest(menu, theme, colors.textTertiary, { menu, theme, textTertiary ->
(0 until menu.size())
.map { position -> menu.getItem(position) }
.forEach { menuItem ->
menuItem?.icon?.run {
setTint(when (menuItem.itemId) {
R.id.info -> textTertiary
else -> theme
})
menuItem.icon = this
}
}
}).autoDisposable(scope(Lifecycle.Event.ON_DESTROY)).subscribe()
colors.textTertiary
.doOnNext { color -> toolbar?.overflowIcon = toolbar?.overflowIcon?.apply { setTint(color) } }
.doOnNext { color -> toolbar?.navigationIcon = toolbar?.navigationIcon?.apply { setTint(color) } }
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe()
colors.popupThemeResource
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe { res -> toolbar?.popupTheme = res }
colors.toolbarColor
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe { color -> toolbar?.setBackgroundColor(color) }
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package presentation.common.base
import android.arch.lifecycle.LifecycleOwner
interface QkView<in State> : LifecycleOwner {
fun render(state: State)
}
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package presentation.common.base
import android.support.v7.widget.RecyclerView
import android.view.View
class QkViewHolder(view: View) : RecyclerView.ViewHolder(view)
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package presentation.common.base
import android.arch.lifecycle.ViewModel
import android.support.annotation.CallSuper
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.kotlin.autoDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.subjects.BehaviorSubject
abstract class QkViewModel<in View : QkView<State>, State>(initialState: State) : ViewModel() {
protected val state: BehaviorSubject<State> = BehaviorSubject.createDefault(initialState)
protected val disposables = CompositeDisposable()
@CallSuper
open fun bindView(view: View) {
state
.observeOn(AndroidSchedulers.mainThread())
.autoDisposable(view.scope())
.subscribe { view.render(it) }
}
protected fun newState(reducer: (State) -> State) {
state.value?.let { state.onNext(reducer(it)) }
}
override fun onCleared() {
super.onCleared()
disposables.dispose()
}
}