diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..4a8851c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Ответственный за все модули +* @maxbach diff --git a/README.md b/README.md index 53640c2..f48dec0 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,9 @@ Roboswag - библиотека решений, ускоряющих разра ## Минимальные требования -* Andoroid Api: 19 +* Android Api: 21 * Kotlin: 1.3.11 -* Gradle: 3.2.1 -* Gradle CPD Plugin: 1.1 -* Detekt Plugin: 1.0.0-RC12 +* Gradle: 4.0.0 ## Основная архитектура За основу архитектуры взят подход от Google - MVVM на основе [Android Architecture Components](https://developer.android.com/jetpack/docs/guide). Данный подход популярен в сообществе Android разработки, позволяет разбивать код на мелкие и независимые части, что ускоряет разработку и последующую поддержку приложения. @@ -22,12 +20,8 @@ Roboswag позволяет сочетать эти три решения в о ## Основные инструменты библиотеки ### Работа с RecyclerView RecyclerView - один из самых часто используемых инструментов Android разработчика. Модуль [recyclerview-adapters](/recyclerview-adapters) позволяет сделать работу с RecyclerView более гибкой и делает работу самого элемента быстрее. -### BuildScripts -[BuildScrpts](https://github.com/TouchInstinct/BuildScripts) - набор скриптов, автоматизирующих разработку. Один из главных скриптов - staticAnalysis - инструмент для автоматической проверки кода на соответствие правилам компании. -### Api Generator -Внутренний инструмент компании Touch Instinct для генерации общего кода на разные платформы - Android, iOS и Server. Описанные в одном месте общие классы и Http методы используются на разных платформах. Данный инструмент позволяет сократить время разработки в два раза. ### Работа с SharedPreferences -Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. +Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. Для шифрования данных в SharedPreferences можно использовать [encrypted-shared-prefs](/encrypted-shared-prefs) ### Утилиты и extension функции В Roboswag также есть много [утилитарных](/utils) классов и [extension](/kotlin-extensions) функций, которые позволяют писать часто используемый код в одну строку. @@ -68,7 +62,8 @@ gradle.ext.roboswag = [ 'tabbar-navigation', 'base-map', 'yandex-map', - 'google-map' + 'google-map', + 'encrypted-shared-prefs' ] gradle.ext.roboswag.forEach { module -> diff --git a/android-configs/app-config.gradle b/android-configs/app-config.gradle new file mode 100644 index 0000000..cf6759d --- /dev/null +++ b/android-configs/app-config.gradle @@ -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' diff --git a/android-configs/common-config.gradle b/android-configs/common-config.gradle new file mode 100644 index 0000000..7871c2e --- /dev/null +++ b/android-configs/common-config.gradle @@ -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" +} diff --git a/android-configs/lib-config.gradle b/android-configs/lib-config.gradle new file mode 100644 index 0000000..954df9f --- /dev/null +++ b/android-configs/lib-config.gradle @@ -0,0 +1,3 @@ +apply plugin: 'com.android.library' + +apply from: '../android-configs/common-config.gradle' \ No newline at end of file diff --git a/api-logansquare/build.gradle b/api-logansquare/build.gradle index c522cc9..46d7270 100644 --- a/api-logansquare/build.gradle +++ b/api-logansquare/build.gradle @@ -1,23 +1,48 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} +apply from: "../android-configs/lib-config.gradle" dependencies { - api project(":storable") - api 'net.danlew:android.joda:2.9.9.4' + implementation project(":utils") + implementation project(":logging") + implementation project(":storable") - implementation "androidx.annotation:annotation:$versions.androidx" - implementation "com.squareup.retrofit2:retrofit:$versions.retrofit" - implementation 'ru.touchin:logansquare:1.4.3' + implementation 'net.danlew:android.joda' + + implementation "androidx.core:core" + implementation "androidx.annotation:annotation" + + implementation "com.squareup.retrofit2:retrofit" + + implementation 'ru.touchin:logansquare' + + constraints { + implementation("androidx.core:core") { + version { + require '1.0.0' + } + } + + implementation("ru.touchin:logansquare") { + version { + require '1.4.3' + } + } + + implementation("com.squareup.retrofit2:retrofit") { + version { + require '2.7.0' + } + } + + implementation("androidx.annotation:annotation") { + version { + require '1.0.0' + } + } + + implementation("net.danlew:android.joda") { + version { + require '2.9.9.4' + } + } + } } diff --git a/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java b/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java index 1086021..c2eab36 100644 --- a/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java +++ b/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java @@ -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); diff --git a/base-map/build.gradle b/base-map/build.gradle index b80165f..abacdd7 100644 --- a/base-map/build.gradle +++ b/base-map/build.gradle @@ -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" diff --git a/navigation-new/.gitignore b/bottom-navigation-base/.gitignore similarity index 100% rename from navigation-new/.gitignore rename to bottom-navigation-base/.gitignore diff --git a/bottom-navigation-base/build.gradle b/bottom-navigation-base/build.gradle new file mode 100644 index 0000000..2d47a0f --- /dev/null +++ b/bottom-navigation-base/build.gradle @@ -0,0 +1,26 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + implementation project(":logging") + implementation project(":navigation-base") + + implementation("org.jetbrains.kotlin:kotlin-stdlib") + + implementation("androidx.core:core-ktx") + + implementation("androidx.appcompat:appcompat") + + constraints { + implementation("androidx.appcompat:appcompat") { + version { + require '1.0.0' + } + } + + implementation("androidx.core:core-ktx") { + version { + require '1.0.0' + } + } + } +} diff --git a/bottom-navigation-base/src/main/AndroidManifest.xml b/bottom-navigation-base/src/main/AndroidManifest.xml new file mode 100644 index 0000000..db41985 --- /dev/null +++ b/bottom-navigation-base/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationActivity.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt similarity index 52% rename from tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationActivity.kt rename to bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt index 69679a6..779602f 100644 --- a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationActivity.kt +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt @@ -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 : NavigationActivity() + where TNavigation : FragmentNavigation, + TNavigationFragment : BaseBottomNavigationFragment<*>, + TNavigationContainer : BaseNavigationContainerFragment<*, TNavigation> +{ - val innerNavigation: ViewControllerNavigation - get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation as ViewControllerNavigation + 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) } diff --git a/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationController.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationController.kt new file mode 100644 index 0000000..8f831ef --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationController.kt @@ -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( + private val tabs: SparseArray, + 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> = 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 } + } + } + } + + } + +} diff --git a/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationFragment.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationFragment.kt new file mode 100644 index 0000000..e59221b --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationFragment.kt @@ -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 : 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 + + 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<*, *, *> + +} diff --git a/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFragment.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFragment.kt new file mode 100644 index 0000000..4b85b23 --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFragment.kt @@ -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 : 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 = + arguments?.getSerializable(CONTAINED_CLASS_ARG) as Class + +} diff --git a/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationTab.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationTab.kt new file mode 100644 index 0000000..194d94a --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationTab.kt @@ -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() + +} diff --git a/navigation/.gitignore b/bottom-navigation-fragment/.gitignore similarity index 100% rename from navigation/.gitignore rename to bottom-navigation-fragment/.gitignore diff --git a/bottom-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle new file mode 100644 index 0000000..70d5eea --- /dev/null +++ b/bottom-navigation-fragment/build.gradle @@ -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' + } + } + } +} diff --git a/bottom-navigation-fragment/src/main/AndroidManifest.xml b/bottom-navigation-fragment/src/main/AndroidManifest.xml new file mode 100644 index 0000000..09792dd --- /dev/null +++ b/bottom-navigation-fragment/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationActivity.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationActivity.kt new file mode 100644 index 0000000..1eae1ca --- /dev/null +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationActivity.kt @@ -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() { + + override val navigation by lazy { + FragmentNavigation( + this, + supportFragmentManager, + fragmentContainerViewId, + transition + ) + } + +} diff --git a/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationController.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationController.kt new file mode 100644 index 0000000..de42784 --- /dev/null +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationController.kt @@ -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, + 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( + tabs = fragments, + context = context, + fragmentManager = fragmentManager, + defaultTabId = defaultTabId, + onReselectListener = onReselectListener, + contentContainerViewId = contentContainerViewId, + contentContainerLayoutId = contentContainerLayoutId, + wrapWithNavigationContainer = wrapWithNavigationContainer +) { + + override fun getNavigationContainerClass() = NavigationContainerFragment::class.java + +} diff --git a/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationFragment.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationFragment.kt new file mode 100644 index 0000000..9a2c20e --- /dev/null +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationFragment.kt @@ -0,0 +1,18 @@ +package ru.touchin.roboswag.bottom_navigation_fragment + +import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationFragment + +abstract class BottomNavigationFragment : BaseBottomNavigationFragment() { + + override fun createNavigationController() = BottomNavigationController( + context = requireContext(), + fragments = tabs, + fragmentManager = childFragmentManager, + defaultTabId = defaultTabId, + contentContainerViewId = contentContainerViewId, + contentContainerLayoutId = contentContainerLayoutId, + wrapWithNavigationContainer = wrapWithNavigationContainer, + onReselectListener = reselectListener + ) + +} diff --git a/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationContainerFragment.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationContainerFragment.kt new file mode 100644 index 0000000..200b7ee --- /dev/null +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationContainerFragment.kt @@ -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, FragmentNavigation>() { + + override val navigation by lazy { + FragmentNavigation( + requireContext(), + childFragmentManager, + containerViewId, + transition + ) + } + + override fun onContainerCreated() { + navigation.setInitial(getContainedClass().kotlin, arguments?.getParcelable(FRAGMENT_STATE_ARG)!!) + } + +} diff --git a/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationTab.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationTab.kt new file mode 100644 index 0000000..81e4c6e --- /dev/null +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationTab.kt @@ -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>, + state: Parcelable, + saveStateOnSwitching: Boolean = true +) : BaseNavigationTab(cls, state, saveStateOnSwitching) diff --git a/tabbar-navigation-new/.gitignore b/bottom-navigation-viewcontroller/.gitignore similarity index 100% rename from tabbar-navigation-new/.gitignore rename to bottom-navigation-viewcontroller/.gitignore diff --git a/tabbar-navigation/README.md b/bottom-navigation-viewcontroller/README.md similarity index 100% rename from tabbar-navigation/README.md rename to bottom-navigation-viewcontroller/README.md diff --git a/bottom-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle new file mode 100644 index 0000000..c4d828d --- /dev/null +++ b/bottom-navigation-viewcontroller/build.gradle @@ -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' + } + } + } +} diff --git a/bottom-navigation-viewcontroller/src/main/AndroidManifest.xml b/bottom-navigation-viewcontroller/src/main/AndroidManifest.xml new file mode 100644 index 0000000..656fcf7 --- /dev/null +++ b/bottom-navigation-viewcontroller/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationActivity.kt b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationActivity.kt new file mode 100644 index 0000000..6361f08 --- /dev/null +++ b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationActivity.kt @@ -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, BottomNavigationFragment, NavigationContainerFragment>() { + + final override val navigation by lazy { + ViewControllerNavigation( + this, + supportFragmentManager, + fragmentContainerViewId, + transition + ) + } + +} diff --git a/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationController.kt b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationController.kt new file mode 100644 index 0000000..92d5ec8 --- /dev/null +++ b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationController.kt @@ -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, + 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( + 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 + } + +} + diff --git a/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationFragment.kt b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationFragment.kt new file mode 100644 index 0000000..18d1643 --- /dev/null +++ b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationFragment.kt @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.bottom_navigation_viewcontroller + +import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationFragment + +abstract class BottomNavigationFragment : BaseBottomNavigationFragment() { + + override fun createNavigationController() = BottomNavigationController( + context = requireContext(), + fragmentManager = childFragmentManager, + viewControllers = tabs, + defaultTabId = defaultTabId, + contentContainerViewId = contentContainerViewId, + contentContainerLayoutId = contentContainerLayoutId, + wrapWithNavigationContainer = wrapWithNavigationContainer, + onReselectListener = reselectListener + ) + +} + diff --git a/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationContainerFragment.kt b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationContainerFragment.kt new file mode 100644 index 0000000..bfffaf4 --- /dev/null +++ b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationContainerFragment.kt @@ -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, + ViewControllerNavigation>() { + + override val navigation by lazy { + ViewControllerNavigation( + requireContext(), + childFragmentManager, + containerViewId, + transition + ) + } + + override fun onContainerCreated() { + navigation.setInitialViewController(getContainedClass(), arguments?.getParcelable(FRAGMENT_STATE_ARG)!!) + } + +} diff --git a/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationTab.kt b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationTab.kt new file mode 100644 index 0000000..bc10917 --- /dev/null +++ b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationTab.kt @@ -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>, + 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) diff --git a/build.gradle b/build.gradle index b85ea36..7a61b19 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,14 @@ buildscript { - ext.kotlin_version = '1.3.50' repositories { google() jcenter() maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:4.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'de.aaschmid:gradle-cpd-plugin:1.1' - classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.0.0-RC12" + classpath 'de.aaschmid:gradle-cpd-plugin:3.1' + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.6.0" } } @@ -17,29 +16,16 @@ allprojects { repositories { google() jcenter() - maven { url "http://dl.bintray.com/touchin/touchin-tools" } + maven { + url "https://dl.bintray.com/touchin/touchin-tools" + metadataSources { + artifact() + } + + } } } task clean(type: Delete) { delete rootProject.buildDir } - -ext { - versions = [ - compileSdk : 29, - appcompat : '1.0.2', - androidx : '1.0.0', - material : '1.0.0', - lifecycle : '2.0.0', - dagger : '2.17', - retrofit : '2.4.0', - rxJava : '2.2.2', - rxAndroid : '2.1.0', - crashlytics : '2.9.5', - location : '16.0.0', - coreKtx : '1.1.0', - yandex_mapkit: '3.4.0', - google_maps : '16.1.0' - ] -} diff --git a/google-map/build.gradle b/google-map/build.gradle index 7673daa..18e0af5 100644 --- a/google-map/build.gradle +++ b/google-map/build.gradle @@ -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' + } + } + } } diff --git a/kotlin-extensions/build.gradle b/kotlin-extensions/build.gradle index 797cd49..007e067 100644 --- a/kotlin-extensions/build.gradle +++ b/kotlin-extensions/build.gradle @@ -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' + } + } + } } diff --git a/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt b/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt index d6e2920..5602cef 100644 --- a/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt +++ b/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt @@ -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() } } diff --git a/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt b/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt new file mode 100644 index 0000000..8721e60 --- /dev/null +++ b/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt @@ -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 + } + } + +} diff --git a/lifecycle-rx/build.gradle b/lifecycle-rx/build.gradle index 4d58f46..f78e445 100644 --- a/lifecycle-rx/build.gradle +++ b/lifecycle-rx/build.gradle @@ -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' + } + } + } } diff --git a/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLiveDataDispatcher.kt b/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLiveDataDispatcher.kt index ec18dbd..0bab96d 100644 --- a/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLiveDataDispatcher.kt +++ b/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLiveDataDispatcher.kt @@ -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 Flowable.dispatchTo(liveData: MutableLiveData>): 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 Observable.dispatchTo(liveData: MutableLiveData>): 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 Single.dispatchTo(liveData: MutableLiveData>): 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 Maybe.dispatchTo(liveData: MutableLiveData>): 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): Disposable { - liveData.value = Event.Loading + liveData.setLoadingEvent() return untilDestroy( { liveData.value = Event.Complete }, { throwable -> liveData.value = Event.Error(throwable) }) } + private fun MutableLiveData>.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.setLoadingEvent() { + val loadingContent = Event.Loading + if (Looper.getMainLooper().thread == Thread.currentThread()) { + this.value = loadingContent + } else { + this.postValue(loadingContent) + } + } + } diff --git a/tabbar-navigation/.gitignore b/lifecycle-viewcontroller/.gitignore similarity index 100% rename from tabbar-navigation/.gitignore rename to lifecycle-viewcontroller/.gitignore diff --git a/lifecycle-viewcontroller/build.gradle b/lifecycle-viewcontroller/build.gradle new file mode 100644 index 0000000..e76fa52 --- /dev/null +++ b/lifecycle-viewcontroller/build.gradle @@ -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' + } + } + } +} diff --git a/lifecycle-viewcontroller/src/main/AndroidManifest.xml b/lifecycle-viewcontroller/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d3c02f7 --- /dev/null +++ b/lifecycle-viewcontroller/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/extensions/ViewModelLazy.kt b/lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/extensions/ViewModelLazy.kt new file mode 100644 index 0000000..f402d47 --- /dev/null +++ b/lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/extensions/ViewModelLazy.kt @@ -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 ViewController<*, *>.viewModels( + noinline ownerProducer: () -> ViewModelStoreOwner = { this.fragment }, + noinline factoryProducer: () -> ViewModelProvider.Factory = { LifecycleViewModelProviders.getViewModelFactory(this) } +) = this.fragment.androidViewModels(ownerProducer, factoryProducer) + +@MainThread +inline fun ViewController<*, *>.parentViewModels( + noinline ownerProducer: () -> ViewModelStoreOwner = { this.fragment.parentFragment!! }, + noinline factoryProducer: () -> ViewModelProvider.Factory = { + LifecycleViewModelProviders.getViewModelFactory(this.fragment.parentFragment!!) + } +) = viewModels(ownerProducer, factoryProducer) + +@MainThread +inline fun ViewController<*, *>.targetViewModels( + noinline ownerProducer: () -> ViewModelStoreOwner = { this.fragment.targetFragment!! }, + noinline factoryProducer: () -> ViewModelProvider.Factory = { + LifecycleViewModelProviders.getViewModelFactory(this.fragment.targetFragment!!) + } +) = viewModels(ownerProducer, factoryProducer) + +@MainThread +inline fun ViewController<*, *>.activityViewModels( + noinline factoryProducer: () -> ViewModelProvider.Factory = { LifecycleViewModelProviders.getViewModelFactory(activity) } +) = this.fragment.androidActivityViewModels(factoryProducer) diff --git a/lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/viewmodel/LifecycleViewModelProviders.kt b/lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/viewmodel/LifecycleViewModelProviders.kt new file mode 100644 index 0000000..f10e7c9 --- /dev/null +++ b/lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/viewmodel/LifecycleViewModelProviders.kt @@ -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}. + *

+ * 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) + } + +} diff --git a/lifecycle/build.gradle b/lifecycle/build.gradle index 41a0ebb..ce6fc3e 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -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' + } + } + } } diff --git a/lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt b/lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt new file mode 100644 index 0000000..a4c1873 --- /dev/null +++ b/lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt @@ -0,0 +1,6 @@ +package ru.touchin.lifecycle.extensions + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +fun MutableLiveData.toImmutable() = this as LiveData diff --git a/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt new file mode 100644 index 0000000..7026ddd --- /dev/null +++ b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt @@ -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}. + *

+ * 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.") + } + +} diff --git a/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt index ae3c610..438cdc4 100644 --- a/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt +++ b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt @@ -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}. - *

- * 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() diff --git a/livedata-location/build.gradle b/livedata-location/build.gradle index 297284c..15afa97 100644 --- a/livedata-location/build.gradle +++ b/livedata-location/build.gradle @@ -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' + } + } + } } diff --git a/logging/build.gradle b/logging/build.gradle index 21ed245..57d4392 100644 --- a/logging/build.gradle +++ b/logging/build.gradle @@ -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' + } + } + } } diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java index f1ea1b0..6994622 100644 --- a/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java @@ -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 DATE_TIME_FORMATTER = new ThreadLocalValue<>(() -> new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())); diff --git a/logging/src/main/java/ru/touchin/roboswag/core/utils/CrashlyticsLogProcessor.java b/logging/src/main/java/ru/touchin/roboswag/core/utils/CrashlyticsLogProcessor.java new file mode 100644 index 0000000..5dd9970 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/CrashlyticsLogProcessor.java @@ -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 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); + } + +} diff --git a/mvi-arch/.gitignore b/mvi-arch/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/mvi-arch/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mvi-arch/README.md b/mvi-arch/README.md new file mode 100644 index 0000000..6cd5bb4 --- /dev/null +++ b/mvi-arch/README.md @@ -0,0 +1,4 @@ +mvi_arch +==== + +TODO: rewrite dependencies diff --git a/mvi-arch/build.gradle b/mvi-arch/build.gradle new file mode 100644 index 0000000..6059357 --- /dev/null +++ b/mvi-arch/build.gradle @@ -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) + } + } + } + +} diff --git a/mvi-arch/src/main/AndroidManifest.xml b/mvi-arch/src/main/AndroidManifest.xml new file mode 100644 index 0000000..25831d9 --- /dev/null +++ b/mvi-arch/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviFragment.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviFragment.kt new file mode 100644 index 0000000..80e7758 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviFragment.kt @@ -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( + @LayoutRes layout: Int, + navArgs: NavArgs = EmptyState as NavArgs +) : BaseFragment(layout) + where NavArgs : Parcelable, + State : ViewState, + Action : ViewAction, + VM : MviViewModel { + + 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, ViewModelAssistedFactory> + + 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 viewModel(): Lazy = + 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) } + } + +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviStoreViewModel.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviStoreViewModel.kt new file mode 100644 index 0000000..811a8cd --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviStoreViewModel.kt @@ -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( + initialState: State, + handle: SavedStateHandle +) : MviViewModel(initialState, handle) { + + private lateinit var store: ChildStore<*, *, *> + + protected fun connectStore( + store: Store, + 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( + val store: Store, + val changeMapper: (Action) -> IChange? + ) { + fun onCleared() { + store.onCleared() + } + + fun dispatchAction(action: Action) { + changeMapper(action)?.let(store::changeState) + } + } + +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt new file mode 100644 index 0000000..0654c0e --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt @@ -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( + 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(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) + } + +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/Store.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/Store.kt new file mode 100644 index 0000000..5655d64 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/Store.kt @@ -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( + initialState: State +) { + + protected val currentState: State + get() = state.value + + private val storeScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + private val effects = Channel(Channel.UNLIMITED) + private val state = MutableStateFlow(initialState) + + private val childStores: MutableList> = 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 + + fun onCleared() { + storeScope.coroutineContext.cancel() + childStores.forEach(Store.ChildStore<*, *, *>::onCleared) + } + + fun State.only(): Pair = this to null + + fun Effect.only(): Pair = currentState to this + + fun same(): Pair = currentState.only() + + protected fun addChildStore( + store: Store, + changeMapper: (Change) -> ChildChange?, + stateMapper: (ChildState) -> State + ) { + childStores.add(ChildStore(store, changeMapper)) + + store + .observeState() + .onEach { state.value = stateMapper(it) } + .launchIn(storeScope) + + } + + protected open fun Flow.handleSideEffect(): Flow = emptyFlow() + + protected abstract fun reduce(currentState: State, change: Change): Pair + + private inner class ChildStore( + val store: Store, + val changeMapper: (Change) -> ChildChange? + ) { + fun onCleared() { + store.onCleared() + } + + fun change(change: Change) { + changeMapper(change)?.let(store::changeState) + } + } + +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelAssistedFactory.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelAssistedFactory.kt new file mode 100644 index 0000000..d958ce0 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelAssistedFactory.kt @@ -0,0 +1,8 @@ +package ru.touchin.roboswag.mvi_arch.di + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel + +interface ViewModelAssistedFactory { + fun create(handle: SavedStateHandle): VM +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelFactory.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelFactory.kt new file mode 100644 index 0000000..5ae4d27 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelFactory.kt @@ -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, ViewModelAssistedFactory>, + owner: SavedStateRegistryOwner, + arguments: Bundle +) : AbstractSavedStateViewModelFactory(owner, arguments) { + + @Suppress("UNCHECKED_CAST") + override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { + return viewModelMap[modelClass]?.create(handle) as? T ?: throw IllegalStateException("Unknown ViewModel class") + } +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelKey.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelKey.kt new file mode 100644 index 0000000..672c4d0 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelKey.kt @@ -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) diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/SideEffect.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/SideEffect.kt new file mode 100644 index 0000000..7fea61c --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/SideEffect.kt @@ -0,0 +1,3 @@ +package ru.touchin.roboswag.mvi_arch.marker + +interface SideEffect diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/StateChange.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/StateChange.kt new file mode 100644 index 0000000..1d5da27 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/StateChange.kt @@ -0,0 +1,3 @@ +package ru.touchin.roboswag.mvi_arch.marker + +interface StateChange diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewAction.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewAction.kt new file mode 100644 index 0000000..6e6d7cd --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewAction.kt @@ -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 diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewState.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewState.kt new file mode 100644 index 0000000..3d065e6 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewState.kt @@ -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 diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/LoggingMediator.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/LoggingMediator.kt new file mode 100644 index 0000000..99ffdaf --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/LoggingMediator.kt @@ -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 logObject( + prefix: String, + obj: T + ) { + val builder = StringBuilder() + pp(obj = obj, writeTo = builder) + + val prettyOutput = builder.toString() + Lc.d("$objectName: $prefix$prettyOutput\n") + } +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/Mediator.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/Mediator.kt new file mode 100644 index 0000000..699b3ba --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/Mediator.kt @@ -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) + +} diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/MediatorStore.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/MediatorStore.kt new file mode 100644 index 0000000..70ede8d --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/MediatorStore.kt @@ -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 { + + 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) } + } +} diff --git a/mvi-arch/src/main/res/values/strings.xml b/mvi-arch/src/main/res/values/strings.xml new file mode 100644 index 0000000..3c24c32 --- /dev/null +++ b/mvi-arch/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + mvi-arch + diff --git a/navigation-base/.gitignore b/navigation-base/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/navigation-base/.gitignore @@ -0,0 +1 @@ +/build diff --git a/navigation-new/README.md b/navigation-base/README.md similarity index 100% rename from navigation-new/README.md rename to navigation-base/README.md diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle new file mode 100644 index 0000000..ef03f06 --- /dev/null +++ b/navigation-base/build.gradle @@ -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' + } + } + } +} diff --git a/navigation-base/src/main/AndroidManifest.xml b/navigation-base/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b05e20d --- /dev/null +++ b/navigation-base/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/FragmentNavigation.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt similarity index 89% rename from navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/FragmentNavigation.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt index 8ef56cf..5a1df6c 100644 --- a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/FragmentNavigation.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt @@ -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 push( - fragmentClass: KClass>, - state: T? = null, + fun push( + fragmentClass: KClass>, + 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 pushForResult( - fragmentClass: KClass>, + fun pushForResult( + fragmentClass: KClass>, 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, 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, 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 setInitial( - fragmentClass: KClass>, - state: T? = null, + fun setInitial( + fragmentClass: KClass>, + 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) } /** diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt similarity index 95% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt index 79d417c..cf2e2b3 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt @@ -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() } diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/TouchinApp.java b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/TouchinApp.java new file mode 100644 index 0000000..60cabfd --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/TouchinApp.java @@ -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()); + } + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt similarity index 76% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt index 8fced4c..be2244e 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt @@ -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() - 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) + } + } diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/NavigationActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/NavigationActivity.kt new file mode 100644 index 0000000..ed45972 --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/NavigationActivity.kt @@ -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 : BaseActivity() { + + protected abstract val fragmentContainerViewId: Int + + protected open val transition = FragmentTransaction.TRANSIT_NONE + + abstract val navigation: TNavigation + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/OnBackPressedListener.java similarity index 54% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/OnBackPressedListener.java index de5d318..f1f05c6 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/OnBackPressedListener.java @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation.activities; +package ru.touchin.roboswag.navigation_base.activities; public interface OnBackPressedListener { diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/extensions/Parcelable.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/extensions/Parcelable.kt new file mode 100644 index 0000000..2ec8844 --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/extensions/Parcelable.kt @@ -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 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(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( + javaClass.classLoader ?: Thread.currentThread().contextClassLoader + ) ?: throw IllegalStateException("Failed to copy tab state") + + parcel.recycle() + + result + } diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/BaseFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/BaseFragment.kt new file mode 100644 index 0000000..b38027f --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/BaseFragment.kt @@ -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 : 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 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) + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/EmptyState.kt similarity index 86% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/EmptyState.kt index 12424bc..0afa2dc 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/EmptyState.kt @@ -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 diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentViewBindingDelegate.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentViewBindingDelegate.kt new file mode 100644 index 0000000..6d7bee4 --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentViewBindingDelegate.kt @@ -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( + val fragment: Fragment, + val viewBindingFactory: (View) -> T +) : ReadOnlyProperty { + 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 Fragment.viewBinding(viewBindingFactory: (View) -> T) = + FragmentViewBindingDelegate(this, viewBindingFactory) diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/LifecycleLoggingObserver.kt similarity index 95% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/LifecycleLoggingObserver.kt index 43ef959..a85255b 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/LifecycleLoggingObserver.kt @@ -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 diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/StatefulFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/StatefulFragment.kt new file mode 100644 index 0000000..4e46f3b --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/StatefulFragment.kt @@ -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( + @LayoutRes layoutRes: Int +) : BaseFragment(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(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) + } + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt similarity index 90% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt index cc6880d..30774a2 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt @@ -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) } diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/keyboard_resizeable/KeyboardResizeableFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt similarity index 83% rename from navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/keyboard_resizeable/KeyboardResizeableFragment.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt index 7b03ce2..e43d405 100644 --- a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/keyboard_resizeable/KeyboardResizeableFragment.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt @@ -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( @LayoutRes layoutRes: Int -) : BaseFragment( +) : StatefulFragment( layoutRes ) { @@ -21,7 +21,7 @@ abstract class KeyboardResizeableFragment diff --git a/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/CiceroneTuner.kt b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/CiceroneTuner.kt new file mode 100644 index 0000000..ba525d3 --- /dev/null +++ b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/CiceroneTuner.kt @@ -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() + } + +} diff --git a/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowFragment.kt b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowFragment.kt new file mode 100644 index 0000000..08903be --- /dev/null +++ b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowFragment.kt @@ -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 +} diff --git a/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigation.kt b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigation.kt new file mode 100644 index 0000000..fa67fc7 --- /dev/null +++ b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigation.kt @@ -0,0 +1,6 @@ +package ru.touchin.roboswag.navigation_cicerone.flow + +import javax.inject.Qualifier + +@Qualifier +annotation class FlowNavigation diff --git a/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigationModule.kt b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigationModule.kt new file mode 100644 index 0000000..2592563 --- /dev/null +++ b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigationModule.kt @@ -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 = Cicerone.create() + + @Provides + @FlowNavigation + fun provideNavigatorHolder(@FlowNavigation cicerone: Cicerone): NavigatorHolder = cicerone.navigatorHolder + + @Provides + @FlowNavigation + fun provideRouter(@FlowNavigation cicerone: Cicerone): Router = cicerone.router + +} diff --git a/navigation-cicerone/src/main/res/layout/fragment_flow.xml b/navigation-cicerone/src/main/res/layout/fragment_flow.xml new file mode 100644 index 0000000..b17fb3e --- /dev/null +++ b/navigation-cicerone/src/main/res/layout/fragment_flow.xml @@ -0,0 +1,5 @@ + + diff --git a/navigation-cicerone/src/main/res/values/strings.xml b/navigation-cicerone/src/main/res/values/strings.xml new file mode 100644 index 0000000..3c24c32 --- /dev/null +++ b/navigation-cicerone/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + mvi-arch + diff --git a/navigation-new/build.gradle b/navigation-new/build.gradle deleted file mode 100644 index c53d4a0..0000000 --- a/navigation-new/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - api project(":utils") - api project(":logging") - api project(":navigation") - api project(":api-logansquare") - - api 'androidx.multidex:multidex:2.0.1' - - api 'net.danlew:android.joda:2.10.2' - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation "androidx.appcompat:appcompat:$versions.appcompat" - - implementation "androidx.fragment:fragment:$versions.fragment" - implementation "androidx.fragment:fragment-ktx:$versions.fragment" - - implementation "com.jakewharton:butterknife:$versions.butterknife" - kapt "com.jakewharton:butterknife-compiler:$versions.butterknife" - - implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { - transitive = true - } -} diff --git a/navigation-new/src/main/AndroidManifest.xml b/navigation-new/src/main/AndroidManifest.xml deleted file mode 100644 index 93721cf..0000000 --- a/navigation-new/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/activities/NavigationActivity.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/activities/NavigationActivity.kt deleted file mode 100644 index 39f6d35..0000000 --- a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/activities/NavigationActivity.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ru.touchin.roboswag.components.navigation_new.activities - -import androidx.fragment.app.FragmentTransaction -import ru.touchin.roboswag.components.navigation.activities.BaseActivity -import ru.touchin.roboswag.components.navigation_new.FragmentNavigation - -/** - * Created by Daniil Borisovskii on 15/08/2019. - * Base activity with nested navigation. - */ -abstract class NavigationActivity : BaseActivity() { - - protected abstract val fragmentContainerViewId: Int - - protected open val transition = FragmentTransaction.TRANSIT_NONE - - open val navigation by lazy { - FragmentNavigation( - this, - supportFragmentManager, - fragmentContainerViewId, - transition - ) - } - -} diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/fragments/BaseFragment.kt b/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/fragments/BaseFragment.kt deleted file mode 100644 index 830952a..0000000 --- a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/fragments/BaseFragment.kt +++ /dev/null @@ -1,100 +0,0 @@ -package ru.touchin.roboswag.components.navigation_new.fragments - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.os.Parcel -import android.os.Parcelable -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 -import butterknife.ButterKnife -import butterknife.Unbinder -import ru.touchin.roboswag.components.navigation_new.BuildConfig -import ru.touchin.roboswag.components.navigation.viewcontrollers.LifecycleLoggingObserver - -open class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(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) } - - // 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 - private fun reserialize(parcelable: T): T { - var parcel = Parcel.obtain() - parcel.writeParcelable(parcelable, 0) - val serializableBytes = parcel.marshall() - parcel.recycle() - parcel = Parcel.obtain() - parcel.unmarshall(serializableBytes, 0, serializableBytes.size) - parcel.setDataPosition(0) - val result = parcel.readParcelable(Thread.currentThread().contextClassLoader) ?: throw IllegalStateException("It must not be null") - parcel.recycle() - return result - } - } - - 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() - - protected lateinit var state: TState - private set - - private lateinit var butterKnifeUnbinder: Unbinder - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setHasOptionsMenu(true) - - state = savedInstanceState?.getParcelable(BASE_FRAGMENT_STATE_EXTRA) - ?: arguments?.getParcelable(BASE_FRAGMENT_STATE_EXTRA) - ?: throw IllegalStateException("Fragment state can't be null") - - if (BuildConfig.DEBUG) { - state = reserialize(state) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - lifecycle.addObserver(LifecycleLoggingObserver(this)) - butterKnifeUnbinder = ButterKnife.bind(this, view) - } - - override fun onDestroyView() { - butterKnifeUnbinder.unbind() - super.onDestroyView() - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelable(BASE_FRAGMENT_STATE_EXTRA, state) - } - - fun 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) - -} diff --git a/navigation-viewcontroller/.gitignore b/navigation-viewcontroller/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/navigation-viewcontroller/.gitignore @@ -0,0 +1 @@ +/build diff --git a/navigation/README.md b/navigation-viewcontroller/README.md similarity index 100% rename from navigation/README.md rename to navigation-viewcontroller/README.md diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle new file mode 100644 index 0000000..95448fe --- /dev/null +++ b/navigation-viewcontroller/build.gradle @@ -0,0 +1,43 @@ +apply from: "../android-configs/lib-config.gradle" + +apply plugin: 'kotlin-kapt' + +dependencies { + implementation project(":utils") + implementation project(":logging") + implementation project(":navigation-base") + + implementation 'androidx.multidex:multidex' + + implementation 'net.danlew:android.joda' + + implementation "androidx.appcompat:appcompat" + + implementation("com.crashlytics.sdk.android:crashlytics") + + constraints { + implementation("androidx.multidex:multidex") { + version { + require '2.0.1' + } + } + + implementation("net.danlew:android.joda") { + version { + require '2.10.2' + } + } + + implementation("androidx.appcompat:appcompat") { + version { + require '1.0.2' + } + } + + implementation("com.crashlytics.sdk.android:crashlytics") { + version { + require '2.10.0' + } + } + } +} diff --git a/navigation-viewcontroller/src/main/AndroidManifest.xml b/navigation-viewcontroller/src/main/AndroidManifest.xml new file mode 100644 index 0000000..79d1d6e --- /dev/null +++ b/navigation-viewcontroller/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/fragments/ViewControllerFragment.kt similarity index 97% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt rename to navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/fragments/ViewControllerFragment.kt index 21a2f33..053f9a9 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/fragments/ViewControllerFragment.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation.fragments +package ru.touchin.roboswag.navigation_viewcontroller.fragments import android.animation.Animator import android.annotation.SuppressLint @@ -35,8 +35,8 @@ import android.view.animation.Animation import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle -import ru.touchin.roboswag.components.navigation.BuildConfig -import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController +import ru.touchin.roboswag.navigation_viewcontroller.BuildConfig +import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController /** * Created by Gavriil Sitnikov on 21/10/2015. diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt similarity index 86% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt rename to navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt index 85a4e78..bd7f7c8 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt @@ -1,13 +1,14 @@ -package ru.touchin.roboswag.components.navigation.keyboard_resizeable +package ru.touchin.roboswag.navigation_viewcontroller.keyboard_resizeable import android.os.Build import android.os.Parcelable +import androidx.annotation.CallSuper 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.viewcontrollers.ViewController import ru.touchin.roboswag.components.utils.UiUtils +import ru.touchin.roboswag.navigation_base.activities.BaseActivity +import ru.touchin.roboswag.navigation_base.activities.OnBackPressedListener +import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController abstract class KeyboardResizeableViewController( @LayoutRes layoutRes: Int, @@ -16,6 +17,7 @@ abstract class KeyboardResizeableViewController= Build.VERSION_CODES.KITKAT_WATCH) { creationContext.container?.requestApplyInsets() @@ -55,6 +57,7 @@ abstract class KeyboardResizeableViewController( transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { addToStack( - ViewControllerFragment::class.java, - null, - 0, - addToStack, - ViewControllerFragment.args(viewControllerClass, state), - backStackName, - tag, - transactionSetup + fragmentClass = ViewControllerFragment::class.java, + targetFragment = null, + targetRequestCode = 0, + addToStack = addToStack, + args = ViewControllerFragment.args(viewControllerClass, state), + backStackName = backStackName, + tag = tag, + transactionSetup = transactionSetup ) } @@ -98,14 +97,14 @@ open class ViewControllerNavigation( transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { addToStack( - ViewControllerFragment::class.java, - targetFragment, - targetRequestCode, - true, - ViewControllerFragment.args(viewControllerClass, state), - backStackName, - tag, - transactionSetup + fragmentClass = ViewControllerFragment::class.java, + targetFragment = targetFragment, + targetRequestCode = targetRequestCode, + addToStack = true, + args = ViewControllerFragment.args(viewControllerClass, state), + backStackName = backStackName, + tag = tag, + transactionSetup = transactionSetup ) } @@ -127,14 +126,14 @@ open class ViewControllerNavigation( transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { addToStack( - ViewControllerFragment::class.java, - null, - 0, - addToStack, - ViewControllerFragment.args(viewControllerClass, state), - TOP_FRAGMENT_TAG_MARK, - tag, - transactionSetup + fragmentClass = ViewControllerFragment::class.java, + targetFragment = null, + targetRequestCode = 0, + addToStack = addToStack, + args = ViewControllerFragment.args(viewControllerClass, state), + backStackName = TOP_FRAGMENT_TAG_MARK, + tag = tag, + transactionSetup = transactionSetup ) } diff --git a/navigation/build.gradle b/navigation/build.gradle deleted file mode 100644 index c4fa596..0000000 --- a/navigation/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - api project(":utils") - api project(":logging") - api project(":api-logansquare") - - api 'androidx.multidex:multidex:2.0.1' - - api 'net.danlew:android.joda:2.10.2' - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation "androidx.appcompat:appcompat:$versions.appcompat" - - implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { - transitive = true - } -} diff --git a/navigation/src/main/AndroidManifest.xml b/navigation/src/main/AndroidManifest.xml deleted file mode 100644 index bd2d3ee..0000000 --- a/navigation/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt deleted file mode 100644 index 2a0aa6a..0000000 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * 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.components.navigation - -import android.content.Context -import android.os.Bundle -import android.view.MenuItem -import androidx.annotation.IdRes -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentTransaction -import ru.touchin.roboswag.core.log.Lc - -/** - * Created by Gavriil Sitnikov on 07/03/2016. - * Navigation which is controlling fragments on activity using [FragmentManager]. - * Basically there are 4 main actions to add fragments to activity. - * 1) [.setInitial] means to set fragment on top and remove all previously added fragments from stack; - * 2) [.push] means to simply add fragment on top of the stack; - * 3) [.setAsTop] means to push fragment on top of the stack with specific [.TOP_FRAGMENT_TAG_MARK] tag. - * It is useful to realize up/back navigation: if [.up] method will be called then stack will go to nearest fragment with TOP tag. - * If [.back] method will be called then stack will go to previous fragment. - * Usually such logic using to set as top fragments from sidebar and show hamburger when some of them appeared; - * 4) [.pushForResult] means to push fragment with target fragment. It is also adding [.WITH_TARGET_FRAGMENT_TAG_MARK] tag. - * Also if such up/back navigation logic is not OK then [.backTo] method could be used with any condition to back to. - * In that case in any stack-change method it is allowed to setup fragment transactions. - */ -open class FragmentNavigation( - private val context: Context, - private val fragmentManager: FragmentManager, - @IdRes private val containerViewId: Int, - private val transition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN -) { - - companion object { - const val TOP_FRAGMENT_TAG_MARK = "TOP_FRAGMENT" - } - - /** - * Returns if last fragment in stack is top (added by [.setAsTop] or [.setInitial]) like fragment from sidebar menu. - * - * @return True if last fragment on stack has TOP_FRAGMENT_TAG_MARK. - */ - fun isCurrentFragmentTop(): Boolean = if (fragmentManager.backStackEntryCount == 0) { - true - } else { - fragmentManager - .getBackStackEntryAt(fragmentManager.backStackEntryCount - 1) - .name - ?.contains(TOP_FRAGMENT_TAG_MARK) ?: false - } - - /** - * Allowed to react on [android.app.Activity]'s menu item selection. - * - * @param item Selected menu item; - * @return True if reaction fired. - */ - fun onOptionsItemSelected(item: MenuItem): Boolean = item.itemId == android.R.id.home && back() - - /** - * Base method which is adding fragment to stack. - * - * @param fragmentClass Class of [Fragment] to instantiate; - * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment]; - * @param addToStack Flag to add this transaction to the back stack; - * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; - * @param backStackName Name of [Fragment] in back stack; - * @param tag Optional tag name for the [Fragment]; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - fun addToStack( - fragmentClass: Class, - targetFragment: Fragment?, - targetRequestCode: Int, - addToStack: Boolean, - args: Bundle?, - backStackName: String?, - tag: String?, - transactionSetup: ((FragmentTransaction) -> Unit)? - ) { - if (fragmentManager.isDestroyed) { - Lc.assertion("FragmentManager is destroyed") - return - } - - val fragment = Fragment.instantiate(context, fragmentClass.name, args) - fragment.setTargetFragment(targetFragment, targetRequestCode) - - val fragmentTransaction = fragmentManager.beginTransaction() - transactionSetup?.invoke(fragmentTransaction) - fragmentTransaction.replace(containerViewId, fragment, tag) - if (addToStack) { - fragmentTransaction - .addToBackStack(backStackName) - .setTransition(transition) - } - fragmentTransaction - .setPrimaryNavigationFragment(fragment) - .commit() - } - - /** - * Simply calls [FragmentManager.popBackStack]. - * - * @return True if it have back to some entry in stack. - */ - fun back(): Boolean { - if (fragmentManager.backStackEntryCount >= 1) { - fragmentManager.popBackStack() - return true - } - return false - } - - /** - * Backs to fragment with specific [.TOP_FRAGMENT_TAG_MARK] tag. - * This tag is adding if fragment added to stack via [.setInitial] or [.setAsTop] methods. - * It can be used to create simple up/back navigation. - * - * @return True if it have back to some entry in stack. - */ - fun up(name: String? = null, inclusive: Boolean = false) { - fragmentManager.popBackStack(name, if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0) - } - - /** - * Pushes [Fragment] on top of stack with specific arguments and transaction setup. - * - * @param fragmentClass Class of [Fragment] to instantiate; - * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; - * @param tag Optional tag name for the [Fragment]; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - fun push( - fragmentClass: Class, - args: Bundle? = null, - addToStack: Boolean = true, - backStackName: String? = null, - tag: String? = null, - transactionSetup: ((FragmentTransaction) -> Unit)? = null - ) { - addToStack(fragmentClass, null, 0, addToStack, args, backStackName, tag, transactionSetup) - } - - /** - * Pushes [Fragment] on top of stack with specific target fragment, arguments and transaction setup. - * - * @param fragmentClass Class of [Fragment] to instantiate; - * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment]; - * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; - * @param tag Optional tag name for the [Fragment]; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - fun pushForResult( - fragmentClass: Class, - targetFragment: Fragment, - targetRequestCode: Int, - args: Bundle? = null, - tag: String? = null, - transactionSetup: ((FragmentTransaction) -> Unit)? = null - ) { - addToStack( - fragmentClass, - targetFragment, - targetRequestCode, - true, - args, - null, - tag, - transactionSetup - ) - } - - /** - * Pushes [Fragment] on top of stack with specific transaction setup, arguments - * and with [.TOP_FRAGMENT_TAG_MARK] tag used for simple up/back navigation. - * - * @param fragmentClass Class of [Fragment] to instantiate; - * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; - * @param tag Optional tag name for the [Fragment]; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - fun setAsTop( - fragmentClass: Class, - args: Bundle? = null, - addToStack: Boolean = true, - tag: String? = null, - transactionSetup: ((FragmentTransaction) -> Unit)? = null - ) { - addToStack(fragmentClass, null, 0, addToStack, args, TOP_FRAGMENT_TAG_MARK, tag, transactionSetup) - } - - /** - * Pops all [Fragment]s and places new initial [Fragment] on top of stack with specific transaction setup and arguments. - * - * @param fragmentClass Class of [Fragment] to instantiate; - * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; - * @param tag Optional tag name for the [Fragment]; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - @JvmOverloads - fun setInitial( - fragmentClass: Class, - args: Bundle? = null, - tag: String? = null, - transactionSetup: ((FragmentTransaction) -> Unit)? = null - ) { - beforeSetInitialActions() - setAsTop(fragmentClass, args, false, tag, transactionSetup) - } - - /** - * Method calls every time before initial [Fragment] will be placed. - */ - protected fun beforeSetInitialActions() { - if (fragmentManager.isDestroyed) { - Lc.assertion("FragmentManager is destroyed") - return - } - - if (fragmentManager.backStackEntryCount > 0) { - fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - } - } - -} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java deleted file mode 100644 index 49f4047..0000000 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.components.navigation; - -import android.app.Application; -import android.content.Context; -import android.os.StrictMode; -import android.util.Log; - -import com.crashlytics.android.Crashlytics; - -import net.danlew.android.joda.JodaTimeAndroid; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.multidex.MultiDex; -import io.fabric.sdk.android.Fabric; -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.log.LogProcessor; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import ru.touchin.templates.ApiModel; - -/** - * 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 - protected void attachBaseContext(@NonNull final Context base) { - super.attachBaseContext(base); - MultiDex.install(base); - } - - @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 Crashlytics crashlytics = new Crashlytics(); - Fabric.with(this, crashlytics); - Fabric.getLogger().setLogLevel(Log.ERROR); - 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" - + "compile('com.crashlytics.sdk.android:crashlytics:+@aar') {\n" - + " transitive = true;\n" - + "}\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()); - } - - private static class CrashlyticsLogProcessor extends LogProcessor { - - @NonNull - private final Crashlytics crashlytics; - - public CrashlyticsLogProcessor(@NonNull final Crashlytics crashlytics) { - super(LcLevel.INFO); - this.crashlytics = crashlytics; - } - - @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.core.log(level.getPriority(), tag, message); - } else if (!level.lessThan(LcLevel.ASSERT) - || (group == ApiModel.API_VALIDATION_LC_GROUP && level == LcLevel.ERROR)) { - Log.e(tag, message); - if (throwable != null) { - crashlytics.core.log(level.getPriority(), tag, message); - crashlytics.core.logException(throwable); - } else { - final ShouldNotHappenException exceptionToLog = new ShouldNotHappenException(tag + ':' + message); - reduceStackTrace(exceptionToLog); - crashlytics.core.logException(exceptionToLog); - } - } - } - - private void reduceStackTrace(@NonNull final Throwable throwable) { - final StackTraceElement[] stackTrace = throwable.getStackTrace(); - final List 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); - } - - } - -} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt deleted file mode 100644 index 51e7cfc..0000000 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ru.touchin.roboswag.components.navigation.activities - -import androidx.fragment.app.FragmentTransaction -import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation - -/** - * Created by Daniil Borisovskii on 15/08/2019. - * Base activity with nested navigation. - */ -abstract class NavigationActivity : BaseActivity() { - - protected abstract val fragmentContainerViewId: Int - - protected open val transition = FragmentTransaction.TRANSIT_NONE - - open val navigation by lazy { - ViewControllerNavigation( - this, - supportFragmentManager, - fragmentContainerViewId, - transition - ) - } - -} diff --git a/pagination/README.md b/pagination/README.md new file mode 100644 index 0000000..7ff06b0 --- /dev/null +++ b/pagination/README.md @@ -0,0 +1,4 @@ +pagination +==== + +TODO: rewrite dependencies diff --git a/pagination/build.gradle b/pagination/build.gradle new file mode 100644 index 0000000..4458f16 --- /dev/null +++ b/pagination/build.gradle @@ -0,0 +1,46 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + implementation project(":mvi-arch") + implementation project(":recyclerview-adapters") + implementation project(":utils") + implementation project(":views") + implementation project(":kotlin-extensions") + + implementation("com.google.android.material:material") + implementation("androidx.swiperefreshlayout:swiperefreshlayout") + implementation("androidx.recyclerview:recyclerview") + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android") + + def coroutinesVersion = "1.3.7" + + constraints { + implementation("com.google.android.material:material") { + version { + require("1.2.0") + } + } + implementation("androidx.swiperefreshlayout:swiperefreshlayout") { + version { + require("1.0.0") + } + } + implementation("androidx.recyclerview:recyclerview") { + version { + require("1.1.0") + } + } + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") { + version { + require(coroutinesVersion) + } + } + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android") { + version { + require(coroutinesVersion) + } + } + } +} diff --git a/pagination/src/main/AndroidManifest.xml b/pagination/src/main/AndroidManifest.xml new file mode 100644 index 0000000..49414e3 --- /dev/null +++ b/pagination/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/ErrorItem.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/ErrorItem.kt new file mode 100644 index 0000000..cb5db29 --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ErrorItem.kt @@ -0,0 +1,3 @@ +package ru.touchin.roboswag.pagination + +object ErrorItem diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt new file mode 100644 index 0000000..a2f456e --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt @@ -0,0 +1,48 @@ +package ru.touchin.roboswag.pagination + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ru.touchin.roboswag.recyclerview_adapters.adapters.AdapterDelegate +import ru.touchin.roboswag.recyclerview_adapters.adapters.DelegationListAdapter + +class PaginationAdapter( + private val nextPageCallback: () -> Unit, + private val itemIdDiff: (old: Any, new: Any) -> Boolean, + vararg delegate: AdapterDelegate +) : DelegationListAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean = itemIdDiff(oldItem, newItem) + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean = oldItem == newItem + } +) { + + internal var fullData = false + + init { + addDelegate(ProgressAdapterDelegate()) + delegate.forEach(this::addDelegate) + } + + fun update(data: List, updateState: UpdateState) { + submitList(data + listOfNotNull(when (updateState) { + is UpdateState.Common -> null + is UpdateState.Progress -> ProgressItem + is UpdateState.Error -> ErrorItem + })) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { + super.onBindViewHolder(holder, position, payloads) + if (!fullData && position >= itemCount - 10) nextPageCallback.invoke() + } + + sealed class UpdateState { + object Common : UpdateState() + object Progress : UpdateState() + object Error : UpdateState() + } + +} diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt new file mode 100644 index 0000000..d6d686a --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt @@ -0,0 +1,69 @@ +package ru.touchin.roboswag.pagination + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import ru.touchin.extensions.setOnRippleClickListener +import ru.touchin.mvi_arch.core_pagination.databinding.ViewPaginationBinding + +// TODO: add an errorview with empty state and error text +class PaginationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + + private lateinit var refreshCallback: (() -> Unit) + private lateinit var adapter: PaginationAdapter + + private val binding = ViewPaginationBinding.inflate(LayoutInflater.from(context), this, true) + + init { + with(binding) { + swipeToRefresh.setOnRefreshListener { refreshCallback() } + elementsRecycler.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) + emptyText.setOnRippleClickListener { refreshCallback() } + } + } + + fun init(refreshCallback: () -> Unit, adapter: PaginationAdapter) { + this.refreshCallback = refreshCallback + this.adapter = adapter + binding.elementsRecycler.adapter = adapter + } + + fun render(state: Paginator.State) { + with(binding) { + swipeToRefresh.isRefreshing = state is Paginator.State.Refresh<*> + swipeToRefresh.isEnabled = state !is Paginator.State.EmptyProgress + switcher.showChild( + when (state) { + Paginator.State.Empty, is Paginator.State.EmptyError -> emptyText.id + Paginator.State.EmptyProgress -> progressBar.id + else -> elementsRecycler.id + } + ) + adapter.fullData = state === Paginator.State.Empty || state is Paginator.State.FullData<*> + + when (state) { + is Paginator.State.EmptyError, Paginator.State.Empty, Paginator.State.EmptyProgress -> { + adapter.update(emptyList(), PaginationAdapter.UpdateState.Common) + } + is Paginator.State.Data<*> -> { + adapter.update(state.data as List, PaginationAdapter.UpdateState.Common) + } + is Paginator.State.Refresh<*> -> { + adapter.update(state.data as List, PaginationAdapter.UpdateState.Common) + } + is Paginator.State.NewPageProgress<*> -> { + adapter.update(state.data as List, PaginationAdapter.UpdateState.Progress) + } + is Paginator.State.FullData<*> -> { + adapter.update(state.data as List, PaginationAdapter.UpdateState.Common) + } + } + } + } + +} diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt new file mode 100644 index 0000000..1393444 --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -0,0 +1,156 @@ +package ru.touchin.roboswag.pagination + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import ru.touchin.roboswag.mvi_arch.core.Store +import ru.touchin.roboswag.mvi_arch.marker.SideEffect +import ru.touchin.roboswag.mvi_arch.marker.StateChange +import ru.touchin.roboswag.mvi_arch.marker.ViewState + +class Paginator( + private val errorHandleMod: ErrorHandleMod, + private val loadPage: suspend (Int) -> List, + private val pageSize: Int +) : Store(State.Empty) { + + sealed class Change : StateChange { + object Refresh : Change() + object Restart : Change() + object LoadMore : Change() + object Reset : Change() + data class NewPageLoaded(val pageNumber: Int, val items: List) : Change() + data class PageLoadError(val error: Throwable) : Change() + } + + sealed class Effect : SideEffect { + data class LoadPage(val page: Int = 0) : Effect() + } + + sealed class State : ViewState { + object Empty : State() + object EmptyProgress : State() + data class EmptyError(val error: Throwable) : State() + data class Data(val pageCount: Int = 0, val data: List, val error: Throwable? = null) : State() + data class Refresh(val pageCount: Int, val data: List) : State() + data class NewPageProgress(val pageCount: Int, val data: List) : State() + data class FullData(val pageCount: Int, val data: List) : State() + } + + sealed class Error { + object NewPageFailed : Error() + object RefreshFailed : Error() + } + + sealed class ErrorHandleMod { + data class Alert(val showError: (Error) -> Unit) : ErrorHandleMod() + object ErrorItem : ErrorHandleMod() + } + + override fun reduce(currentState: State, change: Change): Pair = when (change) { + Change.Refresh -> { + when (currentState) { + State.Empty -> State.EmptyProgress + is State.EmptyError -> State.EmptyProgress + is State.Data<*> -> State.Refresh(currentState.pageCount, currentState.data) + is State.NewPageProgress<*> -> State.Refresh(currentState.pageCount, currentState.data) + is State.FullData<*> -> State.Refresh(currentState.pageCount, currentState.data) + else -> currentState + } to Effect.LoadPage() + } + Change.Restart -> { + State.EmptyProgress to Effect.LoadPage() + } + Change.LoadMore -> { + when (currentState) { + is State.Data<*> -> { + State.NewPageProgress(currentState.pageCount, currentState.data) to Effect.LoadPage(currentState.pageCount + 1) + } + else -> currentState.only() + } + } + Change.Reset -> { + State.Empty.only() + } + is Change.NewPageLoaded<*> -> { + val items = change.items + when (currentState) { + is State.EmptyProgress -> { + when { + items.isEmpty() -> State.Empty + items.size < pageSize -> State.FullData(0, items) + else -> State.Data(0, items) + } + } + is State.Refresh<*> -> { + when { + items.isEmpty() -> State.Empty + items.size < pageSize -> State.FullData(0, items) + else -> State.Data(0, items) + } + } + is State.NewPageProgress<*> -> { + if (items.size < pageSize) { + State.FullData(currentState.pageCount, currentState.data + items) + } else { + State.Data(currentState.pageCount + 1, currentState.data + items) + } + } + else -> currentState + }.only() + } + is Change.PageLoadError -> { + when (currentState) { + is State.EmptyProgress -> State.EmptyError(change.error) + is State.Refresh<*> -> { + when (errorHandleMod) { + is ErrorHandleMod.Alert -> { + errorHandleMod.showError(Error.RefreshFailed) + State.Data(currentState.pageCount, currentState.data) + } + is ErrorHandleMod.ErrorItem -> { + State.Data( + pageCount = currentState.pageCount, + data = currentState.data, + error = change.error + ) + } + } + } + is State.NewPageProgress<*> -> { + when (errorHandleMod) { + is ErrorHandleMod.Alert -> { + errorHandleMod.showError(Error.NewPageFailed) + State.Data(currentState.pageCount, currentState.data) + } + is ErrorHandleMod.ErrorItem -> { + State.Data( + pageCount = currentState.pageCount, + data = currentState.data, + error = change.error + ) + } + } + } + else -> currentState + }.only() + } + } + + override fun Flow.handleSideEffect(): Flow = flatMapLatest { effect -> + flow { + when (effect) { + is Effect.LoadPage -> { + try { + val items = loadPage(effect.page) + emit(Change.NewPageLoaded(effect.page, items)) + } catch (e: Exception) { + emit(Change.PageLoadError(e)) + } + + } + } + } + } + +} diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt new file mode 100644 index 0000000..f9b148c --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt @@ -0,0 +1,21 @@ +package ru.touchin.roboswag.pagination + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import ru.touchin.roboswag.recyclerview_adapters.adapters.ItemAdapterDelegate +import ru.touchin.mvi_arch.core_pagination.R +import ru.touchin.roboswag.components.utils.UiUtils + +class ProgressAdapterDelegate : ItemAdapterDelegate() { + + override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder = + object : RecyclerView.ViewHolder(UiUtils.inflate(R.layout.item_progress, parent)) {} + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + item: ProgressItem, + adapterPosition: Int, + collectionPosition: Int, + payloads: MutableList + ) = Unit +} diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt new file mode 100644 index 0000000..ffcc5fb --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt @@ -0,0 +1,3 @@ +package ru.touchin.roboswag.pagination + +object ProgressItem diff --git a/pagination/src/main/res/layout/item_progress.xml b/pagination/src/main/res/layout/item_progress.xml new file mode 100644 index 0000000..726a55b --- /dev/null +++ b/pagination/src/main/res/layout/item_progress.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/pagination/src/main/res/layout/view_pagination.xml b/pagination/src/main/res/layout/view_pagination.xml new file mode 100644 index 0000000..a930418 --- /dev/null +++ b/pagination/src/main/res/layout/view_pagination.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/pagination/src/main/res/values/strings.xml b/pagination/src/main/res/values/strings.xml new file mode 100644 index 0000000..16f2c81 --- /dev/null +++ b/pagination/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + New page load error. Try again. + Refresh screen error. Try again. + diff --git a/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle index f4730f9..8b8971b 100644 --- a/recyclerview-adapters/build.gradle +++ b/recyclerview-adapters/build.gradle @@ -1,18 +1,22 @@ -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(':kotlin-extensions') + implementation project(':kotlin-extensions') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.recyclerview:recyclerview" - implementation "androidx.recyclerview:recyclerview:$versions.androidx" + implementation "androidx.core:core-ktx" + + constraints { + implementation("androidx.recyclerview:recyclerview") { + version { + require '1.0.0' + } + } + implementation("androidx.core:core-ktx") { + version { + require '1.0.0' + } + } + } } diff --git a/recyclerview-adapters/src/main/AndroidManifest.xml b/recyclerview-adapters/src/main/AndroidManifest.xml index b9c7cac..e662989 100644 --- a/recyclerview-adapters/src/main/AndroidManifest.xml +++ b/recyclerview-adapters/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ + package="ru.touchin.roboswag.recyclerview_adapters"/> diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/AdapterDelegate.java b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/AdapterDelegate.java similarity index 98% rename from recyclerview-adapters/src/main/java/ru/touchin/adapters/AdapterDelegate.java rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/AdapterDelegate.java index f9802cf..884e899 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/AdapterDelegate.java +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/AdapterDelegate.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.adapters; +package ru.touchin.roboswag.recyclerview_adapters.adapters; import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegatesManager.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/DelegatesManager.kt similarity index 97% rename from recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegatesManager.kt rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/DelegatesManager.kt index ed73229..834d7ae 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegatesManager.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/DelegatesManager.kt @@ -1,4 +1,4 @@ -package ru.touchin.adapters +package ru.touchin.roboswag.recyclerview_adapters.adapters import androidx.recyclerview.widget.RecyclerView import android.util.SparseArray diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/DelegationListAdapter.kt similarity index 94% rename from recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/DelegationListAdapter.kt index 8328211..dcc3bc4 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/DelegationListAdapter.kt @@ -1,10 +1,10 @@ -package ru.touchin.adapters +package ru.touchin.roboswag.recyclerview_adapters.adapters +import android.view.ViewGroup import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import android.view.ViewGroup import ru.touchin.extensions.setOnRippleClickListener /** @@ -36,7 +36,7 @@ open class DelegationListAdapter(config: AsyncDifferConfig) : Recy if (collectionPosition in 0 until getList().size) { if (itemClickListener != null) { holder.itemView.setOnRippleClickListener { - itemClickListener?.invoke(getList()[getCollectionPosition(holder.adapterPosition)], holder) + getList().getOrNull(getCollectionPosition(holder.adapterPosition))?.let { item -> itemClickListener?.invoke(item, holder) } } } else { holder.itemView.setOnClickListener(null) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/ItemAdapterDelegate.java b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/ItemAdapterDelegate.java similarity index 98% rename from recyclerview-adapters/src/main/java/ru/touchin/adapters/ItemAdapterDelegate.java rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/ItemAdapterDelegate.java index 25ec210..a290cb1 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/ItemAdapterDelegate.java +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/ItemAdapterDelegate.java @@ -1,4 +1,4 @@ -package ru.touchin.adapters; +package ru.touchin.roboswag.recyclerview_adapters.adapters; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/OffsetAdapterUpdateCallback.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/OffsetAdapterUpdateCallback.kt similarity index 93% rename from recyclerview-adapters/src/main/java/ru/touchin/adapters/OffsetAdapterUpdateCallback.kt rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/OffsetAdapterUpdateCallback.kt index cb480f0..a632734 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/OffsetAdapterUpdateCallback.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/OffsetAdapterUpdateCallback.kt @@ -1,4 +1,4 @@ -package ru.touchin.adapters +package ru.touchin.roboswag.recyclerview_adapters.adapters import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/PositionAdapterDelegate.java b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/PositionAdapterDelegate.java similarity index 97% rename from recyclerview-adapters/src/main/java/ru/touchin/adapters/PositionAdapterDelegate.java rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/PositionAdapterDelegate.java index 7da5e5c..1c5fe74 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/PositionAdapterDelegate.java +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/PositionAdapterDelegate.java @@ -1,4 +1,4 @@ -package ru.touchin.adapters; +package ru.touchin.roboswag.recyclerview_adapters.adapters; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/SimpleDataObserver.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/SimpleDataObserver.kt similarity index 92% rename from recyclerview-adapters/src/main/java/ru/touchin/adapters/SimpleDataObserver.kt rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/SimpleDataObserver.kt index 97fc0c6..ddcd2e2 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/SimpleDataObserver.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/adapters/SimpleDataObserver.kt @@ -1,4 +1,4 @@ -package ru.touchin.adapters +package ru.touchin.roboswag.recyclerview_adapters.adapters import androidx.recyclerview.widget.RecyclerView diff --git a/recyclerview-calendar/build.gradle b/recyclerview-calendar/build.gradle index 09ee166..811a091 100644 --- a/recyclerview-calendar/build.gradle +++ b/recyclerview-calendar/build.gradle @@ -1,16 +1,22 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } -} +apply from: "../android-configs/lib-config.gradle" dependencies { - api project(":logging") - api 'net.danlew:android.joda:2.9.9.4' + implementation project(":logging") + implementation 'net.danlew:android.joda' - implementation "androidx.recyclerview:recyclerview:$versions.androidx" + implementation "androidx.recyclerview:recyclerview" + + constraints { + implementation("androidx.recyclerview:recyclerview") { + version { + require '1.0.0' + } + } + + implementation("net.danlew:android.joda") { + version { + require '2.9.9.4' + } + } + } } diff --git a/recyclerview-decorators/.gitignore b/recyclerview-decorators/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/recyclerview-decorators/.gitignore @@ -0,0 +1 @@ +/build diff --git a/recyclerview-decorators/build.gradle b/recyclerview-decorators/build.gradle new file mode 100644 index 0000000..6b6633f --- /dev/null +++ b/recyclerview-decorators/build.gradle @@ -0,0 +1,32 @@ +apply from: "../android-configs/lib-config.gradle" +apply plugin: 'kotlin-android' + +dependencies { + implementation project(":utils") + implementation project(":kotlin-extensions") + + implementation "com.google.android.material:material" + implementation "androidx.core:core-ktx" + + constraints { + implementation("com.google.android.material:material") { + version { + require '1.0.0' + } + } + implementation("androidx.core:core-ktx") { + version { + require '1.3.1' + } + } + implementation("org.jetbrains.kotlin:kotlin-stdlib") { + version { + require '1.3.0' + } + } + } +} + +repositories { + mavenCentral() +} diff --git a/recyclerview-decorators/src/main/AndroidManifest.xml b/recyclerview-decorators/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11a6418 --- /dev/null +++ b/recyclerview-decorators/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/BottomDividerItemDecoration.kt b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/BottomDividerItemDecoration.kt new file mode 100644 index 0000000..e3599d1 --- /dev/null +++ b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/BottomDividerItemDecoration.kt @@ -0,0 +1,30 @@ +package ru.touchin.roboswag.recyclerview_decorators.decorators + +import android.content.Context +import android.graphics.Rect +import android.view.View +import androidx.annotation.DrawableRes +import androidx.recyclerview.widget.RecyclerView + +open class BottomDividerItemDecoration( + context: Context, + @DrawableRes drawableId: Int? = null, + override val predicate: ((position: Int) -> Boolean) = { true }, + override val startMargin: Int = 0, + override val endMargin: Int = 0, + override val offset: Boolean = true, + override val showOnLastItem: Boolean = false +) : DividerItemDecoration(context, drawableId, predicate, startMargin, endMargin, offset, showOnLastItem) { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val position = parent.getChildAdapterPosition(view) + if (offset && predicate(position) && (position != state.itemCount - 1 || showOnLastItem)) { + outRect.set(0, 0, 0, divider.intrinsicHeight) + } + } + + override fun getDividerTop(child: View): Int = getDividerBottom(child) - divider.intrinsicHeight + + override fun getDividerBottom(child: View): Int = bounds.bottom + child.translationY.toInt() + +} diff --git a/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/DividerItemDecoration.kt b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/DividerItemDecoration.kt new file mode 100644 index 0000000..386dd77 --- /dev/null +++ b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/DividerItemDecoration.kt @@ -0,0 +1,74 @@ +package ru.touchin.roboswag.recyclerview_decorators.decorators + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import androidx.annotation.DrawableRes +import androidx.core.content.res.getDrawableOrThrow +import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView +import ru.touchin.roboswag.components.utils.px + +abstract class DividerItemDecoration( + context: Context, + @DrawableRes drawableId: Int? = null, + protected open val predicate: ((position: Int) -> Boolean) = { true }, + protected open val startMargin: Int = 0, + protected open val endMargin: Int = 0, + protected open val offset: Boolean = true, + protected open val showOnLastItem: Boolean = false +) : RecyclerView.ItemDecoration() { + + protected val bounds = Rect() + protected val divider: Drawable + + init { + if (drawableId == null) { + context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider)).apply { + divider = getDrawableOrThrow(0) + recycle() + } + } else { + divider = context.getDrawable(drawableId)!! + } + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (offset) { + drawDivider(canvas, parent, state) + } + } + + override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (!offset) { + drawDivider(canvas, parent, state) + } + } + + private fun drawDivider(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + canvas.save() + parent.children.forEach { child -> + val position = parent.getChildAdapterPosition(child) + if (predicate(position) && (position != state.itemCount - 1 || showOnLastItem)) { + parent.getDecoratedBoundsWithMargins(child, bounds) + val top = getDividerTop(child) + val bottom = getDividerBottom(child) + divider.setBounds( + bounds.left + startMargin, + top, + bounds.right - (endMargin.toFloat().px).toInt(), + bottom + ) + divider.draw(canvas) + } + } + canvas.restore() + } + + abstract fun getDividerTop(child: View): Int + + abstract fun getDividerBottom(child: View): Int + +} diff --git a/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/GroupItemDecoration.kt b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/GroupItemDecoration.kt new file mode 100644 index 0000000..6ff4d29 --- /dev/null +++ b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/GroupItemDecoration.kt @@ -0,0 +1,108 @@ +package ru.touchin.roboswag.recyclerview_decorators.decorators + +import android.graphics.Canvas +import android.graphics.Rect +import android.util.SparseArray +import android.view.View +import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.core.util.set +import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView + +class GroupItemDecoration( + @RecyclerView.Orientation + private val orientation: Int = RecyclerView.VERTICAL, + private val predicate: (adapterPosition: Int) -> Boolean, + private val onCreateViewHolder: (parent: ViewGroup) -> TViewHolder, + private val onBindViewHolder: (adapterPosition: Int, TViewHolder) -> Unit +) : RecyclerView.ItemDecoration() { + + private val viewHoldersPool = SparseArray() + private val bounds = Rect() + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val adapterPosition = parent.getChildAdapterPosition(view) + if (predicate(adapterPosition)) { + calculateOutRectPosition(adapterPosition, parent, outRect) + } else { + viewHoldersPool.remove(adapterPosition) + } + } + + @Suppress("detekt.NestedBlockDepth") + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) = parent.children.forEach { child -> + val adapterPosition = parent.getChildAdapterPosition(child) + val groupView = viewHoldersPool[adapterPosition]?.view + if (predicate(adapterPosition)) { + if (groupView != null) { + onDrawGroupView(groupView, parent, child, canvas) + } else { + parent.invalidateItemDecorations() + return + } + } else if (groupView != null) { + parent.invalidateItemDecorations() + return + } + } + + private fun calculateOutRectPosition(adapterPosition: Int, parent: RecyclerView, outRect: Rect) { + val groupViewHolder = obtainViewHolder(adapterPosition, parent) + onBindViewHolder(adapterPosition, groupViewHolder) + val groupView = groupViewHolder.view + val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams + val widthSpec = getWidthChildMeasureSpec(parent, layoutParams, groupView) + val heightSpec = getHeightChildMeasureSpec(parent, layoutParams, groupView) + groupView.measure(widthSpec, heightSpec) + groupView.layout(0, 0, groupView.measuredWidth, groupView.measuredHeight) + when (orientation) { + RecyclerView.VERTICAL -> outRect.top = groupView.measuredHeight + RecyclerView.HORIZONTAL -> outRect.left = groupView.measuredWidth + } + } + + private fun onDrawGroupView(groupView: View, parent: RecyclerView, child: View, canvas: Canvas) { + val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams + parent.getDecoratedBoundsWithMargins(child, bounds) + canvas.save() + translateCanvasByOrientation(canvas, parent, layoutParams) + groupView.draw(canvas) + canvas.restore() + } + + private fun translateCanvasByOrientation(canvas: Canvas, parent: RecyclerView, layoutParams: ViewGroup.MarginLayoutParams) { + when (orientation) { + RecyclerView.VERTICAL -> canvas.translate(parent.paddingLeft.toFloat() + layoutParams.leftMargin, bounds.top.toFloat()) + RecyclerView.HORIZONTAL -> canvas.translate(bounds.left.toFloat(), parent.paddingTop.toFloat() + layoutParams.topMargin) + } + } + + private fun obtainViewHolder(adapterPosition: Int, parent: RecyclerView): TViewHolder = viewHoldersPool[adapterPosition] + ?: onCreateViewHolder(parent).also { viewHoldersPool[adapterPosition] = it } + + private fun getHeightChildMeasureSpec( + parent: RecyclerView, + layoutParams: ViewGroup.MarginLayoutParams, + groupView: View + ): Int = ViewGroup.getChildMeasureSpec( + View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.EXACTLY), + parent.paddingTop + parent.paddingBottom + layoutParams.topMargin + layoutParams.bottomMargin, + groupView.layoutParams.height + ) + + private fun getWidthChildMeasureSpec( + parent: RecyclerView, + layoutParams: ViewGroup.MarginLayoutParams, + groupView: View + ): Int = ViewGroup.getChildMeasureSpec( + View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY), + parent.paddingLeft + parent.paddingRight + layoutParams.leftMargin + layoutParams.rightMargin, + groupView.layoutParams.width + ) + + open class ViewHolder(val view: View) { + fun findViewById(@IdRes resId: Int): T = view.findViewById(resId) + } + +} diff --git a/rx-extensions/.gitignore b/rx-extensions/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/rx-extensions/.gitignore @@ -0,0 +1 @@ +/build diff --git a/rx-extensions/build.gradle b/rx-extensions/build.gradle new file mode 100644 index 0000000..3e4f58f --- /dev/null +++ b/rx-extensions/build.gradle @@ -0,0 +1,16 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + implementation project(":utils") + implementation project(":logging") + + implementation "io.reactivex.rxjava2:rxjava" + + constraints { + implementation("io.reactivex.rxjava2:rxjava") { + version { + require '2.2.9' + } + } + } +} diff --git a/rx-extensions/src/main/AndroidManifest.xml b/rx-extensions/src/main/AndroidManifest.xml new file mode 100644 index 0000000..afb773f --- /dev/null +++ b/rx-extensions/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt new file mode 100644 index 0000000..9f2f959 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt @@ -0,0 +1,23 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Flowable +import ru.touchin.extensions.rx.utils.StringConstants +import ru.touchin.roboswag.core.utils.Optional +import ru.touchin.roboswag.core.utils.ShouldNotHappenException + +fun Flowable.emitAfter(other: Completable): Flowable = this.flatMap { value -> + other.andThen(Flowable.just(value)) +} + +fun Flowable>.unwrapOrError( + errorMessage: String = StringConstants.OPTIONAL_UNWRAPPING_ERROR_MESSAGE +): Flowable = this.flatMap { wrapper -> + wrapper.get() + ?.let { Flowable.just(it) } + ?: Flowable.error(ShouldNotHappenException(errorMessage)) +} + +fun Flowable>.unwrapOrFilter(): Flowable = this + .filter { it.get() != null } + .map { it.get() } diff --git a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Maybe.kt b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Maybe.kt new file mode 100644 index 0000000..4fdd873 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Maybe.kt @@ -0,0 +1,19 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Maybe +import ru.touchin.extensions.rx.utils.StringConstants +import ru.touchin.roboswag.core.utils.Optional +import ru.touchin.roboswag.core.utils.ShouldNotHappenException + +fun Maybe.emitAfter(other: Completable): Maybe = this.flatMap { value -> + other.andThen(Maybe.just(value)) +} + +fun Maybe>.unwrapOrError( + errorMessage: String = StringConstants.OPTIONAL_UNWRAPPING_ERROR_MESSAGE +): Maybe = this.flatMap { wrapper -> + wrapper.get() + ?.let { Maybe.just(it) } + ?: Maybe.error(ShouldNotHappenException(errorMessage)) +} diff --git a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt new file mode 100644 index 0000000..eb643dd --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt @@ -0,0 +1,23 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Observable +import ru.touchin.extensions.rx.utils.StringConstants +import ru.touchin.roboswag.core.utils.Optional +import ru.touchin.roboswag.core.utils.ShouldNotHappenException + +fun Observable.emitAfter(other: Completable): Observable = this.flatMap { value -> + other.andThen(Observable.just(value)) +} + +fun Observable>.unwrapOrError( + errorMessage: String = StringConstants.OPTIONAL_UNWRAPPING_ERROR_MESSAGE +): Observable = this.flatMap { wrapper -> + wrapper.get() + ?.let { Observable.just(it) } + ?: Observable.error(ShouldNotHappenException(errorMessage)) +} + +fun Observable>.unwrapOrFilter(): Observable = this + .filter { it.get() != null } + .map { it.get() } diff --git a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Single.kt b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Single.kt new file mode 100644 index 0000000..392de40 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Single.kt @@ -0,0 +1,19 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Single +import ru.touchin.extensions.rx.utils.StringConstants +import ru.touchin.roboswag.core.utils.Optional +import ru.touchin.roboswag.core.utils.ShouldNotHappenException + +fun Single.emitAfter(other: Completable): Single = this.flatMap { value -> + other.andThen(Single.just(value)) +} + +fun Single>.unwrapOrError( + errorMessage: String = StringConstants.OPTIONAL_UNWRAPPING_ERROR_MESSAGE +): Single = this.flatMap { wrapper -> + wrapper.get() + ?.let { Single.just(it) } + ?: Single.error(ShouldNotHappenException(errorMessage)) +} diff --git a/rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/StringConstants.kt b/rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/StringConstants.kt new file mode 100644 index 0000000..ad51ae2 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/StringConstants.kt @@ -0,0 +1,5 @@ +package ru.touchin.extensions.rx.utils + +object StringConstants { + const val OPTIONAL_UNWRAPPING_ERROR_MESSAGE = "Wrapped object must not be null" +} diff --git a/storable/build.gradle b/storable/build.gradle index 340a921..58f4456 100644 --- a/storable/build.gradle +++ b/storable/build.gradle @@ -1,25 +1,38 @@ -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(":utils") - api project(":logging") + implementation project(":utils") + implementation project(":logging") - implementation "androidx.core:core:$versions.androidx" - implementation "androidx.annotation:annotation:$versions.androidx" + implementation "androidx.core:core" + implementation "androidx.annotation:annotation" - implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" - implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid" + implementation "io.reactivex.rxjava2:rxjava" + implementation "io.reactivex.rxjava2:rxandroid" + + constraints { + implementation("androidx.core:core") { + version { + require '1.0.0' + } + } + + implementation("androidx.annotation:annotation") { + version { + require '1.0.0' + } + } + + implementation("io.reactivex.rxjava2:rxjava") { + version { + require '2.2.6' + } + } + + implementation("io.reactivex.rxjava2:rxandroid") { + version { + require '2.1.1' + } + } + } } diff --git a/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java index 9b87a51..e9f85ba 100644 --- a/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java +++ b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java @@ -20,6 +20,7 @@ package ru.touchin.roboswag.components.utils.storables; import android.content.SharedPreferences; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -37,7 +38,7 @@ import ru.touchin.roboswag.core.observables.storable.NonNullStorable; public final class PreferenceUtils { /** - * Creates {@link Storable} that stores string into {@link SharedPreferences}. + * Creates {@link Storable} that stores string into {@link SharedPreferences}. Default value is null * * @param name Name of preference; * @param preferences Preferences to store value; diff --git a/tabbar-navigation-new/build.gradle b/tabbar-navigation-new/build.gradle deleted file mode 100644 index a7e4bf3..0000000 --- a/tabbar-navigation-new/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -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 - } -} - -dependencies { - api project(":navigation-new") - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation "androidx.core:core-ktx:$versions.coreKtx" - - implementation "androidx.appcompat:appcompat:$versions.appcompat" -} diff --git a/tabbar-navigation-new/src/main/AndroidManifest.xml b/tabbar-navigation-new/src/main/AndroidManifest.xml deleted file mode 100644 index 07f130a..0000000 --- a/tabbar-navigation-new/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationActivity.kt b/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationActivity.kt deleted file mode 100644 index faf4c84..0000000 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationActivity.kt +++ /dev/null @@ -1,42 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new - -import android.os.Parcelable -import androidx.annotation.IdRes -import androidx.fragment.app.FragmentManager -import ru.touchin.roboswag.components.navigation_new.activities.NavigationActivity -import ru.touchin.roboswag.components.navigation_new.FragmentNavigation - -/** - * Created by Daniil Borisovskii on 15/08/2019. - * Activity to manage tab container navigation. - */ -abstract class BottomNavigationActivity : NavigationActivity() { - - val innerNavigation: FragmentNavigation - get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation - - /** - * Navigates to the given navigation tab. - * Can be called from any node of navigation graph so all back stack will be cleared. - * - * @param navigationTabId Id of navigation tab. - * @param state State of the given tab. If not null tab's fragment will be recreated, otherwise only in case it has not been created before. - */ - fun navigateTo(@IdRes navigationTabId: Int, state: Parcelable? = null) { - supportFragmentManager.run { - // 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) - } - } - - private fun getNavigationContainer(fragmentManager: FragmentManager?): NavigationContainerFragment? = - fragmentManager - ?.primaryNavigationFragment - ?.let { navigationFragment -> - navigationFragment as? NavigationContainerFragment - ?: getNavigationContainer(navigationFragment.childFragmentManager) - } - -} diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationController.kt b/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationController.kt deleted file mode 100644 index 67c0f95..0000000 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationController.kt +++ /dev/null @@ -1,125 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new - -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.components.navigation_new.fragments.BaseFragment -import ru.touchin.roboswag.core.utils.ShouldNotHappenException - -class BottomNavigationController( - private val context: Context, - private val fragmentManager: FragmentManager, - private val fragments: SparseArray>, Parcelable>>, - @IdRes private val contentContainerViewId: Int, - @LayoutRes private val contentContainerLayoutId: Int, - private val wrapWithNavigationContainer: Boolean = false, - @IdRes private val topLevelFragmentId: Int = 0, // If it zero back press with empty fragment back stack would close the app - private val onReselectListener: (() -> Unit)? = null -) { - - private var callback: FragmentManager.FragmentLifecycleCallbacks? = null - - private var currentFragmentId = -1 - - fun attach(navigationTabsContainer: ViewGroup) { - detach() - - //This is provides to set pressed tab status to isActivated providing an opportunity to specify custom style - callback = object : FragmentManager.FragmentLifecycleCallbacks() { - override fun onFragmentViewCreated(fragmentManager: FragmentManager, fragment: Fragment, view: View, savedInstanceState: Bundle?) { - fragments.forEach { itemId, (fragmentClass, _) -> - if (isFragment(fragment, fragmentClass)) { - navigationTabsContainer.children.forEach { itemView -> itemView.isActivated = itemView.id == itemId } - } - } - } - } - fragmentManager.registerFragmentLifecycleCallbacks(callback!!, false) - - navigationTabsContainer.children.forEach { itemView -> - fragments[itemView.id]?.let { (fragmentClass, _) -> - itemView.setOnClickListener { - if (!isFragment(fragmentManager.primaryNavigationFragment, fragmentClass)) { - navigateTo(itemView.id) - } else { - onReselectListener?.invoke() - } - } - } - } - } - - fun detach() = callback?.let(fragmentManager::unregisterFragmentLifecycleCallbacks) - - fun navigateTo(@IdRes itemId: Int, state: Parcelable? = null) { - // Find fragment class that needs to open - val (fragmentClass, defaultFragmentState) = fragments[itemId] ?: return - if (state != null && state::class != defaultFragmentState::class) { - throw ShouldNotHappenException( - "Incorrect state type for navigation tab root Fragment. Should be ${defaultFragmentState::class}" - ) - } - val fragmentState = state ?: defaultFragmentState - val transaction = fragmentManager.beginTransaction() - // Detach current primary fragment - fragmentManager.primaryNavigationFragment?.let(transaction::detach) - val fragmentName = fragmentClass.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 = if (wrapWithNavigationContainer) { - Fragment.instantiate( - context, - fragmentClass.name, - NavigationContainerFragment.args(fragmentClass, fragmentState, contentContainerViewId, contentContainerLayoutId) - ) - } else { - Fragment.instantiate( - context, - fragmentClass.name, - BaseFragment.args(fragmentState) - ) - } - transaction.add(contentContainerViewId, fragment, fragmentName) - } - - transaction - .setPrimaryNavigationFragment(fragment) - .setReorderingAllowed(true) - .commit() - - currentFragmentId = 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 - && topLevelFragmentId != 0 - && currentFragmentId != topLevelFragmentId) { - navigateTo(topLevelFragmentId) - true - } else { - false - } - - private fun isFragment(fragment: Fragment?, fragmentClass: Class>) = - if (wrapWithNavigationContainer) { - (fragment as NavigationContainerFragment).getFragmentClass() - } else { - (fragment as BaseFragment<*, *>).javaClass - } === fragmentClass -} diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationFragment.kt b/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationFragment.kt deleted file mode 100644 index cb204f3..0000000 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationFragment.kt +++ /dev/null @@ -1,75 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new - -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.components.navigation.activities.OnBackPressedListener -import ru.touchin.roboswag.components.navigation_new.fragments.BaseFragment - -abstract class BottomNavigationFragment : Fragment() { - - private lateinit var bottomNavigationController: BottomNavigationController - - private val backPressedListener = OnBackPressedListener { bottomNavigationController.onBackPressed() } - - protected abstract val rootLayoutId: Int - - protected abstract val navigationContainerViewId: Int - - protected abstract val contentContainerViewId: Int - - protected abstract val contentContainerLayoutId: Int - - protected abstract val topLevelFragmentId: Int - - protected abstract val wrapWithNavigationContainer: Boolean - - protected abstract val navigationFragments: SparseArray>, Parcelable>> - - protected open val reselectListener: (() -> Unit) = { getNavigationActivity().innerNavigation.up(inclusive = true) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - bottomNavigationController = BottomNavigationController( - context = requireContext(), - fragmentManager = childFragmentManager, - fragments = navigationFragments, - contentContainerViewId = contentContainerViewId, - contentContainerLayoutId = contentContainerLayoutId, - topLevelFragmentId = topLevelFragmentId, - wrapWithNavigationContainer = wrapWithNavigationContainer, - onReselectListener = reselectListener - ) - if (savedInstanceState == null) { - bottomNavigationController.navigateTo(topLevelFragmentId) - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val fragmentView = inflater.inflate(rootLayoutId, container, false) - - bottomNavigationController.attach(fragmentView.findViewById(navigationContainerViewId)) - - (activity as BottomNavigationActivity).addOnBackPressedListener(backPressedListener) - - return fragmentView - } - - override fun onDestroyView() { - super.onDestroyView() - (activity as BottomNavigationActivity).removeOnBackPressedListener(backPressedListener) - bottomNavigationController.detach() - } - - fun navigateTo(@IdRes navigationTabId: Int, state: Parcelable? = null) { - bottomNavigationController.navigateTo(navigationTabId, state) - } - - private fun getNavigationActivity() = requireActivity() as BottomNavigationActivity - -} diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/NavigationContainerFragment.kt b/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/NavigationContainerFragment.kt deleted file mode 100644 index 160f883..0000000 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/NavigationContainerFragment.kt +++ /dev/null @@ -1,77 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new - -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.components.navigation_new.FragmentNavigation -import ru.touchin.roboswag.components.navigation_new.fragments.BaseFragment -import ru.touchin.roboswag.core.utils.ShouldNotHappenException - -class NavigationContainerFragment : Fragment() { - - companion object { - private const val FRAGMENT_CLASS_ARG = "FRAGMENT_CLASS_ARG" - private const val FRAGMENT_STATE_ARG = "FRAGMENT_STATE_ARG" - private const val CONTAINER_VIEW_ID_ARG = "CONTAINER_VIEW_ID_ARG" - private const val CONTAINER_LAYOUT_ID_ARG = "CONTAINER_LAYOUT_ID_ARG" - private const val TRANSITION_ARG = "TRANSITION_ARG" - - fun args( - cls: Class>, - state: Parcelable, - @IdRes containerViewId: Int, - @LayoutRes containerLayoutId: Int, - transition: Int = FragmentTransaction.TRANSIT_NONE - ) = Bundle().apply { - putSerializable(FRAGMENT_CLASS_ARG, cls) - putParcelable(FRAGMENT_STATE_ARG, state) - putInt(CONTAINER_VIEW_ID_ARG, containerViewId) - putInt(CONTAINER_LAYOUT_ID_ARG, containerLayoutId) - putInt(TRANSITION_ARG, transition) - } - } - - val navigation by lazy { - FragmentNavigation( - requireContext(), - childFragmentManager, - containerViewId, - transition - ) - } - - @IdRes - private var containerViewId = 0 - - @LayoutRes - private var containerLayoutId = 0 - - private var transition = 0 - - @Suppress("UNCHECKED_CAST") - fun getFragmentClass(): Class> = - arguments?.getSerializable(FRAGMENT_CLASS_ARG) as Class> - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) { - val args = arguments ?: throw ShouldNotHappenException("Fragment is not instantiable without arguments") - with(args) { - containerViewId = getInt(CONTAINER_VIEW_ID_ARG) - containerLayoutId = getInt(CONTAINER_LAYOUT_ID_ARG) - transition = getInt(TRANSITION_ARG) - } - navigation.setInitial(getFragmentClass(), args.getParcelable(FRAGMENT_STATE_ARG)) - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(containerLayoutId, container, false) - -} diff --git a/tabbar-navigation/build.gradle b/tabbar-navigation/build.gradle deleted file mode 100644 index fb98a51..0000000 --- a/tabbar-navigation/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -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 - } -} - -dependencies { - api project(":navigation") - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation "androidx.core:core-ktx:$versions.coreKtx" - - implementation "androidx.appcompat:appcompat:$versions.appcompat" -} diff --git a/tabbar-navigation/src/main/AndroidManifest.xml b/tabbar-navigation/src/main/AndroidManifest.xml deleted file mode 100644 index 9437bbd..0000000 --- a/tabbar-navigation/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationController.kt b/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationController.kt deleted file mode 100644 index b8a1bc6..0000000 --- a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationController.kt +++ /dev/null @@ -1,126 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation - -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.components.navigation.fragments.ViewControllerFragment -import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController -import ru.touchin.roboswag.core.utils.ShouldNotHappenException - -class BottomNavigationController( - private val context: Context, - private val fragmentManager: FragmentManager, - private val viewControllers: SparseArray, - @IdRes private val contentContainerViewId: Int, - @LayoutRes private val contentContainerLayoutId: Int, - private val wrapWithNavigationContainer: Boolean = false, - @IdRes private val topLevelViewControllerId: Int = 0, // If it zero back press with empty fragment back stack would close the app - private val onReselectListener: (() -> Unit)? = null -) { - - private var callback: FragmentManager.FragmentLifecycleCallbacks? = null - - private var currentViewControllerId = -1 - - fun attach(navigationTabsContainer: ViewGroup) { - detach() - - //This is provides to set pressed tab status to isActivated providing an opportunity to specify custom style - callback = object : FragmentManager.FragmentLifecycleCallbacks() { - override fun onFragmentViewCreated(fragmentManager: FragmentManager, fragment: Fragment, view: View, savedInstanceState: Bundle?) { - viewControllers.forEach { itemId, (viewControllerClass, _) -> - if (isViewControllerFragment(fragment, viewControllerClass)) { - navigationTabsContainer.children.forEach { itemView -> itemView.isActivated = itemView.id == itemId } - } - } - } - } - fragmentManager.registerFragmentLifecycleCallbacks(callback!!, false) - - navigationTabsContainer.children.forEach { itemView -> - viewControllers[itemView.id]?.let { (viewControllerClass, _) -> - itemView.setOnClickListener { - if (!isViewControllerFragment(fragmentManager.primaryNavigationFragment, viewControllerClass)) { - navigateTo(itemView.id) - } else { - onReselectListener?.invoke() - } - } - } - } - } - - fun detach() = callback?.let(fragmentManager::unregisterFragmentLifecycleCallbacks) - - fun navigateTo(@IdRes itemId: Int, state: Parcelable? = null) { - // Find view controller class that needs to open - val (viewControllerClass, defaultViewControllerState, saveStateOnSwitching) = viewControllers[itemId] ?: return - if (state != null && state::class != defaultViewControllerState::class) { - throw ShouldNotHappenException( - "Incorrect state type for navigation tab root ViewController. Should be ${defaultViewControllerState::class}" - ) - } - val viewControllerState = state ?: defaultViewControllerState - val transaction = fragmentManager.beginTransaction() - // Detach current primary fragment - fragmentManager.primaryNavigationFragment?.let(transaction::detach) - val viewControllerName = viewControllerClass.canonicalName - var fragment = fragmentManager.findFragmentByTag(viewControllerName) - - if (saveStateOnSwitching && state == null && fragment != null) { - transaction.attach(fragment) - } else { - // If fragment already exist remove it first - if (fragment != null) transaction.remove(fragment) - - fragment = if (wrapWithNavigationContainer) { - Fragment.instantiate( - context, - NavigationContainerFragment::class.java.name, - NavigationContainerFragment.args(viewControllerClass, viewControllerState, contentContainerViewId, contentContainerLayoutId) - ) - } else { - Fragment.instantiate( - context, - ViewControllerFragment::class.java.name, - ViewControllerFragment.args(viewControllerClass, viewControllerState) - ) - } - transaction.add(contentContainerViewId, fragment, viewControllerName) - } - - transaction - .setPrimaryNavigationFragment(fragment) - .setReorderingAllowed(true) - .commit() - - currentViewControllerId = 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 - && topLevelViewControllerId != 0 - && currentViewControllerId != topLevelViewControllerId) { - navigateTo(topLevelViewControllerId) - true - } else { - false - } - - private fun isViewControllerFragment(fragment: Fragment?, viewControllerClass: Class>) = - if (wrapWithNavigationContainer) { - (fragment as NavigationContainerFragment).getViewControllerClass() - } else { - (fragment as ViewControllerFragment<*, *>).viewControllerClass - } === viewControllerClass -} diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationFragment.kt b/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationFragment.kt deleted file mode 100644 index c1364fc..0000000 --- a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationFragment.kt +++ /dev/null @@ -1,118 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation - -import android.os.Bundle -import android.os.Parcel -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.components.navigation.activities.OnBackPressedListener -import ru.touchin.roboswag.components.navigation.viewcontrollers.EmptyState -import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController - -abstract class BottomNavigationFragment : Fragment() { - - private lateinit var bottomNavigationController: BottomNavigationController - - private val backPressedListener = OnBackPressedListener { bottomNavigationController.onBackPressed() } - - protected abstract val rootLayoutId: Int - - protected abstract val navigationContainerViewId: Int - - protected abstract val contentContainerViewId: Int - - protected abstract val contentContainerLayoutId: Int - - protected abstract val topLevelViewControllerId: Int - - protected abstract val wrapWithNavigationContainer: Boolean - - protected abstract val navigationViewControllers: SparseArray - - protected open val reselectListener: (() -> Unit) = { getNavigationActivity().innerNavigation.up(inclusive = true) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - bottomNavigationController = BottomNavigationController( - context = requireContext(), - fragmentManager = childFragmentManager, - viewControllers = navigationViewControllers, - contentContainerViewId = contentContainerViewId, - contentContainerLayoutId = contentContainerLayoutId, - topLevelViewControllerId = topLevelViewControllerId, - wrapWithNavigationContainer = wrapWithNavigationContainer, - onReselectListener = reselectListener - ) - if (savedInstanceState == null) { - bottomNavigationController.navigateTo(topLevelViewControllerId) - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val fragmentView = inflater.inflate(rootLayoutId, container, false) - - bottomNavigationController.attach(fragmentView.findViewById(navigationContainerViewId)) - - (activity as BottomNavigationActivity).addOnBackPressedListener(backPressedListener) - - return fragmentView - } - - override fun onDestroyView() { - super.onDestroyView() - (activity as BottomNavigationActivity).removeOnBackPressedListener(backPressedListener) - bottomNavigationController.detach() - } - - fun navigateTo(@IdRes navigationTabId: Int, state: Parcelable? = null) { - bottomNavigationController.navigateTo(navigationTabId, state) - } - - private fun getNavigationActivity() = requireActivity() as BottomNavigationActivity - - class TabData( - val viewControllerClass: Class>, - viewControllerState: 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 viewControllerState = viewControllerState - get() = field.copy() - - operator fun component1() = viewControllerClass - - operator fun component2() = viewControllerState - - operator fun component3() = saveStateOnSwitching - - private 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( - javaClass.classLoader ?: Thread.currentThread().contextClassLoader - ) ?: throw IllegalStateException("Failed to copy tab state") - parcel.recycle() - result - } - - } - -} diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/NavigationContainerFragment.kt b/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/NavigationContainerFragment.kt deleted file mode 100644 index 7948b9e..0000000 --- a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/NavigationContainerFragment.kt +++ /dev/null @@ -1,79 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation - -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.components.navigation.viewcontrollers.ViewController -import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation -import ru.touchin.roboswag.core.utils.ShouldNotHappenException - -class NavigationContainerFragment : Fragment() { - - companion object { - private const val VIEW_CONTROLLER_CLASS_ARG = "VIEW_CONTROLLER_CLASS_ARG" - private const val VIEW_CONTROLLER_STATE_ARG = "VIEW_CONTROLLER_STATE_ARG" - private const val CONTAINER_VIEW_ID_ARG = "CONTAINER_VIEW_ID_ARG" - private const val CONTAINER_LAYOUT_ID_ARG = "CONTAINER_LAYOUT_ID_ARG" - private const val TRANSITION_ARG = "TRANSITION_ARG" - - fun args( - cls: Class>, - state: Parcelable, - @IdRes containerViewId: Int, - @LayoutRes containerLayoutId: Int, - transition: Int = FragmentTransaction.TRANSIT_NONE - ) = Bundle().apply { - putSerializable(VIEW_CONTROLLER_CLASS_ARG, cls) - putParcelable(VIEW_CONTROLLER_STATE_ARG, state) - putInt(CONTAINER_VIEW_ID_ARG, containerViewId) - putInt(CONTAINER_LAYOUT_ID_ARG, containerLayoutId) - putInt(TRANSITION_ARG, transition) - } - } - - val navigation by lazy { - ViewControllerNavigation( - requireContext(), - childFragmentManager, - containerViewId, - transition - ) - } - - @IdRes - private var containerViewId = 0 - - @LayoutRes - private var containerLayoutId = 0 - - private var transition = 0 - - @Suppress("UNCHECKED_CAST") - fun getViewControllerClass(): Class> = - arguments?.getSerializable(VIEW_CONTROLLER_CLASS_ARG) as Class> - - 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) { - navigation.setInitialViewController(getViewControllerClass(), args.getParcelable(VIEW_CONTROLLER_STATE_ARG) - ?: throw ShouldNotHappenException("Fragment state must not be null")) - } - } ?: throw ShouldNotHappenException("Fragment is not instantiable without arguments") - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(containerLayoutId, container, false) - -} diff --git a/utils/build.gradle b/utils/build.gradle index ac3a82f..2a77fe4 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -1,21 +1,20 @@ -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 { - implementation "androidx.core:core:$versions.androidx" - implementation "androidx.annotation:annotation:$versions.androidx" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.core:core" + implementation "androidx.annotation:annotation" + + constraints { + implementation("androidx.core:core") { + version { + require '1.0.0' + } + } + + implementation("androidx.annotation:annotation") { + version { + require '1.1.0' + } + } + } } diff --git a/utils/src/main/java/ru/touchin/defaults/DefaultActivityLifecycleCallbacks.kt b/utils/src/main/java/ru/touchin/defaults/DefaultActivityLifecycleCallbacks.kt new file mode 100644 index 0000000..d464502 --- /dev/null +++ b/utils/src/main/java/ru/touchin/defaults/DefaultActivityLifecycleCallbacks.kt @@ -0,0 +1,23 @@ +package ru.touchin.defaults + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +open class DefaultActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { + + override fun onActivityPaused(activity: Activity) = Unit + + override fun onActivityResumed(activity: Activity) = Unit + + override fun onActivityStarted(activity: Activity) = Unit + + override fun onActivityDestroyed(activity: Activity) = Unit + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) = Unit + + override fun onActivityStopped(activity: Activity) = Unit + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit + +} diff --git a/utils/src/main/java/ru/touchin/hardware/Extensions.kt b/utils/src/main/java/ru/touchin/hardware/Extensions.kt new file mode 100644 index 0000000..7e76320 --- /dev/null +++ b/utils/src/main/java/ru/touchin/hardware/Extensions.kt @@ -0,0 +1,42 @@ +package ru.touchin.hardware + +import android.Manifest +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.VibrationEffect.DEFAULT_AMPLITUDE +import android.os.Vibrator +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission + +@RequiresPermission(Manifest.permission.VIBRATE) +@RequiresApi(Build.VERSION_CODES.O) +fun Context.startVibrate(vibrationEffect: VibrationEffect) { + (this.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate(vibrationEffect) + +} + +@RequiresPermission(Manifest.permission.VIBRATE) +fun Context.startVibrate(duration: Long = 500, pattern: LongArray = LongArray(0)) { + (this.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.let { vibrationService -> + if (pattern.isEmpty()) { + vibrationService.vibrate(duration) + } else { + vibrationService.vibrate(pattern, duration.toInt()) + } + } +} + +@RequiresPermission(Manifest.permission.VIBRATE) +fun Context.startSimpleVibration(duration: Long = 200) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startVibrate(VibrationEffect.createOneShot(duration, DEFAULT_AMPLITUDE)) + } else { + startVibrate(duration) + } +} + +@RequiresPermission(Manifest.permission.VIBRATE) +fun Context.cancelVibrate() { + (this.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.cancel() +} diff --git a/utils/src/main/java/ru/touchin/roboswag/components/utils/ContextExtensions.kt b/utils/src/main/java/ru/touchin/roboswag/components/utils/ContextExtensions.kt new file mode 100644 index 0000000..b8522ad --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/ContextExtensions.kt @@ -0,0 +1,11 @@ +package ru.touchin.roboswag.components.utils + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat + +fun Context.getColorSimple(@ColorRes id: Int): Int = ContextCompat.getColor(this, id) + +fun Context.getDrawableSimple(@DrawableRes id: Int): Drawable? = ContextCompat.getDrawable(this, id) diff --git a/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt b/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt new file mode 100644 index 0000000..ff91a66 --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt @@ -0,0 +1,56 @@ +package ru.touchin.roboswag.components.utils + +import android.app.Application +import android.content.Context +import android.content.res.Resources +import android.util.DisplayMetrics + +private const val MAX_METRICS_TRIES_COUNT = 5 + +/** + * Returns right metrics with non-zero height/width. + * It is common bug when metrics are calling at [Application.onCreate] method and it returns metrics with zero height/width. + * + * @param context [Context] of metrics; + * @return [DisplayMetrics]. + */ +fun Context.getDisplayMetrics(): DisplayMetrics { + var result = resources.displayMetrics + // it is needed to avoid bug with invalid metrics when user restore application from other application + var metricsTryNumber = 0 + while (metricsTryNumber < MAX_METRICS_TRIES_COUNT && (result.heightPixels <= 0 || result.widthPixels <= 0)) { + try { + Thread.sleep(500) + } catch (ignored: InterruptedException) { + return result + } + + result = resources.displayMetrics + metricsTryNumber++ + } + return result +} + +/** + * Simply converts Dp to pixels. + * + * @return Size in pixels. + */ +val Int.px: Int + get() = (this * Resources.getSystem().displayMetrics.density).toInt() + +/** + * Simply converts Dp to pixels. + * + * @return Size in pixels. + */ +val Float.px: Float + get() = this * Resources.getSystem().displayMetrics.density + +/** + * Simply converts pixels to Dp. + * + * @return Size in dp. + */ +val Int.dp: Int + get() = (this / Resources.getSystem().displayMetrics.density).toInt() diff --git a/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.kt b/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.kt index 642520c..4ea8a15 100644 --- a/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.kt +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.kt @@ -23,17 +23,14 @@ import android.app.Activity import android.app.Application import android.content.Context import android.content.Intent -import android.content.res.Resources import android.os.Build import android.util.DisplayMetrics -import android.util.TypedValue import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewConfiguration import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager import androidx.annotation.LayoutRes import ru.touchin.roboswag.components.utils.spans.getSpannedTextWithUrls @@ -78,8 +75,6 @@ object UiUtils { */ object OfMetrics { - private const val MAX_METRICS_TRIES_COUNT = 5 - /** * Returns right metrics with non-zero height/width. * It is common bug when metrics are calling at [Application.onCreate] method and it returns metrics with zero height/width. @@ -87,22 +82,11 @@ object UiUtils { * @param context [Context] of metrics; * @return [DisplayMetrics]. */ - fun getDisplayMetrics(context: Context): DisplayMetrics { - var result = context.resources.displayMetrics - // it is needed to avoid bug with invalid metrics when user restore application from other application - var metricsTryNumber = 0 - while (metricsTryNumber < MAX_METRICS_TRIES_COUNT && (result.heightPixels <= 0 || result.widthPixels <= 0)) { - try { - Thread.sleep(500) - } catch (ignored: InterruptedException) { - return result - } - - result = context.resources.displayMetrics - metricsTryNumber++ - } - return result - } + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("context.getDisplayMetrics()") + ) + fun getDisplayMetrics(context: Context): DisplayMetrics = context.getDisplayMetrics() /** * Simply converts DP to pixels. @@ -111,10 +95,19 @@ object UiUtils { * @param sizeInDp Size in DP; * @return Size in pixels. */ - fun dpToPixels(context: Context, sizeInDp: Float): Float = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, sizeInDp, getDisplayMetrics(context)) + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("sizeInDp.px") + ) + @Suppress("detekt.UnusedPrivateMember") + fun dpToPixels(context: Context, sizeInDp: Float): Float = sizeInDp.px - fun pixelsToDp(context: Context, pixels: Int): Int = (pixels * getDisplayMetrics(context).density + 0.5f).toInt() + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("pixels.dp") + ) + @Suppress("detekt.UnusedPrivateMember") + fun pixelsToDp(context: Context, pixels: Int): Int = pixels.dp } @@ -129,7 +122,7 @@ object UiUtils { * @param activity Activity of action bar; * @return Height of action bar. */ - fun getActionBarHeight(activity: Activity): Int = OfMetrics.dpToPixels(activity, 56f).toInt() + fun getActionBarHeight(): Int = 56.px /** * Returns status bar (on top where system info is) common height. @@ -205,27 +198,30 @@ object UiUtils { * @param view [View] to get ID from; * @return Readable ID. */ - fun getViewIdString(view: View): String = try { - view.resources.getResourceName(view.id) - } catch (exception: Resources.NotFoundException) { - view.id.toString() - } + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("view.getViewIdString()") + ) + fun getViewIdString(view: View): String = view.getViewIdString() /** * Hides device keyboard for target activity. */ - fun hideSoftInput(activity: Activity) { - activity.currentFocus?.let(this::hideSoftInput) - } + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("activity.hideSoftInput()") + ) + fun hideSoftInput(activity: Activity) = activity.hideSoftInput() + /** * Hides device keyboard for target view. */ - fun hideSoftInput(view: View) { - view.clearFocus() - val inputManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.hideSoftInputFromWindow(view.windowToken, 0) - } + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("view.hideSoftInput()") + ) + fun hideSoftInput(view: View) = view.hideSoftInput() /** * Shows device keyboard over [Activity] and focuses [View]. @@ -234,11 +230,11 @@ object UiUtils { * * @param view View to get focus for input from keyboard. */ - fun showSoftInput(view: View) { - view.requestFocus() - val inputManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) - } + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("view.showSoftInput()") + ) + fun showSoftInput(view: View) = view.showSoftInput() } diff --git a/utils/src/main/java/ru/touchin/roboswag/components/utils/ViewExtensions.kt b/utils/src/main/java/ru/touchin/roboswag/components/utils/ViewExtensions.kt new file mode 100644 index 0000000..9c65ac7 --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/ViewExtensions.kt @@ -0,0 +1,48 @@ +package ru.touchin.roboswag.components.utils + +import android.app.Activity +import android.content.Context +import android.content.res.Resources +import android.view.View +import android.view.inputmethod.InputMethodManager + +/** + * Returns string representation of [View]'s ID. + * + * @param view [View] to get ID from; + * @return Readable ID. + */ +fun View.getViewIdString(): String = try { + resources.getResourceName(id) +} catch (exception: Resources.NotFoundException) { + id.toString() +} + +/** + * Hides device keyboard for target activity. + */ +fun Activity.hideSoftInput() { + currentFocus?.hideSoftInput() +} + +/** + * Hides device keyboard for target view. + */ +fun View.hideSoftInput() { + clearFocus() + val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.hideSoftInputFromWindow(windowToken, 0) +} + +/** + * Shows device keyboard over [Activity] and focuses [View]. + * Do NOT use it if keyboard is over [android.app.Dialog] - it won't work as they have different [Activity.getWindow]. + * Do NOT use it if you are not sure that view is already added on screen. + * + * @param view View to get focus for input from keyboard. + */ +fun View.showSoftInput() { + requestFocus() + val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) +} diff --git a/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/SpanUtils.kt b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/SpanUtils.kt index 5ad792f..25b6fec 100644 --- a/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/SpanUtils.kt +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/SpanUtils.kt @@ -13,8 +13,9 @@ fun String.getSpannedTextWithUrls( removeUnderline: Boolean = true, flags: Int = HtmlCompat.FROM_HTML_MODE_COMPACT ): Spanned { - - val spannableText = SpannableString(HtmlCompat.fromHtml(this, flags)) + // HtmlCompat.fromHtml doesn't respect line breaks + val text = this.replace(lineBreakRegex, "
") + val spannableText = SpannableString(HtmlCompat.fromHtml(text, flags)) // Linkify removes all previous URLSpan's, we need to save all created spans for reapply after Linkify val spans = spannableText.getUrlSpans() @@ -38,6 +39,10 @@ fun String.getSpannedTextWithUrls( return spannableText } +private val lineBreakRegex by lazy(LazyThreadSafetyMode.NONE) { + "\r?\n".toRegex() +} + private fun SpannableString.getUrlSpans() = getSpans(0, length, URLSpan::class.java) .map { UrlSpanWithBorders(it, this.getSpanStart(it), this.getSpanEnd(it)) } diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java index 9768287..d773f3e 100644 --- a/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java @@ -19,12 +19,13 @@ package ru.touchin.roboswag.core.utils; -import androidx.annotation.NonNull; - import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import androidx.annotation.NonNull; + /** * Created by Gavriil Sitnikov on 29/08/2016. * Utility class to providing some string-related helper methods. @@ -40,7 +41,7 @@ public final class StringUtils { @NonNull public static String md5(@NonNull final String string) throws NoSuchAlgorithmException, UnsupportedEncodingException { final MessageDigest digest = MessageDigest.getInstance("MD5"); - digest.update(string.getBytes("UTF-8")); + digest.update(string.getBytes(StandardCharsets.UTF_8)); final byte[] messageDigestArray = digest.digest(); final StringBuilder hexString = new StringBuilder(); diff --git a/utils/src/main/java/ru/touchin/templates/DeviceUtils.java b/utils/src/main/java/ru/touchin/templates/DeviceUtils.java deleted file mode 100644 index 4a48f10..0000000 --- a/utils/src/main/java/ru/touchin/templates/DeviceUtils.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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.templates; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Process; -import androidx.annotation.NonNull; -import android.telephony.TelephonyManager; - -/** - * Utility class that is providing common methods related to android device. - */ -public final class DeviceUtils { - - /** - * Detects active network type. - * - * @param context Application context - * @return Active network type {@link NetworkType} - */ - @NonNull - public static NetworkType getNetworkType(@NonNull final Context context) { - if (context.checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, Process.myPid(), Process.myUid()) - != PackageManager.PERMISSION_GRANTED) { - return NetworkType.UNKNOWN; - } - final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (cm == null) { - return NetworkType.UNKNOWN; - } - @SuppressLint("MissingPermission") final NetworkInfo info = cm.getActiveNetworkInfo(); - if (info == null || !info.isConnected()) { - return NetworkType.NONE; - } - switch (info.getType()) { - case ConnectivityManager.TYPE_WIFI: - return NetworkType.WI_FI; - case ConnectivityManager.TYPE_MOBILE: - return getMobileNetworkType(info); - default: - return NetworkType.UNKNOWN; - } - } - - /** - * Detects if some network connected. - * - * @param context Application context - * @return true if network connected, false otherwise. - */ - public static boolean isNetworkConnected(@NonNull final Context context) { - return getNetworkType(context) != NetworkType.NONE; - } - - @NonNull - private static NetworkType getMobileNetworkType(@NonNull final NetworkInfo info) { - switch (info.getSubtype()) { - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_CDMA: - case TelephonyManager.NETWORK_TYPE_1xRTT: - case TelephonyManager.NETWORK_TYPE_IDEN: - return NetworkType.MOBILE_2G; - case TelephonyManager.NETWORK_TYPE_UMTS: - case TelephonyManager.NETWORK_TYPE_EVDO_0: - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_EVDO_B: - case TelephonyManager.NETWORK_TYPE_EHRPD: - case TelephonyManager.NETWORK_TYPE_HSPAP: - return NetworkType.MOBILE_3G; - case TelephonyManager.NETWORK_TYPE_LTE: - case 19: // NETWORK_TYPE_LTE_CA is hide - return NetworkType.MOBILE_LTE; - case TelephonyManager.NETWORK_TYPE_UNKNOWN: - default: - return NetworkType.UNKNOWN; - } - } - - private DeviceUtils() { - } - - /** - * Available network types. - */ - public enum NetworkType { - /** - * Mobile 2G network. - */ - MOBILE_2G("2g"), - /** - * Mobile 3G network. - */ - MOBILE_3G("3g"), - /** - * Mobile LTE network. - */ - MOBILE_LTE("lte"), - /** - * Wi-Fi network. - */ - WI_FI("Wi-Fi"), - /** - * Unknown network type. - */ - UNKNOWN("unknown"), - /** - * No network. - */ - NONE("none"); - - @NonNull - private final String name; - - NetworkType(@NonNull final String name) { - this.name = name; - } - - /** - * @return Network type readable name. - */ - @NonNull - public String getName() { - return name; - } - - } - -} diff --git a/utils/src/main/java/ru/touchin/templates/DeviceUtils.kt b/utils/src/main/java/ru/touchin/templates/DeviceUtils.kt new file mode 100644 index 0000000..db3b7d0 --- /dev/null +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtils.kt @@ -0,0 +1,115 @@ +/* + * 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.templates + +import android.content.Context +import android.net.NetworkInfo +import android.telephony.TelephonyManager + +/** + * Utility class that is providing common methods related to android device. + */ +object DeviceUtils { + /** + * Detects active network type. + * + * @param context Application context + * @return Active network type [NetworkType] + */ + @Deprecated( + "Use extension instead", + ReplaceWith("context.getNetworkType()") + ) + fun getNetworkType(context: Context) = context.getNetworkType() + + /** + * Detects if some network connected. + * + * @param context Application context + * @return true if network connected, false otherwise. + */ + @Deprecated( + "Use extension instead", + ReplaceWith("context.isNetworkConnected()") + ) + fun isNetworkConnected(context: Context) = context.isNetworkConnected() + + + fun getMobileNetworkType(info: NetworkInfo): NetworkType = + when (info.subtype) { + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT, + TelephonyManager.NETWORK_TYPE_IDEN -> NetworkType.MOBILE_2G + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_HSUPA, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_EVDO_B, + TelephonyManager.NETWORK_TYPE_EHRPD, + TelephonyManager.NETWORK_TYPE_HSPAP -> NetworkType.MOBILE_3G + TelephonyManager.NETWORK_TYPE_LTE, + 19 -> NetworkType.MOBILE_LTE + TelephonyManager.NETWORK_TYPE_UNKNOWN -> NetworkType.UNKNOWN + else -> NetworkType.UNKNOWN + } + + /** + * Available network types. + */ + enum class NetworkType(val networkName: String) { + /** + * Mobile 2G network. + */ + MOBILE_2G("2g"), + + /** + * Mobile 3G network. + */ + MOBILE_3G("3g"), + + /** + * Mobile LTE network. + */ + MOBILE_LTE("lte"), + + /** + * Wi-Fi network. + */ + WI_FI("Wi-Fi"), + + /** + * Unknown network type. + */ + UNKNOWN("unknown"), + + /** + * No network. + */ + NONE("none"); + + /** + * @return Network type readable name. + */ + + } +} diff --git a/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt new file mode 100644 index 0000000..ef213a9 --- /dev/null +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt @@ -0,0 +1,46 @@ +package ru.touchin.templates + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.hardware.biometrics.BiometricManager +import android.net.ConnectivityManager +import android.os.Build +import android.os.Process +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission +import androidx.core.hardware.fingerprint.FingerprintManagerCompat +import ru.touchin.templates.DeviceUtils.NetworkType +import ru.touchin.templates.DeviceUtils.getMobileNetworkType + +fun Context.isNetworkConnected(): Boolean = getNetworkType() != NetworkType.NONE + +fun Context.getNetworkType(): NetworkType { + if (checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, Process.myPid(), Process.myUid()) + != PackageManager.PERMISSION_GRANTED) { + return NetworkType.UNKNOWN + } + val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager? + ?: return NetworkType.UNKNOWN + @SuppressLint("MissingPermission") val info = cm.activeNetworkInfo + return if (info == null || !info.isConnected) { + NetworkType.NONE + } else when (info.type) { + ConnectivityManager.TYPE_WIFI -> NetworkType.WI_FI + ConnectivityManager.TYPE_MOBILE -> getMobileNetworkType(info) + else -> NetworkType.UNKNOWN + } +} + +@RequiresApi(Build.VERSION_CODES.M) +@Suppress("InlinedApi") +@RequiresPermission(anyOf = [Manifest.permission.USE_FINGERPRINT, Manifest.permission.USE_BIOMETRIC]) +fun Context.canAuthenticateWithBiometrics(): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + val fingerprintManagerCompat = FingerprintManagerCompat.from(this) + fingerprintManagerCompat.hasEnrolledFingerprints() && fingerprintManagerCompat.isHardwareDetected +} else { + getSystemService(BiometricManager::class.java)?.let { biometricManager -> + biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS + } ?: false +} diff --git a/views/build.gradle b/views/build.gradle index 6b34f5b..e76deb5 100644 --- a/views/build.gradle +++ b/views/build.gradle @@ -1,24 +1,39 @@ -apply plugin: 'com.android.library' +apply from: "../android-configs/lib-config.gradle" apply plugin: 'kotlin-android' android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + buildFeatures { + viewBinding true } } dependencies { - api project(":utils") - api project(":logging") + implementation project(":utils") + implementation project(":kotlin-extensions") + implementation project(":logging") - implementation "com.google.android.material:material:$versions.material" - implementation "androidx.core:core-ktx:$versions.coreKtx" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "com.google.android.material:material" + implementation "androidx.core:core-ktx" + + constraints { + implementation("com.google.android.material:material") { + version { + require '1.0.0' + } + } + implementation("androidx.core:core-ktx") { + version { + require '1.3.1' + } + } + implementation("org.jetbrains.kotlin:kotlin-stdlib") { + version { + require '1.3.0' + } + } + } +} + +repositories { + mavenCentral() } diff --git a/views/src/main/AndroidManifest.xml b/views/src/main/AndroidManifest.xml index 354617c..6f72453 100644 --- a/views/src/main/AndroidManifest.xml +++ b/views/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ + package="ru.touchin.roboswag.views"/> diff --git a/views/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java b/views/src/main/java/ru/touchin/roboswag/views/MaterialLoadingBar.java similarity index 98% rename from views/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java rename to views/src/main/java/ru/touchin/roboswag/views/MaterialLoadingBar.java index 2592c8a..1b495b2 100644 --- a/views/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java +++ b/views/src/main/java/ru/touchin/roboswag/views/MaterialLoadingBar.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.views; +package ru.touchin.roboswag.views; import android.content.Context; import android.content.res.TypedArray; diff --git a/views/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java b/views/src/main/java/ru/touchin/roboswag/views/MaterialProgressDrawable.java similarity index 99% rename from views/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java rename to views/src/main/java/ru/touchin/roboswag/views/MaterialProgressDrawable.java index 667a757..b102bb3 100644 --- a/views/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java +++ b/views/src/main/java/ru/touchin/roboswag/views/MaterialProgressDrawable.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.views; +package ru.touchin.roboswag.views; import android.content.Context; import android.graphics.Canvas; diff --git a/views/src/main/java/ru/touchin/roboswag/components/views/SkeletonView.kt b/views/src/main/java/ru/touchin/roboswag/views/SkeletonView.kt similarity index 99% rename from views/src/main/java/ru/touchin/roboswag/components/views/SkeletonView.kt rename to views/src/main/java/ru/touchin/roboswag/views/SkeletonView.kt index 5ea869a..327b7fd 100644 --- a/views/src/main/java/ru/touchin/roboswag/components/views/SkeletonView.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/SkeletonView.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.views +package ru.touchin.roboswag.views import android.animation.ValueAnimator import android.content.Context diff --git a/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java b/views/src/main/java/ru/touchin/roboswag/views/TypefacedEditText.java similarity index 98% rename from views/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java rename to views/src/main/java/ru/touchin/roboswag/views/TypefacedEditText.java index 6730704..3d38246 100644 --- a/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java +++ b/views/src/main/java/ru/touchin/roboswag/views/TypefacedEditText.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.views; +package ru.touchin.roboswag.views; import android.content.Context; import android.content.res.TypedArray; @@ -40,7 +40,9 @@ import java.util.ArrayList; import java.util.List; import ru.touchin.defaults.DefaultTextWatcher; -import ru.touchin.roboswag.components.views.internal.AttributesUtils; +import ru.touchin.roboswag.views.BuildConfig; +import ru.touchin.roboswag.views.R; +import ru.touchin.roboswag.views.internal.AttributesUtils; import ru.touchin.roboswag.core.log.Lc; /** diff --git a/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java b/views/src/main/java/ru/touchin/roboswag/views/TypefacedTextView.java similarity index 99% rename from views/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java rename to views/src/main/java/ru/touchin/roboswag/views/TypefacedTextView.java index 8bce61a..ea849ad 100644 --- a/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java +++ b/views/src/main/java/ru/touchin/roboswag/views/TypefacedTextView.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.views; +package ru.touchin.roboswag.views; import android.annotation.SuppressLint; import android.content.Context; @@ -34,7 +34,9 @@ import java.util.ArrayList; import java.util.List; import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.components.views.internal.AttributesUtils; +import ru.touchin.roboswag.views.BuildConfig; +import ru.touchin.roboswag.views.R; +import ru.touchin.roboswag.views.internal.AttributesUtils; import ru.touchin.roboswag.core.log.Lc; /** diff --git a/views/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java b/views/src/main/java/ru/touchin/roboswag/views/internal/AttributesUtils.java similarity index 99% rename from views/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java rename to views/src/main/java/ru/touchin/roboswag/views/internal/AttributesUtils.java index abe7169..16f09cd 100644 --- a/views/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java +++ b/views/src/main/java/ru/touchin/roboswag/views/internal/AttributesUtils.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.views.internal; +package ru.touchin.roboswag.views.internal; import android.content.Context; import android.content.res.TypedArray; diff --git a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt new file mode 100644 index 0000000..526f5a1 --- /dev/null +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -0,0 +1,221 @@ +package ru.touchin.roboswag.views.widget + +import android.widget.EditText +import androidx.core.widget.doOnTextChanged +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.roundToLong + +class AmountWithDecimalDecorator( + val editText: EditText, + val decimalSeparator: String = DEFAULT_DECIMAL_SEPARATOR, + val decimalPartLength: Int = DEFAULT_DECIMAL_PART_LENGTH, + val integerPartLength: Int = DEFAULT_INTEGER_PART_LENGTH, + val isSeparatorCutInvalidDecimalLength: Boolean = false +) { + + companion object { + + private const val COMMON_MONEY_MASK = "###,##0" + private const val DOT_SYMBOL = "." + private const val DEFAULT_DECIMAL_SEPARATOR = DOT_SYMBOL + private const val GROUPING_SEPARATOR = ' ' + private const val DEFAULT_DECIMAL_PART_LENGTH = 2 + private const val DEFAULT_INTEGER_PART_LENGTH = 13 + private val hardcodedSymbols = listOf(GROUPING_SEPARATOR) + private val possibleDecimalSeparators = listOf(",", ".") + + } + + var onTextChanged: (text: String) -> Unit = {} + + private var previousInputtedText = "" + private var isTextWasArtificiallyChanged = true + + init { + if (!possibleDecimalSeparators.contains(decimalSeparator)) { + throw IllegalArgumentException("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") + } + + editText.doOnTextChanged { text, _, _, _ -> doOnTextChanged(text.toString()) } + } + + fun getTextWithoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String = + previousInputtedText.withoutFormatting(decimalSeparatorToReplace) + + @Suppress("detekt.TooGenericExceptionCaught") + private fun doOnTextChanged(text: String) { + if (isTextWasArtificiallyChanged) { + isTextWasArtificiallyChanged = false + val cursorPosition = editText.selectionStart + try { + var currentText = text + possibleDecimalSeparators.forEach { currentText = currentText.replace(it, decimalSeparator) } + val currentIntegerPartLength = currentText.withoutFormatting().split(decimalSeparator)[0].length + + if (isIntegerPartToLong(currentIntegerPartLength)) { + currentText = trimIntegerPart(currentText) + } + + if (isTextFormatIncorrect(currentText)) { + setTextWhenNewInputIncorrect(currentText, cursorPosition) + return + } + + if (isTextHasHeadZero(currentText)) { + setTextWithHeadZero(currentText, cursorPosition) + return + } + + val currentDecimalPartLength = currentText.split(decimalSeparator).getOrNull(1)?.length + if (isDecimalPartTooLong(currentDecimalPartLength)) { + setTextWhenNewInputIncorrect(currentText, cursorPosition) + return + } + + val formattedText = if (currentText.isNotEmpty()) { + currentText.withoutFormatting().formatMoney(currentDecimalPartLength) + } else "" + + if (isTextErased(previousInputtedText, formattedText)) { + setTextAfterUserErase(formattedText, cursorPosition) + } else { + setTextAfterUserInput(formattedText, cursorPosition) + } + } catch (e: Throwable) { + editText.setText(previousInputtedText) + editText.setSelection(previousInputtedText.length) + } + } else { + previousInputtedText = text + isTextWasArtificiallyChanged = true + onTextChanged(text) + } + } + + private fun isIntegerPartToLong(currentIntegerPartLength: Int) = currentIntegerPartLength > integerPartLength + + private fun trimIntegerPart(currentText: String): String { + val splittedText = currentText.withoutFormatting().split(decimalSeparator) + val integerPart = splittedText[0] + val decimalPart = if (splittedText.size > 1) decimalSeparator + splittedText[1] else "" + return integerPart.take(integerPartLength) + decimalPart + } + + private fun isTextFormatIncorrect(currentText: String) = + currentText == decimalSeparator + || currentText.count { it == decimalSeparator[0] } > 1 + || currentText.take(2) == "00" + + private fun isTextHasHeadZero(currentText: String) = + currentText.length >= 2 && currentText[0] == '0' && currentText[1] != decimalSeparator[0] + + private fun setTextWithHeadZero(text: String, cursorPosition: Int) { + if (abs(previousInputtedText.length - text.length) > 1) { + setTextWhichWasInserted(text) + } else { + editText.setText(text.substring(1, text.length)) + editText.setSelection(max(cursorPosition - 1, 0)) + } + } + + private fun setTextWhenNewInputIncorrect(text: String, cursorPosition: Int) { + if (abs(previousInputtedText.length - text.length) > 1) { + setTextWhichWasInserted(text) + } else { + editText.setText(previousInputtedText) + editText.setSelection(max(cursorPosition - 1, 0)) + } + } + + private fun setTextAfterUserInput(formattedText: String, cursorPosition: Int) { + val diff = formattedText.length - previousInputtedText.length - 1 + editText.setText(formattedText) + editText.setSelection(min(cursorPosition + diff, formattedText.length)) + } + + private fun setTextAfterUserErase(formattedText: String, cursorPosition: Int) { + if (!previousInputtedText.contains(decimalSeparator) + && formattedText.contains(decimalSeparator) + ) { + editText.setText(formattedText) + editText.setSelection(min(formattedText.length, formattedText.indexOf(decimalSeparator) + 1)) + return + } + val diff = previousInputtedText.length - formattedText.length + if (diff == 0) { + editText.setText(formattedText) + editText.setSelection(min(cursorPosition, formattedText.length)) + } else { + editText.setText(formattedText) + editText.setSelection(max(cursorPosition - diff + 1, 0)) + } + } + + private fun isDecimalPartTooLong(currentDecimalPartLength: Int?) = + !isSeparatorCutInvalidDecimalLength + && currentDecimalPartLength != null + && currentDecimalPartLength > decimalPartLength + + private fun setTextWhichWasInserted(text: String) { + var result = "" + var decimalLength = -1 + var index = 0 + while (decimalLength < decimalPartLength && index < text.length) { + if (text[index] == decimalSeparator[0]) { + if (decimalLength == -1 && decimalPartLength != 0) { + decimalLength = 0 + result += text[index] + } else { + break + } + } else { + result += text[index] + } + index++ + } + result = result.formatMoney(decimalPartLength) + editText.setText(result) + editText.setSelection(result.length) + } + + private fun String.withoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String { + var result = this + hardcodedSymbols.forEach { result = this.replace(it.toString(), "") } + result = result.replace(decimalSeparator, decimalSeparatorToReplace) + return result + } + + private fun String.replaceSeparatorsToDot(): String { + var result = this + possibleDecimalSeparators.forEach { + result = result.replace(it, DOT_SYMBOL) + } + return result.withoutFormatting() + } + + private fun isTextErased(textBefore: String, formattedText: String) = + formattedText.length <= textBefore.length + + private fun String.formatMoney(currentDecimalPartLength: Int?): String { + var mask = COMMON_MONEY_MASK + if (currentDecimalPartLength != null && decimalPartLength != 0) { + mask += DOT_SYMBOL + "0".repeat(min(currentDecimalPartLength, decimalPartLength)) + } + + val formatter = DecimalFormat(mask) + formatter.decimalFormatSymbols = DecimalFormatSymbols().also { + it.decimalSeparator = decimalSeparator[0] + it.groupingSeparator = GROUPING_SEPARATOR + } + return formatter.format(this.replaceSeparatorsToDot().toDouble().floor()) + } + + // TODO make it simple + private fun Double.floor() = (this * 10.0.pow(decimalPartLength)).roundToLong() / 10.0.pow(decimalPartLength) + +} diff --git a/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt b/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt new file mode 100644 index 0000000..72c5f71 --- /dev/null +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt @@ -0,0 +1,69 @@ +package ru.touchin.roboswag.views.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.core.content.withStyledAttributes +import ru.touchin.extensions.observable +import ru.touchin.extensions.setOnRippleClickListener +import ru.touchin.roboswag.views.databinding.ProgressViewBinding +import ru.touchin.roboswag.views.R +import kotlin.properties.Delegates + +//TODO make customizable views list and views style +class LoadingContentView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : Switcher(context, attrs) { + + private val binding = ProgressViewBinding.inflate(LayoutInflater.from(context), this) + + var state by Delegates.observable(State.Loading, this::updateView) + + init { + if (attrs != null) { + context.withStyledAttributes(attrs, R.styleable.LoadingContentView, defStyleAttr, 0) { + if (hasValue(R.styleable.LoadingContentView_stubText)) { + setStubText(getString(R.styleable.LoadingContentView_stubText)) + } + } + } + } + + private fun setStubText(text: String?) { + binding.textStub.text = text + } + + private fun updateView(state: State) { + if (state == State.ShowContent) { + getChildAt(childCount - 1)?.let { showChild(it.id) } + } else { + when (state) { + is State.Stub -> { + setStubText(state.stubText) + showChild(R.id.text_stub) + } + is State.Loading -> { + showChild(R.id.progress_bar) + } + is State.Error -> { + binding.apply { + errorText.text = state.errorText + errorRepeatButton.setOnRippleClickListener { state.action.invoke() } + errorRepeatButton.text = state.repeatButtonText + showChild(R.id.error_with_repeat) + } + } + } + } + } + + sealed class State { + object ShowContent : State() + data class Stub(val stubText: String) : State() + object Loading : State() + data class Error(val action: () -> Unit, val errorText: String, val repeatButtonText: String) : State() + } + +} diff --git a/views/src/main/java/ru/touchin/widget/Switcher.java b/views/src/main/java/ru/touchin/roboswag/views/widget/Switcher.java similarity index 97% rename from views/src/main/java/ru/touchin/widget/Switcher.java rename to views/src/main/java/ru/touchin/roboswag/views/widget/Switcher.java index 25be341..949f993 100644 --- a/views/src/main/java/ru/touchin/widget/Switcher.java +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/Switcher.java @@ -1,4 +1,4 @@ -package ru.touchin.widget; +package ru.touchin.roboswag.views.widget; import android.content.Context; import android.content.res.TypedArray; @@ -15,7 +15,7 @@ import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; -import ru.touchin.roboswag.components.views.R; +import ru.touchin.roboswag.views.R; public class Switcher extends FrameLayout { diff --git a/views/src/main/res/drawable/light_button_background.xml b/views/src/main/res/drawable/light_button_background.xml new file mode 100644 index 0000000..5719816 --- /dev/null +++ b/views/src/main/res/drawable/light_button_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/views/src/main/res/font/roboto.ttf b/views/src/main/res/font/roboto.ttf new file mode 100644 index 0000000..2c97eea Binary files /dev/null and b/views/src/main/res/font/roboto.ttf differ diff --git a/views/src/main/res/font/roboto_medium.ttf b/views/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000..1a7f3b0 Binary files /dev/null and b/views/src/main/res/font/roboto_medium.ttf differ diff --git a/views/src/main/res/layout/progress_view.xml b/views/src/main/res/layout/progress_view.xml new file mode 100644 index 0000000..6e82f03 --- /dev/null +++ b/views/src/main/res/layout/progress_view.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + diff --git a/views/src/main/res/values/attrs.xml b/views/src/main/res/values/attrs.xml index c445ff7..7c3f8ca 100644 --- a/views/src/main/res/values/attrs.xml +++ b/views/src/main/res/values/attrs.xml @@ -47,4 +47,8 @@ + + + + diff --git a/views/src/main/res/values/colors.xml b/views/src/main/res/values/colors.xml index c447899..e01d549 100644 --- a/views/src/main/res/values/colors.xml +++ b/views/src/main/res/values/colors.xml @@ -4,4 +4,8 @@ #33E5E5EA #D9DFE2 - + + #8D8EA6 + #999BBF + #141233 + \ No newline at end of file diff --git a/views/src/main/res/values/styles.xml b/views/src/main/res/values/styles.xml index 0dbe7b6..06eee69 100644 --- a/views/src/main/res/values/styles.xml +++ b/views/src/main/res/values/styles.xml @@ -4,4 +4,17 @@ @android:anim/fade_in @android:anim/fade_out + + + + + + + diff --git a/yandex-map/build.gradle b/yandex-map/build.gradle index e34fbaf..0f4b147 100644 --- a/yandex-map/build.gradle +++ b/yandex-map/build.gradle @@ -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 project(":base-map") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "com.yandex.android:mapkit" - implementation "com.yandex.android:mapkit:$versions.yandex_mapkit" + constraints { + implementation("com.yandex.android:mapkit") { + version { + require '3.4.0' + } + } + } }