build errors
This commit is contained in:
commit
29ea6350e7
|
|
@ -0,0 +1,2 @@
|
|||
# Ответственный за все модули
|
||||
* @maxbach
|
||||
15
README.md
15
README.md
|
|
@ -4,11 +4,9 @@ Roboswag - библиотека решений, ускоряющих разра
|
|||
|
||||
## Минимальные требования
|
||||
|
||||
* Andoroid Api: 19
|
||||
* Android Api: 21
|
||||
* Kotlin: 1.3.11
|
||||
* Gradle: 3.2.1
|
||||
* Gradle CPD Plugin: 1.1
|
||||
* Detekt Plugin: 1.0.0-RC12
|
||||
* Gradle: 4.0.0
|
||||
|
||||
## Основная архитектура
|
||||
За основу архитектуры взят подход от Google - MVVM на основе [Android Architecture Components](https://developer.android.com/jetpack/docs/guide). Данный подход популярен в сообществе Android разработки, позволяет разбивать код на мелкие и независимые части, что ускоряет разработку и последующую поддержку приложения.
|
||||
|
|
@ -22,12 +20,8 @@ Roboswag позволяет сочетать эти три решения в о
|
|||
## Основные инструменты библиотеки
|
||||
### Работа с RecyclerView
|
||||
RecyclerView - один из самых часто используемых инструментов Android разработчика. Модуль [recyclerview-adapters](/recyclerview-adapters) позволяет сделать работу с RecyclerView более гибкой и делает работу самого элемента быстрее.
|
||||
### BuildScripts
|
||||
[BuildScrpts](https://github.com/TouchInstinct/BuildScripts) - набор скриптов, автоматизирующих разработку. Один из главных скриптов - staticAnalysis - инструмент для автоматической проверки кода на соответствие правилам компании.
|
||||
### Api Generator
|
||||
Внутренний инструмент компании Touch Instinct для генерации общего кода на разные платформы - Android, iOS и Server. Описанные в одном месте общие классы и Http методы используются на разных платформах. Данный инструмент позволяет сократить время разработки в два раза.
|
||||
### Работа с SharedPreferences
|
||||
Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences.
|
||||
Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. Для шифрования данных в SharedPreferences можно использовать [encrypted-shared-prefs](/encrypted-shared-prefs)
|
||||
### Утилиты и extension функции
|
||||
В Roboswag также есть много [утилитарных](/utils) классов и [extension](/kotlin-extensions) функций, которые позволяют писать часто используемый код в одну строку.
|
||||
|
||||
|
|
@ -68,7 +62,8 @@ gradle.ext.roboswag = [
|
|||
'tabbar-navigation',
|
||||
'base-map',
|
||||
'yandex-map',
|
||||
'google-map'
|
||||
'google-map',
|
||||
'encrypted-shared-prefs'
|
||||
]
|
||||
|
||||
gradle.ext.roboswag.forEach { module ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
apply from: '../RoboSwag/android-configs/common-config.gradle'
|
||||
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
apply plugin: 'kotlin-android'
|
||||
|
||||
rootProject.ext {
|
||||
compileSdk = 29
|
||||
|
||||
minSdk = 21
|
||||
targetSdk = 29
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdk
|
||||
targetSdkVersion rootProject.ext.targetSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
apply from: '../android-configs/common-config.gradle'
|
||||
|
|
@ -1,23 +1,48 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
api project(":storable")
|
||||
api 'net.danlew:android.joda:2.9.9.4'
|
||||
implementation project(":utils")
|
||||
implementation project(":logging")
|
||||
implementation project(":storable")
|
||||
|
||||
implementation "androidx.annotation:annotation:$versions.androidx"
|
||||
implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
|
||||
implementation 'ru.touchin:logansquare:1.4.3'
|
||||
implementation 'net.danlew:android.joda'
|
||||
|
||||
implementation "androidx.core:core"
|
||||
implementation "androidx.annotation:annotation"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit"
|
||||
|
||||
implementation 'ru.touchin:logansquare'
|
||||
|
||||
constraints {
|
||||
implementation("androidx.core:core") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("ru.touchin:logansquare") {
|
||||
version {
|
||||
require '1.4.3'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("com.squareup.retrofit2:retrofit") {
|
||||
version {
|
||||
require '2.7.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.annotation:annotation") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("net.danlew:android.joda") {
|
||||
version {
|
||||
require '2.9.9.4'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,13 @@
|
|||
|
||||
package ru.touchin.templates;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
import ru.touchin.roboswag.core.log.LcGroup;
|
||||
|
||||
|
|
@ -38,11 +37,6 @@ public abstract class ApiModel implements Serializable {
|
|||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Logging group to log API validation errors.
|
||||
*/
|
||||
public static final LcGroup API_VALIDATION_LC_GROUP = new LcGroup("API_VALIDATION");
|
||||
|
||||
/**
|
||||
* Validates list of objects. Use it if objects in list extends {@link ApiModel}.
|
||||
*
|
||||
|
|
@ -76,14 +70,14 @@ public abstract class ApiModel implements Serializable {
|
|||
throw exception;
|
||||
case EXCEPTION_IF_ALL_INVALID:
|
||||
iterator.remove();
|
||||
API_VALIDATION_LC_GROUP.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position);
|
||||
LcGroup.API_VALIDATION.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position);
|
||||
if (!iterator.hasNext() && !haveValidItem) {
|
||||
throw new ValidationException("Whole list is invalid at " + Lc.getCodePoint(null, 1));
|
||||
}
|
||||
break;
|
||||
case REMOVE_INVALID_ITEMS:
|
||||
iterator.remove();
|
||||
API_VALIDATION_LC_GROUP.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position);
|
||||
LcGroup.API_VALIDATION.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position);
|
||||
break;
|
||||
default:
|
||||
Lc.assertion("Unexpected rule " + collectionValidationRule);
|
||||
|
|
|
|||
|
|
@ -1,15 +1 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(":logging")
|
||||
implementation project(":navigation-base")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
|
||||
implementation("androidx.core:core-ktx")
|
||||
|
||||
implementation("androidx.appcompat:appcompat")
|
||||
|
||||
constraints {
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.core:core-ktx") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<manifest
|
||||
package="ru.touchin.roboswag.bottom_navigation_base"/>
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
package ru.touchin.roboswag.components.tabbarnavigation
|
||||
package ru.touchin.roboswag.bottom_navigation_base
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import ru.touchin.roboswag.components.navigation.activities.NavigationActivity
|
||||
import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation
|
||||
import ru.touchin.roboswag.navigation_base.FragmentNavigation
|
||||
import ru.touchin.roboswag.navigation_base.activities.NavigationActivity
|
||||
|
||||
/**
|
||||
* Created by Daniil Borisovskii on 15/08/2019.
|
||||
* Activity to manage tab container navigation.
|
||||
*/
|
||||
abstract class BottomNavigationActivity : NavigationActivity() {
|
||||
abstract class BaseBottomNavigationActivity<TNavigation, TNavigationFragment, TNavigationContainer> : NavigationActivity<TNavigation>()
|
||||
where TNavigation : FragmentNavigation,
|
||||
TNavigationFragment : BaseBottomNavigationFragment<*>,
|
||||
TNavigationContainer : BaseNavigationContainerFragment<*, TNavigation>
|
||||
{
|
||||
|
||||
val innerNavigation: ViewControllerNavigation<BottomNavigationActivity>
|
||||
get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation as ViewControllerNavigation<BottomNavigationActivity>
|
||||
val innerNavigation: TNavigation
|
||||
get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation
|
||||
|
||||
/**
|
||||
* Navigates to the given navigation tab.
|
||||
|
|
@ -27,15 +27,15 @@ abstract class BottomNavigationActivity : NavigationActivity() {
|
|||
// Clear all navigation stack unto the main bottom navigation (tagged as top)
|
||||
popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
|
||||
(primaryNavigationFragment as? BottomNavigationFragment)?.navigateTo(navigationTabId, state)
|
||||
(primaryNavigationFragment as? TNavigationFragment)?.navigateTo(navigationTabId, state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNavigationContainer(fragmentManager: FragmentManager?): NavigationContainerFragment? =
|
||||
protected fun getNavigationContainer(fragmentManager: FragmentManager?): TNavigationContainer? =
|
||||
fragmentManager
|
||||
?.primaryNavigationFragment
|
||||
?.let { navigationFragment ->
|
||||
navigationFragment as? NavigationContainerFragment
|
||||
navigationFragment as? TNavigationContainer
|
||||
?: getNavigationContainer(navigationFragment.childFragmentManager)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_base
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.util.forEach
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import ru.touchin.roboswag.core.utils.ShouldNotHappenException
|
||||
import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment
|
||||
|
||||
abstract class BaseBottomNavigationController<TNavigationTab : BaseNavigationTab>(
|
||||
private val tabs: SparseArray<TNavigationTab>,
|
||||
private val context: Context,
|
||||
private val fragmentManager: FragmentManager,
|
||||
@LayoutRes private val contentContainerLayoutId: Int,
|
||||
@IdRes private val contentContainerViewId: Int,
|
||||
@IdRes private val defaultTabId: Int = 0, // If it zero back press with empty fragment back stack would close the app
|
||||
private val wrapWithNavigationContainer: Boolean = false,
|
||||
private val onReselectListener: (() -> Unit)? = null
|
||||
) {
|
||||
|
||||
private var callback: FragmentManager.FragmentLifecycleCallbacks? = null
|
||||
|
||||
private var tabsContainer: ViewGroup? = null
|
||||
|
||||
private var selectedTabId: Int? = null
|
||||
|
||||
fun attach(tabsContainer: ViewGroup) {
|
||||
detach()
|
||||
|
||||
this.tabsContainer = tabsContainer
|
||||
|
||||
initializeCallback()
|
||||
|
||||
tabsContainer.children.forEach { itemView ->
|
||||
tabs[itemView.id]?.let { tab ->
|
||||
itemView.setOnClickListener { buttonView ->
|
||||
if (isTabClass(tab, fragmentManager.primaryNavigationFragment)) {
|
||||
onTabReselected()
|
||||
} else {
|
||||
navigateTo(buttonView.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
callback?.let(fragmentManager::unregisterFragmentLifecycleCallbacks)
|
||||
|
||||
callback = null
|
||||
tabsContainer = null
|
||||
}
|
||||
|
||||
fun navigateTo(@IdRes itemId: Int, state: Parcelable? = null) {
|
||||
// Find fragment class that needs to open
|
||||
val tabClass = tabs[itemId]?.cls ?: return
|
||||
val defaultFragmentState = tabs[itemId]?.state ?: return
|
||||
|
||||
if (state != null && state::class != defaultFragmentState::class) {
|
||||
throw ShouldNotHappenException(
|
||||
"Incorrect state type for navigation tab root Fragment. Should be ${defaultFragmentState::class}"
|
||||
)
|
||||
}
|
||||
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
// Detach current primary fragment
|
||||
fragmentManager.primaryNavigationFragment?.let(transaction::detach)
|
||||
|
||||
val fragmentName = tabClass.canonicalName
|
||||
var fragment = fragmentManager.findFragmentByTag(fragmentName)
|
||||
|
||||
if (state == null && fragment != null) {
|
||||
transaction.attach(fragment)
|
||||
} else {
|
||||
// If fragment already exist remove it first
|
||||
if (fragment != null) transaction.remove(fragment)
|
||||
|
||||
fragment = instantiateFragment(tabClass, state ?: defaultFragmentState)
|
||||
|
||||
transaction.add(contentContainerViewId, fragment, fragmentName)
|
||||
}
|
||||
|
||||
transaction
|
||||
.setPrimaryNavigationFragment(fragment)
|
||||
.setReorderingAllowed(true)
|
||||
.commit()
|
||||
|
||||
selectedTabId = itemId
|
||||
}
|
||||
|
||||
// When you are in any tab instead of main you firstly navigate to main tab before exit application
|
||||
fun onBackPressed() =
|
||||
if (fragmentManager.primaryNavigationFragment?.childFragmentManager?.backStackEntryCount == 0
|
||||
&& defaultTabId != 0
|
||||
&& selectedTabId != defaultTabId) {
|
||||
|
||||
navigateTo(defaultTabId)
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
protected open fun getNavigationContainerClass(): Class<out BaseNavigationContainerFragment<*, *>> = BaseNavigationContainerFragment::class.java
|
||||
|
||||
protected open fun onTabReselected() {
|
||||
onReselectListener?.invoke()
|
||||
}
|
||||
|
||||
protected open fun isTabClass(tab: TNavigationTab, fragment: Fragment?) = if (wrapWithNavigationContainer) {
|
||||
(fragment as BaseNavigationContainerFragment<*, *>).getContainedClass()
|
||||
} else {
|
||||
fragment?.javaClass
|
||||
} == tab.cls
|
||||
|
||||
protected open fun instantiateFragment(clazz: Class<*>, state: Parcelable): Fragment =
|
||||
if (wrapWithNavigationContainer) {
|
||||
Fragment.instantiate(
|
||||
context,
|
||||
getNavigationContainerClass().name,
|
||||
BaseNavigationContainerFragment.args(clazz, state, contentContainerViewId, contentContainerLayoutId)
|
||||
)
|
||||
} else {
|
||||
Fragment.instantiate(
|
||||
context,
|
||||
clazz.name,
|
||||
StatefulFragment.args(state)
|
||||
)
|
||||
}
|
||||
|
||||
private fun initializeCallback() {
|
||||
callback = TabFragmentChangedCallback()
|
||||
|
||||
fragmentManager.registerFragmentLifecycleCallbacks(callback!!, false)
|
||||
}
|
||||
|
||||
private inner class TabFragmentChangedCallback : FragmentManager.FragmentLifecycleCallbacks() {
|
||||
|
||||
// Set selected tab active, disabling all others. Used for styling
|
||||
override fun onFragmentViewCreated(
|
||||
fragmentManager: FragmentManager,
|
||||
fragment: Fragment,
|
||||
view: View,
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
tabs.forEach { itemId, tab ->
|
||||
if (isTabClass(tab, fragment)) {
|
||||
tabsContainer!!.children.forEach { itemView -> itemView.isActivated = itemView.id == itemId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import ru.touchin.roboswag.navigation_base.activities.BaseActivity
|
||||
import ru.touchin.roboswag.navigation_base.activities.OnBackPressedListener
|
||||
|
||||
abstract class BaseBottomNavigationFragment<TNavigationType: BaseNavigationTab> : Fragment() {
|
||||
|
||||
protected abstract val rootLayoutId: Int
|
||||
|
||||
protected abstract val navigationContainerViewId: Int
|
||||
|
||||
protected abstract val contentContainerViewId: Int
|
||||
|
||||
protected abstract val contentContainerLayoutId: Int
|
||||
|
||||
protected abstract val defaultTabId: Int
|
||||
|
||||
protected abstract val wrapWithNavigationContainer: Boolean
|
||||
|
||||
protected abstract val tabs: SparseArray<TNavigationType>
|
||||
|
||||
protected open val backPressedListener = OnBackPressedListener { bottomNavigationController.onBackPressed() }
|
||||
|
||||
protected open val reselectListener: (() -> Unit) = { getNavigationActivity().innerNavigation.up(inclusive = true) }
|
||||
|
||||
private lateinit var bottomNavigationController: BaseBottomNavigationController<*>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bottomNavigationController = createNavigationController()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
bottomNavigationController.navigateTo(defaultTabId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val fragmentView = inflater.inflate(rootLayoutId, container, false)
|
||||
|
||||
bottomNavigationController.attach(fragmentView.findViewById(navigationContainerViewId))
|
||||
|
||||
(activity as BaseActivity).addOnBackPressedListener(backPressedListener)
|
||||
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
(activity as BaseActivity).removeOnBackPressedListener(backPressedListener)
|
||||
|
||||
bottomNavigationController.detach()
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
fun navigateTo(@IdRes navigationTabId: Int, state: Parcelable? = null) {
|
||||
bottomNavigationController.navigateTo(navigationTabId, state)
|
||||
}
|
||||
|
||||
protected abstract fun createNavigationController(): BaseBottomNavigationController<*>
|
||||
|
||||
protected fun getNavigationActivity() = requireActivity() as BaseBottomNavigationActivity<*, *, *>
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import ru.touchin.roboswag.core.utils.ShouldNotHappenException
|
||||
import ru.touchin.roboswag.navigation_base.FragmentNavigation
|
||||
|
||||
abstract class BaseNavigationContainerFragment<TContained, TNavigation : FragmentNavigation> : Fragment() {
|
||||
|
||||
companion object {
|
||||
const val TRANSITION_ARG = "TRANSITION_ARG"
|
||||
const val FRAGMENT_STATE_ARG = "FRAGMENT_STATE_ARG"
|
||||
const val CONTAINED_CLASS_ARG = "FRAGMENT_CLASS_ARG"
|
||||
const val CONTAINER_VIEW_ID_ARG = "CONTAINER_VIEW_ID_ARG"
|
||||
const val CONTAINER_LAYOUT_ID_ARG = "CONTAINER_LAYOUT_ID_ARG"
|
||||
|
||||
fun args(
|
||||
cls: Class<*>,
|
||||
state: Parcelable,
|
||||
@IdRes containerViewId: Int,
|
||||
@LayoutRes containerLayoutId: Int,
|
||||
transition: Int = FragmentTransaction.TRANSIT_NONE
|
||||
) = Bundle().apply {
|
||||
putInt(TRANSITION_ARG, transition)
|
||||
putInt(CONTAINER_VIEW_ID_ARG, containerViewId)
|
||||
putInt(CONTAINER_LAYOUT_ID_ARG, containerLayoutId)
|
||||
putParcelable(FRAGMENT_STATE_ARG, state)
|
||||
putSerializable(CONTAINED_CLASS_ARG, cls)
|
||||
}
|
||||
}
|
||||
|
||||
abstract val navigation: TNavigation
|
||||
|
||||
@IdRes
|
||||
protected var containerViewId = 0
|
||||
private set
|
||||
|
||||
@LayoutRes
|
||||
protected var containerLayoutId = 0
|
||||
private set
|
||||
|
||||
protected var transition = 0
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { args ->
|
||||
transition = args.getInt(TRANSITION_ARG)
|
||||
containerViewId = args.getInt(CONTAINER_VIEW_ID_ARG)
|
||||
containerLayoutId = args.getInt(CONTAINER_LAYOUT_ID_ARG)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
onContainerCreated()
|
||||
}
|
||||
} ?: throw ShouldNotHappenException("Fragment is not instantiable without arguments")
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(containerLayoutId, container, false)
|
||||
|
||||
protected abstract fun onContainerCreated()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun getContainedClass(): Class<TContained> =
|
||||
arguments?.getSerializable(CONTAINED_CLASS_ARG) as Class<TContained>
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_base
|
||||
|
||||
import android.os.Parcelable
|
||||
import ru.touchin.roboswag.navigation_base.extensions.copy
|
||||
|
||||
open class BaseNavigationTab(
|
||||
open val cls: Class<*>,
|
||||
state: Parcelable,
|
||||
/**
|
||||
* It can be useful in some cases when it is necessary to create ViewController
|
||||
* with initial state every time when tab opens.
|
||||
*/
|
||||
val saveStateOnSwitching: Boolean = true
|
||||
) {
|
||||
|
||||
/**
|
||||
* It is value as class body property instead of value as constructor parameter to specify
|
||||
* custom getter of this field which returns copy of Parcelable every time it be called.
|
||||
* This is necessary to avoid modifying this value if it would be a value as constructor parameter
|
||||
* and every getting of this value would return the same instance.
|
||||
*/
|
||||
val state = state
|
||||
get() = field.copy()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(":navigation-base")
|
||||
implementation project(":bottom-navigation-base")
|
||||
|
||||
implementation "androidx.core:core-ktx"
|
||||
|
||||
implementation "androidx.appcompat:appcompat"
|
||||
|
||||
constraints {
|
||||
implementation("androidx.core:core-ktx") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<manifest
|
||||
package="ru.touchin.roboswag.bottom_navigation_fragment"/>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_fragment
|
||||
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationActivity
|
||||
import ru.touchin.roboswag.navigation_base.FragmentNavigation
|
||||
|
||||
abstract class BottomNavigationActivity :
|
||||
BaseBottomNavigationActivity<FragmentNavigation, BottomNavigationFragment, NavigationContainerFragment>() {
|
||||
|
||||
override val navigation by lazy {
|
||||
FragmentNavigation(
|
||||
this,
|
||||
supportFragmentManager,
|
||||
fragmentContainerViewId,
|
||||
transition
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.util.SparseArray
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationController
|
||||
|
||||
class BottomNavigationController(
|
||||
context: Context,
|
||||
fragments: SparseArray<NavigationTab>,
|
||||
fragmentManager: FragmentManager,
|
||||
wrapWithNavigationContainer: Boolean = false,
|
||||
@LayoutRes private val contentContainerLayoutId: Int,
|
||||
@IdRes private val contentContainerViewId: Int,
|
||||
@IdRes private val defaultTabId: Int = 0, // If it zero back press with empty fragment back stack would close the app
|
||||
onReselectListener: (() -> Unit)? = null
|
||||
) : BaseBottomNavigationController<NavigationTab>(
|
||||
tabs = fragments,
|
||||
context = context,
|
||||
fragmentManager = fragmentManager,
|
||||
defaultTabId = defaultTabId,
|
||||
onReselectListener = onReselectListener,
|
||||
contentContainerViewId = contentContainerViewId,
|
||||
contentContainerLayoutId = contentContainerLayoutId,
|
||||
wrapWithNavigationContainer = wrapWithNavigationContainer
|
||||
) {
|
||||
|
||||
override fun getNavigationContainerClass() = NavigationContainerFragment::class.java
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_fragment
|
||||
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationFragment
|
||||
|
||||
abstract class BottomNavigationFragment : BaseBottomNavigationFragment<NavigationTab>() {
|
||||
|
||||
override fun createNavigationController() = BottomNavigationController(
|
||||
context = requireContext(),
|
||||
fragments = tabs,
|
||||
fragmentManager = childFragmentManager,
|
||||
defaultTabId = defaultTabId,
|
||||
contentContainerViewId = contentContainerViewId,
|
||||
contentContainerLayoutId = contentContainerLayoutId,
|
||||
wrapWithNavigationContainer = wrapWithNavigationContainer,
|
||||
onReselectListener = reselectListener
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_fragment
|
||||
|
||||
import android.os.Parcelable
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseNavigationContainerFragment
|
||||
import ru.touchin.roboswag.navigation_base.FragmentNavigation
|
||||
import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment
|
||||
|
||||
class NavigationContainerFragment :
|
||||
BaseNavigationContainerFragment<StatefulFragment<out BottomNavigationActivity, Parcelable>, FragmentNavigation>() {
|
||||
|
||||
override val navigation by lazy {
|
||||
FragmentNavigation(
|
||||
requireContext(),
|
||||
childFragmentManager,
|
||||
containerViewId,
|
||||
transition
|
||||
)
|
||||
}
|
||||
|
||||
override fun onContainerCreated() {
|
||||
navigation.setInitial(getContainedClass().kotlin, arguments?.getParcelable(FRAGMENT_STATE_ARG)!!)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_fragment
|
||||
|
||||
import android.os.Parcelable
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseNavigationTab
|
||||
import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment
|
||||
|
||||
class NavigationTab(
|
||||
override val cls: Class<out StatefulFragment<*, *>>,
|
||||
state: Parcelable,
|
||||
saveStateOnSwitching: Boolean = true
|
||||
) : BaseNavigationTab(cls, state, saveStateOnSwitching)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(":navigation-base")
|
||||
implementation project(":navigation-viewcontroller")
|
||||
implementation project(":bottom-navigation-base")
|
||||
|
||||
implementation "androidx.core:core-ktx"
|
||||
|
||||
implementation "androidx.appcompat:appcompat"
|
||||
|
||||
constraints {
|
||||
implementation("androidx.core:core-ktx") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<manifest
|
||||
package="ru.touchin.roboswag.bottom_navigation_viewcontroller"/>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_viewcontroller
|
||||
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationActivity
|
||||
import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewControllerNavigation
|
||||
|
||||
abstract class BottomNavigationActivity :
|
||||
BaseBottomNavigationActivity<ViewControllerNavigation<BottomNavigationActivity>, BottomNavigationFragment, NavigationContainerFragment>() {
|
||||
|
||||
final override val navigation by lazy {
|
||||
ViewControllerNavigation<BottomNavigationActivity>(
|
||||
this,
|
||||
supportFragmentManager,
|
||||
fragmentContainerViewId,
|
||||
transition
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_viewcontroller
|
||||
|
||||
import android.content.Context
|
||||
import android.util.SparseArray
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationController
|
||||
import ru.touchin.roboswag.navigation_viewcontroller.fragments.ViewControllerFragment
|
||||
|
||||
class BottomNavigationController(
|
||||
context: Context,
|
||||
fragmentManager: FragmentManager,
|
||||
viewControllers: SparseArray<NavigationTab>,
|
||||
private val wrapWithNavigationContainer: Boolean = false,
|
||||
@IdRes private val defaultTabId: Int = 0, // If it zero back press with empty fragment back stack would close the app
|
||||
@IdRes private val contentContainerViewId: Int,
|
||||
@LayoutRes private val contentContainerLayoutId: Int,
|
||||
private val onReselectListener: (() -> Unit)? = null
|
||||
) : BaseBottomNavigationController<NavigationTab>(
|
||||
context = context,
|
||||
fragmentManager = fragmentManager,
|
||||
tabs = viewControllers,
|
||||
defaultTabId = defaultTabId,
|
||||
contentContainerViewId = contentContainerViewId,
|
||||
contentContainerLayoutId = contentContainerLayoutId,
|
||||
wrapWithNavigationContainer = wrapWithNavigationContainer
|
||||
) {
|
||||
|
||||
override fun onTabReselected() {
|
||||
onReselectListener?.invoke()
|
||||
}
|
||||
|
||||
override fun getNavigationContainerClass() = NavigationContainerFragment::class.java
|
||||
|
||||
override fun isTabClass(tab: NavigationTab, fragment: Fragment?): Boolean =
|
||||
if (wrapWithNavigationContainer) {
|
||||
super.isTabClass(tab, fragment)
|
||||
} else {
|
||||
(fragment as ViewControllerFragment<*, *>).viewControllerClass === tab.cls
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_viewcontroller
|
||||
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationFragment
|
||||
|
||||
abstract class BottomNavigationFragment : BaseBottomNavigationFragment<NavigationTab>() {
|
||||
|
||||
override fun createNavigationController() = BottomNavigationController(
|
||||
context = requireContext(),
|
||||
fragmentManager = childFragmentManager,
|
||||
viewControllers = tabs,
|
||||
defaultTabId = defaultTabId,
|
||||
contentContainerViewId = contentContainerViewId,
|
||||
contentContainerLayoutId = contentContainerLayoutId,
|
||||
wrapWithNavigationContainer = wrapWithNavigationContainer,
|
||||
onReselectListener = reselectListener
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_viewcontroller
|
||||
|
||||
import android.os.Parcelable
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseNavigationContainerFragment
|
||||
import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController
|
||||
import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewControllerNavigation
|
||||
|
||||
class NavigationContainerFragment :
|
||||
BaseNavigationContainerFragment<
|
||||
ViewController<out BottomNavigationActivity, Parcelable>,
|
||||
ViewControllerNavigation<BottomNavigationActivity>>() {
|
||||
|
||||
override val navigation by lazy {
|
||||
ViewControllerNavigation<BottomNavigationActivity>(
|
||||
requireContext(),
|
||||
childFragmentManager,
|
||||
containerViewId,
|
||||
transition
|
||||
)
|
||||
}
|
||||
|
||||
override fun onContainerCreated() {
|
||||
navigation.setInitialViewController(getContainedClass(), arguments?.getParcelable(FRAGMENT_STATE_ARG)!!)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.roboswag.bottom_navigation_viewcontroller
|
||||
|
||||
import android.os.Parcelable
|
||||
import ru.touchin.roboswag.bottom_navigation_base.BaseNavigationTab
|
||||
import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController
|
||||
|
||||
class NavigationTab(
|
||||
override val cls: Class<out ViewController<*, *>>,
|
||||
state: Parcelable,
|
||||
/**
|
||||
* It can be useful in some cases when it is necessary to create ViewController
|
||||
* with initial state every time when tab opens.
|
||||
*/
|
||||
saveStateOnSwitching: Boolean = true
|
||||
) : BaseNavigationTab(cls, state, saveStateOnSwitching)
|
||||
34
build.gradle
34
build.gradle
|
|
@ -1,15 +1,14 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'de.aaschmid:gradle-cpd-plugin:1.1'
|
||||
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.0.0-RC12"
|
||||
classpath 'de.aaschmid:gradle-cpd-plugin:3.1'
|
||||
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -17,29 +16,16 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "http://dl.bintray.com/touchin/touchin-tools" }
|
||||
maven {
|
||||
url "https://dl.bintray.com/touchin/touchin-tools"
|
||||
metadataSources {
|
||||
artifact()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
ext {
|
||||
versions = [
|
||||
compileSdk : 29,
|
||||
appcompat : '1.0.2',
|
||||
androidx : '1.0.0',
|
||||
material : '1.0.0',
|
||||
lifecycle : '2.0.0',
|
||||
dagger : '2.17',
|
||||
retrofit : '2.4.0',
|
||||
rxJava : '2.2.2',
|
||||
rxAndroid : '2.1.0',
|
||||
crashlytics : '2.9.5',
|
||||
location : '16.0.0',
|
||||
coreKtx : '1.1.0',
|
||||
yandex_mapkit: '3.4.0',
|
||||
google_maps : '16.1.0'
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 17
|
||||
}
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
api project(":base-map")
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.google.android.gms:play-services-maps"
|
||||
|
||||
implementation "com.google.android.gms:play-services-maps:$versions.google_maps"
|
||||
constraints {
|
||||
implementation("com.google.android.gms:play-services-maps") {
|
||||
version {
|
||||
require '17.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "androidx.recyclerview:recyclerview"
|
||||
|
||||
implementation "androidx.recyclerview:recyclerview:$versions.androidx"
|
||||
constraints {
|
||||
implementation("androidx.recyclerview:recyclerview") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package ru.touchin.extensions
|
|||
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import ru.touchin.utils.ActionThrottler
|
||||
|
||||
private const val RIPPLE_EFFECT_DELAY = 150L
|
||||
const val RIPPLE_EFFECT_DELAY_MS = 150L
|
||||
|
||||
/**
|
||||
* Sets click listener to view. On click it will call something after delay.
|
||||
|
|
@ -12,7 +13,11 @@ private const val RIPPLE_EFFECT_DELAY = 150L
|
|||
*/
|
||||
fun View.setOnRippleClickListener(listener: () -> Unit) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
setOnClickListener { postDelayed({ if (hasWindowFocus()) listener() }, RIPPLE_EFFECT_DELAY) }
|
||||
setOnClickListener {
|
||||
ActionThrottler.throttleAction {
|
||||
postDelayed({ if (hasWindowFocus()) listener() }, RIPPLE_EFFECT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setOnClickListener { listener() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package ru.touchin.utils
|
||||
|
||||
import android.os.SystemClock
|
||||
import ru.touchin.extensions.RIPPLE_EFFECT_DELAY_MS
|
||||
|
||||
object ActionThrottler {
|
||||
|
||||
// It is necessary because in interval after ripple effect finish and before
|
||||
// action invoking start user may be in time to click and launch action again
|
||||
private const val PREVENTION_OF_CLICK_AGAIN_COEFFICIENT = 2
|
||||
private const val DELAY_MS = PREVENTION_OF_CLICK_AGAIN_COEFFICIENT * RIPPLE_EFFECT_DELAY_MS
|
||||
private var lastActionTime = 0L
|
||||
|
||||
fun throttleAction(action: () -> Unit): Boolean {
|
||||
val currentTime = SystemClock.elapsedRealtime()
|
||||
val diff = currentTime - lastActionTime
|
||||
|
||||
return if (diff >= DELAY_MS) {
|
||||
lastActionTime = currentTime
|
||||
action.invoke()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,25 +1,40 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
api project(":utils")
|
||||
api project(":logging")
|
||||
api project(":lifecycle")
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "androidx.appcompat:appcompat"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$versions.appcompat"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle"
|
||||
implementation "io.reactivex.rxjava2:rxjava"
|
||||
implementation "io.reactivex.rxjava2:rxandroid"
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid"
|
||||
constraints {
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-extensions") {
|
||||
version {
|
||||
require '2.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("io.reactivex.rxjava2:rxjava") {
|
||||
version {
|
||||
require '2.2.6'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("io.reactivex.rxjava2:rxandroid") {
|
||||
version {
|
||||
require '2.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package ru.touchin.lifecycle.viewmodel
|
||||
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
|
|
@ -13,7 +14,7 @@ import ru.touchin.lifecycle.event.Event
|
|||
class BaseLiveDataDispatcher(private val destroyable: BaseDestroyable = BaseDestroyable()) : LiveDataDispatcher, Destroyable by destroyable {
|
||||
|
||||
override fun <T> Flowable<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): Disposable {
|
||||
liveData.value = ContentEvent.Loading(liveData.value?.data)
|
||||
liveData.setLoadingEvent()
|
||||
return untilDestroy(
|
||||
{ data -> liveData.value = ContentEvent.Success(data) },
|
||||
{ throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) },
|
||||
|
|
@ -21,7 +22,7 @@ class BaseLiveDataDispatcher(private val destroyable: BaseDestroyable = BaseDest
|
|||
}
|
||||
|
||||
override fun <T> Observable<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): Disposable {
|
||||
liveData.value = ContentEvent.Loading(liveData.value?.data)
|
||||
liveData.setLoadingEvent()
|
||||
return untilDestroy(
|
||||
{ data -> liveData.value = ContentEvent.Success(data) },
|
||||
{ throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) },
|
||||
|
|
@ -29,14 +30,14 @@ class BaseLiveDataDispatcher(private val destroyable: BaseDestroyable = BaseDest
|
|||
}
|
||||
|
||||
override fun <T> Single<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): Disposable {
|
||||
liveData.value = ContentEvent.Loading(liveData.value?.data)
|
||||
liveData.setLoadingEvent()
|
||||
return untilDestroy(
|
||||
{ data -> liveData.value = ContentEvent.Success(data) },
|
||||
{ throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) })
|
||||
}
|
||||
|
||||
override fun <T> Maybe<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): Disposable {
|
||||
liveData.value = ContentEvent.Loading(liveData.value?.data)
|
||||
liveData.setLoadingEvent()
|
||||
return untilDestroy(
|
||||
{ data -> liveData.value = ContentEvent.Success(data) },
|
||||
{ throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) },
|
||||
|
|
@ -44,10 +45,29 @@ class BaseLiveDataDispatcher(private val destroyable: BaseDestroyable = BaseDest
|
|||
}
|
||||
|
||||
override fun Completable.dispatchTo(liveData: MutableLiveData<Event>): Disposable {
|
||||
liveData.value = Event.Loading
|
||||
liveData.setLoadingEvent()
|
||||
return untilDestroy(
|
||||
{ liveData.value = Event.Complete },
|
||||
{ throwable -> liveData.value = Event.Error(throwable) })
|
||||
}
|
||||
|
||||
private fun <T> MutableLiveData<ContentEvent<T>>.setLoadingEvent() {
|
||||
val loadingContent = ContentEvent.Loading(this.value?.data)
|
||||
if (Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||
this.value = loadingContent
|
||||
} else {
|
||||
this.postValue(loadingContent)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("setCompletableLoadingEvent")
|
||||
private fun MutableLiveData<Event>.setLoadingEvent() {
|
||||
val loadingContent = Event.Loading
|
||||
if (Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||
this.value = loadingContent
|
||||
} else {
|
||||
this.postValue(loadingContent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(":lifecycle")
|
||||
implementation project(":navigation-viewcontroller")
|
||||
|
||||
compileOnly "javax.inject:javax.inject:1"
|
||||
|
||||
implementation "androidx.appcompat:appcompat"
|
||||
|
||||
implementation "androidx.fragment:fragment"
|
||||
implementation "androidx.fragment:fragment-ktx"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions"
|
||||
|
||||
constraints {
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.fragment:fragment") {
|
||||
version {
|
||||
require '1.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.fragment:fragment-ktx") {
|
||||
version {
|
||||
require '1.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-extensions") {
|
||||
version {
|
||||
require '2.1.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.lifecycle_viewcontroller"/>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package ru.touchin.lifecycle_viewcontroller.extensions
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import ru.touchin.lifecycle_viewcontroller.viewmodel.LifecycleViewModelProviders
|
||||
import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController
|
||||
import androidx.fragment.app.activityViewModels as androidActivityViewModels
|
||||
import androidx.fragment.app.viewModels as androidViewModels
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ViewController<*, *>.viewModels(
|
||||
noinline ownerProducer: () -> ViewModelStoreOwner = { this.fragment },
|
||||
noinline factoryProducer: () -> ViewModelProvider.Factory = { LifecycleViewModelProviders.getViewModelFactory(this) }
|
||||
) = this.fragment.androidViewModels<VM>(ownerProducer, factoryProducer)
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ViewController<*, *>.parentViewModels(
|
||||
noinline ownerProducer: () -> ViewModelStoreOwner = { this.fragment.parentFragment!! },
|
||||
noinline factoryProducer: () -> ViewModelProvider.Factory = {
|
||||
LifecycleViewModelProviders.getViewModelFactory(this.fragment.parentFragment!!)
|
||||
}
|
||||
) = viewModels<VM>(ownerProducer, factoryProducer)
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ViewController<*, *>.targetViewModels(
|
||||
noinline ownerProducer: () -> ViewModelStoreOwner = { this.fragment.targetFragment!! },
|
||||
noinline factoryProducer: () -> ViewModelProvider.Factory = {
|
||||
LifecycleViewModelProviders.getViewModelFactory(this.fragment.targetFragment!!)
|
||||
}
|
||||
) = viewModels<VM>(ownerProducer, factoryProducer)
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ViewController<*, *>.activityViewModels(
|
||||
noinline factoryProducer: () -> ViewModelProvider.Factory = { LifecycleViewModelProviders.getViewModelFactory(activity) }
|
||||
) = this.fragment.androidActivityViewModels<VM>(factoryProducer)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package ru.touchin.lifecycle_viewcontroller.viewmodel
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import ru.touchin.lifecycle.viewmodel.BaseLifecycleViewModelProviders
|
||||
import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController
|
||||
|
||||
object LifecycleViewModelProviders : BaseLifecycleViewModelProviders() {
|
||||
|
||||
/**
|
||||
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
|
||||
* {@code lifecycleOwner} is alive. More detailed explanation is in {@link ViewModel}.
|
||||
* <p>
|
||||
* It uses the given {@link Factory} to instantiate new ViewModels.
|
||||
*
|
||||
* @param lifecycleOwner a lifecycle owner, in whose scope ViewModels should be retained (ViewController, Fragment, Activity)
|
||||
* @param factory a {@code Factory} to instantiate new ViewModels
|
||||
* @return a ViewModelProvider instance
|
||||
*/
|
||||
override fun of(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
factory: ViewModelProvider.Factory
|
||||
): ViewModelProvider =
|
||||
when (lifecycleOwner) {
|
||||
is ViewController<*, *> -> ViewModelProviders.of(lifecycleOwner.fragment, factory)
|
||||
else -> super.of(lifecycleOwner, factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns ViewModelProvider.Factory instance from current lifecycleOwner.
|
||||
* Search #ViewModelFactoryProvider are produced according to priorities:
|
||||
* 1. View controller;
|
||||
* 2. Fragment;
|
||||
* 3. Parent fragment recursively;
|
||||
* 4. Activity;
|
||||
* 5. Application.
|
||||
*/
|
||||
override fun getViewModelFactory(provider: Any): ViewModelProvider.Factory =
|
||||
when (provider) {
|
||||
is ViewController<*, *> -> getViewModelFactory(provider.fragment)
|
||||
else -> super.getViewModelFactory(provider)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,30 +1,38 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
api project(":navigation")
|
||||
|
||||
compileOnly "javax.inject:javax.inject:1"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "androidx.appcompat:appcompat"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$versions.appcompat"
|
||||
implementation "androidx.fragment:fragment"
|
||||
implementation "androidx.fragment:fragment-ktx"
|
||||
|
||||
implementation "androidx.fragment:fragment:$versions.fragment"
|
||||
implementation "androidx.fragment:fragment-ktx:$versions.fragment"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle"
|
||||
constraints {
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-extensions") {
|
||||
version {
|
||||
require '2.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.fragment:fragment") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.fragment:fragment-ktx") {
|
||||
version {
|
||||
require '1.1.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.lifecycle.extensions
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
fun <T> MutableLiveData<T>.toImmutable() = this as LiveData<T>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package ru.touchin.lifecycle.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
||||
abstract class BaseLifecycleViewModelProviders {
|
||||
|
||||
/**
|
||||
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
|
||||
* {@code lifecycleOwner} is alive. More detailed explanation is in {@link ViewModel}.
|
||||
* <p>
|
||||
* It uses the given {@link Factory} to instantiate new ViewModels.
|
||||
*
|
||||
* @param lifecycleOwner a lifecycle owner, in whose scope ViewModels should be retained (ViewController, Fragment, Activity)
|
||||
* @param factory a {@code Factory} to instantiate new ViewModels
|
||||
* @return a ViewModelProvider instance
|
||||
*/
|
||||
open fun of(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
factory: ViewModelProvider.Factory = getViewModelFactory(lifecycleOwner)
|
||||
): ViewModelProvider =
|
||||
when (lifecycleOwner) {
|
||||
is Fragment -> ViewModelProvider(lifecycleOwner, factory)
|
||||
is FragmentActivity -> ViewModelProvider(lifecycleOwner, factory)
|
||||
else -> throw IllegalArgumentException("Not supported LifecycleOwner.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns ViewModelProvider.Factory instance from current lifecycleOwner.
|
||||
* Search #ViewModelFactoryProvider are produced according to priorities:
|
||||
* 1. Fragment;
|
||||
* 2. Parent fragment recursively;
|
||||
* 3. Activity;
|
||||
* 4. Application.
|
||||
*/
|
||||
open fun getViewModelFactory(provider: Any): ViewModelProvider.Factory =
|
||||
when (provider) {
|
||||
is ViewModelFactoryProvider -> provider.viewModelFactory
|
||||
is Fragment -> getViewModelFactory(provider.parentFragment ?: provider.requireActivity())
|
||||
is Activity -> getViewModelFactory(provider.application)
|
||||
else -> throw IllegalArgumentException("View model factory not found.")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,49 +1,3 @@
|
|||
package ru.touchin.lifecycle.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
|
||||
|
||||
object LifecycleViewModelProviders {
|
||||
|
||||
/**
|
||||
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
|
||||
* {@code lifecycleOwner} is alive. More detailed explanation is in {@link ViewModel}.
|
||||
* <p>
|
||||
* It uses the given {@link Factory} to instantiate new ViewModels.
|
||||
*
|
||||
* @param lifecycleOwner a lifecycle owner, in whose scope ViewModels should be retained (ViewController, Fragment, Activity)
|
||||
* @param factory a {@code Factory} to instantiate new ViewModels
|
||||
* @return a ViewModelProvider instance
|
||||
*/
|
||||
fun of(lifecycleOwner: LifecycleOwner, factory: ViewModelProvider.Factory = getViewModelFactory(lifecycleOwner)): ViewModelProvider =
|
||||
when (lifecycleOwner) {
|
||||
is ViewController<*, *> -> ViewModelProviders.of(lifecycleOwner.fragment, factory)
|
||||
is Fragment -> ViewModelProviders.of(lifecycleOwner, factory)
|
||||
is FragmentActivity -> ViewModelProviders.of(lifecycleOwner, factory)
|
||||
else -> throw IllegalArgumentException("Not supported LifecycleOwner.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns ViewModelProvider.Factory instance from current lifecycleOwner.
|
||||
* Search #ViewModelFactoryProvider are produced according to priorities:
|
||||
* 1. View controller;
|
||||
* 2. Fragment;
|
||||
* 3. Parent fragment recursively;
|
||||
* 4. Activity;
|
||||
* 5. Application.
|
||||
*/
|
||||
fun getViewModelFactory(provider: Any): ViewModelProvider.Factory =
|
||||
when (provider) {
|
||||
is ViewModelFactoryProvider -> provider.viewModelFactory
|
||||
is ViewController<*, *> -> getViewModelFactory(provider.fragment)
|
||||
is Fragment -> getViewModelFactory(provider.parentFragment ?: provider.requireActivity())
|
||||
is Activity -> getViewModelFactory(provider.application)
|
||||
else -> throw IllegalArgumentException("View model factory not found.")
|
||||
}
|
||||
|
||||
}
|
||||
object LifecycleViewModelProviders : BaseLifecycleViewModelProviders()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
api project(":lifecycle")
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.google.android.gms:play-services-location"
|
||||
|
||||
implementation "com.google.android.gms:play-services-location:$versions.location"
|
||||
constraints {
|
||||
implementation("com.google.android.gms:play-services-location") {
|
||||
version {
|
||||
require '17.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:$versions.androidx"
|
||||
implementation "androidx.annotation:annotation"
|
||||
|
||||
implementation "com.google.firebase:firebase-crashlytics"
|
||||
|
||||
constraints {
|
||||
implementation("androidx.annotation:annotation") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("com.google.firebase:firebase-crashlytics") {
|
||||
version {
|
||||
require '17.1.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,11 @@
|
|||
|
||||
package ru.touchin.roboswag.core.log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
|
||||
import ru.touchin.roboswag.core.utils.ThreadLocalValue;
|
||||
|
||||
|
|
@ -45,6 +44,10 @@ public class LcGroup {
|
|||
* Logging group to log UI lifecycle (onCreate, onStart, onResume etc.).
|
||||
*/
|
||||
public static final LcGroup UI_LIFECYCLE = new LcGroup("UI_LIFECYCLE");
|
||||
/**
|
||||
* Logging group to log api validation errors.
|
||||
*/
|
||||
public static final LcGroup API_VALIDATION = new LcGroup("API_VALIDATION");
|
||||
|
||||
private static final ThreadLocalValue<SimpleDateFormat> DATE_TIME_FORMATTER
|
||||
= new ThreadLocalValue<>(() -> new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package ru.touchin.roboswag.core.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
import ru.touchin.roboswag.core.log.LcGroup;
|
||||
import ru.touchin.roboswag.core.log.LcLevel;
|
||||
import ru.touchin.roboswag.core.log.LogProcessor;
|
||||
|
||||
public class CrashlyticsLogProcessor extends LogProcessor {
|
||||
|
||||
@NonNull
|
||||
private final FirebaseCrashlytics crashlytics;
|
||||
|
||||
public CrashlyticsLogProcessor(@NonNull final FirebaseCrashlytics crashlytics) {
|
||||
super(LcLevel.INFO);
|
||||
this.crashlytics = crashlytics;
|
||||
}
|
||||
|
||||
private String getLogMessage(final int priorityLevel, final String tag, final String message) {
|
||||
return "Priority:" + priorityLevel + ' ' + tag + ':' + message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLogMessage(@NonNull final LcGroup group,
|
||||
@NonNull final LcLevel level,
|
||||
@NonNull final String tag,
|
||||
@NonNull final String message,
|
||||
@Nullable final Throwable throwable) {
|
||||
if (group == LcGroup.UI_LIFECYCLE) {
|
||||
crashlytics.log(getLogMessage(level.getPriority(), tag, message));
|
||||
} else if (!level.lessThan(LcLevel.ASSERT)
|
||||
|| (group == LcGroup.API_VALIDATION && level == LcLevel.ERROR)) {
|
||||
Log.e(tag, message);
|
||||
if (throwable != null) {
|
||||
crashlytics.log(getLogMessage(level.getPriority(), tag, message));
|
||||
crashlytics.recordException(throwable);
|
||||
} else {
|
||||
final ShouldNotHappenException exceptionToLog = new ShouldNotHappenException(tag + ':' + message);
|
||||
reduceStackTrace(exceptionToLog);
|
||||
crashlytics.recordException(exceptionToLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reduceStackTrace(@NonNull final Throwable throwable) {
|
||||
final StackTraceElement[] stackTrace = throwable.getStackTrace();
|
||||
final List<StackTraceElement> reducedStackTraceList = new ArrayList<>();
|
||||
for (int i = stackTrace.length - 1; i >= 0; i--) {
|
||||
final StackTraceElement stackTraceElement = stackTrace[i];
|
||||
if (stackTraceElement.getClassName().contains(getClass().getSimpleName())
|
||||
|| stackTraceElement.getClassName().contains(LcGroup.class.getName())
|
||||
|| stackTraceElement.getClassName().contains(Lc.class.getName())) {
|
||||
break;
|
||||
}
|
||||
reducedStackTraceList.add(0, stackTraceElement);
|
||||
}
|
||||
StackTraceElement[] reducedStackTrace = new StackTraceElement[reducedStackTraceList.size()];
|
||||
reducedStackTrace = reducedStackTraceList.toArray(reducedStackTrace);
|
||||
throwable.setStackTrace(reducedStackTrace);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
mvi_arch
|
||||
====
|
||||
|
||||
TODO: rewrite dependencies
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
dependencies {
|
||||
implementation project(":navigation-base")
|
||||
implementation project(":lifecycle")
|
||||
implementation project(":kotlin-extensions")
|
||||
implementation project(":logging")
|
||||
|
||||
implementation("androidx.core:core-ktx")
|
||||
implementation("androidx.appcompat:appcompat")
|
||||
|
||||
implementation("androidx.fragment:fragment")
|
||||
implementation("androidx.fragment:fragment-ktx")
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-extensions")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx")
|
||||
|
||||
implementation("com.google.dagger:dagger")
|
||||
kapt("com.google.dagger:dagger-compiler")
|
||||
implementation("com.github.valeryponomarenko.componentsmanager:androidx")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android")
|
||||
|
||||
implementation("com.tylerthrailkill.helpers:pretty-print:2.0.2")
|
||||
|
||||
def fragmentVersion = "1.2.1"
|
||||
def lifecycleVersion = "2.2.0"
|
||||
def coroutinesVersion = "1.3.7"
|
||||
def daggerVersion = "2.27"
|
||||
|
||||
constraints {
|
||||
implementation("androidx.core:core-ktx") {
|
||||
version {
|
||||
require("1.2.0")
|
||||
}
|
||||
}
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require("1.0.2")
|
||||
}
|
||||
}
|
||||
implementation("androidx.fragment:fragment") {
|
||||
version {
|
||||
require(fragmentVersion)
|
||||
}
|
||||
}
|
||||
implementation("androidx.fragment:fragment-ktx") {
|
||||
version {
|
||||
require(fragmentVersion)
|
||||
}
|
||||
}
|
||||
implementation("androidx.lifecycle:lifecycle-extensions") {
|
||||
version {
|
||||
require(lifecycleVersion)
|
||||
}
|
||||
}
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx") {
|
||||
version {
|
||||
require(lifecycleVersion)
|
||||
}
|
||||
}
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx") {
|
||||
version {
|
||||
require(lifecycleVersion)
|
||||
}
|
||||
}
|
||||
implementation("com.google.dagger:dagger") {
|
||||
version {
|
||||
require(daggerVersion)
|
||||
}
|
||||
}
|
||||
kapt("com.google.dagger:dagger-compiler") {
|
||||
version {
|
||||
require(daggerVersion)
|
||||
}
|
||||
}
|
||||
implementation("com.github.valeryponomarenko.componentsmanager:androidx") {
|
||||
version {
|
||||
require("2.1.0")
|
||||
}
|
||||
}
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") {
|
||||
version {
|
||||
require(coroutinesVersion)
|
||||
}
|
||||
}
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android") {
|
||||
version {
|
||||
require(coroutinesVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.mvi_arch" />
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package ru.touchin.roboswag.mvi_arch.core
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import ru.touchin.extensions.setOnRippleClickListener
|
||||
import ru.touchin.roboswag.mvi_arch.di.ViewModelAssistedFactory
|
||||
import ru.touchin.roboswag.mvi_arch.di.ViewModelFactory
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
||||
import ru.touchin.roboswag.navigation_base.fragments.BaseFragment
|
||||
import ru.touchin.roboswag.navigation_base.fragments.EmptyState
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Base [Fragment] to use in MVI architecture.
|
||||
*
|
||||
* @param NavArgs Type of arguments class of this screen.
|
||||
* It must implement [NavArgs] interface provided by navigation library that is a part of Google Jetpack.
|
||||
* An instance of this class is generated by [SafeArgs](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)
|
||||
* plugin according to related configuration file in navigation resource folder of your project.
|
||||
*
|
||||
* @param State Type of view state class of this screen.
|
||||
* It must implement [ViewState] interface. Usually it's a data class that presents full state of current screen's view.
|
||||
* @see [ViewState] for more information.
|
||||
*
|
||||
* @param Action Type of view actions class of this screen.
|
||||
* It must implement [Action] interface. Usually it's a sealed class that contains classes and objects representing
|
||||
* view actions of this view, e.g. button clicks, text changes, etc.
|
||||
* @see [Action] for more information.
|
||||
*
|
||||
* @param VM Type of view model class of this screen.
|
||||
* It must extends [MviViewModel] class with the same params.
|
||||
* @see [MviViewModel] for more information.
|
||||
*
|
||||
* @author Created by Max Bachinsky and Ivan Vlasov at Touch Instinct.
|
||||
*/
|
||||
abstract class MviFragment<NavArgs, State, Action, VM>(
|
||||
@LayoutRes layout: Int,
|
||||
navArgs: NavArgs = EmptyState as NavArgs
|
||||
) : BaseFragment<FragmentActivity>(layout)
|
||||
where NavArgs : Parcelable,
|
||||
State : ViewState,
|
||||
Action : ViewAction,
|
||||
VM : MviViewModel<NavArgs, Action, State> {
|
||||
|
||||
companion object {
|
||||
const val INIT_ARGS_KEY = "INIT_ARGS"
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [viewModel] extension to get an instance of your view model class.
|
||||
*/
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
/**
|
||||
* Used for smooth view model injection to this class.
|
||||
*/
|
||||
@Inject
|
||||
lateinit var viewModelMap: MutableMap<Class<out ViewModel>, ViewModelAssistedFactory<out ViewModel>>
|
||||
|
||||
init {
|
||||
arguments?.putParcelable(INIT_ARGS_KEY, navArgs) ?: let {
|
||||
arguments = bundleOf(INIT_ARGS_KEY to navArgs)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.state.observe(viewLifecycleOwner, Observer(this::renderState))
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to subscribe on view state changes.
|
||||
*
|
||||
* You should render view state here.
|
||||
*
|
||||
* Must not be called before [onAttach] and after [onDetach].
|
||||
*/
|
||||
protected open fun renderState(viewState: State) {}
|
||||
|
||||
/**
|
||||
* Use this method to dispatch view actions to view model.
|
||||
*/
|
||||
protected fun dispatchAction(actionProvider: () -> Action) {
|
||||
viewModel.dispatchAction(actionProvider.invoke())
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to dispatch view actions to view model.
|
||||
*/
|
||||
protected fun dispatchAction(action: Action) {
|
||||
viewModel.dispatchAction(action)
|
||||
}
|
||||
|
||||
protected fun addOnBackPressedCallback(actionProvider: () -> Action) {
|
||||
addOnBackPressedCallback(actionProvider.invoke())
|
||||
}
|
||||
|
||||
protected fun addOnBackPressedCallback(action: Action) {
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
dispatchAction(action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily provides view model of this screen with transmitted arguments if exist.
|
||||
*
|
||||
* Value of this lazily providing must not be accessed before [onAttach] and after [onDetach].
|
||||
*/
|
||||
@MainThread
|
||||
protected inline fun <reified ViewModel : VM> viewModel(): Lazy<ViewModel> =
|
||||
lazy {
|
||||
val fragmentArguments = arguments ?: bundleOf()
|
||||
|
||||
ViewModelProvider(
|
||||
viewModelStore,
|
||||
ViewModelFactory(viewModelMap, this, fragmentArguments)
|
||||
).get(ViewModel::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple extension for dispatching view events to view model with on click.
|
||||
*/
|
||||
protected fun View.dispatchActionOnClick(actionProvider: () -> Action) {
|
||||
setOnClickListener { dispatchAction(actionProvider) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple extension for dispatching view events to view model with on click.
|
||||
*/
|
||||
protected fun View.dispatchActionOnClick(action: Action) {
|
||||
setOnClickListener { dispatchAction(action) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple extension for dispatching view events to view model with on ripple click.
|
||||
*/
|
||||
protected fun View.dispatchActionOnRippleClick(actionProvider: () -> Action) {
|
||||
setOnRippleClickListener { dispatchAction(actionProvider) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple extension for dispatching view events to view model with on ripple click.
|
||||
*/
|
||||
protected fun View.dispatchActionOnRippleClick(action: Action) {
|
||||
setOnRippleClickListener { dispatchAction(action) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package ru.touchin.roboswag.mvi_arch.core
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import ru.touchin.roboswag.mvi_arch.marker.SideEffect
|
||||
import ru.touchin.roboswag.mvi_arch.marker.StateChange
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
||||
|
||||
/**
|
||||
* Base [ViewModel] to use in MVI architecture.
|
||||
*
|
||||
* @param NavArgs Type of arguments class of this screen.
|
||||
* It must implement [NavArgs] interface provided by navigation library that is a part of Google Jetpack.
|
||||
* An instance of this class is generated by [SafeArgs](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)
|
||||
* plugin according to related configuration file in navigation resource folder of your project.
|
||||
*
|
||||
* @param State Type of view state class of this screen.
|
||||
* It must implement [ViewState] interface. Usually it's a data class that presents full state of current screen's view.
|
||||
* @see [ViewState] for more information.
|
||||
*
|
||||
* @param Action Type of view actions class of this screen.
|
||||
* It must implement [Action] interface. Usually it's a sealed class that contains classes and objects representing
|
||||
* view actions of this view, e.g. button clicks, text changes, etc.
|
||||
* @see [Action] for more information.
|
||||
*
|
||||
* @author Created by Max Bachinsky and Ivan Vlasov at Touch Instinct.
|
||||
*/
|
||||
|
||||
abstract class MviStoreViewModel<NavArgs : Parcelable, Action : ViewAction, State : ViewState>(
|
||||
initialState: State,
|
||||
handle: SavedStateHandle
|
||||
) : MviViewModel<NavArgs, Action, State>(initialState, handle) {
|
||||
|
||||
private lateinit var store: ChildStore<*, *, *>
|
||||
|
||||
protected fun <IChange : StateChange, IEffect : SideEffect, IState : ViewState> connectStore(
|
||||
store: Store<IChange, IEffect, IState>,
|
||||
mapAction: (Action) -> IChange?,
|
||||
mapState: (IState) -> State
|
||||
) {
|
||||
this.store = ChildStore(store, mapAction)
|
||||
|
||||
store
|
||||
.observeState()
|
||||
.map { mapState(it) }
|
||||
.onEach { this._state.postValue(it) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun dispatchAction(action: Action) {
|
||||
store.dispatchAction(action)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
store.onCleared()
|
||||
}
|
||||
|
||||
private inner class ChildStore<IChange : StateChange, IEffect : SideEffect, IState : ViewState>(
|
||||
val store: Store<IChange, IEffect, IState>,
|
||||
val changeMapper: (Action) -> IChange?
|
||||
) {
|
||||
fun onCleared() {
|
||||
store.onCleared()
|
||||
}
|
||||
|
||||
fun dispatchAction(action: Action) {
|
||||
changeMapper(action)?.let(store::changeState)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package ru.touchin.roboswag.mvi_arch.core
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.touchin.mvi_arch.BuildConfig
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
||||
import ru.touchin.roboswag.mvi_arch.mediator.LoggingMediator
|
||||
import ru.touchin.roboswag.mvi_arch.mediator.MediatorStore
|
||||
|
||||
/**
|
||||
* Base [ViewModel] to use in MVI architecture.
|
||||
*
|
||||
* @param NavArgs Type of arguments class of this screen.
|
||||
* It must implement [NavArgs] interface provided by navigation library that is a part of Google Jetpack.
|
||||
* An instance of this class is generated by [SafeArgs](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)
|
||||
* plugin according to related configuration file in navigation resource folder of your project.
|
||||
*
|
||||
* @param State Type of view state class of this screen.
|
||||
* It must implement [ViewState] interface. Usually it's a data class that presents full state of current screen's view.
|
||||
* @see [ViewState] for more information.
|
||||
*
|
||||
* @param Action Type of view actions class of this screen.
|
||||
* It must implement [Action] interface. Usually it's a sealed class that contains classes and objects representing
|
||||
* view actions of this view, e.g. button clicks, text changes, etc.
|
||||
* @see [Action] for more information.
|
||||
*
|
||||
* @author Created by Max Bachinsky and Ivan Vlasov at Touch Instinct.
|
||||
*/
|
||||
|
||||
abstract class MviViewModel<NavArgs : Parcelable, Action : ViewAction, State : ViewState>(
|
||||
private val initialState: State,
|
||||
protected val handle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
private val mediatorStore = MediatorStore(
|
||||
listOfNotNull(
|
||||
LoggingMediator(this::class.simpleName!!).takeIf { BuildConfig.DEBUG }
|
||||
)
|
||||
)
|
||||
|
||||
protected val navArgs: NavArgs = handle.get(MviFragment.INIT_ARGS_KEY) ?: throw IllegalStateException("Nav args mustn't be null")
|
||||
|
||||
protected val _state = MutableLiveData(initialState)
|
||||
internal val state = Transformations.distinctUntilChanged(_state)
|
||||
|
||||
protected val currentState: State
|
||||
get() = _state.value ?: initialState
|
||||
|
||||
private val stateMediatorObserver = Observer<State>(mediatorStore::onNewState)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
state.observeForever(stateMediatorObserver)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun dispatchAction(action: Action) {
|
||||
mediatorStore.onAction(action)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
state.removeObserver(stateMediatorObserver)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package ru.touchin.roboswag.mvi_arch.core
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.touchin.mvi_arch.BuildConfig
|
||||
import ru.touchin.roboswag.mvi_arch.marker.SideEffect
|
||||
import ru.touchin.roboswag.mvi_arch.marker.StateChange
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
||||
import ru.touchin.roboswag.mvi_arch.mediator.LoggingMediator
|
||||
import ru.touchin.roboswag.mvi_arch.mediator.MediatorStore
|
||||
|
||||
abstract class Store<Change : StateChange, Effect : SideEffect, State : ViewState>(
|
||||
initialState: State
|
||||
) {
|
||||
|
||||
protected val currentState: State
|
||||
get() = state.value
|
||||
|
||||
private val storeScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val effects = Channel<Effect?>(Channel.UNLIMITED)
|
||||
private val state = MutableStateFlow(initialState)
|
||||
|
||||
private val childStores: MutableList<ChildStore<*, *, *>> = mutableListOf()
|
||||
|
||||
private val mediatorStore = MediatorStore(
|
||||
listOfNotNull(
|
||||
LoggingMediator(this::class.simpleName!!).takeIf { BuildConfig.DEBUG }
|
||||
)
|
||||
)
|
||||
|
||||
init {
|
||||
storeScope.launch {
|
||||
effects
|
||||
.consumeAsFlow()
|
||||
.filterNotNull()
|
||||
.handleSideEffect()
|
||||
.collect { newChange -> changeState(newChange) }
|
||||
}
|
||||
}
|
||||
|
||||
fun changeState(change: Change) {
|
||||
mediatorStore.onStateChange(change)
|
||||
|
||||
val (newState, newEffect) = reduce(currentState, change)
|
||||
|
||||
if (currentState != newState) {
|
||||
state.value = newState
|
||||
mediatorStore.onNewState(newState)
|
||||
}
|
||||
|
||||
childStores.forEach { childStore ->
|
||||
childStore.change(change)
|
||||
}
|
||||
|
||||
newEffect?.let {
|
||||
effects.offer(it)
|
||||
mediatorStore.onEffect(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun observeState(): Flow<State> = state
|
||||
|
||||
fun onCleared() {
|
||||
storeScope.coroutineContext.cancel()
|
||||
childStores.forEach(Store<Change, Effect, State>.ChildStore<*, *, *>::onCleared)
|
||||
}
|
||||
|
||||
fun State.only(): Pair<State, Effect?> = this to null
|
||||
|
||||
fun Effect.only(): Pair<State, Effect> = currentState to this
|
||||
|
||||
fun same(): Pair<State, Effect?> = currentState.only()
|
||||
|
||||
protected fun <ChildChange : StateChange, ChildEffect : SideEffect, ChildState : ViewState> addChildStore(
|
||||
store: Store<ChildChange, ChildEffect, ChildState>,
|
||||
changeMapper: (Change) -> ChildChange?,
|
||||
stateMapper: (ChildState) -> State
|
||||
) {
|
||||
childStores.add(ChildStore(store, changeMapper))
|
||||
|
||||
store
|
||||
.observeState()
|
||||
.onEach { state.value = stateMapper(it) }
|
||||
.launchIn(storeScope)
|
||||
|
||||
}
|
||||
|
||||
protected open fun Flow<Effect>.handleSideEffect(): Flow<Change> = emptyFlow()
|
||||
|
||||
protected abstract fun reduce(currentState: State, change: Change): Pair<State, Effect?>
|
||||
|
||||
private inner class ChildStore<ChildChange : StateChange, ChildEffect : SideEffect, ChildState : ViewState>(
|
||||
val store: Store<ChildChange, ChildEffect, ChildState>,
|
||||
val changeMapper: (Change) -> ChildChange?
|
||||
) {
|
||||
fun onCleared() {
|
||||
store.onCleared()
|
||||
}
|
||||
|
||||
fun change(change: Change) {
|
||||
changeMapper(change)?.let(store::changeState)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.roboswag.mvi_arch.di
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
interface ViewModelAssistedFactory<VM : ViewModel> {
|
||||
fun create(handle: SavedStateHandle): VM
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package ru.touchin.roboswag.mvi_arch.di
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
|
||||
class ViewModelFactory(
|
||||
private val viewModelMap: MutableMap<Class<out ViewModel>, ViewModelAssistedFactory<out ViewModel>>,
|
||||
owner: SavedStateRegistryOwner,
|
||||
arguments: Bundle
|
||||
) : AbstractSavedStateViewModelFactory(owner, arguments) {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
|
||||
return viewModelMap[modelClass]?.create(handle) as? T ?: throw IllegalStateException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.roboswag.mvi_arch.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.MapKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
|
||||
@MapKey
|
||||
annotation class ViewModelKey(val value: KClass<out ViewModel>)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package ru.touchin.roboswag.mvi_arch.marker
|
||||
|
||||
interface SideEffect
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package ru.touchin.roboswag.mvi_arch.marker
|
||||
|
||||
interface StateChange
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package ru.touchin.roboswag.mvi_arch.marker
|
||||
|
||||
/**
|
||||
* This interface should be implemented to create your own view actions and use it with [MviFragment] and [MviViewModel].
|
||||
*
|
||||
* Usually it's sealed class with nested classes and objects representing view actions.
|
||||
*
|
||||
* Quite common cases:
|
||||
* 1. View contains simple button:
|
||||
* object OnButtonClicked : YourViewAction()
|
||||
*
|
||||
* 2. View contains button with parameter:
|
||||
* data class OnButtonWithParamClicked(val param: Param): YourViewAction()
|
||||
*
|
||||
* 3. View contains text input field:
|
||||
* data class OnInputChanged(val input: String): YourViewAction()
|
||||
*
|
||||
* Exemplars of this classes used to generate new [ViewState] in [MviViewModel].
|
||||
*
|
||||
* @author Created by Max Bachinsky and Ivan Vlasov at Touch Instinct.
|
||||
*/
|
||||
interface ViewAction
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.roboswag.mvi_arch.marker
|
||||
|
||||
/**
|
||||
* This interface should be implemented to create your own view state and use it with [MviFragment] and [MviViewModel].
|
||||
*
|
||||
* Usually it's a data class that presents full state of view.
|
||||
*
|
||||
* You should not use mutable values here. All values should be immutable.
|
||||
*
|
||||
* @author Created by Max Bachinsky and Ivan Vlasov at Touch Instinct.
|
||||
*/
|
||||
interface ViewState
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package ru.touchin.roboswag.mvi_arch.mediator
|
||||
|
||||
import com.tylerthrailkill.helpers.prettyprint.pp
|
||||
import ru.touchin.roboswag.core.log.Lc
|
||||
import ru.touchin.roboswag.mvi_arch.marker.SideEffect
|
||||
import ru.touchin.roboswag.mvi_arch.marker.StateChange
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
||||
|
||||
class LoggingMediator(private val objectName: String) : Mediator {
|
||||
override fun onEffect(effect: SideEffect) {
|
||||
logObject(
|
||||
prefix = "New Effect:\n",
|
||||
obj = effect
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAction(action: ViewAction) {
|
||||
logObject(
|
||||
prefix = "New Action:\n",
|
||||
obj = action
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNewState(state: ViewState) {
|
||||
logObject(
|
||||
prefix = "New State:\n",
|
||||
obj = state
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStateChange(change: StateChange) {
|
||||
logObject(
|
||||
prefix = "New State change:\n",
|
||||
obj = change
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> logObject(
|
||||
prefix: String,
|
||||
obj: T
|
||||
) {
|
||||
val builder = StringBuilder()
|
||||
pp(obj = obj, writeTo = builder)
|
||||
|
||||
val prettyOutput = builder.toString()
|
||||
Lc.d("$objectName: $prefix$prettyOutput\n")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package ru.touchin.roboswag.mvi_arch.mediator
|
||||
|
||||
import ru.touchin.roboswag.mvi_arch.marker.SideEffect
|
||||
import ru.touchin.roboswag.mvi_arch.marker.StateChange
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
||||
|
||||
interface Mediator {
|
||||
|
||||
fun onEffect(effect: SideEffect)
|
||||
|
||||
fun onAction(action: ViewAction)
|
||||
|
||||
fun onNewState(state: ViewState)
|
||||
|
||||
fun onStateChange(change: StateChange)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package ru.touchin.roboswag.mvi_arch.mediator
|
||||
|
||||
import ru.touchin.roboswag.mvi_arch.marker.SideEffect
|
||||
import ru.touchin.roboswag.mvi_arch.marker.StateChange
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
|
||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
||||
|
||||
class MediatorStore(private val mediators: List<Mediator>) : Mediator {
|
||||
|
||||
override fun onAction(action: ViewAction) {
|
||||
mediators.forEach { it.onAction(action) }
|
||||
}
|
||||
|
||||
override fun onEffect(effect: SideEffect) {
|
||||
mediators.forEach { it.onEffect(effect) }
|
||||
}
|
||||
|
||||
override fun onNewState(state: ViewState) {
|
||||
mediators.forEach { it.onNewState(state) }
|
||||
}
|
||||
|
||||
override fun onStateChange(change: StateChange) {
|
||||
mediators.forEach { it.onStateChange(change) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">mvi-arch</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
buildFeatures.viewBinding = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":utils")
|
||||
implementation project(":logging")
|
||||
|
||||
implementation "com.google.dagger:dagger"
|
||||
|
||||
implementation 'net.danlew:android.joda'
|
||||
|
||||
implementation "androidx.appcompat:appcompat"
|
||||
|
||||
implementation "androidx.fragment:fragment"
|
||||
implementation "androidx.fragment:fragment-ktx"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx"
|
||||
|
||||
implementation "com.google.firebase:firebase-crashlytics"
|
||||
|
||||
constraints {
|
||||
|
||||
implementation("com.google.dagger:dagger") {
|
||||
version {
|
||||
require '2.10'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("net.danlew:android.joda") {
|
||||
version {
|
||||
require '2.10.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.appcompat:appcompat") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.fragment:fragment") {
|
||||
version {
|
||||
require '1.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.fragment:fragment-ktx") {
|
||||
version {
|
||||
require '1.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("com.google.firebase:firebase-crashlytics") {
|
||||
version {
|
||||
require '17.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8") {
|
||||
version {
|
||||
require '2.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx") {
|
||||
version {
|
||||
require '2.2.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.roboswag.navigation_base"/>
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.navigation_new
|
||||
package ru.touchin.roboswag.navigation_base
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
|
|
@ -28,8 +28,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import ru.touchin.roboswag.core.log.Lc
|
||||
import ru.touchin.roboswag.components.navigation_new.fragments.BaseFragment
|
||||
import ru.touchin.roboswag.components.navigation.viewcontrollers.EmptyState
|
||||
import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
|
|
@ -96,6 +95,7 @@ open class FragmentNavigation(
|
|||
addToStack: Boolean,
|
||||
args: Bundle?,
|
||||
backStackName: String?,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)?
|
||||
) {
|
||||
if (fragmentManager.isDestroyed) {
|
||||
|
|
@ -108,7 +108,7 @@ open class FragmentNavigation(
|
|||
|
||||
val fragmentTransaction = fragmentManager.beginTransaction()
|
||||
transactionSetup?.invoke(fragmentTransaction)
|
||||
fragmentTransaction.replace(containerViewId, fragment, null)
|
||||
fragmentTransaction.replace(containerViewId, fragment, tag)
|
||||
if (addToStack) {
|
||||
fragmentTransaction
|
||||
.addToBackStack(backStackName)
|
||||
|
|
@ -155,9 +155,10 @@ open class FragmentNavigation(
|
|||
args: Bundle? = null,
|
||||
addToStack: Boolean = true,
|
||||
backStackName: String? = null,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)? = null
|
||||
) {
|
||||
addToStack(fragmentClass, null, 0, addToStack, args, backStackName, transactionSetup)
|
||||
addToStack(fragmentClass, null, 0, addToStack, args, backStackName, tag, transactionSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -167,14 +168,15 @@ open class FragmentNavigation(
|
|||
* @param state State of instantiated [Fragment];
|
||||
* @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
|
||||
*/
|
||||
fun <T: Parcelable> push(
|
||||
fragmentClass: KClass<out BaseFragment<*, out T>>,
|
||||
state: T? = null,
|
||||
fun <T : Parcelable> push(
|
||||
fragmentClass: KClass<out StatefulFragment<*, out T>>,
|
||||
state: T,
|
||||
addToStack: Boolean = true,
|
||||
backStackName: String? = null,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)? = null
|
||||
) {
|
||||
push(fragmentClass.java, BaseFragment.args(state ?: EmptyState), addToStack, backStackName, transactionSetup)
|
||||
push(fragmentClass.java, StatefulFragment.args(state), addToStack, backStackName, tag, transactionSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -190,6 +192,7 @@ open class FragmentNavigation(
|
|||
targetFragment: Fragment,
|
||||
targetRequestCode: Int,
|
||||
args: Bundle? = null,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)? = null
|
||||
) {
|
||||
addToStack(
|
||||
|
|
@ -199,6 +202,7 @@ open class FragmentNavigation(
|
|||
true,
|
||||
args,
|
||||
null,
|
||||
tag,
|
||||
transactionSetup
|
||||
)
|
||||
}
|
||||
|
|
@ -211,14 +215,15 @@ open class FragmentNavigation(
|
|||
* @param state State of instantiated [Fragment];
|
||||
* @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
|
||||
*/
|
||||
fun <T: Parcelable> pushForResult(
|
||||
fragmentClass: KClass<out BaseFragment<*, out T>>,
|
||||
fun <T : Parcelable> pushForResult(
|
||||
fragmentClass: KClass<out StatefulFragment<*, out T>>,
|
||||
targetFragment: Fragment,
|
||||
targetRequestCode: Int,
|
||||
state: T? = null,
|
||||
state: T,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)? = null
|
||||
) {
|
||||
pushForResult(fragmentClass.java, targetFragment, targetRequestCode, BaseFragment.args(state ?: EmptyState), transactionSetup)
|
||||
pushForResult(fragmentClass.java, targetFragment, targetRequestCode, StatefulFragment.args(state), tag, transactionSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -233,9 +238,10 @@ open class FragmentNavigation(
|
|||
fragmentClass: Class<out Fragment>,
|
||||
args: Bundle? = null,
|
||||
addToStack: Boolean = true,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)? = null
|
||||
) {
|
||||
addToStack(fragmentClass, null, 0, addToStack, args, TOP_FRAGMENT_TAG_MARK, transactionSetup)
|
||||
addToStack(fragmentClass, null, 0, addToStack, args, TOP_FRAGMENT_TAG_MARK, tag, transactionSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,10 +255,11 @@ open class FragmentNavigation(
|
|||
fun setInitial(
|
||||
fragmentClass: Class<out Fragment>,
|
||||
args: Bundle? = null,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)? = null
|
||||
) {
|
||||
beforeSetInitialActions()
|
||||
setAsTop(fragmentClass, args, false, transactionSetup)
|
||||
setAsTop(fragmentClass, args, false, tag, transactionSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -262,13 +269,14 @@ open class FragmentNavigation(
|
|||
* @param state State of instantiated [Fragment];
|
||||
* @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info.
|
||||
*/
|
||||
fun <T: Parcelable> setInitial(
|
||||
fragmentClass: KClass<out BaseFragment<*, out T>>,
|
||||
state: T? = null,
|
||||
fun <T : Parcelable> setInitial(
|
||||
fragmentClass: KClass<out StatefulFragment<*, out T>>,
|
||||
state: T,
|
||||
tag: String? = null,
|
||||
transactionSetup: ((FragmentTransaction) -> Unit)? = null
|
||||
) {
|
||||
beforeSetInitialActions()
|
||||
setAsTop(fragmentClass.java, BaseFragment.args(state ?: EmptyState), false, transactionSetup)
|
||||
setAsTop(fragmentClass.java, StatefulFragment.args(state), false, tag, transactionSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.navigation
|
||||
package ru.touchin.roboswag.navigation_base
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.MenuItem
|
||||
|
|
@ -25,9 +25,9 @@ import android.view.View
|
|||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import ru.touchin.roboswag.components.navigation.activities.BaseActivity
|
||||
import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener
|
||||
import ru.touchin.roboswag.components.utils.UiUtils
|
||||
import ru.touchin.roboswag.components.utils.hideSoftInput
|
||||
import ru.touchin.roboswag.navigation_base.activities.BaseActivity
|
||||
import ru.touchin.roboswag.navigation_base.activities.OnBackPressedListener
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 11/03/16.
|
||||
|
|
@ -174,7 +174,7 @@ class SimpleActionBarDrawerToggle(
|
|||
}
|
||||
|
||||
override fun onDrawerOpened(drawerView: View) {
|
||||
UiUtils.OfViews.hideSoftInput(activity)
|
||||
activity.hideSoftInput()
|
||||
if (isInvalidateOptionsMenuSupported) {
|
||||
activity.invalidateOptionsMenu()
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2016 Touch Instinct
|
||||
*
|
||||
* This file is part of RoboSwag library.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.navigation_base;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.StrictMode;
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics;
|
||||
|
||||
import net.danlew.android.joda.JodaTimeAndroid;
|
||||
|
||||
import ru.touchin.roboswag.core.log.ConsoleLogProcessor;
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
import ru.touchin.roboswag.core.log.LcGroup;
|
||||
import ru.touchin.roboswag.core.log.LcLevel;
|
||||
import ru.touchin.roboswag.core.utils.CrashlyticsLogProcessor;
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 10/03/16.
|
||||
* Base class of application to extends for Touch Instinct related projects.
|
||||
*/
|
||||
public abstract class TouchinApp extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
JodaTimeAndroid.init(this);
|
||||
if (BuildConfig.DEBUG) {
|
||||
enableStrictMode();
|
||||
Lc.initialize(new ConsoleLogProcessor(LcLevel.VERBOSE), true);
|
||||
LcGroup.UI_LIFECYCLE.disable();
|
||||
} else {
|
||||
try {
|
||||
final FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
|
||||
crashlytics.setCrashlyticsCollectionEnabled(true);
|
||||
Lc.initialize(new CrashlyticsLogProcessor(crashlytics), false);
|
||||
} catch (final NoClassDefFoundError error) {
|
||||
Lc.initialize(new ConsoleLogProcessor(LcLevel.INFO), false);
|
||||
Lc.e("Crashlytics initialization error! Did you forget to add\n"
|
||||
+ "com.google.firebase:firebase-crashlytics\n"
|
||||
+ "to your build.gradle?", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enableStrictMode() {
|
||||
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.permitDiskReads()
|
||||
.permitDiskWrites()
|
||||
.penaltyLog()
|
||||
.build());
|
||||
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,16 +17,19 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.navigation.activities
|
||||
package ru.touchin.roboswag.navigation_base.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ru.touchin.roboswag.components.navigation.keyboard_resizeable.KeyboardBehaviorDetector
|
||||
import ru.touchin.roboswag.components.navigation.viewcontrollers.LifecycleLoggingObserver
|
||||
import ru.touchin.roboswag.core.log.Lc
|
||||
import ru.touchin.roboswag.core.log.LcGroup
|
||||
import ru.touchin.roboswag.navigation_base.fragments.LifecycleLoggingObserver
|
||||
import ru.touchin.roboswag.navigation_base.keyboard_resizeable.KeyboardBehaviorDetector
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 08/03/2016.
|
||||
|
|
@ -36,7 +39,9 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||
|
||||
private val onBackPressedListeners = ArrayList<OnBackPressedListener>()
|
||||
|
||||
open val keyboardBehaviorDetector: KeyboardBehaviorDetector? = null
|
||||
var keyboardBehaviorDetector: KeyboardBehaviorDetector? = null
|
||||
|
||||
open val freezeFontScaleFactor: Boolean = true
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(LifecycleLoggingObserver(this))
|
||||
|
|
@ -44,6 +49,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||
super.onCreate(savedInstanceState, persistentState)
|
||||
|
||||
// Possible work around for market launches. See http://code.google.com/p/android/issues/detail?id=2373
|
||||
// for more details. Essentially, the market launches the main activity on top of other activities.
|
||||
// we never want this to happen. Instead, we check if we are the root and if not, we finish.
|
||||
|
|
@ -51,6 +57,10 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||
Lc.e("Finishing activity as it is launcher but not root")
|
||||
finish()
|
||||
}
|
||||
|
||||
if (freezeFontScaleFactor) {
|
||||
adjustFontScale(resources.configuration)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
|
|
@ -89,4 +99,12 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||
super.onBackPressed()
|
||||
}
|
||||
|
||||
private fun adjustFontScale(configuration: Configuration) {
|
||||
configuration.fontScale = 1f
|
||||
val metrics = resources.displayMetrics
|
||||
(getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(metrics)
|
||||
metrics.scaledDensity = configuration.fontScale * metrics.density
|
||||
baseContext.resources.updateConfiguration(configuration, metrics)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.roboswag.navigation_base.activities
|
||||
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import ru.touchin.roboswag.navigation_base.FragmentNavigation
|
||||
|
||||
abstract class NavigationActivity<TNavigation : FragmentNavigation> : BaseActivity() {
|
||||
|
||||
protected abstract val fragmentContainerViewId: Int
|
||||
|
||||
protected open val transition = FragmentTransaction.TRANSIT_NONE
|
||||
|
||||
abstract val navigation: TNavigation
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.touchin.roboswag.components.navigation.activities;
|
||||
package ru.touchin.roboswag.navigation_base.activities;
|
||||
|
||||
public interface OnBackPressedListener {
|
||||
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package ru.touchin.roboswag.navigation_base.extensions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import ru.touchin.roboswag.navigation_base.fragments.EmptyState
|
||||
|
||||
// This method used to check unique state of each fragment.
|
||||
// If two fragments share same class for state, you should not pass state instance of current fragment to the one you transition to
|
||||
@SuppressLint("Recycle")
|
||||
fun <T : Parcelable> Parcelable.reserialize(): T {
|
||||
var parcel = Parcel.obtain()
|
||||
|
||||
parcel.writeParcelable(this, 0)
|
||||
|
||||
val serializableBytes = parcel.marshall()
|
||||
|
||||
parcel.recycle()
|
||||
|
||||
parcel = Parcel.obtain().apply {
|
||||
unmarshall(serializableBytes, 0, serializableBytes.size)
|
||||
setDataPosition(0)
|
||||
}
|
||||
|
||||
val result = parcel.readParcelable<T>(Thread.currentThread().contextClassLoader)
|
||||
?: throw IllegalStateException("It must not be null")
|
||||
|
||||
parcel.recycle()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
fun Parcelable.copy(): Parcelable =
|
||||
if (this is EmptyState) {
|
||||
EmptyState
|
||||
} else {
|
||||
val parcel = Parcel.obtain()
|
||||
|
||||
parcel.writeParcelable(this, 0)
|
||||
parcel.setDataPosition(0)
|
||||
|
||||
val result = parcel.readParcelable<Parcelable>(
|
||||
javaClass.classLoader ?: Thread.currentThread().contextClassLoader
|
||||
) ?: throw IllegalStateException("Failed to copy tab state")
|
||||
|
||||
parcel.recycle()
|
||||
|
||||
result
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package ru.touchin.roboswag.navigation_base.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
open class BaseFragment<TActivity : FragmentActivity> : Fragment {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(@LayoutRes layoutRes: Int) : super(layoutRes)
|
||||
|
||||
protected val view: View
|
||||
@JvmName("requireViewKtx") get() = requireView()
|
||||
|
||||
protected val activity: TActivity
|
||||
@JvmName("requireActivityKtx") get() = requireActivity() as TActivity
|
||||
|
||||
protected val context: Context
|
||||
@JvmName("requireContextKtx") get() = requireContext()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
lifecycle.addObserver(LifecycleLoggingObserver(this))
|
||||
}
|
||||
|
||||
fun <T : View> findViewById(@IdRes id: Int): T = view.findViewById(id)
|
||||
|
||||
@ColorInt
|
||||
fun getColor(@ColorRes resId: Int): Int = ContextCompat.getColor(requireContext(), resId)
|
||||
|
||||
fun getColorStateList(@ColorRes resId: Int): ColorStateList? = ContextCompat.getColorStateList(context, resId)
|
||||
|
||||
fun getDrawable(@DrawableRes resId: Int): Drawable? = ContextCompat.getDrawable(context, resId)
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.touchin.roboswag.components.navigation.viewcontrollers
|
||||
package ru.touchin.roboswag.navigation_base.fragments
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package ru.touchin.roboswag.navigation_base.fragments
|
||||
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class FragmentViewBindingDelegate<T : ViewBinding>(
|
||||
val fragment: Fragment,
|
||||
val viewBindingFactory: (View) -> T
|
||||
) : ReadOnlyProperty<Fragment, T> {
|
||||
private var binding: T? = null
|
||||
|
||||
init {
|
||||
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
|
||||
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
binding = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||
val binding = binding
|
||||
if (binding != null) {
|
||||
return binding
|
||||
}
|
||||
|
||||
val lifecycle = fragment.viewLifecycleOwner.lifecycle
|
||||
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
|
||||
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
|
||||
}
|
||||
|
||||
return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
|
||||
FragmentViewBindingDelegate(this, viewBindingFactory)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.touchin.roboswag.components.navigation.viewcontrollers
|
||||
package ru.touchin.roboswag.navigation_base.fragments
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package ru.touchin.roboswag.navigation_base.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ru.touchin.roboswag.navigation_base.BuildConfig
|
||||
import ru.touchin.roboswag.navigation_base.extensions.reserialize
|
||||
|
||||
open class StatefulFragment<TActivity : FragmentActivity, TState : Parcelable>(
|
||||
@LayoutRes layoutRes: Int
|
||||
) : BaseFragment<TActivity>(layoutRes) {
|
||||
|
||||
companion object {
|
||||
private const val BASE_FRAGMENT_STATE_EXTRA = "BASE_FRAGMENT_STATE_EXTRA"
|
||||
|
||||
fun args(state: Parcelable?): Bundle = Bundle().also { it.putParcelable(BASE_FRAGMENT_STATE_EXTRA, state) }
|
||||
|
||||
}
|
||||
|
||||
protected lateinit var state: TState
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
state = savedInstanceState?.getParcelable<TState>(BASE_FRAGMENT_STATE_EXTRA)
|
||||
?: arguments?.getParcelable(BASE_FRAGMENT_STATE_EXTRA)
|
||||
?: throw IllegalStateException("Fragment state can't be null")
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
state = state.reserialize()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelable(BASE_FRAGMENT_STATE_EXTRA, state)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package ru.touchin.roboswag.components.navigation.keyboard_resizeable
|
||||
package ru.touchin.roboswag.navigation_base.keyboard_resizeable
|
||||
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import ru.touchin.roboswag.components.navigation.activities.BaseActivity
|
||||
import ru.touchin.roboswag.navigation_base.activities.BaseActivity
|
||||
|
||||
/**
|
||||
* This detector NOT detect landscape fullscreen keyboard
|
||||
|
|
@ -43,7 +43,7 @@ class KeyboardBehaviorDetector(
|
|||
|
||||
if (startNavigationBarHeight == -1) startNavigationBarHeight = bottomInset
|
||||
|
||||
windowInsets
|
||||
ViewCompat.onApplyWindowInsets(view, windowInsets)
|
||||
}
|
||||
ViewCompat.requestApplyInsets(view)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.touchin.roboswag.components.navigation_new.keyboard_resizeable
|
||||
package ru.touchin.roboswag.navigation_base.keyboard_resizeable
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
|
@ -6,14 +6,14 @@ import android.os.Parcelable
|
|||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import ru.touchin.roboswag.components.navigation.activities.BaseActivity
|
||||
import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener
|
||||
import ru.touchin.roboswag.components.navigation_new.fragments.BaseFragment
|
||||
import ru.touchin.roboswag.components.utils.UiUtils
|
||||
import ru.touchin.roboswag.components.utils.hideSoftInput
|
||||
import ru.touchin.roboswag.navigation_base.activities.BaseActivity
|
||||
import ru.touchin.roboswag.navigation_base.activities.OnBackPressedListener
|
||||
import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment
|
||||
|
||||
abstract class KeyboardResizeableFragment<TActivity : BaseActivity, TState : Parcelable>(
|
||||
@LayoutRes layoutRes: Int
|
||||
) : BaseFragment<TActivity, TState>(
|
||||
) : StatefulFragment<TActivity, TState>(
|
||||
layoutRes
|
||||
) {
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ abstract class KeyboardResizeableFragment<TActivity : BaseActivity, TState : Par
|
|||
|
||||
private val keyboardHideListener = OnBackPressedListener {
|
||||
if (isKeyboardVisible) {
|
||||
UiUtils.OfViews.hideSoftInput(activity)
|
||||
activity.hideSoftInput()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.roboswag.navigation_base.scopes
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
annotation class FeatureScope
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.roboswag.navigation_base.scopes
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
annotation class FragmentScope
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.roboswag.navigation_base.scopes
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
annotation class UserLoggedScope
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
navigation-cicerone
|
||||
====
|
||||
|
||||
TODO: rewrite dependencies
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
dependencies {
|
||||
implementation project(":navigation-base")
|
||||
|
||||
implementation("ru.terrakok.cicerone:cicerone")
|
||||
implementation("androidx.fragment:fragment")
|
||||
implementation("com.google.dagger:dagger")
|
||||
kapt("com.google.dagger:dagger-compiler")
|
||||
|
||||
def daggerVersion = "2.27"
|
||||
|
||||
constraints {
|
||||
implementation("ru.terrakok.cicerone:cicerone") {
|
||||
version {
|
||||
require("5.1.0")
|
||||
}
|
||||
}
|
||||
implementation("androidx.fragment:fragment") {
|
||||
version {
|
||||
require("1.2.1")
|
||||
}
|
||||
}
|
||||
implementation("com.google.dagger:dagger") {
|
||||
version {
|
||||
require(daggerVersion)
|
||||
}
|
||||
}
|
||||
kapt("com.google.dagger:dagger-compiler") {
|
||||
version {
|
||||
require(daggerVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.mvi_arch.core_nav" />
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package ru.touchin.roboswag.navigation_cicerone
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import ru.terrakok.cicerone.Navigator
|
||||
import ru.terrakok.cicerone.NavigatorHolder
|
||||
|
||||
class CiceroneTuner(
|
||||
private val navigatorHolder: NavigatorHolder,
|
||||
private val navigator: Navigator
|
||||
) : LifecycleObserver {
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun addNavigator() {
|
||||
navigatorHolder.setNavigator(navigator)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun removeNavigator() {
|
||||
navigatorHolder.removeNavigator()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package ru.touchin.roboswag.navigation_cicerone.flow
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import ru.terrakok.cicerone.Navigator
|
||||
import ru.terrakok.cicerone.NavigatorHolder
|
||||
import ru.terrakok.cicerone.Router
|
||||
import ru.terrakok.cicerone.android.support.SupportAppNavigator
|
||||
import ru.terrakok.cicerone.android.support.SupportAppScreen
|
||||
import ru.touchin.mvi_arch.core_nav.R
|
||||
import ru.touchin.roboswag.navigation_cicerone.CiceroneTuner
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class FlowFragment : Fragment(R.layout.fragment_flow) {
|
||||
|
||||
@Inject
|
||||
@FlowNavigation
|
||||
lateinit var navigatorHolder: NavigatorHolder
|
||||
|
||||
@Inject
|
||||
@FlowNavigation
|
||||
lateinit var router: Router
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
injectComponent()
|
||||
if (childFragmentManager.fragments.isEmpty()) {
|
||||
router.newRootScreen(getLaunchScreen())
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun injectComponent()
|
||||
|
||||
private val exitRouterOnBackPressed = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
router.exit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewLifecycleOwner.lifecycle.addObserver(
|
||||
CiceroneTuner(navigatorHolder = navigatorHolder, navigator = createNavigator())
|
||||
)
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, exitRouterOnBackPressed)
|
||||
}
|
||||
|
||||
open fun createNavigator(): Navigator = SupportAppNavigator(
|
||||
requireActivity(),
|
||||
childFragmentManager,
|
||||
getFragmentContainerId()
|
||||
)
|
||||
|
||||
@IdRes
|
||||
protected fun getFragmentContainerId(): Int = R.id.flow_parent
|
||||
|
||||
abstract fun getLaunchScreen(): SupportAppScreen
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.roboswag.navigation_cicerone.flow
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class FlowNavigation
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package ru.touchin.roboswag.navigation_cicerone.flow
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import ru.terrakok.cicerone.Cicerone
|
||||
import ru.terrakok.cicerone.NavigatorHolder
|
||||
import ru.terrakok.cicerone.Router
|
||||
import ru.touchin.roboswag.navigation_base.scopes.FeatureScope
|
||||
|
||||
@Module
|
||||
class FlowNavigationModule {
|
||||
|
||||
@Provides
|
||||
@FlowNavigation
|
||||
@FeatureScope
|
||||
fun provideCicerone(): Cicerone<Router> = Cicerone.create()
|
||||
|
||||
@Provides
|
||||
@FlowNavigation
|
||||
fun provideNavigatorHolder(@FlowNavigation cicerone: Cicerone<Router>): NavigatorHolder = cicerone.navigatorHolder
|
||||
|
||||
@Provides
|
||||
@FlowNavigation
|
||||
fun provideRouter(@FlowNavigation cicerone: Cicerone<Router>): Router = cicerone.router
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue