build errors

This commit is contained in:
SergeyChernonog 2020-10-17 00:30:56 +03:00
commit 29ea6350e7
199 changed files with 4284 additions and 1950 deletions

2
CODEOWNERS Normal file
View File

@ -0,0 +1,2 @@
# Ответственный за все модули
* @maxbach

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
apply plugin: 'com.android.library'
apply from: '../android-configs/common-config.gradle'

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.bottom_navigation_base"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.bottom_navigation_fragment"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.bottom_navigation_viewcontroller"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.lifecycle_viewcontroller"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
mvi-arch/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

4
mvi-arch/README.md Normal file
View File

@ -0,0 +1,4 @@
mvi_arch
====
TODO: rewrite dependencies

97
mvi-arch/build.gradle Normal file
View File

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

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.mvi_arch" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
package ru.touchin.roboswag.mvi_arch.marker
interface SideEffect

View File

@ -0,0 +1,3 @@
package ru.touchin.roboswag.mvi_arch.marker
interface StateChange

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">mvi-arch</string>
</resources>

1
navigation-base/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

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

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.roboswag.navigation_base"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package ru.touchin.roboswag.components.navigation.activities;
package ru.touchin.roboswag.navigation_base.activities;
public interface OnBackPressedListener {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
package ru.touchin.roboswag.navigation_base.scopes
import javax.inject.Scope
@Scope
annotation class FeatureScope

View File

@ -0,0 +1,6 @@
package ru.touchin.roboswag.navigation_base.scopes
import javax.inject.Scope
@Scope
annotation class FragmentScope

View File

@ -0,0 +1,6 @@
package ru.touchin.roboswag.navigation_base.scopes
import javax.inject.Scope
@Scope
annotation class UserLoggedScope

View File

@ -0,0 +1,4 @@
navigation-cicerone
====
TODO: rewrite dependencies

View File

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

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.mvi_arch.core_nav" />

View File

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

View File

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

View File

@ -0,0 +1,6 @@
package ru.touchin.roboswag.navigation_cicerone.flow
import javax.inject.Qualifier
@Qualifier
annotation class FlowNavigation

View File

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