From dcfb62ef3efe84fa7a55706416e18776bb97a7b1 Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Thu, 14 Nov 2019 01:52:32 +0300 Subject: [PATCH 001/154] Fixed keyboard variable --- .../roboswag/components/navigation/activities/BaseActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt index 8fced4c..b325d90 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt @@ -36,7 +36,7 @@ abstract class BaseActivity : AppCompatActivity() { private val onBackPressedListeners = ArrayList() - open val keyboardBehaviorDetector: KeyboardBehaviorDetector? = null + var keyboardBehaviorDetector: KeyboardBehaviorDetector? = null init { lifecycle.addObserver(LifecycleLoggingObserver(this)) From 3cb729c66f5d5fe2b114e334c9037048461f2349 Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Mon, 25 Nov 2019 16:03:54 +0300 Subject: [PATCH 002/154] Added call super --- .../keyboard_resizeable/KeyboardResizeableViewController.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt index 85a4e78..24d935e 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt @@ -2,6 +2,7 @@ package ru.touchin.roboswag.components.navigation.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 @@ -55,6 +56,7 @@ abstract class KeyboardResizeableViewController Date: Mon, 25 Nov 2019 23:19:10 +0300 Subject: [PATCH 003/154] Fixed nav. bar --- .../navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt index cc6880d..e7faad3 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt @@ -43,7 +43,7 @@ class KeyboardBehaviorDetector( if (startNavigationBarHeight == -1) startNavigationBarHeight = bottomInset - windowInsets + ViewCompat.onApplyWindowInsets(view, windowInsets) } ViewCompat.requestApplyInsets(view) } From f430d0059144983a02315522c5eed62d46b6a305 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Tue, 26 Nov 2019 14:59:41 +0300 Subject: [PATCH 004/154] add freezing font scale factor in base activity --- .../navigation/activities/BaseActivity.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt index b325d90..10cc05e 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt @@ -19,9 +19,12 @@ package ru.touchin.roboswag.components.navigation.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 @@ -38,12 +41,15 @@ abstract class BaseActivity : AppCompatActivity() { var keyboardBehaviorDetector: KeyboardBehaviorDetector? = null + open val freezeFontScaleFactor: Boolean = true + init { lifecycle.addObserver(LifecycleLoggingObserver(this)) } 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) + } + } From c1f6084890c5c225915442f543cd4521b456996f Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Fri, 13 Dec 2019 16:22:51 +0300 Subject: [PATCH 005/154] Added vibration extensions --- .../java/ru/touchin/hardware/Extensions.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 utils/src/main/java/ru/touchin/hardware/Extensions.kt 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..e0c74f5 --- /dev/null +++ b/utils/src/main/java/ru/touchin/hardware/Extensions.kt @@ -0,0 +1,32 @@ +package ru.touchin.hardware + +import android.Manifest +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +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.cancelVibrate() { + (this.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.cancel() +} From dfe6cac3c1f513cf662fd8c1fa330af6a9c766cf Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Fri, 13 Dec 2019 16:48:13 +0300 Subject: [PATCH 006/154] Added default vibration --- utils/src/main/java/ru/touchin/hardware/Extensions.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/utils/src/main/java/ru/touchin/hardware/Extensions.kt b/utils/src/main/java/ru/touchin/hardware/Extensions.kt index e0c74f5..80852c7 100644 --- a/utils/src/main/java/ru/touchin/hardware/Extensions.kt +++ b/utils/src/main/java/ru/touchin/hardware/Extensions.kt @@ -4,6 +4,7 @@ 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 @@ -26,6 +27,15 @@ fun Context.startVibrate(duration: Long = 500, pattern: LongArray = LongArray(0) } } +@RequiresPermission(Manifest.permission.VIBRATE) +fun Context.startSimpleVibration() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startVibrate(VibrationEffect.createOneShot(200, DEFAULT_AMPLITUDE)) + } else { + startVibrate(200) + } +} + @RequiresPermission(Manifest.permission.VIBRATE) fun Context.cancelVibrate() { (this.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.cancel() From ded05f524c210233e1dd9a28a7df979b2b2deb1a Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Fri, 13 Dec 2019 17:10:43 +0300 Subject: [PATCH 007/154] Added duration --- utils/src/main/java/ru/touchin/hardware/Extensions.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/src/main/java/ru/touchin/hardware/Extensions.kt b/utils/src/main/java/ru/touchin/hardware/Extensions.kt index 80852c7..7e76320 100644 --- a/utils/src/main/java/ru/touchin/hardware/Extensions.kt +++ b/utils/src/main/java/ru/touchin/hardware/Extensions.kt @@ -28,11 +28,11 @@ fun Context.startVibrate(duration: Long = 500, pattern: LongArray = LongArray(0) } @RequiresPermission(Manifest.permission.VIBRATE) -fun Context.startSimpleVibration() { +fun Context.startSimpleVibration(duration: Long = 200) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startVibrate(VibrationEffect.createOneShot(200, DEFAULT_AMPLITUDE)) + startVibrate(VibrationEffect.createOneShot(duration, DEFAULT_AMPLITUDE)) } else { - startVibrate(200) + startVibrate(duration) } } From aab78f7895cfd549cf9c848da8816922398aa505 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 15 Jan 2020 15:24:53 +0300 Subject: [PATCH 008/154] Added DefaultActivityLifecycleCallbacks --- .../DefaultActivityLifecycleCallbacks.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 utils/src/main/java/ru/touchin/defaults/DefaultActivityLifecycleCallbacks.kt 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 + +} From ea1804cc6b2f603a22017a93e4e56c9c784d7467 Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Wed, 5 Feb 2020 15:21:55 +0300 Subject: [PATCH 009/154] Migrate FB crashlytics --- build.gradle | 2 +- navigation/build.gradle | 6 ++-- .../components/navigation/TouchinApp.java | 28 +++++++++---------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 65450e5..eb10492 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ ext { retrofit : '2.4.0', rxJava : '2.2.2', rxAndroid : '2.1.0', - crashlytics : '2.9.5', + crashlytics : '17.0.0-beta01', location : '16.0.0', coreKtx : '1.0.1', yandex_mapkit: '3.4.0', diff --git a/navigation/build.gradle b/navigation/build.gradle index c4fa596..bb97748 100644 --- a/navigation/build.gradle +++ b/navigation/build.gradle @@ -27,8 +27,6 @@ dependencies { 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 - } + + implementation "com.google.firebase:firebase-crashlytics:$versions.crashlytics" } 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 index 49f4047..b95c2e0 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java @@ -24,7 +24,7 @@ import android.content.Context; import android.os.StrictMode; import android.util.Log; -import com.crashlytics.android.Crashlytics; +import com.google.firebase.crashlytics.FirebaseCrashlytics; import net.danlew.android.joda.JodaTimeAndroid; @@ -34,7 +34,6 @@ 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; @@ -65,16 +64,13 @@ public abstract class TouchinApp extends Application { LcGroup.UI_LIFECYCLE.disable(); } else { try { - final Crashlytics crashlytics = new Crashlytics(); - Fabric.with(this, crashlytics); - Fabric.getLogger().setLogLevel(Log.ERROR); + 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" - + "compile('com.crashlytics.sdk.android:crashlytics:+@aar') {\n" - + " transitive = true;\n" - + "}\n" + + "com.google.firebase:firebase-crashlytics\n" + "to your build.gradle?", error); } } @@ -96,13 +92,17 @@ public abstract class TouchinApp extends Application { private static class CrashlyticsLogProcessor extends LogProcessor { @NonNull - private final Crashlytics crashlytics; + private final FirebaseCrashlytics crashlytics; - public CrashlyticsLogProcessor(@NonNull final Crashlytics 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, @@ -110,17 +110,17 @@ public abstract class TouchinApp extends Application { @NonNull final String message, @Nullable final Throwable throwable) { if (group == LcGroup.UI_LIFECYCLE) { - crashlytics.core.log(level.getPriority(), tag, message); + crashlytics.log(getLogMessage(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); + crashlytics.log(getLogMessage(level.getPriority(), tag, message)); + crashlytics.recordException(throwable); } else { final ShouldNotHappenException exceptionToLog = new ShouldNotHappenException(tag + ':' + message); reduceStackTrace(exceptionToLog); - crashlytics.core.logException(exceptionToLog); + crashlytics.recordException(exceptionToLog); } } } From e9bf8e29d0a5a4ded4d2f13efdb4148135cc3857 Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Wed, 5 Feb 2020 19:23:13 +0300 Subject: [PATCH 010/154] Upd naming --- build.gradle | 1 - navigation/build.gradle | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index eb10492..3ae8bcf 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,6 @@ ext { retrofit : '2.4.0', rxJava : '2.2.2', rxAndroid : '2.1.0', - crashlytics : '17.0.0-beta01', location : '16.0.0', coreKtx : '1.0.1', yandex_mapkit: '3.4.0', diff --git a/navigation/build.gradle b/navigation/build.gradle index bb97748..284cb76 100644 --- a/navigation/build.gradle +++ b/navigation/build.gradle @@ -28,5 +28,5 @@ dependencies { implementation "androidx.appcompat:appcompat:$versions.appcompat" - implementation "com.google.firebase:firebase-crashlytics:$versions.crashlytics" + implementation "com.google.firebase:firebase-crashlytics:$versions.crashlyticsFirebase" } From 0ee9f72e266c4d49b329959fac3c094cd3c090fa Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Thu, 6 Feb 2020 13:08:01 +0300 Subject: [PATCH 011/154] Removed useless deps. --- build.gradle | 21 +-------------------- navigation/build.gradle | 2 +- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index 3ae8bcf..235e5bf 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,4 @@ buildscript { - ext.kotlin_version = '1.3.50' repositories { google() jcenter() @@ -23,22 +22,4 @@ allprojects { 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', - location : '16.0.0', - coreKtx : '1.0.1', - yandex_mapkit: '3.4.0', - google_maps : '16.1.0' - ] -} +} \ No newline at end of file diff --git a/navigation/build.gradle b/navigation/build.gradle index 284cb76..20cec33 100644 --- a/navigation/build.gradle +++ b/navigation/build.gradle @@ -28,5 +28,5 @@ dependencies { implementation "androidx.appcompat:appcompat:$versions.appcompat" - implementation "com.google.firebase:firebase-crashlytics:$versions.crashlyticsFirebase" + implementation "com.google.firebase:firebase-crashlytics:$versions.firebaseCrashlytics" } From 2302fbc16d4e6250136ab17da65b554217ef0a4f Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Thu, 6 Feb 2020 13:09:17 +0300 Subject: [PATCH 012/154] Added new line --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 235e5bf..41d4c15 100644 --- a/build.gradle +++ b/build.gradle @@ -22,4 +22,4 @@ allprojects { task clean(type: Delete) { delete rootProject.buildDir -} \ No newline at end of file +} From 6c795a6f4a01df0003e9f59bfb3c1249cb406799 Mon Sep 17 00:00:00 2001 From: Stanislav Date: Wed, 12 Feb 2020 19:49:43 +0300 Subject: [PATCH 013/154] fixed inner navigation with navigation_new & tabbarnavigation_new fixed inner navigation with navigation_new & tabbarnavigation_new --- .../tabbarnavigation_new/BottomNavigationController.kt | 2 +- .../tabbarnavigation_new/NavigationContainerFragment.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 67c0f95..f14504b 100644 --- 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 @@ -84,7 +84,7 @@ class BottomNavigationController( fragment = if (wrapWithNavigationContainer) { Fragment.instantiate( context, - fragmentClass.name, + NavigationContainerFragment::class.java.name, NavigationContainerFragment.args(fragmentClass, fragmentState, contentContainerViewId, contentContainerLayoutId) ) } else { 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 index 160f883..3d9e5a9 100644 --- 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 @@ -67,7 +67,7 @@ class NavigationContainerFragment : Fragment() { containerLayoutId = getInt(CONTAINER_LAYOUT_ID_ARG) transition = getInt(TRANSITION_ARG) } - navigation.setInitial(getFragmentClass(), args.getParcelable(FRAGMENT_STATE_ARG)) + navigation.setInitial(getFragmentClass().kotlin, args.getParcelable(FRAGMENT_STATE_ARG)) } } From 4a97b6cd66e11a6845c62fe5080f72ba842e9a38 Mon Sep 17 00:00:00 2001 From: Stanislav Date: Wed, 12 Feb 2020 19:49:43 +0300 Subject: [PATCH 014/154] fixed inner navigation with navigation_new & tabbarnavigation_new fixed inner navigation with navigation_new & tabbarnavigation_new --- .../tabbarnavigation_new/BottomNavigationController.kt | 2 +- .../tabbarnavigation_new/NavigationContainerFragment.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 67c0f95..f14504b 100644 --- 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 @@ -84,7 +84,7 @@ class BottomNavigationController( fragment = if (wrapWithNavigationContainer) { Fragment.instantiate( context, - fragmentClass.name, + NavigationContainerFragment::class.java.name, NavigationContainerFragment.args(fragmentClass, fragmentState, contentContainerViewId, contentContainerLayoutId) ) } else { 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 index 160f883..3d9e5a9 100644 --- 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 @@ -67,7 +67,7 @@ class NavigationContainerFragment : Fragment() { containerLayoutId = getInt(CONTAINER_LAYOUT_ID_ARG) transition = getInt(TRANSITION_ARG) } - navigation.setInitial(getFragmentClass(), args.getParcelable(FRAGMENT_STATE_ARG)) + navigation.setInitial(getFragmentClass().kotlin, args.getParcelable(FRAGMENT_STATE_ARG)) } } From 69f269528e5ba0ad5234190a56dc1a625c348e02 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Thu, 13 Feb 2020 12:57:51 +0300 Subject: [PATCH 015/154] Update README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 53640c2..3413e51 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ Roboswag - библиотека решений, ускоряющих разра * Andoroid Api: 19 * Kotlin: 1.3.11 * Gradle: 3.2.1 -* Gradle CPD Plugin: 1.1 -* Detekt Plugin: 1.0.0-RC12 ## Основная архитектура За основу архитектуры взят подход от Google - MVVM на основе [Android Architecture Components](https://developer.android.com/jetpack/docs/guide). Данный подход популярен в сообществе Android разработки, позволяет разбивать код на мелкие и независимые части, что ускоряет разработку и последующую поддержку приложения. @@ -22,10 +20,6 @@ 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. ### Утилиты и extension функции From 672936b8c1e1c931b7cf43a20bde1754306aaf55 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Thu, 13 Feb 2020 13:46:05 +0300 Subject: [PATCH 016/154] add java version to kotlin compile --- lifecycle/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lifecycle/build.gradle b/lifecycle/build.gradle index 41a0ebb..f67ddbe 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -12,6 +12,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } dependencies { From 833215eb6d2602b6cbcc2f3a5fc4dac999c3a139 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Tue, 10 Mar 2020 15:56:15 +0300 Subject: [PATCH 017/154] fix outOfBoundsException in delegationListAdapter --- .../main/java/ru/touchin/adapters/DelegationListAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt b/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt index 8328211..a47fbe6 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt @@ -1,10 +1,10 @@ package ru.touchin.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) From 642c7f7a27dde261f72fb9e1535814328d0b51aa Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Tue, 10 Mar 2020 15:56:15 +0300 Subject: [PATCH 018/154] fix outOfBoundsException in delegationListAdapter --- .../main/java/ru/touchin/adapters/DelegationListAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt b/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt index 8328211..a47fbe6 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/adapters/DelegationListAdapter.kt @@ -1,10 +1,10 @@ package ru.touchin.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) From a9ba1c79719fdf16b0a0edb40938cd9c731420e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=91=D0=B0=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D0=BA=D0=B8=D0=B8=CC=86?= Date: Thu, 19 Mar 2020 21:19:30 +0300 Subject: [PATCH 019/154] Revert "Migrate FB crashlytics" This reverts commit ea1804cc --- navigation/build.gradle | 4 ++- .../components/navigation/TouchinApp.java | 28 +++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/navigation/build.gradle b/navigation/build.gradle index 20cec33..e70ea8f 100644 --- a/navigation/build.gradle +++ b/navigation/build.gradle @@ -28,5 +28,7 @@ dependencies { implementation "androidx.appcompat:appcompat:$versions.appcompat" - implementation "com.google.firebase:firebase-crashlytics:$versions.firebaseCrashlytics" + implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { + transitive = true + } } 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 index b95c2e0..49f4047 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java @@ -24,7 +24,7 @@ import android.content.Context; import android.os.StrictMode; import android.util.Log; -import com.google.firebase.crashlytics.FirebaseCrashlytics; +import com.crashlytics.android.Crashlytics; import net.danlew.android.joda.JodaTimeAndroid; @@ -34,6 +34,7 @@ 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; @@ -64,13 +65,16 @@ public abstract class TouchinApp extends Application { LcGroup.UI_LIFECYCLE.disable(); } else { try { - final FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance(); - crashlytics.setCrashlyticsCollectionEnabled(true); + 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" - + "com.google.firebase:firebase-crashlytics\n" + + "compile('com.crashlytics.sdk.android:crashlytics:+@aar') {\n" + + " transitive = true;\n" + + "}\n" + "to your build.gradle?", error); } } @@ -92,17 +96,13 @@ public abstract class TouchinApp extends Application { private static class CrashlyticsLogProcessor extends LogProcessor { @NonNull - private final FirebaseCrashlytics crashlytics; + private final Crashlytics crashlytics; - public CrashlyticsLogProcessor(@NonNull final FirebaseCrashlytics crashlytics) { + public CrashlyticsLogProcessor(@NonNull final Crashlytics 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, @@ -110,17 +110,17 @@ public abstract class TouchinApp extends Application { @NonNull final String message, @Nullable final Throwable throwable) { if (group == LcGroup.UI_LIFECYCLE) { - crashlytics.log(getLogMessage(level.getPriority(), tag, message)); + 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.log(getLogMessage(level.getPriority(), tag, message)); - crashlytics.recordException(throwable); + crashlytics.core.log(level.getPriority(), tag, message); + crashlytics.core.logException(throwable); } else { final ShouldNotHappenException exceptionToLog = new ShouldNotHappenException(tag + ':' + message); reduceStackTrace(exceptionToLog); - crashlytics.recordException(exceptionToLog); + crashlytics.core.logException(exceptionToLog); } } } From 1976c3095b845d0f6d8237956023edd5a3d20c07 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 27 Mar 2020 11:49:52 +0300 Subject: [PATCH 020/154] "fix" static --- .../components/tabbarnavigation/BottomNavigationController.kt | 1 + 1 file changed, 1 insertion(+) 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 index b8a1bc6..8e21e3c 100644 --- 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 @@ -61,6 +61,7 @@ class BottomNavigationController( fun detach() = callback?.let(fragmentManager::unregisterFragmentLifecycleCallbacks) + @Suppress("detekt.ComplexMethod") fun navigateTo(@IdRes itemId: Int, state: Parcelable? = null) { // Find view controller class that needs to open val (viewControllerClass, defaultViewControllerState, saveStateOnSwitching) = viewControllers[itemId] ?: return From 96fb2bf13dd760d89fa34109e5869a42f5f782fb Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 30 Mar 2020 13:41:05 +0300 Subject: [PATCH 021/154] fix IllegalStateException when try to call MutableLiveData.setValue() outside main thread --- .../viewmodel/BaseLiveDataDispatcher.kt | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) 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) + } + } + } From 43fd0f6f40cee9b9e890e4fafc53e7d62a878861 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 3 Apr 2020 13:04:31 +0300 Subject: [PATCH 022/154] fix line breaks abscence in spanned text --- .../touchin/roboswag/components/utils/spans/SpanUtils.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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)) } From e03381b937f19ceeadb3fbd1d6080356b97187b0 Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Mon, 13 Apr 2020 20:17:38 +0300 Subject: [PATCH 023/154] Add throttling in `View.setOnRippleClickListener()` implementation to prevent multiple actions during ripple delay --- .../main/java/ru/touchin/extensions/View.kt | 9 +++++-- .../java/ru/touchin/utils/ActionThrottler.kt | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt 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..41217ce 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 = 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) + } + } } 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..830e89e --- /dev/null +++ b/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt @@ -0,0 +1,26 @@ +package ru.touchin.utils + +import android.os.SystemClock +import ru.touchin.extensions.RIPPLE_EFFECT_DELAY + +object ActionThrottler { + + private const val DELAY = 2 * RIPPLE_EFFECT_DELAY + private var lastActionTime = 0L + + fun throttleAction(action: () -> Unit): Boolean { + val currentTime = SystemClock.elapsedRealtime() + val diff = currentTime - lastActionTime + + return if (diff >= DELAY) { + lastActionTime = currentTime + action.invoke() + true + } else { + false + } + } + +} + + From 2df5e2e3c37ddd7da1f7010aa47526f9b152c474 Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Mon, 13 Apr 2020 20:42:12 +0300 Subject: [PATCH 024/154] Add explaining comment in `ActionThrottler` --- .../src/main/java/ru/touchin/utils/ActionThrottler.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt b/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt index 830e89e..f0bc035 100644 --- a/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt +++ b/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt @@ -5,6 +5,8 @@ import ru.touchin.extensions.RIPPLE_EFFECT_DELAY object ActionThrottler { + // Multiplied by 2 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 DELAY = 2 * RIPPLE_EFFECT_DELAY private var lastActionTime = 0L From 8b2c061528fcfbdc3ab85d81afd08e390e753717 Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Tue, 14 Apr 2020 13:41:51 +0300 Subject: [PATCH 025/154] Fixed comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/TouchInstinct/RoboSwag/pull/121#discussion_r408021807 – prevention of click again coefficient extracted into a value https://github.com/TouchInstinct/RoboSwag/pull/121#discussion_r408025993 – empty line removed https://github.com/TouchInstinct/RoboSwag/pull/121#discussion_r408027482 – "MS" posfix was added to delay constants --- .../src/main/java/ru/touchin/extensions/View.kt | 4 ++-- .../src/main/java/ru/touchin/utils/ActionThrottler.kt | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) 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 41217ce..5602cef 100644 --- a/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt +++ b/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt @@ -4,7 +4,7 @@ import android.os.Build import android.view.View import ru.touchin.utils.ActionThrottler -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. @@ -15,7 +15,7 @@ fun View.setOnRippleClickListener(listener: () -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOnClickListener { ActionThrottler.throttleAction { - postDelayed({ if (hasWindowFocus()) listener() }, RIPPLE_EFFECT_DELAY) + postDelayed({ if (hasWindowFocus()) listener() }, RIPPLE_EFFECT_DELAY_MS) } } } else { diff --git a/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt b/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt index f0bc035..8721e60 100644 --- a/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt +++ b/kotlin-extensions/src/main/java/ru/touchin/utils/ActionThrottler.kt @@ -1,20 +1,21 @@ package ru.touchin.utils import android.os.SystemClock -import ru.touchin.extensions.RIPPLE_EFFECT_DELAY +import ru.touchin.extensions.RIPPLE_EFFECT_DELAY_MS object ActionThrottler { - // Multiplied by 2 because in interval after ripple effect finish and before + // 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 DELAY = 2 * RIPPLE_EFFECT_DELAY + 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) { + return if (diff >= DELAY_MS) { lastActionTime = currentTime action.invoke() true @@ -24,5 +25,3 @@ object ActionThrottler { } } - - From f602f9ce13804b18c687361c6f7dbcba97918e8f Mon Sep 17 00:00:00 2001 From: Daniil Shevtsov Date: Fri, 17 Apr 2020 14:03:09 +0300 Subject: [PATCH 026/154] lifecycle: add toLiveData extension for easy immutable live data fields creation --- .../java/ru/touchin/lifecycle/extensions/ImmutableExt.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt 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..1449ad7 --- /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.toLiveData() = this as LiveData From 98c7f33e277858579a048250e1ef873c06120eac Mon Sep 17 00:00:00 2001 From: Daniil Shevtsov Date: Fri, 17 Apr 2020 14:15:49 +0300 Subject: [PATCH 027/154] Rename toLiveData to toImmutable --- .../main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt b/lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt index 1449ad7..a4c1873 100644 --- a/lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt +++ b/lifecycle/src/main/java/ru/touchin/lifecycle/extensions/ImmutableExt.kt @@ -3,4 +3,4 @@ package ru.touchin.lifecycle.extensions import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -fun MutableLiveData.toLiveData() = this as LiveData +fun MutableLiveData.toImmutable() = this as LiveData From 7ef5facf2ec144a405206378d14916a659db513e Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 24 Apr 2020 15:48:06 +0300 Subject: [PATCH 028/154] removed butterknife from base fragment --- navigation-new/build.gradle | 3 --- .../navigation_new/fragments/BaseFragment.kt | 10 ---------- 2 files changed, 13 deletions(-) diff --git a/navigation-new/build.gradle b/navigation-new/build.gradle index c53d4a0..64af432 100644 --- a/navigation-new/build.gradle +++ b/navigation-new/build.gradle @@ -32,9 +32,6 @@ dependencies { 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/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 index 830952a..a1c1128 100644 --- 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 @@ -15,8 +15,6 @@ 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 @@ -55,8 +53,6 @@ open class BaseFragment(@Layo protected lateinit var state: TState private set - private lateinit var butterKnifeUnbinder: Unbinder - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -75,12 +71,6 @@ open class BaseFragment(@Layo 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) { From 5c4742c3934f0bb1c391ed8fb8fc1ee789578405 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 24 Apr 2020 19:14:27 +0300 Subject: [PATCH 029/154] navigation_new became navigation_base, tabbar navigation has appropriate postfix now --- lifecycle/build.gradle | 2 +- .../viewmodel/LifecycleViewModelProviders.kt | 2 +- .../.gitignore | 0 {navigation-new => navigation-base}/README.md | 0 .../build.gradle | 1 - .../src/main/AndroidManifest.xml | 2 +- .../navigation_base}/FragmentNavigation.kt | 40 +-- .../SimpleActionBarDrawerToggle.kt | 6 +- .../navigation_base}/TouchinApp.java | 2 +- .../activities/BaseActivity.kt | 6 +- .../activities/NavigationActivity.kt | 5 +- .../activities/OnBackPressedListener.java | 2 +- .../fragments/BaseFragment.kt | 5 +- .../navigation_base/fragments}/EmptyState.kt | 2 +- .../fragments}/LifecycleLoggingObserver.kt | 2 +- .../KeyboardBehaviorDetector.kt | 4 +- .../KeyboardResizeableFragment.kt | 8 +- .../.gitignore | 0 .../README.md | 0 .../build.gradle | 1 + .../src/main/AndroidManifest.xml | 2 +- .../fragments/ViewControllerFragment.kt | 6 +- .../KeyboardResizeableViewController.kt | 8 +- .../viewcontrollers/ViewController.kt | 5 +- .../ViewControllerNavigation.kt | 54 ++-- .../navigation/FragmentNavigation.kt | 244 ------------------ .../activities/NavigationActivity.kt | 25 -- .../.gitignore | 0 .../build.gradle | 2 +- .../src/main/AndroidManifest.xml | 2 + .../BottomNavigationActivity.kt | 6 +- .../BottomNavigationController.kt | 4 +- .../BottomNavigationFragment.kt | 6 +- .../NavigationContainerFragment.kt | 6 +- .../src/main/AndroidManifest.xml | 2 - .../.gitignore | 0 .../README.md | 0 .../build.gradle | 2 +- .../src/main/AndroidManifest.xml | 2 + .../BottomNavigationActivity.kt | 0 .../BottomNavigationController.kt | 0 .../BottomNavigationFragment.kt | 0 .../NavigationContainerFragment.kt | 0 .../src/main/AndroidManifest.xml | 2 - 44 files changed, 103 insertions(+), 365 deletions(-) rename {navigation-new => navigation-base}/.gitignore (100%) rename {navigation-new => navigation-base}/README.md (100%) rename {navigation-new => navigation-base}/build.gradle (97%) rename {navigation => navigation-base}/src/main/AndroidManifest.xml (53%) rename {navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/FragmentNavigation.kt (91%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/SimpleActionBarDrawerToggle.kt (96%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/TouchinApp.java (99%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/activities/BaseActivity.kt (93%) rename {navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/activities/NavigationActivity.kt (72%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/activities/OnBackPressedListener.java (52%) rename {navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/fragments/BaseFragment.kt (94%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments}/EmptyState.kt (86%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments}/LifecycleLoggingObserver.kt (95%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/keyboard_resizeable/KeyboardBehaviorDetector.kt (91%) rename {navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new => navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base}/keyboard_resizeable/KeyboardResizeableFragment.kt (88%) rename {navigation => navigation-viewcontroller}/.gitignore (100%) rename {navigation => navigation-viewcontroller}/README.md (100%) rename {navigation => navigation-viewcontroller}/build.gradle (95%) rename {navigation-new => navigation-viewcontroller}/src/main/AndroidManifest.xml (50%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller}/fragments/ViewControllerFragment.kt (97%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller}/keyboard_resizeable/KeyboardResizeableViewController.kt (87%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller}/viewcontrollers/ViewController.kt (97%) rename {navigation/src/main/java/ru/touchin/roboswag/components/navigation => navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller}/viewcontrollers/ViewControllerNavigation.kt (80%) delete mode 100644 navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt delete mode 100644 navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/NavigationActivity.kt rename {tabbar-navigation-new => tabbar-navigation-fragment}/.gitignore (100%) rename {tabbar-navigation-new => tabbar-navigation-fragment}/build.gradle (93%) create mode 100644 tabbar-navigation-fragment/src/main/AndroidManifest.xml rename {tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new => tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment}/BottomNavigationActivity.kt (87%) rename {tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new => tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment}/BottomNavigationController.kt (97%) rename {tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new => tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment}/BottomNavigationFragment.kt (92%) rename {tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new => tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment}/NavigationContainerFragment.kt (92%) delete mode 100644 tabbar-navigation-new/src/main/AndroidManifest.xml rename {tabbar-navigation => tabbar-navigation-viewcontroller}/.gitignore (100%) rename {tabbar-navigation => tabbar-navigation-viewcontroller}/README.md (100%) rename {tabbar-navigation => tabbar-navigation-viewcontroller}/build.gradle (92%) create mode 100644 tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml rename {tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation => tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller}/BottomNavigationActivity.kt (100%) rename {tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation => tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller}/BottomNavigationController.kt (100%) rename {tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation => tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller}/BottomNavigationFragment.kt (100%) rename {tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation => tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller}/NavigationContainerFragment.kt (100%) delete mode 100644 tabbar-navigation/src/main/AndroidManifest.xml diff --git a/lifecycle/build.gradle b/lifecycle/build.gradle index f67ddbe..2ad19c2 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -19,7 +19,7 @@ android { } dependencies { - api project(":navigation") + api project(":navigation-viewcontroller") compileOnly "javax.inject:javax.inject:1" 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..f49d498 100644 --- a/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt +++ b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt @@ -6,7 +6,7 @@ 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 +import ru.touchin.roboswag.components.navigation_viewcontroller.viewcontrollers.ViewController object LifecycleViewModelProviders { diff --git a/navigation-new/.gitignore b/navigation-base/.gitignore similarity index 100% rename from navigation-new/.gitignore rename to navigation-base/.gitignore 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-new/build.gradle b/navigation-base/build.gradle similarity index 97% rename from navigation-new/build.gradle rename to navigation-base/build.gradle index c53d4a0..8d3b8e9 100644 --- a/navigation-new/build.gradle +++ b/navigation-base/build.gradle @@ -18,7 +18,6 @@ android { dependencies { api project(":utils") api project(":logging") - api project(":navigation") api project(":api-logansquare") api 'androidx.multidex:multidex:2.0.1' diff --git a/navigation/src/main/AndroidManifest.xml b/navigation-base/src/main/AndroidManifest.xml similarity index 53% rename from navigation/src/main/AndroidManifest.xml rename to navigation-base/src/main/AndroidManifest.xml index bd2d3ee..74b4a83 100644 --- a/navigation/src/main/AndroidManifest.xml +++ b/navigation-base/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="ru.touchin.roboswag.components.navigation_base"/> 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/components/navigation_base/FragmentNavigation.kt similarity index 91% 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/components/navigation_base/FragmentNavigation.kt index 8ef56cf..42dfca8 100644 --- a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/FragmentNavigation.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/FragmentNavigation.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation_new +package ru.touchin.roboswag.components.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.components.navigation_base.fragments.BaseFragment import kotlin.reflect.KClass /** @@ -96,6 +95,7 @@ open class FragmentNavigation( addToStack: Boolean, args: Bundle?, backStackName: String?, + tag: String?, 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( + fun push( fragmentClass: KClass>, - state: T? = null, + 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, BaseFragment.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( + 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, BaseFragment.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( + fun setInitial( fragmentClass: KClass>, - state: T? = null, + state: T, + tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { beforeSetInitialActions() - setAsTop(fragmentClass.java, BaseFragment.args(state ?: EmptyState), false, transactionSetup) + setAsTop(fragmentClass.java, BaseFragment.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/components/navigation_base/SimpleActionBarDrawerToggle.kt similarity index 96% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/SimpleActionBarDrawerToggle.kt index 79d417c..00c5ecb 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/SimpleActionBarDrawerToggle.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation +package ru.touchin.roboswag.components.navigation_base import android.animation.ValueAnimator import android.view.MenuItem @@ -25,8 +25,8 @@ 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.navigation_base.activities.BaseActivity +import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener import ru.touchin.roboswag.components.utils.UiUtils /** diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/TouchinApp.java similarity index 99% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java rename to navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/TouchinApp.java index 49f4047..88bc813 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/TouchinApp.java +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/TouchinApp.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation; +package ru.touchin.roboswag.components.navigation_base; import android.app.Application; import android.content.Context; diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/BaseActivity.kt similarity index 93% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/BaseActivity.kt index 10cc05e..af047ab 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/BaseActivity.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation.activities +package ru.touchin.roboswag.components.navigation_base.activities import android.content.Context import android.content.Intent @@ -26,8 +26,8 @@ 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.components.navigation_viewcontroller.keyboard_resizeable.KeyboardBehaviorDetector +import ru.touchin.roboswag.components.navigation_base.fragments.LifecycleLoggingObserver import ru.touchin.roboswag.core.log.Lc import ru.touchin.roboswag.core.log.LcGroup diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/activities/NavigationActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/NavigationActivity.kt similarity index 72% rename from navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/activities/NavigationActivity.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/NavigationActivity.kt index 39f6d35..a157813 100644 --- a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/activities/NavigationActivity.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/NavigationActivity.kt @@ -1,8 +1,7 @@ -package ru.touchin.roboswag.components.navigation_new.activities +package ru.touchin.roboswag.components.navigation_base.activities import androidx.fragment.app.FragmentTransaction -import ru.touchin.roboswag.components.navigation.activities.BaseActivity -import ru.touchin.roboswag.components.navigation_new.FragmentNavigation +import ru.touchin.roboswag.components.navigation_base.FragmentNavigation /** * Created by Daniil Borisovskii on 15/08/2019. diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/OnBackPressedListener.java similarity index 52% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java rename to navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/OnBackPressedListener.java index de5d318..1cf02ab 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/OnBackPressedListener.java @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation.activities; +package ru.touchin.roboswag.components.navigation_base.activities; public interface OnBackPressedListener { diff --git a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/fragments/BaseFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt similarity index 94% rename from navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/fragments/BaseFragment.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt index 830952a..e99477a 100644 --- a/navigation-new/src/main/java/ru/touchin/roboswag/components/navigation_new/fragments/BaseFragment.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation_new.fragments +package ru.touchin.roboswag.components.navigation_base.fragments import android.content.Context import android.content.res.ColorStateList @@ -17,8 +17,7 @@ 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 +import ru.touchin.roboswag.components.navigation_base.BuildConfig open class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes) { diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt b/navigation-base/src/main/java/ru/touchin/roboswag/components/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/components/navigation_base/fragments/EmptyState.kt index 12424bc..fcac033 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/EmptyState.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation.viewcontrollers +package ru.touchin.roboswag.components.navigation_base.fragments import android.os.Parcel import android.os.Parcelable diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt b/navigation-base/src/main/java/ru/touchin/roboswag/components/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/components/navigation_base/fragments/LifecycleLoggingObserver.kt index 43ef959..631d9fd 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/LifecycleLoggingObserver.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/LifecycleLoggingObserver.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation.viewcontrollers +package ru.touchin.roboswag.components.navigation_base.fragments import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver 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/components/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt similarity index 91% 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/components/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt index e7faad3..4081118 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardBehaviorDetector.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt @@ -1,11 +1,11 @@ -package ru.touchin.roboswag.components.navigation.keyboard_resizeable +package ru.touchin.roboswag.components.navigation_viewcontroller.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.components.navigation_base.activities.BaseActivity /** * This detector NOT detect landscape fullscreen keyboard 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/components/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt similarity index 88% 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/components/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt index 7b03ce2..b9d4dec 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/components/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation_new.keyboard_resizeable +package ru.touchin.roboswag.components.navigation_base.keyboard_resizeable import android.os.Build import android.os.Bundle @@ -6,9 +6,9 @@ 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.navigation_base.activities.BaseActivity +import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener +import ru.touchin.roboswag.components.navigation_base.fragments.BaseFragment import ru.touchin.roboswag.components.utils.UiUtils abstract class KeyboardResizeableFragment( diff --git a/navigation/.gitignore b/navigation-viewcontroller/.gitignore similarity index 100% rename from navigation/.gitignore rename to navigation-viewcontroller/.gitignore 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/build.gradle b/navigation-viewcontroller/build.gradle similarity index 95% rename from navigation/build.gradle rename to navigation-viewcontroller/build.gradle index e70ea8f..dd1ac36 100644 --- a/navigation/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -19,6 +19,7 @@ dependencies { api project(":utils") api project(":logging") api project(":api-logansquare") + api project(":navigation-base") api 'androidx.multidex:multidex:2.0.1' diff --git a/navigation-new/src/main/AndroidManifest.xml b/navigation-viewcontroller/src/main/AndroidManifest.xml similarity index 50% rename from navigation-new/src/main/AndroidManifest.xml rename to navigation-viewcontroller/src/main/AndroidManifest.xml index 93721cf..b0f6ab4 100644 --- a/navigation-new/src/main/AndroidManifest.xml +++ b/navigation-viewcontroller/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="ru.touchin.roboswag.components.navigation_viewcontroller"/> diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/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/components/navigation_viewcontroller/fragments/ViewControllerFragment.kt index 21a2f33..63e75df 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/fragments/ViewControllerFragment.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation.fragments +package ru.touchin.roboswag.components.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.components.navigation_viewcontroller.BuildConfig +import ru.touchin.roboswag.components.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/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt similarity index 87% 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/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt index 24d935e..930ee38 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/keyboard_resizeable/KeyboardResizeableViewController.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt @@ -1,13 +1,13 @@ -package ru.touchin.roboswag.components.navigation.keyboard_resizeable +package ru.touchin.roboswag.components.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.navigation_base.activities.BaseActivity +import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener +import ru.touchin.roboswag.components.navigation_viewcontroller.viewcontrollers.ViewController import ru.touchin.roboswag.components.utils.UiUtils abstract class KeyboardResizeableViewController( diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewController.kt similarity index 97% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.kt rename to navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewController.kt index 3a47352..0dca42a 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewController.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation.viewcontrollers +package ru.touchin.roboswag.components.navigation_viewcontroller.viewcontrollers import android.animation.Animator import android.content.Intent @@ -44,7 +44,8 @@ import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner -import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment +import ru.touchin.roboswag.components.navigation_viewcontroller.fragments.ViewControllerFragment +import ru.touchin.roboswag.components.navigation_base.fragments.LifecycleLoggingObserver import ru.touchin.roboswag.components.utils.UiUtils /** diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt similarity index 80% rename from navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt rename to navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt index 97eeb3b..fe9c85d 100644 --- a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation.viewcontrollers +package ru.touchin.roboswag.components.navigation_viewcontroller.viewcontrollers import android.content.Context import android.os.Parcelable @@ -27,8 +27,8 @@ import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction -import ru.touchin.roboswag.components.navigation.FragmentNavigation -import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment +import ru.touchin.roboswag.components.navigation_viewcontroller.fragments.ViewControllerFragment +import ru.touchin.roboswag.components.navigation_base.FragmentNavigation /** * Created by Gavriil Sitnikov on 07/03/2016. @@ -64,14 +64,14 @@ open class ViewControllerNavigation( 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 +98,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 +127,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/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/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/tabbar-navigation-new/.gitignore b/tabbar-navigation-fragment/.gitignore similarity index 100% rename from tabbar-navigation-new/.gitignore rename to tabbar-navigation-fragment/.gitignore diff --git a/tabbar-navigation-new/build.gradle b/tabbar-navigation-fragment/build.gradle similarity index 93% rename from tabbar-navigation-new/build.gradle rename to tabbar-navigation-fragment/build.gradle index a7e4bf3..1871f15 100644 --- a/tabbar-navigation-new/build.gradle +++ b/tabbar-navigation-fragment/build.gradle @@ -15,7 +15,7 @@ android { } dependencies { - api project(":navigation-new") + api project(":navigation-base") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/tabbar-navigation-fragment/src/main/AndroidManifest.xml b/tabbar-navigation-fragment/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fbf5f9f --- /dev/null +++ b/tabbar-navigation-fragment/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationActivity.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt similarity index 87% rename from tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationActivity.kt rename to tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt index faf4c84..44d9516 100644 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationActivity.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt @@ -1,10 +1,10 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new +package ru.touchin.roboswag.components.tabbarnavigation_fragment 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 +import ru.touchin.roboswag.components.navigation_base.activities.NavigationActivity +import ru.touchin.roboswag.components.navigation_base.FragmentNavigation /** * Created by Daniil Borisovskii on 15/08/2019. diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationController.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt similarity index 97% rename from tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationController.kt rename to tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt index f14504b..fc218f4 100644 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationController.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new +package ru.touchin.roboswag.components.tabbarnavigation_fragment import android.content.Context import android.os.Bundle @@ -12,7 +12,7 @@ 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.components.navigation_base.fragments.BaseFragment import ru.touchin.roboswag.core.utils.ShouldNotHappenException class BottomNavigationController( diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationFragment.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt similarity index 92% rename from tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationFragment.kt rename to tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt index cb204f3..c5ee281 100644 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/BottomNavigationFragment.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new +package ru.touchin.roboswag.components.tabbarnavigation_fragment import android.os.Bundle import android.os.Parcelable @@ -8,8 +8,8 @@ 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 +import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener +import ru.touchin.roboswag.components.navigation_base.fragments.BaseFragment abstract class BottomNavigationFragment : Fragment() { diff --git a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/NavigationContainerFragment.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt similarity index 92% rename from tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/NavigationContainerFragment.kt rename to tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt index 3d9e5a9..3326f5d 100644 --- a/tabbar-navigation-new/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_new/NavigationContainerFragment.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.tabbarnavigation_new +package ru.touchin.roboswag.components.tabbarnavigation_fragment import android.os.Bundle import android.os.Parcelable @@ -9,8 +9,8 @@ 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.components.navigation_base.FragmentNavigation +import ru.touchin.roboswag.components.navigation_base.fragments.BaseFragment import ru.touchin.roboswag.core.utils.ShouldNotHappenException class NavigationContainerFragment : Fragment() { 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/.gitignore b/tabbar-navigation-viewcontroller/.gitignore similarity index 100% rename from tabbar-navigation/.gitignore rename to tabbar-navigation-viewcontroller/.gitignore diff --git a/tabbar-navigation/README.md b/tabbar-navigation-viewcontroller/README.md similarity index 100% rename from tabbar-navigation/README.md rename to tabbar-navigation-viewcontroller/README.md diff --git a/tabbar-navigation/build.gradle b/tabbar-navigation-viewcontroller/build.gradle similarity index 92% rename from tabbar-navigation/build.gradle rename to tabbar-navigation-viewcontroller/build.gradle index fb98a51..1e40362 100644 --- a/tabbar-navigation/build.gradle +++ b/tabbar-navigation-viewcontroller/build.gradle @@ -15,7 +15,7 @@ android { } dependencies { - api project(":navigation") + api project(":navigation-viewcontroller") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml b/tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7c7d821 --- /dev/null +++ b/tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationActivity.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationActivity.kt similarity index 100% rename from tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationActivity.kt rename to tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationActivity.kt diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationController.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationController.kt similarity index 100% rename from tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationController.kt rename to tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationController.kt diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationFragment.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationFragment.kt similarity index 100% rename from tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/BottomNavigationFragment.kt rename to tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationFragment.kt diff --git a/tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/NavigationContainerFragment.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/NavigationContainerFragment.kt similarity index 100% rename from tabbar-navigation/src/main/java/ru/touchin/roboswag/components/tabbarnavigation/NavigationContainerFragment.kt rename to tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/NavigationContainerFragment.kt 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 @@ - From fa759e60ccbf016965377edd5e23abf4af02ff18 Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Mon, 27 Apr 2020 11:16:38 +0300 Subject: [PATCH 030/154] Added module 'rx-extensions' --- rx-extensions/.gitignore | 1 + rx-extensions/build.gradle | 6 ++++++ rx-extensions/src/main/AndroidManifest.xml | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 rx-extensions/.gitignore create mode 100644 rx-extensions/build.gradle create mode 100644 rx-extensions/src/main/AndroidManifest.xml 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..3b49d3c --- /dev/null +++ b/rx-extensions/build.gradle @@ -0,0 +1,6 @@ +apply plugin: 'kotlin' + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" +} 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 @@ + From 4597623cbffca9cba5797fec651be97d2ddc0f14 Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Mon, 27 Apr 2020 11:18:02 +0300 Subject: [PATCH 031/154] Added extensions 'emitAfter()' to main RxJava types --- .../src/main/java/ru/touchin/extensions/rx/Flowable.kt | 8 ++++++++ .../src/main/java/ru/touchin/extensions/rx/Maybe.kt | 8 ++++++++ .../src/main/java/ru/touchin/extensions/rx/Observable.kt | 8 ++++++++ .../src/main/java/ru/touchin/extensions/rx/Single.kt | 8 ++++++++ 4 files changed, 32 insertions(+) create mode 100644 rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt create mode 100644 rx-extensions/src/main/java/ru/touchin/extensions/rx/Maybe.kt create mode 100644 rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt create mode 100644 rx-extensions/src/main/java/ru/touchin/extensions/rx/Single.kt 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..043cf09 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt @@ -0,0 +1,8 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Flowable + +fun Flowable.emitAfter(other: Completable): Flowable = this.flatMap { value -> + other.andThen(Flowable.just(value)) +} 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..5f5fe6c --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Maybe.kt @@ -0,0 +1,8 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Maybe + +fun Maybe.emitAfter(other: Completable): Maybe = this.flatMap { value -> + other.andThen(Maybe.just(value)) +} 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..0c5a8c5 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt @@ -0,0 +1,8 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Observable + +fun Observable.emitAfter(other: Completable): Observable = this.flatMap { value -> + other.andThen(Observable.just(value)) +} 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..9c3b6c7 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Single.kt @@ -0,0 +1,8 @@ +package ru.touchin.extensions.rx + +import io.reactivex.Completable +import io.reactivex.Single + +fun Single.emitAfter(other: Completable): Single = this.flatMap { value -> + other.andThen(Single.just(value)) +} From 7cd318cc74b09f86cf4d67e5cc79488c16e7ccf4 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Apr 2020 12:45:15 +0300 Subject: [PATCH 032/154] merge fix --- .../components/navigation_base/fragments/BaseFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt index 07c653e..beb9369 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt @@ -16,7 +16,6 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import ru.touchin.roboswag.components.navigation_base.BuildConfig -import ru.touchin.roboswag.components.navigation_base.LifecycleLoggingObserver open class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes) { From 175affafb3883c1935f46ffae3c0fa898e801cde Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Mon, 27 Apr 2020 13:11:04 +0300 Subject: [PATCH 033/154] Added extension 'unwrapOrError' to main RX classes typed with Optional --- rx-extensions/build.gradle | 14 +++++++++++++- .../main/java/ru/touchin/extensions/rx/Flowable.kt | 11 +++++++++++ .../main/java/ru/touchin/extensions/rx/Maybe.kt | 11 +++++++++++ .../java/ru/touchin/extensions/rx/Observable.kt | 11 +++++++++++ .../main/java/ru/touchin/extensions/rx/Single.kt | 11 +++++++++++ .../ru/touchin/extensions/rx/utils/Constants.kt | 5 +++++ 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/Constants.kt diff --git a/rx-extensions/build.gradle b/rx-extensions/build.gradle index 3b49d3c..14da026 100644 --- a/rx-extensions/build.gradle +++ b/rx-extensions/build.gradle @@ -1,6 +1,18 @@ -apply plugin: 'kotlin' +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 16 + } +} dependencies { + api project(":utils") + api project(":logging") + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" } 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 index 043cf09..acf4e0b 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt @@ -2,7 +2,18 @@ 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)) +} 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 index 5f5fe6c..4fdd873 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Maybe.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Maybe.kt @@ -2,7 +2,18 @@ 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 index 0c5a8c5..6f1fc21 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt @@ -2,7 +2,18 @@ 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)) +} 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 index 9c3b6c7..392de40 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Single.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Single.kt @@ -2,7 +2,18 @@ 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/Constants.kt b/rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/Constants.kt new file mode 100644 index 0000000..ad51ae2 --- /dev/null +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/Constants.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" +} From 263ac4d6599673c494b35c412ee0dc4d4319592b Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Apr 2020 16:34:45 +0300 Subject: [PATCH 034/154] removed `components` middle package --- navigation-base/src/main/AndroidManifest.xml | 4 +--- .../navigation_base/FragmentNavigation.kt | 4 ++-- .../navigation_base/SimpleActionBarDrawerToggle.kt | 6 +++--- .../{components => }/navigation_base/TouchinApp.java | 2 +- .../navigation_base/activities/BaseActivity.kt | 4 ++-- .../navigation_base/activities/NavigationActivity.kt | 4 ++-- .../navigation_base/activities/OnBackPressedListener.java | 2 +- .../navigation_base/fragments/BaseFragment.kt | 4 ++-- .../navigation_base/fragments/EmptyState.kt | 2 +- .../navigation_base/fragments/LifecycleLoggingObserver.kt | 2 +- .../keyboard_resizeable/KeyboardBehaviorDetector.kt | 2 +- .../keyboard_resizeable/KeyboardResizeableFragment.kt | 8 ++++---- .../KeyboardResizeableViewController.kt | 4 ++-- .../viewcontrollers/ViewController.kt | 2 +- .../viewcontrollers/ViewControllerNavigation.kt | 2 +- .../tabbarnavigation_fragment/BottomNavigationActivity.kt | 4 ++-- .../BottomNavigationController.kt | 2 +- .../tabbarnavigation_fragment/BottomNavigationFragment.kt | 4 ++-- .../NavigationContainerFragment.kt | 4 ++-- 19 files changed, 32 insertions(+), 34 deletions(-) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/FragmentNavigation.kt (98%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/SimpleActionBarDrawerToggle.kt (96%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/TouchinApp.java (99%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/activities/BaseActivity.kt (96%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/activities/NavigationActivity.kt (79%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/activities/OnBackPressedListener.java (52%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/fragments/BaseFragment.kt (96%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/fragments/EmptyState.kt (86%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/fragments/LifecycleLoggingObserver.kt (95%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt (96%) rename navigation-base/src/main/java/ru/touchin/roboswag/{components => }/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt (88%) diff --git a/navigation-base/src/main/AndroidManifest.xml b/navigation-base/src/main/AndroidManifest.xml index 74b4a83..b05e20d 100644 --- a/navigation-base/src/main/AndroidManifest.xml +++ b/navigation-base/src/main/AndroidManifest.xml @@ -1,3 +1 @@ - + diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/FragmentNavigation.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt similarity index 98% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/FragmentNavigation.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt index 42dfca8..3dc6fe0 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/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_base +package ru.touchin.roboswag.navigation_base import android.content.Context import android.os.Bundle @@ -28,7 +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_base.fragments.BaseFragment +import ru.touchin.roboswag.navigation_base.fragments.BaseFragment import kotlin.reflect.KClass /** diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/SimpleActionBarDrawerToggle.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt similarity index 96% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/SimpleActionBarDrawerToggle.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt index 00c5ecb..166a561 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/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_base +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_base.activities.BaseActivity -import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener import ru.touchin.roboswag.components.utils.UiUtils +import ru.touchin.roboswag.navigation_base.activities.BaseActivity +import ru.touchin.roboswag.navigation_base.activities.OnBackPressedListener /** * Created by Gavriil Sitnikov on 11/03/16. diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/TouchinApp.java b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/TouchinApp.java similarity index 99% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/TouchinApp.java rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/TouchinApp.java index 88bc813..ac977ac 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/TouchinApp.java +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/TouchinApp.java @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation_base; +package ru.touchin.roboswag.navigation_base; import android.app.Application; import android.content.Context; diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/BaseActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt similarity index 96% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/BaseActivity.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt index af047ab..b1437d3 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/BaseActivity.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt @@ -17,7 +17,7 @@ * */ -package ru.touchin.roboswag.components.navigation_base.activities +package ru.touchin.roboswag.navigation_base.activities import android.content.Context import android.content.Intent @@ -27,9 +27,9 @@ import android.os.PersistableBundle import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import ru.touchin.roboswag.components.navigation_viewcontroller.keyboard_resizeable.KeyboardBehaviorDetector -import ru.touchin.roboswag.components.navigation_base.fragments.LifecycleLoggingObserver import ru.touchin.roboswag.core.log.Lc import ru.touchin.roboswag.core.log.LcGroup +import ru.touchin.roboswag.navigation_base.fragments.LifecycleLoggingObserver /** * Created by Gavriil Sitnikov on 08/03/2016. diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/NavigationActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/NavigationActivity.kt similarity index 79% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/NavigationActivity.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/NavigationActivity.kt index a157813..7c1d669 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/NavigationActivity.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/NavigationActivity.kt @@ -1,7 +1,7 @@ -package ru.touchin.roboswag.components.navigation_base.activities +package ru.touchin.roboswag.navigation_base.activities import androidx.fragment.app.FragmentTransaction -import ru.touchin.roboswag.components.navigation_base.FragmentNavigation +import ru.touchin.roboswag.navigation_base.FragmentNavigation /** * Created by Daniil Borisovskii on 15/08/2019. diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/OnBackPressedListener.java b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/OnBackPressedListener.java similarity index 52% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/activities/OnBackPressedListener.java rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/OnBackPressedListener.java index 1cf02ab..f1f05c6 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/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_base.activities; +package ru.touchin.roboswag.navigation_base.activities; public interface OnBackPressedListener { diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/BaseFragment.kt similarity index 96% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/BaseFragment.kt index beb9369..c8a85df 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/BaseFragment.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/BaseFragment.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation_base.fragments +package ru.touchin.roboswag.navigation_base.fragments import android.content.Context import android.content.res.ColorStateList @@ -15,7 +15,7 @@ import androidx.annotation.LayoutRes import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import ru.touchin.roboswag.components.navigation_base.BuildConfig +import ru.touchin.roboswag.navigation_base.BuildConfig open class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes) { diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/EmptyState.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/EmptyState.kt similarity index 86% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/EmptyState.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/EmptyState.kt index fcac033..0afa2dc 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/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_base.fragments +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/components/navigation_base/fragments/LifecycleLoggingObserver.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/LifecycleLoggingObserver.kt similarity index 95% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/LifecycleLoggingObserver.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/LifecycleLoggingObserver.kt index 631d9fd..a85255b 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/fragments/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_base.fragments +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/components/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt similarity index 96% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt index 4081118..07cf436 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt @@ -5,7 +5,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent -import ru.touchin.roboswag.components.navigation_base.activities.BaseActivity +import ru.touchin.roboswag.navigation_base.activities.BaseActivity /** * This detector NOT detect landscape fullscreen keyboard diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt similarity index 88% rename from navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt index b9d4dec..7775227 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/components/navigation_base/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_base.keyboard_resizeable +package ru.touchin.roboswag.navigation_base.keyboard_resizeable import android.os.Build import android.os.Bundle @@ -6,10 +6,10 @@ import android.os.Parcelable import android.view.View import androidx.annotation.LayoutRes import androidx.lifecycle.LifecycleObserver -import ru.touchin.roboswag.components.navigation_base.activities.BaseActivity -import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener -import ru.touchin.roboswag.components.navigation_base.fragments.BaseFragment 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_base.fragments.BaseFragment abstract class KeyboardResizeableFragment( @LayoutRes layoutRes: Int diff --git a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt index 930ee38..1a8c5f3 100644 --- a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt @@ -5,10 +5,10 @@ import android.os.Parcelable import androidx.annotation.CallSuper import androidx.annotation.LayoutRes import androidx.lifecycle.LifecycleObserver -import ru.touchin.roboswag.components.navigation_base.activities.BaseActivity -import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener import ru.touchin.roboswag.components.navigation_viewcontroller.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 abstract class KeyboardResizeableViewController( @LayoutRes layoutRes: Int, diff --git a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewController.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewController.kt index 0dca42a..bbbff94 100644 --- a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewController.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewController.kt @@ -45,8 +45,8 @@ import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import ru.touchin.roboswag.components.navigation_viewcontroller.fragments.ViewControllerFragment -import ru.touchin.roboswag.components.navigation_base.fragments.LifecycleLoggingObserver import ru.touchin.roboswag.components.utils.UiUtils +import ru.touchin.roboswag.navigation_base.fragments.LifecycleLoggingObserver /** * Created by Gavriil Sitnikov on 21/10/2015. diff --git a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt index fe9c85d..b473508 100644 --- a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt @@ -28,7 +28,7 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import ru.touchin.roboswag.components.navigation_viewcontroller.fragments.ViewControllerFragment -import ru.touchin.roboswag.components.navigation_base.FragmentNavigation +import ru.touchin.roboswag.navigation_base.FragmentNavigation /** * Created by Gavriil Sitnikov on 07/03/2016. diff --git a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt index 44d9516..dec1437 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt @@ -3,8 +3,8 @@ package ru.touchin.roboswag.components.tabbarnavigation_fragment import android.os.Parcelable import androidx.annotation.IdRes import androidx.fragment.app.FragmentManager -import ru.touchin.roboswag.components.navigation_base.activities.NavigationActivity -import ru.touchin.roboswag.components.navigation_base.FragmentNavigation +import ru.touchin.roboswag.navigation_base.FragmentNavigation +import ru.touchin.roboswag.navigation_base.activities.NavigationActivity /** * Created by Daniil Borisovskii on 15/08/2019. diff --git a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt index fc218f4..bc771b2 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt @@ -12,8 +12,8 @@ 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_base.fragments.BaseFragment import ru.touchin.roboswag.core.utils.ShouldNotHappenException +import ru.touchin.roboswag.navigation_base.fragments.BaseFragment class BottomNavigationController( private val context: Context, diff --git a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt index c5ee281..194cb05 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt @@ -8,8 +8,8 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.IdRes import androidx.fragment.app.Fragment -import ru.touchin.roboswag.components.navigation_base.activities.OnBackPressedListener -import ru.touchin.roboswag.components.navigation_base.fragments.BaseFragment +import ru.touchin.roboswag.navigation_base.activities.OnBackPressedListener +import ru.touchin.roboswag.navigation_base.fragments.BaseFragment abstract class BottomNavigationFragment : Fragment() { diff --git a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt index 3326f5d..d0d2cef 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt +++ b/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt @@ -9,9 +9,9 @@ import androidx.annotation.IdRes import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction -import ru.touchin.roboswag.components.navigation_base.FragmentNavigation -import ru.touchin.roboswag.components.navigation_base.fragments.BaseFragment import ru.touchin.roboswag.core.utils.ShouldNotHappenException +import ru.touchin.roboswag.navigation_base.FragmentNavigation +import ru.touchin.roboswag.navigation_base.fragments.BaseFragment class NavigationContainerFragment : Fragment() { From b5f0ab736c3db37400487a0a40b42e435efd97db Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Apr 2020 17:16:30 +0300 Subject: [PATCH 035/154] moved most of the bottom_navigation_viewcontroller logic to bottom_navigation_fragment --- .../.gitignore | 0 .../build.gradle | 0 .../src/main/AndroidManifest.xml | 2 + .../BottomNavigationActivity.kt | 8 +- .../BottomNavigationController.kt | 2 +- .../BottomNavigationFragment.kt | 2 +- .../NavigationContainerFragment.kt | 2 +- .../.gitignore | 0 .../README.md | 0 .../build.gradle | 1 + .../src/main/AndroidManifest.xml | 2 + .../BottomNavigationActivity.kt | 16 +++ .../navigation_base/FragmentNavigation.kt | 2 +- .../src/main/AndroidManifest.xml | 2 - .../src/main/AndroidManifest.xml | 2 - .../BottomNavigationActivity.kt | 42 ------ .../BottomNavigationController.kt | 127 ------------------ .../BottomNavigationFragment.kt | 118 ---------------- .../NavigationContainerFragment.kt | 79 ----------- 19 files changed, 27 insertions(+), 380 deletions(-) rename {tabbar-navigation-fragment => bottom-navigation-fragment}/.gitignore (100%) rename {tabbar-navigation-fragment => bottom-navigation-fragment}/build.gradle (100%) create mode 100644 bottom-navigation-fragment/src/main/AndroidManifest.xml rename {tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment => bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment}/BottomNavigationActivity.kt (88%) rename {tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment => bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment}/BottomNavigationController.kt (98%) rename {tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment => bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment}/BottomNavigationFragment.kt (97%) rename {tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment => bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment}/NavigationContainerFragment.kt (97%) rename {tabbar-navigation-viewcontroller => bottom-navigation-viewcontroller}/.gitignore (100%) rename {tabbar-navigation-viewcontroller => bottom-navigation-viewcontroller}/README.md (100%) rename {tabbar-navigation-viewcontroller => bottom-navigation-viewcontroller}/build.gradle (92%) create mode 100644 bottom-navigation-viewcontroller/src/main/AndroidManifest.xml create mode 100644 bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationActivity.kt delete mode 100644 tabbar-navigation-fragment/src/main/AndroidManifest.xml delete mode 100644 tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml delete mode 100644 tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationActivity.kt delete mode 100644 tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationController.kt delete mode 100644 tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationFragment.kt delete mode 100644 tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/NavigationContainerFragment.kt diff --git a/tabbar-navigation-fragment/.gitignore b/bottom-navigation-fragment/.gitignore similarity index 100% rename from tabbar-navigation-fragment/.gitignore rename to bottom-navigation-fragment/.gitignore diff --git a/tabbar-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle similarity index 100% rename from tabbar-navigation-fragment/build.gradle rename to bottom-navigation-fragment/build.gradle 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/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationActivity.kt similarity index 88% rename from tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt rename to bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationActivity.kt index dec1437..6565e91 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationActivity.kt +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationActivity.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.tabbarnavigation_fragment +package ru.touchin.roboswag.bottom_navigation_fragment import android.os.Parcelable import androidx.annotation.IdRes @@ -6,13 +6,9 @@ import androidx.fragment.app.FragmentManager 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() { - val innerNavigation: FragmentNavigation + open val innerNavigation: FragmentNavigation get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation /** diff --git a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationController.kt similarity index 98% rename from tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt rename to bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationController.kt index bc771b2..ccecf1e 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationController.kt +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationController.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.tabbarnavigation_fragment +package ru.touchin.roboswag.bottom_navigation_fragment import android.content.Context import android.os.Bundle diff --git a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationFragment.kt similarity index 97% rename from tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt rename to bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationFragment.kt index 194cb05..c23969c 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/BottomNavigationFragment.kt +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/BottomNavigationFragment.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.tabbarnavigation_fragment +package ru.touchin.roboswag.bottom_navigation_fragment import android.os.Bundle import android.os.Parcelable diff --git a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationContainerFragment.kt similarity index 97% rename from tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt rename to bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationContainerFragment.kt index d0d2cef..71fb9eb 100644 --- a/tabbar-navigation-fragment/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_fragment/NavigationContainerFragment.kt +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationContainerFragment.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.tabbarnavigation_fragment +package ru.touchin.roboswag.bottom_navigation_fragment import android.os.Bundle import android.os.Parcelable diff --git a/tabbar-navigation-viewcontroller/.gitignore b/bottom-navigation-viewcontroller/.gitignore similarity index 100% rename from tabbar-navigation-viewcontroller/.gitignore rename to bottom-navigation-viewcontroller/.gitignore diff --git a/tabbar-navigation-viewcontroller/README.md b/bottom-navigation-viewcontroller/README.md similarity index 100% rename from tabbar-navigation-viewcontroller/README.md rename to bottom-navigation-viewcontroller/README.md diff --git a/tabbar-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle similarity index 92% rename from tabbar-navigation-viewcontroller/build.gradle rename to bottom-navigation-viewcontroller/build.gradle index 1e40362..260741e 100644 --- a/tabbar-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -16,6 +16,7 @@ android { dependencies { api project(":navigation-viewcontroller") + api project(":bottom-navigation-fragment") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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..9634b43 --- /dev/null +++ b/bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationActivity.kt @@ -0,0 +1,16 @@ +package ru.touchin.roboswag.components.tabbarnavigation + +import android.os.Parcelable +import androidx.annotation.IdRes +import androidx.fragment.app.FragmentManager +import ru.touchin.roboswag.navigation.activities.NavigationActivity +import ru.touchin.roboswag.navigation.viewcontrollers.ViewControllerNavigation +import ru.touchin.roboswag.bottom_navigation_fragment.BottomNavigationActivity as FragmentBottomNavigationActivity + +abstract class BottomNavigationActivity : FragmentBottomNavigationActivity() { + + final override val innerNavigation: ViewControllerNavigation + get() = getNavigationContainer(supportFragmentManager)?.navigation + ?: navigation as ViewControllerNavigation + +} diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt index 3dc6fe0..bb43c5e 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt @@ -95,7 +95,7 @@ open class FragmentNavigation( addToStack: Boolean, args: Bundle?, backStackName: String?, - tag: String?, + tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? ) { if (fragmentManager.isDestroyed) { diff --git a/tabbar-navigation-fragment/src/main/AndroidManifest.xml b/tabbar-navigation-fragment/src/main/AndroidManifest.xml deleted file mode 100644 index fbf5f9f..0000000 --- a/tabbar-navigation-fragment/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml b/tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml deleted file mode 100644 index 7c7d821..0000000 --- a/tabbar-navigation-viewcontroller/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationActivity.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationActivity.kt deleted file mode 100644 index 69679a6..0000000 --- a/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationActivity.kt +++ /dev/null @@ -1,42 +0,0 @@ -package ru.touchin.roboswag.components.tabbarnavigation - -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 - -/** - * Created by Daniil Borisovskii on 15/08/2019. - * Activity to manage tab container navigation. - */ -abstract class BottomNavigationActivity : NavigationActivity() { - - val innerNavigation: ViewControllerNavigation - get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation as ViewControllerNavigation - - /** - * 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-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationController.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationController.kt deleted file mode 100644 index 8e21e3c..0000000 --- a/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationController.kt +++ /dev/null @@ -1,127 +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) - - @Suppress("detekt.ComplexMethod") - 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-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationFragment.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/BottomNavigationFragment.kt deleted file mode 100644 index c1364fc..0000000 --- a/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/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-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/NavigationContainerFragment.kt b/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/NavigationContainerFragment.kt deleted file mode 100644 index 7948b9e..0000000 --- a/tabbar-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/tabbarnavigation_viewcontroller/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) - -} From fe3876d886d20e8001109394e08096f40d6d7628 Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Tue, 28 Apr 2020 18:58:50 +0300 Subject: [PATCH 036/154] Added function "unwrapOrSkip()" for Observable and Flowable --- .../src/main/java/ru/touchin/extensions/rx/Flowable.kt | 6 ++++++ .../src/main/java/ru/touchin/extensions/rx/Observable.kt | 6 ++++++ 2 files changed, 12 insertions(+) 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 index acf4e0b..12f7397 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt @@ -17,3 +17,9 @@ fun Flowable>.unwrapOrError( ?.let { Flowable.just(it) } ?: Flowable.error(ShouldNotHappenException(errorMessage)) } + +fun Flowable>.unwrapOrSkip(): Flowable = this.flatMap { wrapper -> + wrapper.get() + ?.let { Flowable.just(it) } + ?: Flowable.empty().skip(1) +} 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 index 6f1fc21..399dff2 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt @@ -17,3 +17,9 @@ fun Observable>.unwrapOrError( ?.let { Observable.just(it) } ?: Observable.error(ShouldNotHappenException(errorMessage)) } + +fun Observable>.unwrapOrSkip(): Observable = this.flatMap { wrapper -> + wrapper.get() + ?.let { Observable.just(it) } + ?: Observable.empty().skip(1) +} From 4e9e10412442c604a5cfa72f0aadac18109c6e0d Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Tue, 28 Apr 2020 20:22:36 +0300 Subject: [PATCH 037/154] Make function 'unwrapOrSkip' a little bit more prettier --- .../src/main/java/ru/touchin/extensions/rx/Flowable.kt | 8 +++----- .../src/main/java/ru/touchin/extensions/rx/Observable.kt | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) 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 index 12f7397..6ed222d 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt @@ -18,8 +18,6 @@ fun Flowable>.unwrapOrError( ?: Flowable.error(ShouldNotHappenException(errorMessage)) } -fun Flowable>.unwrapOrSkip(): Flowable = this.flatMap { wrapper -> - wrapper.get() - ?.let { Flowable.just(it) } - ?: Flowable.empty().skip(1) -} +fun Flowable>.unwrapOrSkip(): Flowable = this + .filter { it.get() != null } + .map { it.get() } 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 index 399dff2..92f9f8e 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt @@ -18,8 +18,6 @@ fun Observable>.unwrapOrError( ?: Observable.error(ShouldNotHappenException(errorMessage)) } -fun Observable>.unwrapOrSkip(): Observable = this.flatMap { wrapper -> - wrapper.get() - ?.let { Observable.just(it) } - ?: Observable.empty().skip(1) -} +fun Observable>.unwrapOrSkip(): Observable = this + .filter { it.get() != null } + .map { it.get() } From 6ce2bbf82fb574846e72683d52ad39fb16c6be38 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 28 Apr 2020 21:02:50 +0300 Subject: [PATCH 038/154] added bottom navigation base as separate module --- bottom-navigation-base/.gitignore | 1 + bottom-navigation-base/build.gradle | 30 ++++ .../src/main/AndroidManifest.xml | 2 + .../BaseBottomNavigationActivity.kt | 41 +++++ .../BaseBottomNavigationController.kt | 161 ++++++++++++++++++ .../BaseBottomNavigationFragment.kt | 72 ++++++++ .../BaseNavigationContainerFramgent.kt | 78 +++++++++ .../BaseNavigationTab.kt | 44 +++++ bottom-navigation-fragment/build.gradle | 1 + .../BottomNavigationActivity.kt | 39 +---- .../BottomNavigationController.kt | 123 ++----------- .../BottomNavigationFragment.kt | 81 ++------- .../NavigationContainerFragment.kt | 63 +------ .../NavigationTab.kt | 10 ++ bottom-navigation-viewcontroller/build.gradle | 2 +- .../BottomNavigationActivity.kt | 24 +-- .../BottomNavigationController.kt | 45 +++++ .../BottomNavigationFragment.kt | 19 +++ .../NavigationContainerFragment.kt | 26 +++ .../NavigationTab.kt | 15 ++ .../viewmodel/LifecycleViewModelProviders.kt | 6 +- .../activities/BaseActivity.kt | 2 +- .../activities/NavigationActivity.kt | 15 +- .../KeyboardBehaviorDetector.kt | 2 +- .../src/main/AndroidManifest.xml | 4 +- .../fragments/ViewControllerFragment.kt | 6 +- .../KeyboardResizeableViewController.kt | 6 +- .../viewcontrollers/ViewController.kt | 4 +- .../ViewControllerNavigation.kt | 5 +- 29 files changed, 618 insertions(+), 309 deletions(-) create mode 100644 bottom-navigation-base/.gitignore create mode 100644 bottom-navigation-base/build.gradle create mode 100644 bottom-navigation-base/src/main/AndroidManifest.xml create mode 100644 bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt create mode 100644 bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationController.kt create mode 100644 bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationFragment.kt create mode 100644 bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFramgent.kt create mode 100644 bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationTab.kt create mode 100644 bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationTab.kt create mode 100644 bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationController.kt create mode 100644 bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/BottomNavigationFragment.kt create mode 100644 bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationContainerFragment.kt create mode 100644 bottom-navigation-viewcontroller/src/main/java/ru/touchin/roboswag/bottom_navigation_viewcontroller/NavigationTab.kt rename navigation-viewcontroller/src/main/java/ru/touchin/roboswag/{components => }/navigation_viewcontroller/fragments/ViewControllerFragment.kt (97%) rename navigation-viewcontroller/src/main/java/ru/touchin/roboswag/{components => }/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt (93%) rename navigation-viewcontroller/src/main/java/ru/touchin/roboswag/{components => }/navigation_viewcontroller/viewcontrollers/ViewController.kt (98%) rename navigation-viewcontroller/src/main/java/ru/touchin/roboswag/{components => }/navigation_viewcontroller/viewcontrollers/ViewControllerNavigation.kt (97%) diff --git a/bottom-navigation-base/.gitignore b/bottom-navigation-base/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/bottom-navigation-base/.gitignore @@ -0,0 +1 @@ +/build diff --git a/bottom-navigation-base/build.gradle b/bottom-navigation-base/build.gradle new file mode 100644 index 0000000..40be017 --- /dev/null +++ b/bottom-navigation-base/build.gradle @@ -0,0 +1,30 @@ +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 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + +} + +dependencies { + api project(":navigation-base") + + 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/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/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt new file mode 100644 index 0000000..86a185e --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt @@ -0,0 +1,41 @@ +package ru.touchin.roboswag.bottom_navigation_fragment + +import android.os.Parcelable +import androidx.annotation.IdRes +import androidx.fragment.app.FragmentManager +import ru.touchin.roboswag.navigation_base.FragmentNavigation +import ru.touchin.roboswag.navigation_base.activities.NavigationActivity + +abstract class BaseBottomNavigationActivity< + TNavigation : FragmentNavigation, + TNavigationFragment : BaseBottomNavigationFragment<*>, + TNavigationContainer : BaseNavigationContainerFragment<*, TNavigation>> : NavigationActivity() { + + val innerNavigation: TNavigation + 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? TNavigationFragment)?.navigateTo(navigationTabId, state) + } + } + + protected fun getNavigationContainer(fragmentManager: FragmentManager?): TNavigationContainer? = + fragmentManager + ?.primaryNavigationFragment + ?.let { navigationFragment -> + 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..ec69c76 --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationController.kt @@ -0,0 +1,161 @@ +package ru.touchin.roboswag.bottom_navigation_fragment + +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.BaseFragment + +abstract class BaseBottomNavigationController( + private val tabs: SparseArray, + private val context: Context, + private val fragmentManager: FragmentManager, + @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 wrapWithNavigationContainer: Boolean = false +) { + + 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() = Unit + + 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, + BaseFragment.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..f721e59 --- /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_fragment + +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/BaseNavigationContainerFramgent.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFramgent.kt new file mode 100644 index 0000000..35fe22c --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFramgent.kt @@ -0,0 +1,78 @@ +package ru.touchin.roboswag.bottom_navigation_fragment + +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..6f4643c --- /dev/null +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationTab.kt @@ -0,0 +1,44 @@ +package ru.touchin.roboswag.bottom_navigation_fragment + +import android.os.Parcel +import android.os.Parcelable +import ru.touchin.roboswag.navigation_base.fragments.EmptyState + +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() + + 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/bottom-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle index 1871f15..f24dfbf 100644 --- a/bottom-navigation-fragment/build.gradle +++ b/bottom-navigation-fragment/build.gradle @@ -16,6 +16,7 @@ android { dependencies { api project(":navigation-base") + api project(":bottom-navigation-base") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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 index 6565e91..214ea8f 100644 --- 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 @@ -1,38 +1,17 @@ package ru.touchin.roboswag.bottom_navigation_fragment -import android.os.Parcelable -import androidx.annotation.IdRes -import androidx.fragment.app.FragmentManager import ru.touchin.roboswag.navigation_base.FragmentNavigation -import ru.touchin.roboswag.navigation_base.activities.NavigationActivity -abstract class BottomNavigationActivity : NavigationActivity() { +abstract class BottomNavigationActivity : + BaseBottomNavigationActivity() { - open 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) - } + override val navigation by lazy { + FragmentNavigation( + this, + supportFragmentManager, + fragmentContainerViewId, + transition + ) } - private fun getNavigationContainer(fragmentManager: FragmentManager?): NavigationContainerFragment? = - fragmentManager - ?.primaryNavigationFragment - ?.let { navigationFragment -> - navigationFragment as? NavigationContainerFragment - ?: getNavigationContainer(navigationFragment.childFragmentManager) - } - } 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 index ccecf1e..eeb51ff 100644 --- 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 @@ -1,125 +1,34 @@ package ru.touchin.roboswag.bottom_navigation_fragment 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.BaseFragment class BottomNavigationController( - private val context: Context, - private val fragmentManager: FragmentManager, - private val fragments: SparseArray>, Parcelable>>, + context: Context, + fragments: SparseArray, + fragmentManager: FragmentManager, + 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 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 +) : BaseBottomNavigationController( + tabs = fragments, + context = context, + fragmentManager = fragmentManager, + defaultTabId = defaultTabId, + contentContainerViewId = contentContainerViewId, + contentContainerLayoutId = contentContainerLayoutId, + wrapWithNavigationContainer = wrapWithNavigationContainer ) { - private var callback: FragmentManager.FragmentLifecycleCallbacks? = null + override fun getNavigationContainerClass() = NavigationContainerFragment::class.java - 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() - } - } - } - } + override fun onTabReselected() { + 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, - NavigationContainerFragment::class.java.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/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 index c23969c..5445473 100644 --- 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 @@ -1,75 +1,16 @@ package ru.touchin.roboswag.bottom_navigation_fragment -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.OnBackPressedListener -import ru.touchin.roboswag.navigation_base.fragments.BaseFragment +abstract class BottomNavigationFragment : BaseBottomNavigationFragment() { -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 + 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 index 71fb9eb..7d8356b 100644 --- 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 @@ -1,43 +1,12 @@ package ru.touchin.roboswag.bottom_navigation_fragment -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 import ru.touchin.roboswag.navigation_base.fragments.BaseFragment -class NavigationContainerFragment : Fragment() { +class NavigationContainerFragment : BaseNavigationContainerFragment, FragmentNavigation>() { - 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 { + override val navigation by lazy { FragmentNavigation( requireContext(), childFragmentManager, @@ -46,32 +15,8 @@ class NavigationContainerFragment : Fragment() { ) } - @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().kotlin, args.getParcelable(FRAGMENT_STATE_ARG)) - } + override fun onContainerCreated() { + navigation.setInitial(getContainedClass().kotlin, arguments?.getParcelable(FRAGMENT_STATE_ARG)!!) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(containerLayoutId, container, false) - } 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..1a78e34 --- /dev/null +++ b/bottom-navigation-fragment/src/main/java/ru/touchin/roboswag/bottom_navigation_fragment/NavigationTab.kt @@ -0,0 +1,10 @@ +package ru.touchin.roboswag.bottom_navigation_fragment + +import android.os.Parcelable +import ru.touchin.roboswag.navigation_base.fragments.BaseFragment + +class NavigationTab( + override val cls: Class>, + state: Parcelable, + saveStateOnSwitching: Boolean = true +) : BaseNavigationTab(cls, state, saveStateOnSwitching) diff --git a/bottom-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle index 260741e..0a4487e 100644 --- a/bottom-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -16,7 +16,7 @@ android { dependencies { api project(":navigation-viewcontroller") - api project(":bottom-navigation-fragment") + api project(":bottom-navigation-base") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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 index 9634b43..a9135fc 100644 --- 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 @@ -1,16 +1,18 @@ -package ru.touchin.roboswag.components.tabbarnavigation +package ru.touchin.roboswag.bottom_navigation_viewcontroller -import android.os.Parcelable -import androidx.annotation.IdRes -import androidx.fragment.app.FragmentManager -import ru.touchin.roboswag.navigation.activities.NavigationActivity -import ru.touchin.roboswag.navigation.viewcontrollers.ViewControllerNavigation -import ru.touchin.roboswag.bottom_navigation_fragment.BottomNavigationActivity as FragmentBottomNavigationActivity +import ru.touchin.roboswag.bottom_navigation_fragment.BaseBottomNavigationActivity +import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewControllerNavigation -abstract class BottomNavigationActivity : FragmentBottomNavigationActivity() { +abstract class BottomNavigationActivity : + BaseBottomNavigationActivity, BottomNavigationFragment, NavigationContainerFragment>() { - final override val innerNavigation: ViewControllerNavigation - get() = getNavigationContainer(supportFragmentManager)?.navigation - ?: navigation as ViewControllerNavigation + 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..6597ae3 --- /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_fragment.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..8a2f3fc --- /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_fragment.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..c70e1fd --- /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_fragment.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..7ee96ed --- /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_fragment.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/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt index f49d498..8bdb97b 100644 --- a/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt +++ b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/LifecycleViewModelProviders.kt @@ -1,12 +1,12 @@ 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 import androidx.lifecycle.ViewModelProviders -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import ru.touchin.roboswag.components.navigation_viewcontroller.viewcontrollers.ViewController +import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController object LifecycleViewModelProviders { diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt index b1437d3..eda7d0c 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt @@ -26,10 +26,10 @@ import android.os.Bundle import android.os.PersistableBundle import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity -import ru.touchin.roboswag.components.navigation_viewcontroller.keyboard_resizeable.KeyboardBehaviorDetector 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_viewcontroller.keyboard_resizeable.KeyboardBehaviorDetector /** * Created by Gavriil Sitnikov on 08/03/2016. 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 index 7c1d669..ed45972 100644 --- 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 @@ -3,23 +3,12 @@ package ru.touchin.roboswag.navigation_base.activities import androidx.fragment.app.FragmentTransaction import ru.touchin.roboswag.navigation_base.FragmentNavigation -/** - * Created by Daniil Borisovskii on 15/08/2019. - * Base activity with nested navigation. - */ -abstract class NavigationActivity : BaseActivity() { +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 - ) - } + abstract val navigation: TNavigation } diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt index 07cf436..7f93232 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.components.navigation_viewcontroller.keyboard_resizeable +package ru.touchin.roboswag.navigation_viewcontroller.keyboard_resizeable import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat diff --git a/navigation-viewcontroller/src/main/AndroidManifest.xml b/navigation-viewcontroller/src/main/AndroidManifest.xml index b0f6ab4..79d1d6e 100644 --- a/navigation-viewcontroller/src/main/AndroidManifest.xml +++ b/navigation-viewcontroller/src/main/AndroidManifest.xml @@ -1,3 +1 @@ - + diff --git a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/fragments/ViewControllerFragment.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/fragments/ViewControllerFragment.kt similarity index 97% rename from navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/fragments/ViewControllerFragment.kt rename to navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/fragments/ViewControllerFragment.kt index 63e75df..053f9a9 100644 --- a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/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_viewcontroller.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_viewcontroller.BuildConfig -import ru.touchin.roboswag.components.navigation_viewcontroller.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-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt similarity index 93% rename from navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt rename to navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt index 1a8c5f3..bd7f7c8 100644 --- a/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/components/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt +++ b/navigation-viewcontroller/src/main/java/ru/touchin/roboswag/navigation_viewcontroller/keyboard_resizeable/KeyboardResizeableViewController.kt @@ -1,14 +1,14 @@ -package ru.touchin.roboswag.components.navigation_viewcontroller.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_viewcontroller.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, @@ -17,6 +17,7 @@ abstract class KeyboardResizeableViewController= Build.VERSION_CODES.KITKAT_WATCH) { creationContext.container?.requestApplyInsets() @@ -85,4 +86,5 @@ abstract class KeyboardResizeableViewController Date: Tue, 28 Apr 2020 21:25:03 +0300 Subject: [PATCH 039/154] Renamed 'unwrapAndSkip()' to 'unwrapAndFilter()' --- .../src/main/java/ru/touchin/extensions/rx/Flowable.kt | 2 +- .../src/main/java/ru/touchin/extensions/rx/Observable.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 6ed222d..9f2f959 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Flowable.kt @@ -18,6 +18,6 @@ fun Flowable>.unwrapOrError( ?: Flowable.error(ShouldNotHappenException(errorMessage)) } -fun Flowable>.unwrapOrSkip(): Flowable = this +fun Flowable>.unwrapOrFilter(): Flowable = this .filter { it.get() != null } .map { it.get() } 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 index 92f9f8e..eb643dd 100644 --- a/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt +++ b/rx-extensions/src/main/java/ru/touchin/extensions/rx/Observable.kt @@ -18,6 +18,6 @@ fun Observable>.unwrapOrError( ?: Observable.error(ShouldNotHappenException(errorMessage)) } -fun Observable>.unwrapOrSkip(): Observable = this +fun Observable>.unwrapOrFilter(): Observable = this .filter { it.get() != null } .map { it.get() } From c1b12544b8711d2d86dcd0c4c8a2f6018bf0cc3f Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Tue, 28 Apr 2020 22:27:16 +0300 Subject: [PATCH 040/154] Rename 'Constants' to 'StringConstants' --- .../extensions/rx/utils/{Constants.kt => StringConstants.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/{Constants.kt => StringConstants.kt} (100%) diff --git a/rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/Constants.kt b/rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/StringConstants.kt similarity index 100% rename from rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/Constants.kt rename to rx-extensions/src/main/java/ru/touchin/extensions/rx/utils/StringConstants.kt From 8399f01d506361f0ac61ecd635c99b662a1e6a91 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 29 Apr 2020 13:44:27 +0300 Subject: [PATCH 041/154] kotlinOptions to java 8 --- bottom-navigation-fragment/build.gradle | 5 +++++ bottom-navigation-viewcontroller/build.gradle | 5 +++++ navigation-base/build.gradle | 5 +++++ navigation-viewcontroller/build.gradle | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/bottom-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle index f24dfbf..87c56a9 100644 --- a/bottom-navigation-fragment/build.gradle +++ b/bottom-navigation-fragment/build.gradle @@ -12,6 +12,11 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + } dependencies { diff --git a/bottom-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle index 0a4487e..cdae20e 100644 --- a/bottom-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -12,6 +12,11 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + } dependencies { diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index 5aa8b2b..af04146 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -13,6 +13,11 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + } dependencies { diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index dd1ac36..20888aa 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -13,6 +13,11 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + } dependencies { From 45dddff10aeec7e9536b5ab4db056521e31e88d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=91=D0=B0=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D0=BA=D0=B8=D0=B8=CC=86?= Date: Wed, 27 May 2020 20:41:46 +0300 Subject: [PATCH 042/154] split basefragment and state saving --- .../activities/BaseActivity.kt | 2 +- .../navigation_base/fragments/BaseFragment.kt | 42 +------------- .../fragments/FragmentWithState.kt | 55 +++++++++++++++++++ .../KeyboardBehaviorDetector.kt | 2 +- 4 files changed, 60 insertions(+), 41 deletions(-) create mode 100644 navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentWithState.kt diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt index eda7d0c..be2244e 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/activities/BaseActivity.kt @@ -29,7 +29,7 @@ import androidx.appcompat.app.AppCompatActivity 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_viewcontroller.keyboard_resizeable.KeyboardBehaviorDetector +import ru.touchin.roboswag.navigation_base.keyboard_resizeable.KeyboardBehaviorDetector /** * Created by Gavriil Sitnikov on 08/03/2016. 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 index c8a85df..b38027f 100644 --- 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 @@ -4,8 +4,6 @@ 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 @@ -15,30 +13,12 @@ import androidx.annotation.LayoutRes import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import ru.touchin.roboswag.navigation_base.BuildConfig -open class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes) { +open class BaseFragment : Fragment { - companion object { - private const val BASE_FRAGMENT_STATE_EXTRA = "BASE_FRAGMENT_STATE_EXTRA" + constructor() : super() - 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 - } - } + constructor(@LayoutRes layoutRes: Int) : super(layoutRes) protected val view: View @JvmName("requireViewKtx") get() = requireView() @@ -49,21 +29,10 @@ open class BaseFragment(@Layo protected val context: Context @JvmName("requireContextKtx") get() = requireContext() - protected lateinit var state: TState - private set - 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?) { @@ -72,11 +41,6 @@ open class BaseFragment(@Layo lifecycle.addObserver(LifecycleLoggingObserver(this)) } - 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 diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentWithState.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentWithState.kt new file mode 100644 index 0000000..dedf118 --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentWithState.kt @@ -0,0 +1,55 @@ +package ru.touchin.roboswag.navigation_base.fragments + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import androidx.annotation.LayoutRes +import androidx.fragment.app.FragmentActivity +import ru.touchin.roboswag.navigation_base.BuildConfig + +open class FragmentWithState( + @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) } + + // 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 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 = reserialize(state) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable(BASE_FRAGMENT_STATE_EXTRA, state) + } + +} diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt index 7f93232..30774a2 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardBehaviorDetector.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.navigation_viewcontroller.keyboard_resizeable +package ru.touchin.roboswag.navigation_base.keyboard_resizeable import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat From f3bd3b2f694f124d12260a47024442e851b5831e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=91=D0=B0=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D0=BA=D0=B8=D0=B8=CC=86?= Date: Wed, 27 May 2020 20:58:05 +0300 Subject: [PATCH 043/154] move logging stuff to logging module --- .../java/ru/touchin/templates/ApiModel.java | 14 ++-- logging/build.gradle | 4 ++ .../ru/touchin/roboswag/core/log/LcGroup.java | 9 ++- .../core/utils/CrashlyticsLogProcessor.java | 66 +++++++++++++++++++ navigation-base/build.gradle | 1 - .../roboswag/navigation_base/TouchinApp.java | 59 +---------------- navigation-viewcontroller/build.gradle | 1 - 7 files changed, 81 insertions(+), 73 deletions(-) create mode 100644 logging/src/main/java/ru/touchin/roboswag/core/utils/CrashlyticsLogProcessor.java 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/logging/build.gradle b/logging/build.gradle index 21ed245..2b79ac2 100644 --- a/logging/build.gradle +++ b/logging/build.gradle @@ -15,4 +15,8 @@ android { dependencies { implementation "androidx.annotation:annotation:$versions.androidx" + + implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { + transitive = true + } } 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..41e877f 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 UI lifecycle (onCreate, onStart, onResume etc.). + */ + 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..1234660 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/CrashlyticsLogProcessor.java @@ -0,0 +1,66 @@ +package ru.touchin.roboswag.core.utils; + +import android.util.Log; + +import com.crashlytics.android.Crashlytics; + +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 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 == LcGroup.API_VALIDATION && 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-base/build.gradle b/navigation-base/build.gradle index af04146..2a000a0 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -23,7 +23,6 @@ android { dependencies { api project(":utils") api project(":logging") - api project(":api-logansquare") api 'androidx.multidex:multidex:2.0.1' 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 index ac977ac..ca05782 100644 --- 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 @@ -28,20 +28,14 @@ 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; +import ru.touchin.roboswag.core.utils.CrashlyticsLogProcessor; /** * Created by Gavriil Sitnikov on 10/03/16. @@ -93,55 +87,4 @@ public abstract class TouchinApp extends Application { .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-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index 20888aa..59cbb01 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -23,7 +23,6 @@ android { dependencies { api project(":utils") api project(":logging") - api project(":api-logansquare") api project(":navigation-base") api 'androidx.multidex:multidex:2.0.1' From 62d3467a59e602f52eb7fde6689e452bd75419ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=91=D0=B0=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D0=BA=D0=B8=D0=B8=CC=86?= Date: Thu, 28 May 2020 00:15:53 +0300 Subject: [PATCH 044/154] update comment to new lc group --- logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 41e877f..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 @@ -45,7 +45,7 @@ public class LcGroup { */ public static final LcGroup UI_LIFECYCLE = new LcGroup("UI_LIFECYCLE"); /** - * Logging group to log UI lifecycle (onCreate, onStart, onResume etc.). + * Logging group to log api validation errors. */ public static final LcGroup API_VALIDATION = new LcGroup("API_VALIDATION"); From 2015daf5d18c32cebfe3d1af566421767c693f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=91=D0=B0=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D0=BA=D0=B8=D0=B8=CC=86?= Date: Thu, 28 May 2020 00:26:43 +0300 Subject: [PATCH 045/154] fix errors with fragmentwithstate --- .../roboswag/navigation_base/FragmentNavigation.kt | 14 +++++++------- .../KeyboardResizeableFragment.kt | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt index bb43c5e..0cb1043 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt @@ -28,7 +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.navigation_base.fragments.BaseFragment +import ru.touchin.roboswag.navigation_base.fragments.FragmentWithState import kotlin.reflect.KClass /** @@ -169,14 +169,14 @@ open class FragmentNavigation( * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. */ fun push( - fragmentClass: KClass>, + fragmentClass: KClass>, state: T, addToStack: Boolean = true, backStackName: String? = null, tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { - push(fragmentClass.java, BaseFragment.args(state), addToStack, backStackName, tag, transactionSetup) + push(fragmentClass.java, FragmentWithState.args(state), addToStack, backStackName, tag, transactionSetup) } /** @@ -216,14 +216,14 @@ open class FragmentNavigation( * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. */ fun pushForResult( - fragmentClass: KClass>, + fragmentClass: KClass>, targetFragment: Fragment, targetRequestCode: Int, state: T, tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { - pushForResult(fragmentClass.java, targetFragment, targetRequestCode, BaseFragment.args(state), tag, transactionSetup) + pushForResult(fragmentClass.java, targetFragment, targetRequestCode, FragmentWithState.args(state), tag, transactionSetup) } /** @@ -270,13 +270,13 @@ open class FragmentNavigation( * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. */ fun setInitial( - fragmentClass: KClass>, + fragmentClass: KClass>, state: T, tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { beforeSetInitialActions() - setAsTop(fragmentClass.java, BaseFragment.args(state), false, tag, transactionSetup) + setAsTop(fragmentClass.java, FragmentWithState.args(state), false, tag, transactionSetup) } /** diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt index 7775227..13cec6d 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt @@ -10,10 +10,11 @@ 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_base.fragments.BaseFragment +import ru.touchin.roboswag.navigation_base.fragments.FragmentWithState abstract class KeyboardResizeableFragment( @LayoutRes layoutRes: Int -) : BaseFragment( +) : FragmentWithState( layoutRes ) { From 372750eee4928dfe2f67670a11db8b2e33f512c8 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 28 May 2020 13:26:54 +0300 Subject: [PATCH 046/154] viewcontroller viewModel management logic now in separate module rename FragmentWithState to StatefulFragment fix navigation class files packages --- bottom-navigation-base/build.gradle | 3 +- .../BaseBottomNavigationActivity.kt | 13 ++--- .../BaseBottomNavigationController.kt | 17 ++++--- .../BaseBottomNavigationFragment.kt | 2 +- .../BaseNavigationContainerFramgent.kt | 2 +- .../BaseNavigationTab.kt | 39 ++++----------- bottom-navigation-fragment/build.gradle | 4 +- .../BottomNavigationActivity.kt | 1 + .../BottomNavigationController.kt | 12 ++--- .../BottomNavigationFragment.kt | 2 + .../NavigationContainerFragment.kt | 6 ++- .../NavigationTab.kt | 5 +- lifecycle-viewcontroller/.gitignore | 1 + lifecycle-viewcontroller/build.gradle | 35 ++++++++++++++ .../src/main/AndroidManifest.xml | 1 + .../extensions/ViewModelLazy.kt | 37 ++++++++++++++ .../viewmodel/LifecycleViewModelProviders.kt | 30 ++++++++++++ lifecycle/build.gradle | 2 - .../BaseLifecycleViewModelProviders.kt | 47 ++++++++++++++++++ .../viewmodel/LifecycleViewModelProviders.kt | 48 +------------------ navigation-base/build.gradle | 8 ++-- .../navigation_base/FragmentNavigation.kt | 14 +++--- .../navigation_base/extensions/Parcelable.kt | 47 ++++++++++++++++++ ...agmentWithState.kt => StatefulFragment.kt} | 20 ++------ .../KeyboardResizeableFragment.kt | 5 +- navigation-viewcontroller/build.gradle | 10 ++-- 26 files changed, 268 insertions(+), 143 deletions(-) create mode 100644 lifecycle-viewcontroller/.gitignore create mode 100644 lifecycle-viewcontroller/build.gradle create mode 100644 lifecycle-viewcontroller/src/main/AndroidManifest.xml create mode 100644 lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/extensions/ViewModelLazy.kt create mode 100644 lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/viewmodel/LifecycleViewModelProviders.kt create mode 100644 lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt create mode 100644 navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/extensions/Parcelable.kt rename navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/{FragmentWithState.kt => StatefulFragment.kt} (55%) diff --git a/bottom-navigation-base/build.gradle b/bottom-navigation-base/build.gradle index 40be017..f97eb55 100644 --- a/bottom-navigation-base/build.gradle +++ b/bottom-navigation-base/build.gradle @@ -20,7 +20,8 @@ android { } dependencies { - api project(":navigation-base") + implementation project(":logging") + implementation project(":navigation-base") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt index 86a185e..779602f 100644 --- a/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt +++ b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseBottomNavigationActivity.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.bottom_navigation_fragment +package ru.touchin.roboswag.bottom_navigation_base import android.os.Parcelable import androidx.annotation.IdRes @@ -6,13 +6,14 @@ import androidx.fragment.app.FragmentManager import ru.touchin.roboswag.navigation_base.FragmentNavigation import ru.touchin.roboswag.navigation_base.activities.NavigationActivity -abstract class BaseBottomNavigationActivity< - TNavigation : FragmentNavigation, - TNavigationFragment : BaseBottomNavigationFragment<*>, - TNavigationContainer : BaseNavigationContainerFragment<*, TNavigation>> : NavigationActivity() { +abstract class BaseBottomNavigationActivity : NavigationActivity() + where TNavigation : FragmentNavigation, + TNavigationFragment : BaseBottomNavigationFragment<*>, + TNavigationContainer : BaseNavigationContainerFragment<*, TNavigation> +{ val innerNavigation: TNavigation - get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation + get() = getNavigationContainer(supportFragmentManager)?.navigation ?: navigation /** * Navigates to the given navigation tab. 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 index ec69c76..8f831ef 100644 --- 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 @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.bottom_navigation_fragment +package ru.touchin.roboswag.bottom_navigation_base import android.content.Context import android.os.Bundle @@ -13,16 +13,17 @@ 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.BaseFragment +import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment abstract class BaseBottomNavigationController( private val tabs: SparseArray, private val context: Context, private val fragmentManager: FragmentManager, - @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 wrapWithNavigationContainer: Boolean = false + @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 @@ -111,7 +112,9 @@ abstract class BaseBottomNavigationController> = BaseNavigationContainerFragment::class.java - protected open fun onTabReselected() = Unit + protected open fun onTabReselected() { + onReselectListener?.invoke() + } protected open fun isTabClass(tab: TNavigationTab, fragment: Fragment?) = if (wrapWithNavigationContainer) { (fragment as BaseNavigationContainerFragment<*, *>).getContainedClass() @@ -130,7 +133,7 @@ abstract class BaseBottomNavigationController, @@ -14,31 +13,13 @@ open class BaseNavigationTab( 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() - - 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 - } + /** + * 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/bottom-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle index 87c56a9..69410ca 100644 --- a/bottom-navigation-fragment/build.gradle +++ b/bottom-navigation-fragment/build.gradle @@ -20,8 +20,8 @@ android { } dependencies { - api project(":navigation-base") - api project(":bottom-navigation-base") + implementation project(":navigation-base") + implementation project(":bottom-navigation-base") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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 index 214ea8f..1eae1ca 100644 --- 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 @@ -1,5 +1,6 @@ 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 : 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 index eeb51ff..de42784 100644 --- 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 @@ -5,21 +5,23 @@ 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, - @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 + @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 @@ -27,8 +29,4 @@ class BottomNavigationController( override fun getNavigationContainerClass() = NavigationContainerFragment::class.java - override fun onTabReselected() { - onReselectListener?.invoke() - } - } 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 index 5445473..9a2c20e 100644 --- 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 @@ -1,5 +1,7 @@ package ru.touchin.roboswag.bottom_navigation_fragment +import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationFragment + abstract class BottomNavigationFragment : BaseBottomNavigationFragment() { override fun createNavigationController() = BottomNavigationController( 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 index 7d8356b..200b7ee 100644 --- 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 @@ -1,10 +1,12 @@ 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.BaseFragment +import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment -class NavigationContainerFragment : BaseNavigationContainerFragment, FragmentNavigation>() { +class NavigationContainerFragment : + BaseNavigationContainerFragment, FragmentNavigation>() { override val navigation by lazy { FragmentNavigation( 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 index 1a78e34..81e4c6e 100644 --- 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 @@ -1,10 +1,11 @@ package ru.touchin.roboswag.bottom_navigation_fragment import android.os.Parcelable -import ru.touchin.roboswag.navigation_base.fragments.BaseFragment +import ru.touchin.roboswag.bottom_navigation_base.BaseNavigationTab +import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment class NavigationTab( - override val cls: Class>, + override val cls: Class>, state: Parcelable, saveStateOnSwitching: Boolean = true ) : BaseNavigationTab(cls, state, saveStateOnSwitching) diff --git a/lifecycle-viewcontroller/.gitignore b/lifecycle-viewcontroller/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/lifecycle-viewcontroller/.gitignore @@ -0,0 +1 @@ +/build diff --git a/lifecycle-viewcontroller/build.gradle b/lifecycle-viewcontroller/build.gradle new file mode 100644 index 0000000..7ceb229 --- /dev/null +++ b/lifecycle-viewcontroller/build.gradle @@ -0,0 +1,35 @@ +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 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation project(":lifecycle") + implementation project(":navigation-viewcontroller") + + compileOnly "javax.inject:javax.inject:1" + + 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 "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle" +} 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..7d93a36 --- /dev/null +++ b/lifecycle-viewcontroller/src/main/java/ru/touchin/lifecycle_viewcontroller/viewmodel/LifecycleViewModelProviders.kt @@ -0,0 +1,30 @@ +package ru.touchin.lifecycle_viewcontroller.viewmodel + +import android.app.Activity +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import ru.touchin.lifecycle.viewmodel.BaseLifecycleViewModelProviders +import ru.touchin.lifecycle.viewmodel.ViewModelFactoryProvider +import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController + +object LifecycleViewModelProviders : BaseLifecycleViewModelProviders() { + + /** + * 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 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.") + } + +} diff --git a/lifecycle/build.gradle b/lifecycle/build.gradle index 2ad19c2..6ca35ff 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -19,8 +19,6 @@ android { } dependencies { - api project(":navigation-viewcontroller") - compileOnly "javax.inject:javax.inject:1" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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..f03c55b --- /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 = LifecycleViewModelProviders.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 -> LifecycleViewModelProviders.getViewModelFactory(provider.parentFragment ?: provider.requireActivity()) + is Activity -> LifecycleViewModelProviders.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 8bdb97b..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.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders -import ru.touchin.roboswag.navigation_viewcontroller.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/navigation-base/build.gradle b/navigation-base/build.gradle index 2a000a0..ed356be 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -21,12 +21,12 @@ android { } dependencies { - api project(":utils") - api project(":logging") + implementation project(":utils") + implementation project(":logging") - api 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.multidex:multidex:2.0.1' - api 'net.danlew:android.joda:2.10.2' + implementation 'net.danlew:android.joda:2.10.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt index 0cb1043..5a1df6c 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/FragmentNavigation.kt @@ -28,7 +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.navigation_base.fragments.FragmentWithState +import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment import kotlin.reflect.KClass /** @@ -169,14 +169,14 @@ open class FragmentNavigation( * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. */ fun push( - fragmentClass: KClass>, + fragmentClass: KClass>, state: T, addToStack: Boolean = true, backStackName: String? = null, tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { - push(fragmentClass.java, FragmentWithState.args(state), addToStack, backStackName, tag, transactionSetup) + push(fragmentClass.java, StatefulFragment.args(state), addToStack, backStackName, tag, transactionSetup) } /** @@ -216,14 +216,14 @@ open class FragmentNavigation( * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. */ fun pushForResult( - fragmentClass: KClass>, + fragmentClass: KClass>, targetFragment: Fragment, targetRequestCode: Int, state: T, tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { - pushForResult(fragmentClass.java, targetFragment, targetRequestCode, FragmentWithState.args(state), tag, transactionSetup) + pushForResult(fragmentClass.java, targetFragment, targetRequestCode, StatefulFragment.args(state), tag, transactionSetup) } /** @@ -270,13 +270,13 @@ open class FragmentNavigation( * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. */ fun setInitial( - fragmentClass: KClass>, + fragmentClass: KClass>, state: T, tag: String? = null, transactionSetup: ((FragmentTransaction) -> Unit)? = null ) { beforeSetInitialActions() - setAsTop(fragmentClass.java, FragmentWithState.args(state), false, tag, transactionSetup) + setAsTop(fragmentClass.java, StatefulFragment.args(state), false, tag, transactionSetup) } /** 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..5c4f26c --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/extensions/Parcelable.kt @@ -0,0 +1,47 @@ +package ru.touchin.roboswag.navigation_base.extensions + +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 +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 +} + +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/FragmentWithState.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/StatefulFragment.kt similarity index 55% rename from navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentWithState.kt rename to navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/StatefulFragment.kt index dedf118..4e46f3b 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentWithState.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/StatefulFragment.kt @@ -1,13 +1,13 @@ package ru.touchin.roboswag.navigation_base.fragments import android.os.Bundle -import android.os.Parcel 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 FragmentWithState( +open class StatefulFragment( @LayoutRes layoutRes: Int ) : BaseFragment(layoutRes) { @@ -16,20 +16,6 @@ open class FragmentWithState( 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 lateinit var state: TState @@ -43,7 +29,7 @@ open class FragmentWithState( ?: throw IllegalStateException("Fragment state can't be null") if (BuildConfig.DEBUG) { - state = reserialize(state) + state = state.reserialize() } } diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt index 13cec6d..ed13fbf 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt @@ -9,12 +9,11 @@ import androidx.lifecycle.LifecycleObserver 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_base.fragments.BaseFragment -import ru.touchin.roboswag.navigation_base.fragments.FragmentWithState +import ru.touchin.roboswag.navigation_base.fragments.StatefulFragment abstract class KeyboardResizeableFragment( @LayoutRes layoutRes: Int -) : FragmentWithState( +) : StatefulFragment( layoutRes ) { diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index 59cbb01..e40ef52 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -21,13 +21,13 @@ android { } dependencies { - api project(":utils") - api project(":logging") - api project(":navigation-base") + implementation project(":utils") + implementation project(":logging") + implementation project(":navigation-base") - api 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.multidex:multidex:2.0.1' - api 'net.danlew:android.joda:2.10.2' + implementation 'net.danlew:android.joda:2.10.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" From bd9accacaa3590dc1a9b15db84167e2049bc26ce Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 4 Jun 2020 19:30:45 +0300 Subject: [PATCH 047/154] view controller fixes --- bottom-navigation-viewcontroller/build.gradle | 5 ++-- .../BottomNavigationActivity.kt | 2 +- .../BottomNavigationController.kt | 2 +- .../BottomNavigationFragment.kt | 2 +- .../NavigationContainerFragment.kt | 2 +- .../NavigationTab.kt | 2 +- .../viewmodel/LifecycleViewModelProviders.kt | 26 ++++++++++++++++--- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/bottom-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle index cdae20e..8a5beda 100644 --- a/bottom-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -20,8 +20,9 @@ android { } dependencies { - api project(":navigation-viewcontroller") - api project(":bottom-navigation-base") + implementation project(":navigation-base") + implementation project(":navigation-viewcontroller") + implementation project(":bottom-navigation-base") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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 index a9135fc..6361f08 100644 --- 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 @@ -1,6 +1,6 @@ package ru.touchin.roboswag.bottom_navigation_viewcontroller -import ru.touchin.roboswag.bottom_navigation_fragment.BaseBottomNavigationActivity +import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationActivity import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewControllerNavigation abstract class BottomNavigationActivity : 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 index 6597ae3..babcf94 100644 --- 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 @@ -6,7 +6,7 @@ import androidx.annotation.IdRes import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import ru.touchin.roboswag.bottom_navigation_fragment.BaseBottomNavigationController +import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationController import ru.touchin.roboswag.navigation_viewcontroller.fragments.ViewControllerFragment class BottomNavigationController( 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 index 8a2f3fc..18d1643 100644 --- 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 @@ -1,6 +1,6 @@ package ru.touchin.roboswag.bottom_navigation_viewcontroller -import ru.touchin.roboswag.bottom_navigation_fragment.BaseBottomNavigationFragment +import ru.touchin.roboswag.bottom_navigation_base.BaseBottomNavigationFragment abstract class BottomNavigationFragment : BaseBottomNavigationFragment() { 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 index c70e1fd..bfffaf4 100644 --- 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 @@ -1,7 +1,7 @@ package ru.touchin.roboswag.bottom_navigation_viewcontroller import android.os.Parcelable -import ru.touchin.roboswag.bottom_navigation_fragment.BaseNavigationContainerFragment +import ru.touchin.roboswag.bottom_navigation_base.BaseNavigationContainerFragment import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewControllerNavigation 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 index 7ee96ed..bc10917 100644 --- 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 @@ -1,7 +1,7 @@ package ru.touchin.roboswag.bottom_navigation_viewcontroller import android.os.Parcelable -import ru.touchin.roboswag.bottom_navigation_fragment.BaseNavigationTab +import ru.touchin.roboswag.bottom_navigation_base.BaseNavigationTab import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController class NavigationTab( 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 index 7d93a36..22d7448 100644 --- 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 @@ -2,13 +2,34 @@ package ru.touchin.lifecycle_viewcontroller.viewmodel import android.app.Activity import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders import ru.touchin.lifecycle.viewmodel.BaseLifecycleViewModelProviders import ru.touchin.lifecycle.viewmodel.ViewModelFactoryProvider 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: @@ -20,11 +41,8 @@ object LifecycleViewModelProviders : BaseLifecycleViewModelProviders() { */ override 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.") + else -> super.getViewModelFactory(provider) } } From 2788c6df66073d79babec930f7699fd94a3005d4 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 4 Jun 2020 20:27:43 +0300 Subject: [PATCH 048/154] truly open --- .../lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt index f03c55b..7026ddd 100644 --- a/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt +++ b/lifecycle/src/main/java/ru/touchin/lifecycle/viewmodel/BaseLifecycleViewModelProviders.kt @@ -20,7 +20,7 @@ abstract class BaseLifecycleViewModelProviders { */ open fun of( lifecycleOwner: LifecycleOwner, - factory: ViewModelProvider.Factory = LifecycleViewModelProviders.getViewModelFactory(lifecycleOwner) + factory: ViewModelProvider.Factory = getViewModelFactory(lifecycleOwner) ): ViewModelProvider = when (lifecycleOwner) { is Fragment -> ViewModelProvider(lifecycleOwner, factory) @@ -39,8 +39,8 @@ abstract class BaseLifecycleViewModelProviders { open fun getViewModelFactory(provider: Any): ViewModelProvider.Factory = when (provider) { is ViewModelFactoryProvider -> provider.viewModelFactory - is Fragment -> LifecycleViewModelProviders.getViewModelFactory(provider.parentFragment ?: provider.requireActivity()) - is Activity -> LifecycleViewModelProviders.getViewModelFactory(provider.application) + is Fragment -> getViewModelFactory(provider.parentFragment ?: provider.requireActivity()) + is Activity -> getViewModelFactory(provider.application) else -> throw IllegalArgumentException("View model factory not found.") } From cb4000b48dc0e46f394c98d380632c1ff1007eba Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 5 Jun 2020 12:21:29 +0300 Subject: [PATCH 049/154] fix static --- ...ContainerFramgent.kt => BaseNavigationContainerFragment.kt} | 0 .../viewmodel/LifecycleViewModelProviders.kt | 3 --- .../touchin/roboswag/navigation_base/extensions/Parcelable.kt | 3 +++ 3 files changed, 3 insertions(+), 3 deletions(-) rename bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/{BaseNavigationContainerFramgent.kt => BaseNavigationContainerFragment.kt} (100%) diff --git a/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFramgent.kt b/bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFragment.kt similarity index 100% rename from bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFramgent.kt rename to bottom-navigation-base/src/main/java/ru/touchin/roboswag/bottom_navigation_base/BaseNavigationContainerFragment.kt 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 index 22d7448..f10e7c9 100644 --- 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 @@ -1,12 +1,9 @@ package ru.touchin.lifecycle_viewcontroller.viewmodel -import android.app.Activity -import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import ru.touchin.lifecycle.viewmodel.BaseLifecycleViewModelProviders -import ru.touchin.lifecycle.viewmodel.ViewModelFactoryProvider import ru.touchin.roboswag.navigation_viewcontroller.viewcontrollers.ViewController object LifecycleViewModelProviders : BaseLifecycleViewModelProviders() { 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 index 5c4f26c..2ec8844 100644 --- 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 @@ -1,11 +1,13 @@ 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() @@ -28,6 +30,7 @@ fun Parcelable.reserialize(): T { return result } +@SuppressLint("Recycle") fun Parcelable.copy(): Parcelable = if (this is EmptyState) { EmptyState From e34e78d16335198f01f102655a5839456278038c Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 5 Jun 2020 19:19:36 +0300 Subject: [PATCH 050/154] changed version to constraints in roboswag modules --- api-logansquare/build.gradle | 48 +++++++++++++++-- base-map/build.gradle | 2 +- bottom-navigation-base/build.gradle | 20 +++++-- bottom-navigation-fragment/build.gradle | 20 +++++-- bottom-navigation-viewcontroller/build.gradle | 20 +++++-- build.gradle | 14 +++-- google-map/build.gradle | 12 ++++- kotlin-extensions/build.gradle | 12 ++++- lifecycle-rx/build.gradle | 36 +++++++++++-- lifecycle-viewcontroller/build.gradle | 36 +++++++++++-- lifecycle/build.gradle | 36 +++++++++++-- livedata-location/build.gradle | 12 ++++- logging/build.gradle | 18 +++++-- navigation-base/build.gradle | 52 ++++++++++++++++--- navigation-viewcontroller/build.gradle | 36 ++++++++++--- recyclerview-adapters/build.gradle | 14 +++-- recyclerview-calendar/build.gradle | 20 +++++-- rx-extensions/build.gradle | 16 ++++-- storable/build.gradle | 38 +++++++++++--- utils/build.gradle | 20 +++++-- views/build.gradle | 14 +++-- yandex-map/build.gradle | 14 +++-- 22 files changed, 429 insertions(+), 81 deletions(-) diff --git a/api-logansquare/build.gradle b/api-logansquare/build.gradle index c522cc9..9582901 100644 --- a/api-logansquare/build.gradle +++ b/api-logansquare/build.gradle @@ -14,10 +14,48 @@ android { } 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/base-map/build.gradle b/base-map/build.gradle index b80165f..f8f30bc 100644 --- a/base-map/build.gradle +++ b/base-map/build.gradle @@ -10,6 +10,6 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" } diff --git a/bottom-navigation-base/build.gradle b/bottom-navigation-base/build.gradle index f97eb55..28e0ffc 100644 --- a/bottom-navigation-base/build.gradle +++ b/bottom-navigation-base/build.gradle @@ -23,9 +23,23 @@ dependencies { implementation project(":logging") implementation project(":navigation-base") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") - implementation "androidx.core:core-ktx:$versions.coreKtx" + implementation("androidx.core:core-ktx") - implementation "androidx.appcompat:appcompat:$versions.appcompat" + 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-fragment/build.gradle b/bottom-navigation-fragment/build.gradle index 69410ca..56537e8 100644 --- a/bottom-navigation-fragment/build.gradle +++ b/bottom-navigation-fragment/build.gradle @@ -23,9 +23,23 @@ dependencies { implementation project(":navigation-base") implementation project(":bottom-navigation-base") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.core:core-ktx:$versions.coreKtx" + implementation "androidx.core:core-ktx" - implementation "androidx.appcompat:appcompat:$versions.appcompat" + 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/build.gradle b/bottom-navigation-viewcontroller/build.gradle index 8a5beda..8bfb006 100644 --- a/bottom-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -24,9 +24,23 @@ dependencies { implementation project(":navigation-viewcontroller") implementation project(":bottom-navigation-base") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.core:core-ktx:$versions.coreKtx" + implementation "androidx.core:core-ktx" - implementation "androidx.appcompat:appcompat:$versions.appcompat" + 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/build.gradle b/build.gradle index 41d4c15..7a61b19 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,10 @@ buildscript { 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" } } @@ -16,7 +16,13 @@ allprojects { repositories { google() jcenter() - maven { url "http://dl.bintray.com/touchin/touchin-tools" } + maven { + url "https://dl.bintray.com/touchin/touchin-tools" + metadataSources { + artifact() + } + + } } } diff --git a/google-map/build.gradle b/google-map/build.gradle index 7673daa..3ec91fe 100644 --- a/google-map/build.gradle +++ b/google-map/build.gradle @@ -12,7 +12,15 @@ android { dependencies { api project(":base-map") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "com.google.android.gms:play-services-maps:$versions.google_maps" + implementation "com.google.android.gms:play-services-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..eed4ded 100644 --- a/kotlin-extensions/build.gradle +++ b/kotlin-extensions/build.gradle @@ -10,7 +10,15 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.recyclerview:recyclerview:$versions.androidx" + implementation "androidx.recyclerview:recyclerview" + + constraints { + implementation("androidx.recyclerview:recyclerview") { + version { + require '1.0.0' + } + } + } } diff --git a/lifecycle-rx/build.gradle b/lifecycle-rx/build.gradle index 4d58f46..0613fa6 100644 --- a/lifecycle-rx/build.gradle +++ b/lifecycle-rx/build.gradle @@ -14,12 +14,38 @@ dependencies { api project(":logging") api project(":lifecycle") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.appcompat:appcompat:$versions.appcompat" + implementation "androidx.appcompat:appcompat" - implementation "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle" + implementation "androidx.lifecycle:lifecycle-extensions" - 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.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-viewcontroller/build.gradle b/lifecycle-viewcontroller/build.gradle index 7ceb229..bffe90f 100644 --- a/lifecycle-viewcontroller/build.gradle +++ b/lifecycle-viewcontroller/build.gradle @@ -24,12 +24,38 @@ dependencies { compileOnly "javax.inject:javax.inject:1" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.appcompat:appcompat:$versions.appcompat" + implementation "androidx.appcompat:appcompat" - implementation "androidx.fragment:fragment:$versions.fragment" - implementation "androidx.fragment:fragment-ktx:$versions.fragment" + implementation "androidx.fragment:fragment" + implementation "androidx.fragment:fragment-ktx" - implementation "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle" + 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/build.gradle b/lifecycle/build.gradle index 6ca35ff..226543e 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -21,12 +21,38 @@ android { dependencies { compileOnly "javax.inject:javax.inject:1" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.appcompat:appcompat:$versions.appcompat" + implementation "androidx.appcompat:appcompat" - implementation "androidx.fragment:fragment:$versions.fragment" - implementation "androidx.fragment:fragment-ktx:$versions.fragment" + implementation "androidx.fragment:fragment" + implementation "androidx.fragment:fragment-ktx" - implementation "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle" + implementation "androidx.lifecycle:lifecycle-extensions" + + 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/livedata-location/build.gradle b/livedata-location/build.gradle index 297284c..46a7624 100644 --- a/livedata-location/build.gradle +++ b/livedata-location/build.gradle @@ -13,7 +13,15 @@ android { dependencies { api project(":lifecycle") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "com.google.android.gms:play-services-location:$versions.location" + implementation "com.google.android.gms:play-services-location" + + constraints { + implementation("com.google.android.gms:play-services-location") { + version { + require '1.0.0' + } + } + } } diff --git a/logging/build.gradle b/logging/build.gradle index 2b79ac2..cf9568f 100644 --- a/logging/build.gradle +++ b/logging/build.gradle @@ -14,9 +14,21 @@ android { } dependencies { - implementation "androidx.annotation:annotation:$versions.androidx" + implementation "androidx.annotation:annotation" - implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { - transitive = true + implementation "com.crashlytics.sdk.android:crashlytics" + + constraints { + implementation("androidx.annotation:annotation") { + version { + require '1.0.0' + } + } + + implementation("com.crashlytics.sdk.android:crashlytics") { + version { + require '2.5.0' + } + } } } diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index ed356be..2310e15 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -24,18 +24,56 @@ dependencies { implementation project(":utils") implementation project(":logging") - implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.multidex:multidex' - implementation 'net.danlew:android.joda:2.10.2' + implementation 'net.danlew:android.joda' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.appcompat:appcompat:$versions.appcompat" + implementation "androidx.appcompat:appcompat" - implementation "androidx.fragment:fragment:$versions.fragment" - implementation "androidx.fragment:fragment-ktx:$versions.fragment" + implementation "androidx.fragment:fragment" + implementation "androidx.fragment:fragment-ktx" - implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { + implementation("com.crashlytics.sdk.android:crashlytics") { transitive = true } + + constraints { + implementation("androidx.multidex:multidex") { + version { + require '2.0.0' + } + } + + 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.crashlytics.sdk.android:crashlytics") { + version { + require '2.0.0' + } + } + } } diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index e40ef52..955461a 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -25,15 +25,39 @@ dependencies { implementation project(":logging") implementation project(":navigation-base") - implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.multidex:multidex' - implementation 'net.danlew:android.joda:2.10.2' + implementation 'net.danlew:android.joda' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.appcompat:appcompat:$versions.appcompat" + implementation "androidx.appcompat:appcompat" - implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { - transitive = true + 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.0.0' + } + } } } diff --git a/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle index f4730f9..40b114c 100644 --- a/recyclerview-adapters/build.gradle +++ b/recyclerview-adapters/build.gradle @@ -10,9 +10,17 @@ android { } dependencies { - api project(':kotlin-extensions') + implementation project(':kotlin-extensions') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "androidx.recyclerview:recyclerview:$versions.androidx" + implementation "androidx.recyclerview:recyclerview" + + constraints { + implementation("androidx.recyclerview:recyclerview") { + version { + require '1.0.0' + } + } + } } diff --git a/recyclerview-calendar/build.gradle b/recyclerview-calendar/build.gradle index 09ee166..a783c55 100644 --- a/recyclerview-calendar/build.gradle +++ b/recyclerview-calendar/build.gradle @@ -9,8 +9,22 @@ android { } 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/rx-extensions/build.gradle b/rx-extensions/build.gradle index 14da026..0099f13 100644 --- a/rx-extensions/build.gradle +++ b/rx-extensions/build.gradle @@ -10,9 +10,17 @@ android { } dependencies { - api project(":utils") - api project(":logging") + implementation project(":utils") + implementation project(":logging") - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" + implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation "io.reactivex.rxjava2:rxjava" + + constraints { + implementation("io.reactivex.rxjava2:rxjava") { + version { + require '2.2.9' + } + } + } } diff --git a/storable/build.gradle b/storable/build.gradle index 340a921..b4b59bc 100644 --- a/storable/build.gradle +++ b/storable/build.gradle @@ -14,12 +14,38 @@ android { } 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/utils/build.gradle b/utils/build.gradle index ac3a82f..447ddcd 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -15,7 +15,21 @@ android { } 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" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + + constraints { + implementation("androidx.core:core") { + version { + require '1.0.0' + } + } + + implementation("androidx.annotation:annotation") { + version { + require '1.1.0' + } + } + } } diff --git a/views/build.gradle b/views/build.gradle index 7f190bd..6bc166b 100644 --- a/views/build.gradle +++ b/views/build.gradle @@ -14,8 +14,16 @@ android { } dependencies { - api project(":utils") - api project(":logging") + implementation project(":utils") + implementation project(":logging") - implementation "com.google.android.material:material:$versions.material" + implementation "com.google.android.material:material" + + constraints { + implementation("com.google.android.material:material") { + version { + require '1.0.0' + } + } + } } diff --git a/yandex-map/build.gradle b/yandex-map/build.gradle index e34fbaf..9fe6bf5 100644 --- a/yandex-map/build.gradle +++ b/yandex-map/build.gradle @@ -10,9 +10,17 @@ android { } dependencies { - api project(":base-map") + implementation project(":base-map") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" - implementation "com.yandex.android:mapkit:$versions.yandex_mapkit" + implementation "com.yandex.android:mapkit" + + constraints { + implementation("com.yandex.android:mapkit") { + version { + require '3.4.0' + } + } + } } From a3cad2a1d60c8912b7fcd8c85c8a5e5a00c1faf5 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 5 Jun 2020 23:25:43 +0300 Subject: [PATCH 051/154] added binary prefs --- storable/build.gradle | 2 ++ .../utils/storables/PreferenceUtils.java | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/storable/build.gradle b/storable/build.gradle index 340a921..2983f29 100644 --- a/storable/build.gradle +++ b/storable/build.gradle @@ -22,4 +22,6 @@ dependencies { implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid" + + implementation "com.github.yandextaxitech:binaryprefs:$versions.binaryprefs" } 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..2b526f3 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,10 @@ package ru.touchin.roboswag.components.utils.storables; import android.content.SharedPreferences; + +import com.ironz.binaryprefs.BinaryPreferencesBuilder; +import com.ironz.binaryprefs.Preferences; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -54,6 +58,24 @@ public final class PreferenceUtils { ).build(); } + /** + * Creates {@link Storable} that stores string into {@link Preferences} from https://github.com/yandextaxitech/binaryprefs + * + * @param name Name of preference; + * @param preferences Preferences to store value; + * @return {@link Storable} for string. + */ + @NonNull + public static Storable stringStorable(@NonNull final String name, @NonNull final Preferences preferences) { + return new Storable.Builder( + name, + String.class, + String.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).build(); + } + /** * Creates {@link NonNullStorable} that stores string into {@link SharedPreferences} with default value. * From 09809eb07b857fb8922bcff7b08a94d511f911b5 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 6 Jun 2020 13:13:08 +0300 Subject: [PATCH 052/154] upd name --- .../roboswag/components/utils/storables/PreferenceUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2b526f3..b46b5e7 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 @@ -66,7 +66,7 @@ public final class PreferenceUtils { * @return {@link Storable} for string. */ @NonNull - public static Storable stringStorable(@NonNull final String name, @NonNull final Preferences preferences) { + public static Storable binaryprefsStringStorable(@NonNull final String name, @NonNull final Preferences preferences) { return new Storable.Builder( name, String.class, From 8cc377304fbfef928d23307679f519083ed43bf9 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 6 Jun 2020 14:02:04 +0300 Subject: [PATCH 053/154] upd name --- .../roboswag/components/utils/storables/PreferenceUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b46b5e7..f541878 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 @@ -66,7 +66,7 @@ public final class PreferenceUtils { * @return {@link Storable} for string. */ @NonNull - public static Storable binaryprefsStringStorable(@NonNull final String name, @NonNull final Preferences preferences) { + public static Storable binaryPreferencesStringStorable(@NonNull final String name, @NonNull final Preferences preferences) { return new Storable.Builder( name, String.class, From 874b0f5924406bfd7448d5de0c51073f0b903667 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 9 Jun 2020 18:01:40 +0300 Subject: [PATCH 054/154] changed kotlin-stdlib-jdk7 to kotlin-stdlib --- base-map/build.gradle | 2 +- bottom-navigation-base/build.gradle | 2 +- bottom-navigation-fragment/build.gradle | 2 +- bottom-navigation-viewcontroller/build.gradle | 2 +- google-map/build.gradle | 2 +- kotlin-extensions/build.gradle | 2 +- lifecycle-rx/build.gradle | 2 +- lifecycle-viewcontroller/build.gradle | 2 +- lifecycle/build.gradle | 2 +- livedata-location/build.gradle | 2 +- navigation-base/build.gradle | 2 +- navigation-viewcontroller/build.gradle | 2 +- recyclerview-adapters/build.gradle | 2 +- utils/build.gradle | 2 +- yandex-map/build.gradle | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/base-map/build.gradle b/base-map/build.gradle index f8f30bc..d5a44da 100644 --- a/base-map/build.gradle +++ b/base-map/build.gradle @@ -10,6 +10,6 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" } diff --git a/bottom-navigation-base/build.gradle b/bottom-navigation-base/build.gradle index 28e0ffc..5ee554e 100644 --- a/bottom-navigation-base/build.gradle +++ b/bottom-navigation-base/build.gradle @@ -23,7 +23,7 @@ dependencies { implementation project(":logging") implementation project(":navigation-base") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") + implementation("org.jetbrains.kotlin:kotlin-stdlib") implementation("androidx.core:core-ktx") diff --git a/bottom-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle index 56537e8..c534245 100644 --- a/bottom-navigation-fragment/build.gradle +++ b/bottom-navigation-fragment/build.gradle @@ -23,7 +23,7 @@ dependencies { implementation project(":navigation-base") implementation project(":bottom-navigation-base") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.core:core-ktx" diff --git a/bottom-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle index 8bfb006..d6fd6e6 100644 --- a/bottom-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -24,7 +24,7 @@ dependencies { implementation project(":navigation-viewcontroller") implementation project(":bottom-navigation-base") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.core:core-ktx" diff --git a/google-map/build.gradle b/google-map/build.gradle index 3ec91fe..20c4a09 100644 --- a/google-map/build.gradle +++ b/google-map/build.gradle @@ -12,7 +12,7 @@ android { dependencies { api project(":base-map") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "com.google.android.gms:play-services-maps" diff --git a/kotlin-extensions/build.gradle b/kotlin-extensions/build.gradle index eed4ded..ad799de 100644 --- a/kotlin-extensions/build.gradle +++ b/kotlin-extensions/build.gradle @@ -10,7 +10,7 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.recyclerview:recyclerview" diff --git a/lifecycle-rx/build.gradle b/lifecycle-rx/build.gradle index 0613fa6..c885051 100644 --- a/lifecycle-rx/build.gradle +++ b/lifecycle-rx/build.gradle @@ -14,7 +14,7 @@ dependencies { api project(":logging") api project(":lifecycle") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.appcompat:appcompat" diff --git a/lifecycle-viewcontroller/build.gradle b/lifecycle-viewcontroller/build.gradle index bffe90f..e40e049 100644 --- a/lifecycle-viewcontroller/build.gradle +++ b/lifecycle-viewcontroller/build.gradle @@ -24,7 +24,7 @@ dependencies { compileOnly "javax.inject:javax.inject:1" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.appcompat:appcompat" diff --git a/lifecycle/build.gradle b/lifecycle/build.gradle index 226543e..322c98f 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -21,7 +21,7 @@ android { dependencies { compileOnly "javax.inject:javax.inject:1" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.appcompat:appcompat" diff --git a/livedata-location/build.gradle b/livedata-location/build.gradle index 46a7624..43615ea 100644 --- a/livedata-location/build.gradle +++ b/livedata-location/build.gradle @@ -13,7 +13,7 @@ android { dependencies { api project(":lifecycle") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "com.google.android.gms:play-services-location" diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index 2310e15..3aadeea 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation 'net.danlew:android.joda' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.appcompat:appcompat" diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index 955461a..34c2b1b 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -29,7 +29,7 @@ dependencies { implementation 'net.danlew:android.joda' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.appcompat:appcompat" diff --git a/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle index 40b114c..552fc4f 100644 --- a/recyclerview-adapters/build.gradle +++ b/recyclerview-adapters/build.gradle @@ -12,7 +12,7 @@ android { dependencies { implementation project(':kotlin-extensions') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "androidx.recyclerview:recyclerview" diff --git a/utils/build.gradle b/utils/build.gradle index 447ddcd..b69e92a 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -17,7 +17,7 @@ android { dependencies { implementation "androidx.core:core" implementation "androidx.annotation:annotation" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" constraints { implementation("androidx.core:core") { diff --git a/yandex-map/build.gradle b/yandex-map/build.gradle index 9fe6bf5..2d08918 100644 --- a/yandex-map/build.gradle +++ b/yandex-map/build.gradle @@ -12,7 +12,7 @@ android { dependencies { implementation project(":base-map") - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7" + implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "com.yandex.android:mapkit" From 9eec9dffe358c5661f1e70e4f973ac6e0b504d4f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 10 Jun 2020 16:27:59 +0300 Subject: [PATCH 055/154] Added touchin shared prefs --- encrypted-shared-prefs/.gitignore | 1 + encrypted-shared-prefs/build.gradle | 24 ++++ .../src/main/AndroidManifest.xml | 1 + .../java/ru/touchin/roboswag/Extensions.kt | 9 ++ .../roboswag/TouchinSharedPreferences.kt | 105 ++++++++++++++++ .../TouchinSharedPreferencesCryptoUtils.kt | 115 ++++++++++++++++++ storable/build.gradle | 2 - .../utils/storables/PreferenceUtils.java | 21 ---- 8 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 encrypted-shared-prefs/.gitignore create mode 100644 encrypted-shared-prefs/build.gradle create mode 100644 encrypted-shared-prefs/src/main/AndroidManifest.xml create mode 100644 encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt create mode 100644 encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt create mode 100644 encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt diff --git a/encrypted-shared-prefs/.gitignore b/encrypted-shared-prefs/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/encrypted-shared-prefs/.gitignore @@ -0,0 +1 @@ +/build diff --git a/encrypted-shared-prefs/build.gradle b/encrypted-shared-prefs/build.gradle new file mode 100644 index 0000000..085c337 --- /dev/null +++ b/encrypted-shared-prefs/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 21 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "androidx.core:core:$versions.androidx" + implementation "androidx.annotation:annotation:$versions.androidx" + implementation "androidx.appcompat:appcompat:$versions.appcompat" + +} diff --git a/encrypted-shared-prefs/src/main/AndroidManifest.xml b/encrypted-shared-prefs/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a068e5f --- /dev/null +++ b/encrypted-shared-prefs/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt new file mode 100644 index 0000000..a4305ed --- /dev/null +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt @@ -0,0 +1,9 @@ +package ru.touchin.roboswag + +import android.content.SharedPreferences + +fun TouchinSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { + if (!from.contains(key)) return this + edit().putString(key, from.getString(key, "") ?: "").apply() + return this +} diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt new file mode 100644 index 0000000..c7bf5a3 --- /dev/null +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt @@ -0,0 +1,105 @@ +package ru.touchin.roboswag + +import android.content.Context +import android.content.SharedPreferences + +class TouchinSharedPreferences(name: String, context: Context, val isEncryption: Boolean = false) : SharedPreferences { + + private val currentPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) + private val cryptoUtils = TouchinSharedPreferencesCryptoUtils(context) + + override fun contains(key: String?) = currentPreferences.contains(key) + + override fun getBoolean(key: String?, defaultValue: Boolean) = get(key, defaultValue) + + override fun unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener?) { + currentPreferences.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } + + override fun getInt(key: String?, defaultValue: Int) = get(key, defaultValue) + + override fun getAll(): MutableMap { + return if (isEncryption) { + currentPreferences.all.mapValues { it.value.toString().decrypt() }.toMutableMap() + } else { + currentPreferences.all.mapValues { it.value.toString() }.toMutableMap() + } + } + + override fun edit() = TouchinEditor() + + override fun getLong(key: String?, defaultValue: Long) = get(key, defaultValue) + + override fun getFloat(key: String?, defaultValue: Float) = get(key, defaultValue) + + override fun getString(key: String?, defaultValue: String?): String = get(key, defaultValue ?: "") + + override fun getStringSet(key: String?, set: MutableSet?): MutableSet? { + return if (isEncryption) { + val value = currentPreferences.getStringSet(key, set) + if (value == set) { + set + } else { + value?.map { it.decrypt() }?.toMutableSet() + } + } else { + currentPreferences.getStringSet(key, set) + } + } + + override fun registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener?) { + currentPreferences.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } + + private fun get(key: String?, defaultValue: T): T { + if (!currentPreferences.contains(key)) return defaultValue + val value = currentPreferences.getString(key, "")?.decrypt() + return when (defaultValue) { + is Boolean -> value?.toBoolean() as? T + is Long -> value?.toLong() as? T + is String -> value as? T + is Int -> value?.toInt() as? T + is Float -> value?.toFloat() as? T + else -> value as? T + } ?: defaultValue + } + + private fun String.decrypt() = if (isEncryption) cryptoUtils.decrypt(this) else this + + inner class TouchinEditor : SharedPreferences.Editor { + + override fun clear() = currentPreferences.edit().clear() + + override fun putLong(key: String?, value: Long) = put(key, value) + + override fun putInt(key: String?, value: Int) = put(key, value) + + override fun remove(key: String?) = currentPreferences.edit().remove(key) + + override fun putBoolean(key: String?, value: Boolean) = put(key, value) + + override fun putStringSet(key: String?, value: MutableSet?): SharedPreferences.Editor { + return if (isEncryption) { + currentPreferences.edit().putStringSet(key, value?.map { it.encrypt() }?.toMutableSet()) + } else { + currentPreferences.edit().putStringSet(key, value) + } + } + + override fun commit() = currentPreferences.edit().commit() + + override fun putFloat(key: String?, value: Float) = put(key, value) + + override fun apply() = currentPreferences.edit().apply() + + override fun putString(key: String?, value: String?) = put(key, value) + + private fun put(key: String?, value: T): SharedPreferences.Editor { + return currentPreferences.edit().putString(key, value.toString().encrypt()) + } + + private fun String.encrypt() = if (isEncryption) cryptoUtils.encrypt(this) else this + + } + +} diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt new file mode 100644 index 0000000..2cc1b82 --- /dev/null +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt @@ -0,0 +1,115 @@ +package ru.touchin.roboswag + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.math.BigInteger +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.util.Calendar +import javax.crypto.Cipher +import javax.security.auth.x500.X500Principal + +// https://proandroiddev.com/secure-data-in-android-encryption-in-android-part-2-991a89e55a23 +@Suppress("detekt.TooGenericExceptionCaught") +class TouchinSharedPreferencesCryptoUtils constructor(val context: Context) { + + companion object { + + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val STORAGE_KEY = "STORAGE_KEY" + private const val KEY_ALGORITHM_RSA = "RSA" + private const val TRANSFORMATION_ASYMMETRIC = "RSA/ECB/PKCS1Padding" + + private fun getAndroidKeystore(): KeyStore? = try { + KeyStore.getInstance(ANDROID_KEY_STORE).also { it.load(null) } + } catch (exception: Exception) { + null + } + + private fun getAndroidKeyStoreAsymmetricKeyPair(): KeyPair? { + val privateKey = getAndroidKeystore()?.getKey(STORAGE_KEY, null) as PrivateKey? + val publicKey = getAndroidKeystore()?.getCertificate(STORAGE_KEY)?.publicKey + return if (privateKey != null && publicKey != null) { + KeyPair(publicKey, privateKey) + } else { + null + } + } + + private fun createAndroidKeyStoreAsymmetricKey(context: Context): KeyPair { + val generator = KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, ANDROID_KEY_STORE) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + initGeneratorWithKeyPairGeneratorSpec(generator) + } else { + initGeneratorWithKeyGenParameterSpec(generator, context) + } + + // Generates Key with given spec and saves it to the KeyStore + return generator.generateKeyPair() + } + + private fun initGeneratorWithKeyGenParameterSpec(generator: KeyPairGenerator, context: Context) { + val startDate = Calendar.getInstance() + val endDate = Calendar.getInstance() + endDate.add(Calendar.YEAR, 20) + + val builder = KeyPairGeneratorSpec.Builder(context) + .setAlias(STORAGE_KEY) + .setSerialNumber(BigInteger.ONE) + .setSubject(X500Principal("CN=$STORAGE_KEY CA Certificate")) + .setStartDate(startDate.time) + .setEndDate(endDate.time) + + generator.initialize(builder.build()) + } + + @TargetApi(Build.VERSION_CODES.M) + private fun initGeneratorWithKeyPairGeneratorSpec(generator: KeyPairGenerator) { + val builder = KeyGenParameterSpec.Builder(STORAGE_KEY, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_ECB) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + generator.initialize(builder.build()) + } + + private fun createCipher(): Cipher? = try { + Cipher.getInstance(TRANSFORMATION_ASYMMETRIC) + } catch (exception: Exception) { + null + } + + } + + private val cipher = createCipher() + private val keyPair = getAndroidKeyStoreAsymmetricKeyPair() + ?: createAndroidKeyStoreAsymmetricKey(context) + + // Those methods should not take and return strings, only char[] and those arrays should be cleared right after usage + // See for explanation https://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html#PBEEx + + @Synchronized + fun encrypt(data: String): String { + cipher?.init(Cipher.ENCRYPT_MODE, keyPair.public) + val bytes = cipher?.doFinal(data.toByteArray()) + return Base64.encodeToString(bytes, Base64.DEFAULT) + } + + @Synchronized + fun decrypt(data: String?): String { + cipher?.init(Cipher.DECRYPT_MODE, keyPair.private) + if (data.isNullOrBlank()) { + return String() + } + val encryptedData = Base64.decode(data, Base64.DEFAULT) + val decodedData = cipher?.doFinal(encryptedData) + return decodedData?.let { decodedData -> String(decodedData) } ?: "" + } + +} diff --git a/storable/build.gradle b/storable/build.gradle index 2983f29..340a921 100644 --- a/storable/build.gradle +++ b/storable/build.gradle @@ -22,6 +22,4 @@ dependencies { implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid" - - implementation "com.github.yandextaxitech:binaryprefs:$versions.binaryprefs" } 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 f541878..1204a53 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 @@ -21,9 +21,6 @@ package ru.touchin.roboswag.components.utils.storables; import android.content.SharedPreferences; -import com.ironz.binaryprefs.BinaryPreferencesBuilder; -import com.ironz.binaryprefs.Preferences; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -58,24 +55,6 @@ public final class PreferenceUtils { ).build(); } - /** - * Creates {@link Storable} that stores string into {@link Preferences} from https://github.com/yandextaxitech/binaryprefs - * - * @param name Name of preference; - * @param preferences Preferences to store value; - * @return {@link Storable} for string. - */ - @NonNull - public static Storable binaryPreferencesStringStorable(@NonNull final String name, @NonNull final Preferences preferences) { - return new Storable.Builder( - name, - String.class, - String.class, - new PreferenceStore<>(preferences), - new SameTypesConverter<>() - ).build(); - } - /** * Creates {@link NonNullStorable} that stores string into {@link SharedPreferences} with default value. * From 37c4d284fa57e40b2dda50bff6e603c4bbd2e1d3 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 10 Jun 2020 16:52:30 +0300 Subject: [PATCH 056/154] Fixed static --- .../main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt index c7bf5a3..dae6b07 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt @@ -94,9 +94,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun putString(key: String?, value: String?) = put(key, value) - private fun put(key: String?, value: T): SharedPreferences.Editor { - return currentPreferences.edit().putString(key, value.toString().encrypt()) - } + private fun put(key: String?, value: T) = currentPreferences.edit().putString(key, value.toString().encrypt()) private fun String.encrypt() = if (isEncryption) cryptoUtils.encrypt(this) else this From f324f5621ceb8313950d2becb32756846a0aa775 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 10 Jun 2020 19:12:54 +0300 Subject: [PATCH 057/154] upd migration --- .../src/main/java/ru/touchin/roboswag/Extensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt index a4305ed..4e4fc87 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt @@ -5,5 +5,6 @@ import android.content.SharedPreferences fun TouchinSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { if (!from.contains(key)) return this edit().putString(key, from.getString(key, "") ?: "").apply() + from.edit().remove(key).apply() return this } From 67b491b50dbd1ea2da6181ed6277c383c08914ed Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 11 Jun 2020 22:52:26 +0300 Subject: [PATCH 058/154] Added block writing --- .../roboswag/TouchinSharedPreferences.kt | 53 ++++++++++++++++--- .../TouchinSharedPreferencesCryptoUtils.kt | 6 ++- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt index dae6b07..c6c65e9 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt @@ -2,6 +2,8 @@ package ru.touchin.roboswag import android.content.Context import android.content.SharedPreferences +import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPTED_BASE64_STRING_LENGTH +import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPT_BLOCK_SIZE class TouchinSharedPreferences(name: String, context: Context, val isEncryption: Boolean = false) : SharedPreferences { @@ -53,14 +55,30 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: private fun get(key: String?, defaultValue: T): T { if (!currentPreferences.contains(key)) return defaultValue - val value = currentPreferences.getString(key, "")?.decrypt() + val value = currentPreferences.getString(key, "") + var resultValue = "" + value?.let { + var currentValue = "" + var pos = 0 + while (pos < value.length) { + currentValue += value[pos] + pos++ + if (currentValue.length == ENCRYPTED_BASE64_STRING_LENGTH) { + resultValue += currentValue.decrypt() + currentValue = "" + } + } + if (currentValue.isNotEmpty()) { + resultValue += currentValue.decrypt() + } + } return when (defaultValue) { - is Boolean -> value?.toBoolean() as? T - is Long -> value?.toLong() as? T - is String -> value as? T - is Int -> value?.toInt() as? T - is Float -> value?.toFloat() as? T - else -> value as? T + is Boolean -> resultValue.toBoolean() as? T + is Long -> resultValue.toLong() as? T + is String -> resultValue as? T + is Int -> resultValue.toInt() as? T + is Float -> resultValue.toFloat() as? T + else -> resultValue as? T } ?: defaultValue } @@ -94,7 +112,26 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun putString(key: String?, value: String?) = put(key, value) - private fun put(key: String?, value: T) = currentPreferences.edit().putString(key, value.toString().encrypt()) + private fun put(key: String?, value: T): SharedPreferences.Editor { + val value = value?.toString() + var resultValue = "" + value?.let { + var currentValue = "" + var pos = 0 + while (pos < value.length) { + if (currentValue.length == ENCRYPT_BLOCK_SIZE) { + resultValue += currentValue.encrypt() + currentValue = "" + } + currentValue += value[pos] + pos++ + } + if (currentValue.isNotEmpty()) { + resultValue += currentValue.encrypt() + } + } + return currentPreferences.edit().putString(key, resultValue) + } private fun String.encrypt() = if (isEncryption) cryptoUtils.encrypt(this) else this diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt index 2cc1b82..6913324 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt @@ -23,9 +23,13 @@ class TouchinSharedPreferencesCryptoUtils constructor(val context: Context) { companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" - private const val STORAGE_KEY = "STORAGE_KEY" private const val KEY_ALGORITHM_RSA = "RSA" private const val TRANSFORMATION_ASYMMETRIC = "RSA/ECB/PKCS1Padding" + private const val CIPHER_STRING_SIZE_BYTES = 256 + private const val BASE_64_PADDING = 2 + private const val STORAGE_KEY = "STORAGE_KEY" + const val ENCRYPTED_BASE64_STRING_LENGTH = (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * 4 / 3 + 5 + const val ENCRYPT_BLOCK_SIZE = 128 private fun getAndroidKeystore(): KeyStore? = try { KeyStore.getInstance(ANDROID_KEY_STORE).also { it.load(null) } From 8a4533f937a4e617765a52900011b8eb70d0fe9e Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 11 Jun 2020 22:54:34 +0300 Subject: [PATCH 059/154] Fixed names --- .../main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt | 4 ++-- .../touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt index c6c65e9..db36411 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt @@ -2,7 +2,7 @@ package ru.touchin.roboswag import android.content.Context import android.content.SharedPreferences -import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPTED_BASE64_STRING_LENGTH +import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPT_BASE64_STRING_LENGTH import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPT_BLOCK_SIZE class TouchinSharedPreferences(name: String, context: Context, val isEncryption: Boolean = false) : SharedPreferences { @@ -63,7 +63,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: while (pos < value.length) { currentValue += value[pos] pos++ - if (currentValue.length == ENCRYPTED_BASE64_STRING_LENGTH) { + if (currentValue.length == ENCRYPT_BASE64_STRING_LENGTH) { resultValue += currentValue.decrypt() currentValue = "" } diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt index 6913324..86e5abd 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt @@ -28,7 +28,7 @@ class TouchinSharedPreferencesCryptoUtils constructor(val context: Context) { private const val CIPHER_STRING_SIZE_BYTES = 256 private const val BASE_64_PADDING = 2 private const val STORAGE_KEY = "STORAGE_KEY" - const val ENCRYPTED_BASE64_STRING_LENGTH = (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * 4 / 3 + 5 + const val ENCRYPT_BASE64_STRING_LENGTH = (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * 4 / 3 + 5 const val ENCRYPT_BLOCK_SIZE = 128 private fun getAndroidKeystore(): KeyStore? = try { From a50531a294d165fe1f2749d5f0a42772796d1013 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 11 Jun 2020 23:00:10 +0300 Subject: [PATCH 060/154] Updated readme --- README.md | 5 +++-- encrypted-shared-prefs/README.md | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 encrypted-shared-prefs/README.md diff --git a/README.md b/README.md index 3413e51..b9fbe85 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Roboswag позволяет сочетать эти три решения в о ### Работа с RecyclerView RecyclerView - один из самых часто используемых инструментов Android разработчика. Модуль [recyclerview-adapters](/recyclerview-adapters) позволяет сделать работу с RecyclerView более гибкой и делает работу самого элемента быстрее. ### Работа с SharedPreferences -Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. +Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. Для шифрования данных в SharedPreferences можно использовать [encrypted-shared-prefs](/encrypted-shared-prefs) ### Утилиты и extension функции В Roboswag также есть много [утилитарных](/utils) классов и [extension](/kotlin-extensions) функций, которые позволяют писать часто используемый код в одну строку. @@ -62,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/encrypted-shared-prefs/README.md b/encrypted-shared-prefs/README.md new file mode 100644 index 0000000..1b72271 --- /dev/null +++ b/encrypted-shared-prefs/README.md @@ -0,0 +1,14 @@ +Encrypted shared preferences +============================ + +Модуль с реализацией интерфейса `SharedPreferences`, который дает возможность шифровать содержимое. + +### Пример + +Пример создания получения экземпляра `TouchinSharedPreferences`. При isEncryption = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` + +```kotlin +val prefs = TouchinSharedPreferences(name = "APPLICATION_DATA_ENCRYPTED", context = context, isEncryption = true) +``` + +Важно помнить, что в одном файле `TouchinSharedPreferences` могут храниться только либо полностью зашифрованные данные, либо полностью незашифрованные. Флаг `isEncryption` должен быть в соответствующем положении \ No newline at end of file From 51a73cba9db9638e54b4d82ede9b32fd969703a6 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 15 Jun 2020 13:55:26 +0300 Subject: [PATCH 061/154] Fixed review --- encrypted-shared-prefs/README.md | 2 +- .../roboswag/TouchinSharedPreferences.kt | 61 ++++++------------- .../TouchinSharedPreferencesCryptoUtils.kt | 1 + 3 files changed, 19 insertions(+), 45 deletions(-) diff --git a/encrypted-shared-prefs/README.md b/encrypted-shared-prefs/README.md index 1b72271..d6ee538 100644 --- a/encrypted-shared-prefs/README.md +++ b/encrypted-shared-prefs/README.md @@ -5,7 +5,7 @@ Encrypted shared preferences ### Пример -Пример создания получения экземпляра `TouchinSharedPreferences`. При isEncryption = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` +Пример получения экземпляра `TouchinSharedPreferences`. При isEncryption = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` ```kotlin val prefs = TouchinSharedPreferences(name = "APPLICATION_DATA_ENCRYPTED", context = context, isEncryption = true) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt index db36411..8cfae10 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt @@ -5,7 +5,7 @@ import android.content.SharedPreferences import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPT_BASE64_STRING_LENGTH import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPT_BLOCK_SIZE -class TouchinSharedPreferences(name: String, context: Context, val isEncryption: Boolean = false) : SharedPreferences { +class TouchinSharedPreferences(name: String, context: Context, val encrypt: Boolean = false) : SharedPreferences { private val currentPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) private val cryptoUtils = TouchinSharedPreferencesCryptoUtils(context) @@ -21,7 +21,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun getInt(key: String?, defaultValue: Int) = get(key, defaultValue) override fun getAll(): MutableMap { - return if (isEncryption) { + return if (encrypt) { currentPreferences.all.mapValues { it.value.toString().decrypt() }.toMutableMap() } else { currentPreferences.all.mapValues { it.value.toString() }.toMutableMap() @@ -37,7 +37,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun getString(key: String?, defaultValue: String?): String = get(key, defaultValue ?: "") override fun getStringSet(key: String?, set: MutableSet?): MutableSet? { - return if (isEncryption) { + return if (encrypt) { val value = currentPreferences.getStringSet(key, set) if (value == set) { set @@ -55,34 +55,22 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: private fun get(key: String?, defaultValue: T): T { if (!currentPreferences.contains(key)) return defaultValue - val value = currentPreferences.getString(key, "") - var resultValue = "" - value?.let { - var currentValue = "" - var pos = 0 - while (pos < value.length) { - currentValue += value[pos] - pos++ - if (currentValue.length == ENCRYPT_BASE64_STRING_LENGTH) { - resultValue += currentValue.decrypt() - currentValue = "" - } - } - if (currentValue.isNotEmpty()) { - resultValue += currentValue.decrypt() - } - } + val resultValue = currentPreferences.getString(key, "") + ?.trim() + ?.chunked(ENCRYPT_BASE64_STRING_LENGTH) + ?.joinToString(separator = "", transform = { it.decrypt() }) + return when (defaultValue) { - is Boolean -> resultValue.toBoolean() as? T - is Long -> resultValue.toLong() as? T + is Boolean -> resultValue?.toBoolean() as? T + is Long -> resultValue?.toLong() as? T is String -> resultValue as? T - is Int -> resultValue.toInt() as? T - is Float -> resultValue.toFloat() as? T + is Int -> resultValue?.toInt() as? T + is Float -> resultValue?.toFloat() as? T else -> resultValue as? T } ?: defaultValue } - private fun String.decrypt() = if (isEncryption) cryptoUtils.decrypt(this) else this + private fun String.decrypt() = if (encrypt) cryptoUtils.decrypt(this) else this inner class TouchinEditor : SharedPreferences.Editor { @@ -97,7 +85,7 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun putBoolean(key: String?, value: Boolean) = put(key, value) override fun putStringSet(key: String?, value: MutableSet?): SharedPreferences.Editor { - return if (isEncryption) { + return if (encrypt) { currentPreferences.edit().putStringSet(key, value?.map { it.encrypt() }?.toMutableSet()) } else { currentPreferences.edit().putStringSet(key, value) @@ -113,27 +101,12 @@ class TouchinSharedPreferences(name: String, context: Context, val isEncryption: override fun putString(key: String?, value: String?) = put(key, value) private fun put(key: String?, value: T): SharedPreferences.Editor { - val value = value?.toString() - var resultValue = "" - value?.let { - var currentValue = "" - var pos = 0 - while (pos < value.length) { - if (currentValue.length == ENCRYPT_BLOCK_SIZE) { - resultValue += currentValue.encrypt() - currentValue = "" - } - currentValue += value[pos] - pos++ - } - if (currentValue.isNotEmpty()) { - resultValue += currentValue.encrypt() - } - } + val resultValue = value?.toString()?.chunked(ENCRYPT_BLOCK_SIZE)?.joinToString(separator = "", transform = { it.encrypt() }) + return currentPreferences.edit().putString(key, resultValue) } - private fun String.encrypt() = if (isEncryption) cryptoUtils.encrypt(this) else this + private fun String.encrypt() = if (encrypt) cryptoUtils.encrypt(this) else this } diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt index 86e5abd..f897546 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt @@ -28,6 +28,7 @@ class TouchinSharedPreferencesCryptoUtils constructor(val context: Context) { private const val CIPHER_STRING_SIZE_BYTES = 256 private const val BASE_64_PADDING = 2 private const val STORAGE_KEY = "STORAGE_KEY" + //https://stackoverflow.com/questions/13378815/base64-length-calculation const val ENCRYPT_BASE64_STRING_LENGTH = (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * 4 / 3 + 5 const val ENCRYPT_BLOCK_SIZE = 128 From 494e665df42cb0d0be826a96e22e9fb01d9142d7 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 15 Jun 2020 13:56:51 +0300 Subject: [PATCH 062/154] fixed readme --- encrypted-shared-prefs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encrypted-shared-prefs/README.md b/encrypted-shared-prefs/README.md index d6ee538..9646cf9 100644 --- a/encrypted-shared-prefs/README.md +++ b/encrypted-shared-prefs/README.md @@ -8,7 +8,7 @@ Encrypted shared preferences Пример получения экземпляра `TouchinSharedPreferences`. При isEncryption = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` ```kotlin -val prefs = TouchinSharedPreferences(name = "APPLICATION_DATA_ENCRYPTED", context = context, isEncryption = true) +val prefs = TouchinSharedPreferences(name = "APPLICATION_DATA_ENCRYPTED", context = context, encrypt = true) ``` Важно помнить, что в одном файле `TouchinSharedPreferences` могут храниться только либо полностью зашифрованные данные, либо полностью незашифрованные. Флаг `isEncryption` должен быть в соответствующем положении \ No newline at end of file From 48017f9803a174fc5f8c40e61b67333a76ca30bb Mon Sep 17 00:00:00 2001 From: Aksenov Vladimir Date: Mon, 15 Jun 2020 14:11:13 +0300 Subject: [PATCH 063/154] Update encrypted-shared-prefs/README.md Co-authored-by: Maxim Bachinsky --- encrypted-shared-prefs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/encrypted-shared-prefs/README.md b/encrypted-shared-prefs/README.md index 9646cf9..481ca7d 100644 --- a/encrypted-shared-prefs/README.md +++ b/encrypted-shared-prefs/README.md @@ -5,10 +5,10 @@ Encrypted shared preferences ### Пример -Пример получения экземпляра `TouchinSharedPreferences`. При isEncryption = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` +Пример получения экземпляра `TouchinSharedPreferences`. При encrypt = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` ```kotlin val prefs = TouchinSharedPreferences(name = "APPLICATION_DATA_ENCRYPTED", context = context, encrypt = true) ``` -Важно помнить, что в одном файле `TouchinSharedPreferences` могут храниться только либо полностью зашифрованные данные, либо полностью незашифрованные. Флаг `isEncryption` должен быть в соответствующем положении \ No newline at end of file +Важно помнить, что в одном файле `TouchinSharedPreferences` могут храниться только либо полностью зашифрованные данные, либо полностью незашифрованные. Флаг `isEncryption` должен быть в соответствующем положении From 0ccaba37c5ccd4694a53444af5d2716e9dbdda47 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 15 Jun 2020 14:52:13 +0300 Subject: [PATCH 064/154] fixed review --- ...inSharedPreferences.kt => CipherSharedPreferences.kt} | 8 ++++---- .../src/main/java/ru/touchin/roboswag/Extensions.kt | 2 +- ...aredPreferencesCryptoUtils.kt => PrefsCryptoUtils.kt} | 9 +++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) rename encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/{TouchinSharedPreferences.kt => CipherSharedPreferences.kt} (91%) rename encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/{TouchinSharedPreferencesCryptoUtils.kt => PrefsCryptoUtils.kt} (92%) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt similarity index 91% rename from encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt rename to encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt index 8cfae10..65eb56d 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferences.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt @@ -2,13 +2,13 @@ package ru.touchin.roboswag import android.content.Context import android.content.SharedPreferences -import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPT_BASE64_STRING_LENGTH -import ru.touchin.roboswag.TouchinSharedPreferencesCryptoUtils.Companion.ENCRYPT_BLOCK_SIZE +import ru.touchin.roboswag.PrefsCryptoUtils.Companion.ENCRYPT_BASE64_STRING_LENGTH +import ru.touchin.roboswag.PrefsCryptoUtils.Companion.ENCRYPT_BLOCK_SIZE -class TouchinSharedPreferences(name: String, context: Context, val encrypt: Boolean = false) : SharedPreferences { +class CipherSharedPreferences(name: String, context: Context, val encrypt: Boolean = false) : SharedPreferences { private val currentPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) - private val cryptoUtils = TouchinSharedPreferencesCryptoUtils(context) + private val cryptoUtils = PrefsCryptoUtils(context) override fun contains(key: String?) = currentPreferences.contains(key) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt index 4e4fc87..bcab65b 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt @@ -2,7 +2,7 @@ package ru.touchin.roboswag import android.content.SharedPreferences -fun TouchinSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { +fun CipherSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { if (!from.contains(key)) return this edit().putString(key, from.getString(key, "") ?: "").apply() from.edit().remove(key).apply() diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt similarity index 92% rename from encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt rename to encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt index f897546..671ef19 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/TouchinSharedPreferencesCryptoUtils.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt @@ -18,7 +18,7 @@ import javax.security.auth.x500.X500Principal // https://proandroiddev.com/secure-data-in-android-encryption-in-android-part-2-991a89e55a23 @Suppress("detekt.TooGenericExceptionCaught") -class TouchinSharedPreferencesCryptoUtils constructor(val context: Context) { +class PrefsCryptoUtils constructor(val context: Context) { companion object { @@ -28,8 +28,13 @@ class TouchinSharedPreferencesCryptoUtils constructor(val context: Context) { private const val CIPHER_STRING_SIZE_BYTES = 256 private const val BASE_64_PADDING = 2 private const val STORAGE_KEY = "STORAGE_KEY" + //https://stackoverflow.com/questions/13378815/base64-length-calculation - const val ENCRYPT_BASE64_STRING_LENGTH = (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * 4 / 3 + 5 + private const val ORIGINALLY_BYTES_COUNT = 3 + private const val ENCRYPT_BYTES_COUNT = 4 + private const val BASE64_DIVIDER_COUNT = 5 + const val ENCRYPT_BASE64_STRING_LENGTH = + (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * ENCRYPT_BYTES_COUNT / ORIGINALLY_BYTES_COUNT + BASE64_DIVIDER_COUNT const val ENCRYPT_BLOCK_SIZE = 128 private fun getAndroidKeystore(): KeyStore? = try { From 6af592c4f8fc2131224d38810bf36698fa53803d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 15 Jun 2020 15:02:19 +0300 Subject: [PATCH 065/154] Fixed review --- .../src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt index 671ef19..3a3c888 100644 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt +++ b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt @@ -30,11 +30,11 @@ class PrefsCryptoUtils constructor(val context: Context) { private const val STORAGE_KEY = "STORAGE_KEY" //https://stackoverflow.com/questions/13378815/base64-length-calculation - private const val ORIGINALLY_BYTES_COUNT = 3 - private const val ENCRYPT_BYTES_COUNT = 4 + private const val DECRYPTED_BYTES_COUNT = 3 + private const val ENCRYPTED_BYTES_COUNT = 4 private const val BASE64_DIVIDER_COUNT = 5 const val ENCRYPT_BASE64_STRING_LENGTH = - (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * ENCRYPT_BYTES_COUNT / ORIGINALLY_BYTES_COUNT + BASE64_DIVIDER_COUNT + (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * ENCRYPTED_BYTES_COUNT / DECRYPTED_BYTES_COUNT + BASE64_DIVIDER_COUNT const val ENCRYPT_BLOCK_SIZE = 128 private fun getAndroidKeystore(): KeyStore? = try { From 7f10b89b86042c7516a4794914e9603a55b56350 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 15 Jun 2020 17:00:31 +0300 Subject: [PATCH 066/154] move common android build settings to RoboSwag/android-configs --- api-logansquare/build.gradle | 15 +------------ base-map/build.gradle | 11 +--------- bottom-navigation-base/build.gradle | 21 +----------------- bottom-navigation-fragment/build.gradle | 21 +----------------- bottom-navigation-viewcontroller/build.gradle | 21 +----------------- google-map/build.gradle | 11 +--------- kotlin-extensions/build.gradle | 11 +--------- lifecycle-rx/build.gradle | 11 +--------- lifecycle-viewcontroller/build.gradle | 20 +---------------- lifecycle/build.gradle | 20 +---------------- livedata-location/build.gradle | 12 +--------- logging/build.gradle | 15 +------------ navigation-base/build.gradle | 22 ++----------------- navigation-viewcontroller/build.gradle | 22 ++----------------- recyclerview-adapters/build.gradle | 11 +--------- recyclerview-calendar/build.gradle | 10 +-------- rx-extensions/build.gradle | 11 +--------- storable/build.gradle | 15 +------------ utils/build.gradle | 16 +------------- views/build.gradle | 15 +------------ yandex-map/build.gradle | 11 +--------- 21 files changed, 23 insertions(+), 299 deletions(-) diff --git a/api-logansquare/build.gradle b/api-logansquare/build.gradle index 9582901..46d7270 100644 --- a/api-logansquare/build.gradle +++ b/api-logansquare/build.gradle @@ -1,17 +1,4 @@ -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 project(":utils") diff --git a/base-map/build.gradle b/base-map/build.gradle index d5a44da..28cb33f 100644 --- a/base-map/build.gradle +++ b/base-map/build.gradle @@ -1,13 +1,4 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 17 - } -} +apply from: "../android-configs/lib-config.gradle" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" diff --git a/bottom-navigation-base/build.gradle b/bottom-navigation-base/build.gradle index 5ee554e..2d47a0f 100644 --- a/bottom-navigation-base/build.gradle +++ b/bottom-navigation-base/build.gradle @@ -1,23 +1,4 @@ -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 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - -} +apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(":logging") diff --git a/bottom-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle index c534245..7be765e 100644 --- a/bottom-navigation-fragment/build.gradle +++ b/bottom-navigation-fragment/build.gradle @@ -1,23 +1,4 @@ -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 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - -} +apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(":navigation-base") diff --git a/bottom-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle index d6fd6e6..ab6dfc9 100644 --- a/bottom-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -1,23 +1,4 @@ -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 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - -} +apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(":navigation-base") diff --git a/google-map/build.gradle b/google-map/build.gradle index 20c4a09..5dbe162 100644 --- a/google-map/build.gradle +++ b/google-map/build.gradle @@ -1,13 +1,4 @@ -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") diff --git a/kotlin-extensions/build.gradle b/kotlin-extensions/build.gradle index ad799de..9a33a8e 100644 --- a/kotlin-extensions/build.gradle +++ b/kotlin-extensions/build.gradle @@ -1,13 +1,4 @@ -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" diff --git a/lifecycle-rx/build.gradle b/lifecycle-rx/build.gradle index c885051..fb335b2 100644 --- a/lifecycle-rx/build.gradle +++ b/lifecycle-rx/build.gradle @@ -1,13 +1,4 @@ -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") diff --git a/lifecycle-viewcontroller/build.gradle b/lifecycle-viewcontroller/build.gradle index e40e049..22d42c4 100644 --- a/lifecycle-viewcontroller/build.gradle +++ b/lifecycle-viewcontroller/build.gradle @@ -1,22 +1,4 @@ -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 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } -} +apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(":lifecycle") diff --git a/lifecycle/build.gradle b/lifecycle/build.gradle index 322c98f..6538e89 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -1,22 +1,4 @@ -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 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } -} +apply from: "../android-configs/lib-config.gradle" dependencies { compileOnly "javax.inject:javax.inject:1" diff --git a/livedata-location/build.gradle b/livedata-location/build.gradle index 43615ea..b61b9ce 100644 --- a/livedata-location/build.gradle +++ b/livedata-location/build.gradle @@ -1,14 +1,4 @@ -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") diff --git a/logging/build.gradle b/logging/build.gradle index cf9568f..e1c188e 100644 --- a/logging/build.gradle +++ b/logging/build.gradle @@ -1,17 +1,4 @@ -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" diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index 3aadeea..d99d1b4 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -1,25 +1,7 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply from: "../android-configs/lib-config.gradle" + apply plugin: 'kotlin-kapt' -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - -} - dependencies { implementation project(":utils") implementation project(":logging") diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index 34c2b1b..9c1aae5 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -1,25 +1,7 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply from: "../android-configs/lib-config.gradle" + apply plugin: 'kotlin-kapt' -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - -} - dependencies { implementation project(":utils") implementation project(":logging") diff --git a/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle index 552fc4f..f733886 100644 --- a/recyclerview-adapters/build.gradle +++ b/recyclerview-adapters/build.gradle @@ -1,13 +1,4 @@ -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 project(':kotlin-extensions') diff --git a/recyclerview-calendar/build.gradle b/recyclerview-calendar/build.gradle index a783c55..811a091 100644 --- a/recyclerview-calendar/build.gradle +++ b/recyclerview-calendar/build.gradle @@ -1,12 +1,4 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 16 - } -} +apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(":logging") diff --git a/rx-extensions/build.gradle b/rx-extensions/build.gradle index 0099f13..bb4e059 100644 --- a/rx-extensions/build.gradle +++ b/rx-extensions/build.gradle @@ -1,13 +1,4 @@ -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 project(":utils") diff --git a/storable/build.gradle b/storable/build.gradle index b4b59bc..58f4456 100644 --- a/storable/build.gradle +++ b/storable/build.gradle @@ -1,17 +1,4 @@ -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 project(":utils") diff --git a/utils/build.gradle b/utils/build.gradle index b69e92a..f8763bd 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -1,18 +1,4 @@ -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" diff --git a/views/build.gradle b/views/build.gradle index 6bc166b..623cb8b 100644 --- a/views/build.gradle +++ b/views/build.gradle @@ -1,17 +1,4 @@ -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 project(":utils") diff --git a/yandex-map/build.gradle b/yandex-map/build.gradle index 2d08918..88830e4 100644 --- a/yandex-map/build.gradle +++ b/yandex-map/build.gradle @@ -1,13 +1,4 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 17 - } -} +apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(":base-map") From ba78cd441b10f990cb6b0a2550a3194c4746bc80 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 15 Jun 2020 17:13:48 +0300 Subject: [PATCH 067/154] added config.gradle to RoboSwag --- android-configs/app-config.gradle | 7 +++++ android-configs/common-config.gradle | 22 +++++++++++++++ android-configs/lib-config.gradle | 3 +++ android-plugins/build.gradle.kts | 18 +++++++++++++ .../kotlin/plugins/lib-settings.gradle.kts | 27 +++++++++++++++++++ .../gradle-plugins/android_app.properties | 1 + 6 files changed, 78 insertions(+) create mode 100644 android-configs/app-config.gradle create mode 100644 android-configs/common-config.gradle create mode 100644 android-configs/lib-config.gradle create mode 100644 android-plugins/build.gradle.kts create mode 100644 android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts create mode 100644 android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties diff --git a/android-configs/app-config.gradle b/android-configs/app-config.gradle new file mode 100644 index 0000000..2d0cbf8 --- /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' \ No newline at end of file diff --git a/android-configs/common-config.gradle b/android-configs/common-config.gradle new file mode 100644 index 0000000..f74b344 --- /dev/null +++ b/android-configs/common-config.gradle @@ -0,0 +1,22 @@ +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 29 + + multiDexEnabled = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + +} 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/android-plugins/build.gradle.kts b/android-plugins/build.gradle.kts new file mode 100644 index 0000000..b044b2e --- /dev/null +++ b/android-plugins/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `kotlin-dsl` + `kotlin-dsl-precompiled-script-plugins` +} + +repositories { + jcenter() + google() +} + +dependencies { + implementation("com.android.tools.build:gradle:4.0.0") + + implementation("com.android.tools.build:gradle-api:4.0.0") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61") + +} diff --git a/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts b/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts new file mode 100644 index 0000000..ff0c3cd --- /dev/null +++ b/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts @@ -0,0 +1,27 @@ + +plugins { + id("com.android.application") apply false + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdkVersion(29) + + defaultConfig { + minSdkVersion(21) + targetSdkVersion(29) + + multiDexEnabled = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + +} diff --git a/android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties b/android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties new file mode 100644 index 0000000..e5d4100 --- /dev/null +++ b/android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties @@ -0,0 +1 @@ +implementation-class=plugins.AndroidAppPlugin From 7f90d6d756d2f229067d730f37c4474fc3adaded Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Tue, 16 Jun 2020 11:57:20 +0300 Subject: [PATCH 068/154] add code owners --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..4a8851c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Ответственный за все модули +* @maxbach From 2599f4a2c7c73bd63a1f23ed0ef0688ff5a40598 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Tue, 23 Jun 2020 17:41:47 +0300 Subject: [PATCH 069/154] update crashlytics required version --- logging/build.gradle | 2 +- navigation-base/build.gradle | 2 +- navigation-viewcontroller/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/logging/build.gradle b/logging/build.gradle index e1c188e..1cc7977 100644 --- a/logging/build.gradle +++ b/logging/build.gradle @@ -14,7 +14,7 @@ dependencies { implementation("com.crashlytics.sdk.android:crashlytics") { version { - require '2.5.0' + require '2.10.0' } } } diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index d99d1b4..e444e8a 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -54,7 +54,7 @@ dependencies { implementation("com.crashlytics.sdk.android:crashlytics") { version { - require '2.0.0' + require '2.10.0' } } } diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index 9c1aae5..05bbe39 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation("com.crashlytics.sdk.android:crashlytics") { version { - require '2.0.0' + require '2.10.0' } } } From 382f0d1da07cffbb9088dadb7ce3392fd52e9aed Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 25 Jun 2020 11:30:30 +0300 Subject: [PATCH 070/154] formatting fix --- android-configs/common-config.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android-configs/common-config.gradle b/android-configs/common-config.gradle index f74b344..cda3a5c 100644 --- a/android-configs/common-config.gradle +++ b/android-configs/common-config.gradle @@ -5,9 +5,9 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 29 - multiDexEnabled = true + multiDexEnabled = true } compileOptions { From b6c5ca3a754e763e9f6444da06d4e0bb0754796d Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 25 Jun 2020 15:51:22 +0300 Subject: [PATCH 071/154] constants to root extensions --- android-configs/app-config.gradle | 2 +- android-configs/common-config.gradle | 16 ++++++++++------ .../main/kotlin/plugins/lib-settings.gradle.kts | 6 +----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/android-configs/app-config.gradle b/android-configs/app-config.gradle index 2d0cbf8..cf6759d 100644 --- a/android-configs/app-config.gradle +++ b/android-configs/app-config.gradle @@ -4,4 +4,4 @@ apply from: '../RoboSwag/android-configs/common-config.gradle' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' \ No newline at end of file +apply plugin: 'kotlin-kapt' diff --git a/android-configs/common-config.gradle b/android-configs/common-config.gradle index cda3a5c..4949440 100644 --- a/android-configs/common-config.gradle +++ b/android-configs/common-config.gradle @@ -1,13 +1,18 @@ apply plugin: 'kotlin-android' +rootProject.ext { + compileSdk = 29 + + minSdk = 21 + targetSdk = 29 +} + android { - compileSdkVersion 29 + compileSdkVersion rootProject.ext.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion 29 - - multiDexEnabled = true + minSdkVersion rootProject.ext.minSdk + targetSdkVersion rootProject.ext.targetSdk } compileOptions { @@ -18,5 +23,4 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } - } diff --git a/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts b/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts index ff0c3cd..016b271 100644 --- a/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts +++ b/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts @@ -1,4 +1,3 @@ - plugins { id("com.android.application") apply false id("kotlin-android") @@ -10,9 +9,7 @@ android { defaultConfig { minSdkVersion(21) - targetSdkVersion(29) - - multiDexEnabled = true + targetSdkVersion(29) } compileOptions { @@ -23,5 +20,4 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } - } From d565637407ce2ad4c662617e59bcddd284f83c8f Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Sat, 27 Jun 2020 18:42:58 +0300 Subject: [PATCH 072/154] move to roboswag modules (mvi, pagination, cicerone, viewbinding) from test proj --- README.md | 4 +- encrypted-shared-prefs/README.md | 14 -- encrypted-shared-prefs/build.gradle | 24 --- .../src/main/AndroidManifest.xml | 1 - .../roboswag/CipherSharedPreferences.kt | 113 ------------- .../java/ru/touchin/roboswag/Extensions.kt | 10 -- .../ru/touchin/roboswag/PrefsCryptoUtils.kt | 125 --------------- .../.gitignore | 0 mvi-arch/README.md | 4 + mvi-arch/build.gradle.kts | 13 ++ mvi-arch/src/main/AndroidManifest.xml | 1 + .../roboswag/mvi_arch/core/MviFragment.kt | 150 ++++++++++++++++++ .../mvi_arch/core/MviStoreViewModel.kt | 82 ++++++++++ .../roboswag/mvi_arch/core/MviViewModel.kt | 46 ++++++ .../touchin/roboswag/mvi_arch/core/Store.kt | 106 +++++++++++++ .../mvi_arch/di/ViewModelAssistedFactory.kt | 8 + .../roboswag/mvi_arch/di/ViewModelFactory.kt | 19 +++ .../roboswag/mvi_arch/di/ViewModelKey.kt | 9 ++ .../roboswag/mvi_arch/marker/SideEffect.kt | 3 + .../roboswag/mvi_arch/marker/StateChange.kt | 4 + .../roboswag/mvi_arch/marker/ViewAction.kt | 22 +++ .../roboswag/mvi_arch/marker/ViewState.kt | 12 ++ mvi-arch/src/main/res/values/strings.xml | 3 + navigation-base/build.gradle | 19 +++ .../fragments/FragmentViewBindingDelegate.kt | 49 ++++++ navigation-cicerone/README.md | 4 + navigation-cicerone/build.gradle.kts | 10 ++ .../src/main/AndroidManifest.xml | 1 + .../navigation_cicerone/CiceroneTuner.kt | 44 +++++ .../navigation_cicerone/flow/FlowFragment.kt | 46 ++++++ .../flow/FlowNavigation.kt | 6 + .../flow/FlowNavigationModule.kt | 26 +++ .../src/main/res/layout/fragment_flow.xml | 5 + .../src/main/res/values/strings.xml | 3 + pagination/README.md | 4 + pagination/build.gradle.kts | 12 ++ pagination/src/main/AndroidManifest.xml | 1 + .../roboswag/pagination/PaginationAdapter.kt | 39 +++++ .../roboswag/pagination/PaginationView.kt | 76 +++++++++ .../touchin/roboswag/pagination/Paginator.kt | 128 +++++++++++++++ .../pagination/ProgressAdapterDelegate.kt | 22 +++ .../roboswag/pagination/ProgressItem.kt | 3 + .../src/main/res/layout/item_progress.xml | 12 ++ .../src/main/res/layout/view_pagination.xml | 33 ++++ pagination/src/main/res/values/strings.xml | 5 + 45 files changed, 1032 insertions(+), 289 deletions(-) delete mode 100644 encrypted-shared-prefs/README.md delete mode 100644 encrypted-shared-prefs/build.gradle delete mode 100644 encrypted-shared-prefs/src/main/AndroidManifest.xml delete mode 100644 encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt delete mode 100644 encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt delete mode 100644 encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt rename {encrypted-shared-prefs => mvi-arch}/.gitignore (100%) create mode 100644 mvi-arch/README.md create mode 100644 mvi-arch/build.gradle.kts create mode 100644 mvi-arch/src/main/AndroidManifest.xml create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviFragment.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviStoreViewModel.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/Store.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelAssistedFactory.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelFactory.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/di/ViewModelKey.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/SideEffect.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/StateChange.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewAction.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/ViewState.kt create mode 100644 mvi-arch/src/main/res/values/strings.xml create mode 100644 navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/fragments/FragmentViewBindingDelegate.kt create mode 100644 navigation-cicerone/README.md create mode 100644 navigation-cicerone/build.gradle.kts create mode 100644 navigation-cicerone/src/main/AndroidManifest.xml create mode 100644 navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/CiceroneTuner.kt create mode 100644 navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowFragment.kt create mode 100644 navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigation.kt create mode 100644 navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowNavigationModule.kt create mode 100644 navigation-cicerone/src/main/res/layout/fragment_flow.xml create mode 100644 navigation-cicerone/src/main/res/values/strings.xml create mode 100644 pagination/README.md create mode 100644 pagination/build.gradle.kts create mode 100644 pagination/src/main/AndroidManifest.xml create mode 100644 pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt create mode 100644 pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt create mode 100644 pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt create mode 100644 pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt create mode 100644 pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt create mode 100644 pagination/src/main/res/layout/item_progress.xml create mode 100644 pagination/src/main/res/layout/view_pagination.xml create mode 100644 pagination/src/main/res/values/strings.xml diff --git a/README.md b/README.md index b9fbe85..f48dec0 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Roboswag - библиотека решений, ускоряющих разра ## Минимальные требования -* Andoroid Api: 19 +* Android Api: 21 * Kotlin: 1.3.11 -* Gradle: 3.2.1 +* Gradle: 4.0.0 ## Основная архитектура За основу архитектуры взят подход от Google - MVVM на основе [Android Architecture Components](https://developer.android.com/jetpack/docs/guide). Данный подход популярен в сообществе Android разработки, позволяет разбивать код на мелкие и независимые части, что ускоряет разработку и последующую поддержку приложения. diff --git a/encrypted-shared-prefs/README.md b/encrypted-shared-prefs/README.md deleted file mode 100644 index 481ca7d..0000000 --- a/encrypted-shared-prefs/README.md +++ /dev/null @@ -1,14 +0,0 @@ -Encrypted shared preferences -============================ - -Модуль с реализацией интерфейса `SharedPreferences`, который дает возможность шифровать содержимое. - -### Пример - -Пример получения экземпляра `TouchinSharedPreferences`. При encrypt = false, `TouchinSharedPreferences` абсолютно аналогичны стандартной реализации `SharedPreferences` - -```kotlin -val prefs = TouchinSharedPreferences(name = "APPLICATION_DATA_ENCRYPTED", context = context, encrypt = true) -``` - -Важно помнить, что в одном файле `TouchinSharedPreferences` могут храниться только либо полностью зашифрованные данные, либо полностью незашифрованные. Флаг `isEncryption` должен быть в соответствующем положении diff --git a/encrypted-shared-prefs/build.gradle b/encrypted-shared-prefs/build.gradle deleted file mode 100644 index 085c337..0000000 --- a/encrypted-shared-prefs/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion 21 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation "androidx.core:core:$versions.androidx" - implementation "androidx.annotation:annotation:$versions.androidx" - implementation "androidx.appcompat:appcompat:$versions.appcompat" - -} diff --git a/encrypted-shared-prefs/src/main/AndroidManifest.xml b/encrypted-shared-prefs/src/main/AndroidManifest.xml deleted file mode 100644 index a068e5f..0000000 --- a/encrypted-shared-prefs/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt deleted file mode 100644 index 65eb56d..0000000 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/CipherSharedPreferences.kt +++ /dev/null @@ -1,113 +0,0 @@ -package ru.touchin.roboswag - -import android.content.Context -import android.content.SharedPreferences -import ru.touchin.roboswag.PrefsCryptoUtils.Companion.ENCRYPT_BASE64_STRING_LENGTH -import ru.touchin.roboswag.PrefsCryptoUtils.Companion.ENCRYPT_BLOCK_SIZE - -class CipherSharedPreferences(name: String, context: Context, val encrypt: Boolean = false) : SharedPreferences { - - private val currentPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) - private val cryptoUtils = PrefsCryptoUtils(context) - - override fun contains(key: String?) = currentPreferences.contains(key) - - override fun getBoolean(key: String?, defaultValue: Boolean) = get(key, defaultValue) - - override fun unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener?) { - currentPreferences.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) - } - - override fun getInt(key: String?, defaultValue: Int) = get(key, defaultValue) - - override fun getAll(): MutableMap { - return if (encrypt) { - currentPreferences.all.mapValues { it.value.toString().decrypt() }.toMutableMap() - } else { - currentPreferences.all.mapValues { it.value.toString() }.toMutableMap() - } - } - - override fun edit() = TouchinEditor() - - override fun getLong(key: String?, defaultValue: Long) = get(key, defaultValue) - - override fun getFloat(key: String?, defaultValue: Float) = get(key, defaultValue) - - override fun getString(key: String?, defaultValue: String?): String = get(key, defaultValue ?: "") - - override fun getStringSet(key: String?, set: MutableSet?): MutableSet? { - return if (encrypt) { - val value = currentPreferences.getStringSet(key, set) - if (value == set) { - set - } else { - value?.map { it.decrypt() }?.toMutableSet() - } - } else { - currentPreferences.getStringSet(key, set) - } - } - - override fun registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener?) { - currentPreferences.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) - } - - private fun get(key: String?, defaultValue: T): T { - if (!currentPreferences.contains(key)) return defaultValue - val resultValue = currentPreferences.getString(key, "") - ?.trim() - ?.chunked(ENCRYPT_BASE64_STRING_LENGTH) - ?.joinToString(separator = "", transform = { it.decrypt() }) - - return when (defaultValue) { - is Boolean -> resultValue?.toBoolean() as? T - is Long -> resultValue?.toLong() as? T - is String -> resultValue as? T - is Int -> resultValue?.toInt() as? T - is Float -> resultValue?.toFloat() as? T - else -> resultValue as? T - } ?: defaultValue - } - - private fun String.decrypt() = if (encrypt) cryptoUtils.decrypt(this) else this - - inner class TouchinEditor : SharedPreferences.Editor { - - override fun clear() = currentPreferences.edit().clear() - - override fun putLong(key: String?, value: Long) = put(key, value) - - override fun putInt(key: String?, value: Int) = put(key, value) - - override fun remove(key: String?) = currentPreferences.edit().remove(key) - - override fun putBoolean(key: String?, value: Boolean) = put(key, value) - - override fun putStringSet(key: String?, value: MutableSet?): SharedPreferences.Editor { - return if (encrypt) { - currentPreferences.edit().putStringSet(key, value?.map { it.encrypt() }?.toMutableSet()) - } else { - currentPreferences.edit().putStringSet(key, value) - } - } - - override fun commit() = currentPreferences.edit().commit() - - override fun putFloat(key: String?, value: Float) = put(key, value) - - override fun apply() = currentPreferences.edit().apply() - - override fun putString(key: String?, value: String?) = put(key, value) - - private fun put(key: String?, value: T): SharedPreferences.Editor { - val resultValue = value?.toString()?.chunked(ENCRYPT_BLOCK_SIZE)?.joinToString(separator = "", transform = { it.encrypt() }) - - return currentPreferences.edit().putString(key, resultValue) - } - - private fun String.encrypt() = if (encrypt) cryptoUtils.encrypt(this) else this - - } - -} diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt deleted file mode 100644 index bcab65b..0000000 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/Extensions.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.touchin.roboswag - -import android.content.SharedPreferences - -fun CipherSharedPreferences.migrateFromSharedPreferences(from: SharedPreferences, key: String): SharedPreferences { - if (!from.contains(key)) return this - edit().putString(key, from.getString(key, "") ?: "").apply() - from.edit().remove(key).apply() - return this -} diff --git a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt b/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt deleted file mode 100644 index 3a3c888..0000000 --- a/encrypted-shared-prefs/src/main/java/ru/touchin/roboswag/PrefsCryptoUtils.kt +++ /dev/null @@ -1,125 +0,0 @@ -package ru.touchin.roboswag - -import android.annotation.TargetApi -import android.content.Context -import android.os.Build -import android.security.KeyPairGeneratorSpec -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import android.util.Base64 -import java.math.BigInteger -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.KeyStore -import java.security.PrivateKey -import java.util.Calendar -import javax.crypto.Cipher -import javax.security.auth.x500.X500Principal - -// https://proandroiddev.com/secure-data-in-android-encryption-in-android-part-2-991a89e55a23 -@Suppress("detekt.TooGenericExceptionCaught") -class PrefsCryptoUtils constructor(val context: Context) { - - companion object { - - private const val ANDROID_KEY_STORE = "AndroidKeyStore" - private const val KEY_ALGORITHM_RSA = "RSA" - private const val TRANSFORMATION_ASYMMETRIC = "RSA/ECB/PKCS1Padding" - private const val CIPHER_STRING_SIZE_BYTES = 256 - private const val BASE_64_PADDING = 2 - private const val STORAGE_KEY = "STORAGE_KEY" - - //https://stackoverflow.com/questions/13378815/base64-length-calculation - private const val DECRYPTED_BYTES_COUNT = 3 - private const val ENCRYPTED_BYTES_COUNT = 4 - private const val BASE64_DIVIDER_COUNT = 5 - const val ENCRYPT_BASE64_STRING_LENGTH = - (CIPHER_STRING_SIZE_BYTES + BASE_64_PADDING) * ENCRYPTED_BYTES_COUNT / DECRYPTED_BYTES_COUNT + BASE64_DIVIDER_COUNT - const val ENCRYPT_BLOCK_SIZE = 128 - - private fun getAndroidKeystore(): KeyStore? = try { - KeyStore.getInstance(ANDROID_KEY_STORE).also { it.load(null) } - } catch (exception: Exception) { - null - } - - private fun getAndroidKeyStoreAsymmetricKeyPair(): KeyPair? { - val privateKey = getAndroidKeystore()?.getKey(STORAGE_KEY, null) as PrivateKey? - val publicKey = getAndroidKeystore()?.getCertificate(STORAGE_KEY)?.publicKey - return if (privateKey != null && publicKey != null) { - KeyPair(publicKey, privateKey) - } else { - null - } - } - - private fun createAndroidKeyStoreAsymmetricKey(context: Context): KeyPair { - val generator = KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, ANDROID_KEY_STORE) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - initGeneratorWithKeyPairGeneratorSpec(generator) - } else { - initGeneratorWithKeyGenParameterSpec(generator, context) - } - - // Generates Key with given spec and saves it to the KeyStore - return generator.generateKeyPair() - } - - private fun initGeneratorWithKeyGenParameterSpec(generator: KeyPairGenerator, context: Context) { - val startDate = Calendar.getInstance() - val endDate = Calendar.getInstance() - endDate.add(Calendar.YEAR, 20) - - val builder = KeyPairGeneratorSpec.Builder(context) - .setAlias(STORAGE_KEY) - .setSerialNumber(BigInteger.ONE) - .setSubject(X500Principal("CN=$STORAGE_KEY CA Certificate")) - .setStartDate(startDate.time) - .setEndDate(endDate.time) - - generator.initialize(builder.build()) - } - - @TargetApi(Build.VERSION_CODES.M) - private fun initGeneratorWithKeyPairGeneratorSpec(generator: KeyPairGenerator) { - val builder = KeyGenParameterSpec.Builder(STORAGE_KEY, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_ECB) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) - generator.initialize(builder.build()) - } - - private fun createCipher(): Cipher? = try { - Cipher.getInstance(TRANSFORMATION_ASYMMETRIC) - } catch (exception: Exception) { - null - } - - } - - private val cipher = createCipher() - private val keyPair = getAndroidKeyStoreAsymmetricKeyPair() - ?: createAndroidKeyStoreAsymmetricKey(context) - - // Those methods should not take and return strings, only char[] and those arrays should be cleared right after usage - // See for explanation https://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html#PBEEx - - @Synchronized - fun encrypt(data: String): String { - cipher?.init(Cipher.ENCRYPT_MODE, keyPair.public) - val bytes = cipher?.doFinal(data.toByteArray()) - return Base64.encodeToString(bytes, Base64.DEFAULT) - } - - @Synchronized - fun decrypt(data: String?): String { - cipher?.init(Cipher.DECRYPT_MODE, keyPair.private) - if (data.isNullOrBlank()) { - return String() - } - val encryptedData = Base64.decode(data, Base64.DEFAULT) - val decodedData = cipher?.doFinal(encryptedData) - return decodedData?.let { decodedData -> String(decodedData) } ?: "" - } - -} diff --git a/encrypted-shared-prefs/.gitignore b/mvi-arch/.gitignore similarity index 100% rename from encrypted-shared-prefs/.gitignore rename to mvi-arch/.gitignore 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.kts b/mvi-arch/build.gradle.kts new file mode 100644 index 0000000..99febc6 --- /dev/null +++ b/mvi-arch/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG) +} + +dependencies { + androidX() + fragment() + lifecycle() + + dagger() + + coroutines() +} 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..2cf6857 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviFragment.kt @@ -0,0 +1,150 @@ +package ru.touchin.roboswag.mvi_arch.core + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +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) + } + + /** + * 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..71ef368 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt @@ -0,0 +1,46 @@ +package ru.touchin.roboswag.mvi_arch.core + +import android.os.Parcelable +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +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 MviViewModel( + private val initialState: State, + protected val handle: SavedStateHandle +) : ViewModel() { + + 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 + + abstract fun dispatchAction(action: Action) + +} 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..56eef64 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/Store.kt @@ -0,0 +1,106 @@ +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.roboswag.mvi_arch.marker.SideEffect +import ru.touchin.roboswag.mvi_arch.marker.StateChange +import ru.touchin.roboswag.mvi_arch.marker.ViewState + +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() + + init { + storeScope.launch { + effects + .consumeAsFlow() + .filterNotNull() + .handleSideEffect() + .collect { newChange -> changeState(newChange) } + } + } + + fun changeState(change: Change) { + val (newState, newEffect) = reduce(currentState, change) + + if (currentState != newState) { + state.value = newState + } + + childStores.forEach { childStore -> + childStore.change(change) + } + + newEffect?.let { + effects.offer(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..93f3378 --- /dev/null +++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/marker/StateChange.kt @@ -0,0 +1,4 @@ +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/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/build.gradle b/navigation-base/build.gradle index e444e8a..5f1a068 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -2,6 +2,10 @@ apply from: "../android-configs/lib-config.gradle" apply plugin: 'kotlin-kapt' +android { + buildFeatures.viewBinding = true +} + dependencies { implementation project(":utils") implementation project(":logging") @@ -17,6 +21,9 @@ dependencies { implementation "androidx.fragment:fragment" implementation "androidx.fragment:fragment-ktx" + implementation "androidx.lifecycle:lifecycle-common-java8" + implementation "androidx.lifecycle:lifecycle-livedata-ktx" + implementation("com.crashlytics.sdk.android:crashlytics") { transitive = true } @@ -57,5 +64,17 @@ dependencies { require '2.10.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/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-cicerone/README.md b/navigation-cicerone/README.md new file mode 100644 index 0000000..bca41e1 --- /dev/null +++ b/navigation-cicerone/README.md @@ -0,0 +1,4 @@ +navigation-cicerone +==== + +TODO: rewrite dependencies diff --git a/navigation-cicerone/build.gradle.kts b/navigation-cicerone/build.gradle.kts new file mode 100644 index 0000000..16b2d76 --- /dev/null +++ b/navigation-cicerone/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG) +} + +dependencies { + implementationModule(Module.Core.DI) + implementation(Library.CICERONE) + fragment() + dagger(withAssistedInject = false) +} diff --git a/navigation-cicerone/src/main/AndroidManifest.xml b/navigation-cicerone/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a6abf81 --- /dev/null +++ b/navigation-cicerone/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + 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..2f2d1c9 --- /dev/null +++ b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/CiceroneTuner.kt @@ -0,0 +1,44 @@ +package ru.touchin.roboswag.navigation_cicerone + +import androidx.annotation.IdRes +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import ru.terrakok.cicerone.NavigatorHolder +import ru.terrakok.cicerone.android.support.SupportAppNavigator + +class CiceroneTuner( + private val activity: FragmentActivity, + private val navigatorHolder: NavigatorHolder, + @IdRes private val fragmentContainerId: Int, + private val fragmentManager: FragmentManager? = null +) : LifecycleObserver { + + val navigator by lazy(this::createNavigator) + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun addNavigator() { + navigatorHolder.setNavigator(navigator) + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun removeNavigator() { + navigatorHolder.removeNavigator() + } + + private fun createNavigator() = if (fragmentManager != null) { + SupportAppNavigator( + activity, + fragmentManager, + fragmentContainerId + ) + } else { + SupportAppNavigator( + activity, + fragmentContainerId + ) + } + +} 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..239c943 --- /dev/null +++ b/navigation-cicerone/src/main/java/ru/touchin/roboswag/navigation_cicerone/flow/FlowFragment.kt @@ -0,0 +1,46 @@ +package ru.touchin.roboswag.navigation_cicerone.flow + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import ru.terrakok.cicerone.NavigatorHolder +import ru.terrakok.cicerone.Router +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() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewLifecycleOwner.lifecycle.addObserver( + CiceroneTuner( + activity = requireActivity(), + navigatorHolder = navigatorHolder, + fragmentContainerId = R.id.flow_parent, + fragmentManager = childFragmentManager + ) + ) + } + + 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..27864ac --- /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.mvi_arch.di.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/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.kts b/pagination/build.gradle.kts new file mode 100644 index 0000000..53f07c6 --- /dev/null +++ b/pagination/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG) +} + +dependencies { + mvi() + materialDesign() + recyclerView() + implementationModule(Module.RoboSwag.KOTLIN_EXTENSIONS) + implementationModule(Module.RoboSwag.VIEWS) + implementationModule(Module.RoboSwag.UTILS) +} 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/PaginationAdapter.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt new file mode 100644 index 0000000..13b8d37 --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt @@ -0,0 +1,39 @@ +package ru.touchin.roboswag.pagination + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ru.touchin.adapters.AdapterDelegate +import ru.touchin.adapters.DelegationListAdapter +import ru.touchin.mvi_test.core_ui.pagination.ProgressItem + +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, isPageProgress: Boolean) { + submitList(data + listOfNotNull(ProgressItem.takeIf { isPageProgress })) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { + super.onBindViewHolder(holder, position, payloads) + if (!fullData && position >= itemCount - 10) nextPageCallback.invoke() + } + +} 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..7311245 --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt @@ -0,0 +1,76 @@ +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 +import ru.touchin.mvi_test.core_ui.pagination.Paginator + +// 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.Empty -> { + adapter.update(emptyList(), false) + } + is Paginator.State.EmptyProgress -> { + adapter.update(emptyList(), false) + } + is Paginator.State.EmptyError -> { + adapter.update(emptyList(), false) + } + is Paginator.State.Data<*> -> { + adapter.update(state.data as List, false) + } + is Paginator.State.Refresh<*> -> { + adapter.update(state.data as List, false) + } + is Paginator.State.NewPageProgress<*> -> { + adapter.update(state.data as List, true) + } + is Paginator.State.FullData<*> -> { + adapter.update(state.data as List, false) + } + } + } + } + +} 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..7081d8e --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -0,0 +1,128 @@ +package ru.touchin.mvi_test.core_ui.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 showError: (Error) -> Unit, + private val loadPage: suspend (Int) -> List +) : 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) : 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() + } + + 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 -> { + if (items.isEmpty()) { + State.Empty + } else { + State.Data(0, items) + } + } + is State.Refresh<*> -> { + if (items.isEmpty()) { + State.Empty + } else { + State.Data(0, items) + } + } + is State.NewPageProgress<*> -> { + if (items.isEmpty()) { + State.FullData(currentState.pageCount, currentState.data) + } 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<*> -> { + showError(Error.RefreshFailed) + State.Data(currentState.pageCount, currentState.data) + } + is State.NewPageProgress<*> -> { + showError(Error.NewPageFailed) + State.Data(currentState.pageCount, currentState.data) + } + 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..093ddfe --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt @@ -0,0 +1,22 @@ +package ru.touchin.roboswag.pagination + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import ru.touchin.adapters.ItemAdapterDelegate +import ru.touchin.mvi_arch.core_pagination.R +import ru.touchin.mvi_test.core_ui.pagination.ProgressItem +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..7e46cec --- /dev/null +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt @@ -0,0 +1,3 @@ +package ru.touchin.mvi_test.core_ui.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..b022ded --- /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. + From 73453070050363058cd05648362723b44b30d9a6 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Sun, 28 Jun 2020 18:40:28 +0300 Subject: [PATCH 073/154] move di scopes to roboswag --- navigation-base/build.gradle | 8 ++++++++ .../roboswag/navigation_base/scopes/FeatureScope.kt | 6 ++++++ .../roboswag/navigation_base/scopes/FragmentScope.kt | 6 ++++++ .../roboswag/navigation_base/scopes/UserLoggedScope.kt | 6 ++++++ navigation-cicerone/build.gradle.kts | 2 +- .../navigation_cicerone/flow/FlowNavigationModule.kt | 2 +- 6 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FeatureScope.kt create mode 100644 navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FragmentScope.kt create mode 100644 navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/UserLoggedScope.kt diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index 5f1a068..0b97055 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -12,6 +12,8 @@ dependencies { implementation 'androidx.multidex:multidex' + implementation "com.google.dagger:dagger" + implementation 'net.danlew:android.joda' implementation "org.jetbrains.kotlin:kotlin-stdlib" @@ -35,6 +37,12 @@ dependencies { } } + implementation("com.google.dagger:dagger") { + version { + require '2.10' + } + } + implementation("net.danlew:android.joda") { version { require '2.10.0' diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FeatureScope.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FeatureScope.kt new file mode 100644 index 0000000..c0e8bc8 --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FeatureScope.kt @@ -0,0 +1,6 @@ +package ru.touchin.roboswag.navigation_base.scopes + +import javax.inject.Scope + +@Scope +annotation class FeatureScope diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FragmentScope.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FragmentScope.kt new file mode 100644 index 0000000..d22943f --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/FragmentScope.kt @@ -0,0 +1,6 @@ +package ru.touchin.roboswag.navigation_base.scopes + +import javax.inject.Scope + +@Scope +annotation class FragmentScope diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/UserLoggedScope.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/UserLoggedScope.kt new file mode 100644 index 0000000..d429d93 --- /dev/null +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/scopes/UserLoggedScope.kt @@ -0,0 +1,6 @@ +package ru.touchin.roboswag.navigation_base.scopes + +import javax.inject.Scope + +@Scope +annotation class UserLoggedScope diff --git a/navigation-cicerone/build.gradle.kts b/navigation-cicerone/build.gradle.kts index 16b2d76..a4dc829 100644 --- a/navigation-cicerone/build.gradle.kts +++ b/navigation-cicerone/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - implementationModule(Module.Core.DI) + implementationModule(Module.RoboSwag.NAVIGATION_BASE) implementation(Library.CICERONE) fragment() dagger(withAssistedInject = false) 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 index 27864ac..2592563 100644 --- 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 @@ -5,7 +5,7 @@ import dagger.Provides import ru.terrakok.cicerone.Cicerone import ru.terrakok.cicerone.NavigatorHolder import ru.terrakok.cicerone.Router -import ru.touchin.mvi_arch.di.FeatureScope +import ru.touchin.roboswag.navigation_base.scopes.FeatureScope @Module class FlowNavigationModule { From fcc1105b047e3b83bb0753b059a75e4eb53a51a9 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Tue, 7 Jul 2020 15:20:43 +0300 Subject: [PATCH 074/154] migrate crashlytics to firebase and remove multidex --- logging/build.gradle | 6 ++--- .../core/utils/CrashlyticsLogProcessor.java | 18 +++++++++------ navigation-base/build.gradle | 15 +++---------- .../roboswag/navigation_base/TouchinApp.java | 22 ++++--------------- 4 files changed, 21 insertions(+), 40 deletions(-) diff --git a/logging/build.gradle b/logging/build.gradle index 1cc7977..57d4392 100644 --- a/logging/build.gradle +++ b/logging/build.gradle @@ -3,7 +3,7 @@ apply from: "../android-configs/lib-config.gradle" dependencies { implementation "androidx.annotation:annotation" - implementation "com.crashlytics.sdk.android:crashlytics" + implementation "com.google.firebase:firebase-crashlytics" constraints { implementation("androidx.annotation:annotation") { @@ -12,9 +12,9 @@ dependencies { } } - implementation("com.crashlytics.sdk.android:crashlytics") { + implementation("com.google.firebase:firebase-crashlytics") { version { - require '2.10.0' + require '17.1.0' } } } 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 index 1234660..5dd9970 100644 --- a/logging/src/main/java/ru/touchin/roboswag/core/utils/CrashlyticsLogProcessor.java +++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/CrashlyticsLogProcessor.java @@ -2,7 +2,7 @@ package ru.touchin.roboswag.core.utils; import android.util.Log; -import com.crashlytics.android.Crashlytics; +import com.google.firebase.crashlytics.FirebaseCrashlytics; import java.util.ArrayList; import java.util.List; @@ -17,13 +17,17 @@ import ru.touchin.roboswag.core.log.LogProcessor; public class CrashlyticsLogProcessor extends LogProcessor { @NonNull - private final Crashlytics crashlytics; + private final FirebaseCrashlytics crashlytics; - public CrashlyticsLogProcessor(@NonNull final Crashlytics 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, @@ -31,17 +35,17 @@ public class CrashlyticsLogProcessor extends LogProcessor { @NonNull final String message, @Nullable final Throwable throwable) { if (group == LcGroup.UI_LIFECYCLE) { - crashlytics.core.log(level.getPriority(), tag, message); + 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.core.log(level.getPriority(), tag, message); - crashlytics.core.logException(throwable); + crashlytics.log(getLogMessage(level.getPriority(), tag, message)); + crashlytics.recordException(throwable); } else { final ShouldNotHappenException exceptionToLog = new ShouldNotHappenException(tag + ':' + message); reduceStackTrace(exceptionToLog); - crashlytics.core.logException(exceptionToLog); + crashlytics.recordException(exceptionToLog); } } } diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index 0b97055..2b9ae58 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -10,8 +10,6 @@ dependencies { implementation project(":utils") implementation project(":logging") - implementation 'androidx.multidex:multidex' - implementation "com.google.dagger:dagger" implementation 'net.danlew:android.joda' @@ -26,16 +24,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8" implementation "androidx.lifecycle:lifecycle-livedata-ktx" - implementation("com.crashlytics.sdk.android:crashlytics") { - transitive = true - } + implementation "com.google.firebase:firebase-crashlytics" constraints { - implementation("androidx.multidex:multidex") { - version { - require '2.0.0' - } - } implementation("com.google.dagger:dagger") { version { @@ -67,9 +58,9 @@ dependencies { } } - implementation("com.crashlytics.sdk.android:crashlytics") { + implementation("com.google.firebase:firebase-crashlytics") { version { - require '2.10.0' + require '17.1.0' } } 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 index ca05782..60cabfd 100644 --- 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 @@ -20,17 +20,12 @@ package ru.touchin.roboswag.navigation_base; import android.app.Application; -import android.content.Context; import android.os.StrictMode; -import android.util.Log; -import com.crashlytics.android.Crashlytics; +import com.google.firebase.crashlytics.FirebaseCrashlytics; import net.danlew.android.joda.JodaTimeAndroid; -import androidx.annotation.NonNull; -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; @@ -43,12 +38,6 @@ import ru.touchin.roboswag.core.utils.CrashlyticsLogProcessor; */ 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(); @@ -59,16 +48,13 @@ public abstract class TouchinApp extends Application { LcGroup.UI_LIFECYCLE.disable(); } else { try { - final Crashlytics crashlytics = new Crashlytics(); - Fabric.with(this, crashlytics); - Fabric.getLogger().setLogLevel(Log.ERROR); + 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" - + "compile('com.crashlytics.sdk.android:crashlytics:+@aar') {\n" - + " transitive = true;\n" - + "}\n" + + "com.google.firebase:firebase-crashlytics\n" + "to your build.gradle?", error); } } From 13ef85a371ce0b0bb0a59e8bd27582abaaca196a Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Tue, 7 Jul 2020 15:22:05 +0300 Subject: [PATCH 075/154] reformat code --- .../ru/touchin/roboswag/mvi_arch/marker/StateChange.kt | 3 +-- .../java/ru/touchin/roboswag/core/utils/StringUtils.java | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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 index 93f3378..1d5da27 100644 --- 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 @@ -1,4 +1,3 @@ package ru.touchin.roboswag.mvi_arch.marker -interface StateChange { -} +interface StateChange 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(); From c0545e88f735ec542dab07e026d7f5305f7d7625 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Jul 2020 15:19:23 +0300 Subject: [PATCH 076/154] Added show/hide keyboard input ext. --- .../src/main/java/ru/touchin/extensions/View.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 5602cef..6c59e27 100644 --- a/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt +++ b/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt @@ -1,7 +1,9 @@ package ru.touchin.extensions +import android.content.Context import android.os.Build import android.view.View +import android.view.inputmethod.InputMethodManager import ru.touchin.utils.ActionThrottler const val RIPPLE_EFFECT_DELAY_MS = 150L @@ -22,3 +24,15 @@ fun View.setOnRippleClickListener(listener: () -> Unit) { setOnClickListener { listener() } } } + +fun View.showSoftInput() { + requestFocus() + val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) +} + +fun View.hideSoftInput() { + clearFocus() + val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.hideSoftInputFromWindow(windowToken, 0) +} \ No newline at end of file From 6dfbe07a5d664e23a789b6a594110adeafbecdac Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Jul 2020 16:17:26 +0300 Subject: [PATCH 077/154] updated softKeyboard extensions --- .../main/java/ru/touchin/extensions/View.kt | 12 ----- .../utils/storables/PreferenceUtils.java | 2 +- .../roboswag/components/utils/UiUtils.kt | 16 +++++++ .../components/utils/ViewExtensions.kt | 48 +++++++++++++++++++ 4 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 utils/src/main/java/ru/touchin/roboswag/components/utils/ViewExtensions.kt 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 6c59e27..52fdfa5 100644 --- a/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt +++ b/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt @@ -24,15 +24,3 @@ fun View.setOnRippleClickListener(listener: () -> Unit) { setOnClickListener { listener() } } } - -fun View.showSoftInput() { - requestFocus() - val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) -} - -fun View.hideSoftInput() { - clearFocus() - val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.hideSoftInputFromWindow(windowToken, 0) -} \ No newline at end of file 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 1204a53..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 @@ -38,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/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..d21acd6 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 @@ -205,6 +205,10 @@ object UiUtils { * @param view [View] to get ID from; * @return Readable ID. */ + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("view.getViewIdString()") + ) fun getViewIdString(view: View): String = try { view.resources.getResourceName(view.id) } catch (exception: Resources.NotFoundException) { @@ -214,6 +218,10 @@ object UiUtils { /** * Hides device keyboard for target activity. */ + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("activity.hideSoftInput()") + ) fun hideSoftInput(activity: Activity) { activity.currentFocus?.let(this::hideSoftInput) } @@ -221,6 +229,10 @@ object UiUtils { /** * Hides device keyboard for target view. */ + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("view.hideSoftInput()") + ) fun hideSoftInput(view: View) { view.clearFocus() val inputManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager @@ -234,6 +246,10 @@ object UiUtils { * * @param view View to get focus for input from keyboard. */ + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("view.showSoftInput()") + ) fun showSoftInput(view: View) { view.requestFocus() val inputManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 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..041f926 --- /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) +} \ No newline at end of file From 110ad3b433f434a1a002d7c9607fd73b0039164e Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Jul 2020 16:34:08 +0300 Subject: [PATCH 078/154] fixed review --- .../roboswag/components/utils/UiUtils.kt | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) 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 d21acd6..7592cc1 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,7 +23,6 @@ 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 @@ -33,7 +32,6 @@ 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 @@ -209,11 +207,7 @@ object UiUtils { message = "Use extension instead", replaceWith = ReplaceWith("view.getViewIdString()") ) - fun getViewIdString(view: View): String = try { - view.resources.getResourceName(view.id) - } catch (exception: Resources.NotFoundException) { - view.id.toString() - } + fun getViewIdString(view: View): String = view.getViewIdString() /** * Hides device keyboard for target activity. @@ -222,9 +216,8 @@ object UiUtils { message = "Use extension instead", replaceWith = ReplaceWith("activity.hideSoftInput()") ) - fun hideSoftInput(activity: Activity) { - activity.currentFocus?.let(this::hideSoftInput) - } + fun hideSoftInput(activity: Activity) = activity.hideSoftInput() + /** * Hides device keyboard for target view. @@ -233,11 +226,7 @@ object UiUtils { message = "Use extension instead", replaceWith = ReplaceWith("view.hideSoftInput()") ) - fun hideSoftInput(view: View) { - view.clearFocus() - val inputManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.hideSoftInputFromWindow(view.windowToken, 0) - } + fun hideSoftInput(view: View) = view.hideSoftInput() /** * Shows device keyboard over [Activity] and focuses [View]. @@ -250,11 +239,7 @@ object UiUtils { message = "Use extension instead", replaceWith = ReplaceWith("view.showSoftInput()") ) - fun showSoftInput(view: View) { - view.requestFocus() - val inputManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) - } + fun showSoftInput(view: View) = view.showSoftInput() } From b96269ba89b0867ce371687582cafa65ee2c21b7 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Jul 2020 16:35:20 +0300 Subject: [PATCH 079/154] removed imports --- kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt | 2 -- 1 file changed, 2 deletions(-) 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 52fdfa5..5602cef 100644 --- a/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt +++ b/kotlin-extensions/src/main/java/ru/touchin/extensions/View.kt @@ -1,9 +1,7 @@ package ru.touchin.extensions -import android.content.Context import android.os.Build import android.view.View -import android.view.inputmethod.InputMethodManager import ru.touchin.utils.ActionThrottler const val RIPPLE_EFFECT_DELAY_MS = 150L From cf09511e1612ca0b699e495340056a8ae5a900af Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 9 Jul 2020 11:22:14 +0300 Subject: [PATCH 080/154] Metric extensions --- .../components/utils/MetricExtensions.kt | 55 +++++++++++++++++++ .../roboswag/components/utils/UiUtils.kt | 35 +++++------- 2 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt 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..16f4772 --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt @@ -0,0 +1,55 @@ +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. + */ +fun Int.toPixels() = (this * Resources.getSystem().displayMetrics.density).toInt() + + +/** + * Simply converts Dp to pixels. + * + * @return Size in pixels. + */ +fun Float.toPixels() = this * Resources.getSystem().displayMetrics.density + + +/** + * Simply converts pixels to Dp. + * + * @return Size in dp. + */ +fun Int.toDp() = (this / Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file 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 7592cc1..ceb7a39 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 @@ -25,7 +25,6 @@ import android.content.Context import android.content.Intent import android.os.Build import android.util.DisplayMetrics -import android.util.TypedValue import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.LayoutInflater @@ -85,22 +84,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. @@ -109,10 +97,17 @@ 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.toPixels()") + ) + fun dpToPixels(context: Context, sizeInDp: Float): Float = sizeInDp.toPixels() - fun pixelsToDp(context: Context, pixels: Int): Int = (pixels * getDisplayMetrics(context).density + 0.5f).toInt() + @Deprecated( + message = "Use extension instead", + replaceWith = ReplaceWith("pixels.toDp()") + ) + fun pixelsToDp(context: Context, pixels: Int): Int = pixels.toDp() } From b2084ed4a1347b344956fbde329395ad159780fb Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 9 Jul 2020 11:38:03 +0300 Subject: [PATCH 081/154] fixed review --- .../touchin/roboswag/components/utils/MetricExtensions.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 index 16f4772..5d0ce66 100644 --- a/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt @@ -36,20 +36,18 @@ fun Context.getDisplayMetrics(): DisplayMetrics { * * @return Size in pixels. */ -fun Int.toPixels() = (this * Resources.getSystem().displayMetrics.density).toInt() - +fun Int.toPixels(): Int = (this * Resources.getSystem().displayMetrics.density).toInt() /** * Simply converts Dp to pixels. * * @return Size in pixels. */ -fun Float.toPixels() = this * Resources.getSystem().displayMetrics.density - +fun Float.toPixels(): Float = this * Resources.getSystem().displayMetrics.density /** * Simply converts pixels to Dp. * * @return Size in dp. */ -fun Int.toDp() = (this / Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file +fun Int.toDp(): Int = (this / Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file From 4b6fc34979eaea7fd71f5d914e98d0a85c780d5f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 9 Jul 2020 13:43:08 +0300 Subject: [PATCH 082/154] fixed review --- .../roboswag/components/utils/MetricExtensions.kt | 9 ++++++--- .../java/ru/touchin/roboswag/components/utils/UiUtils.kt | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) 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 index 5d0ce66..0f627f3 100644 --- a/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt @@ -36,18 +36,21 @@ fun Context.getDisplayMetrics(): DisplayMetrics { * * @return Size in pixels. */ -fun Int.toPixels(): Int = (this * Resources.getSystem().displayMetrics.density).toInt() +val Int.px: Int + get() = (this * Resources.getSystem().displayMetrics.density).toInt() /** * Simply converts Dp to pixels. * * @return Size in pixels. */ -fun Float.toPixels(): Float = this * Resources.getSystem().displayMetrics.density +val Float.px: Float + get() = this * Resources.getSystem().displayMetrics.density /** * Simply converts pixels to Dp. * * @return Size in dp. */ -fun Int.toDp(): Int = (this / Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file +val Int.dp: Int + get() = (this / Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file 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 ceb7a39..9ea02be 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 @@ -101,13 +101,13 @@ object UiUtils { message = "Use extension instead", replaceWith = ReplaceWith("sizeInDp.toPixels()") ) - fun dpToPixels(context: Context, sizeInDp: Float): Float = sizeInDp.toPixels() + fun dpToPixels(context: Context, sizeInDp: Float): Float = sizeInDp.px @Deprecated( message = "Use extension instead", replaceWith = ReplaceWith("pixels.toDp()") ) - fun pixelsToDp(context: Context, pixels: Int): Int = pixels.toDp() + fun pixelsToDp(context: Context, pixels: Int): Int = pixels.dp } From 98ce1556f8970653b5625815ee544a076bcee1fa Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Thu, 9 Jul 2020 14:31:38 +0300 Subject: [PATCH 083/154] add back navigation handling --- .../roboswag/navigation_cicerone/flow/FlowFragment.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 index 239c943..221e72a 100644 --- 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 @@ -2,6 +2,7 @@ package ru.touchin.roboswag.navigation_cicerone.flow import android.os.Bundle import android.view.View +import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import ru.terrakok.cicerone.NavigatorHolder import ru.terrakok.cicerone.Router @@ -40,6 +41,12 @@ abstract class FlowFragment : Fragment(R.layout.fragment_flow) { fragmentManager = childFragmentManager ) ) + + requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + router.exit() + } + }) } abstract fun getLaunchScreen(): SupportAppScreen From a49eea6aa4229fad31e2e2239a165174a361e4a3 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Thu, 9 Jul 2020 14:32:34 +0300 Subject: [PATCH 084/154] update code with new extensions --- .../roboswag/navigation_base/SimpleActionBarDrawerToggle.kt | 4 ++-- .../keyboard_resizeable/KeyboardResizeableFragment.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt index 166a561..cf2e2b3 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/SimpleActionBarDrawerToggle.kt @@ -25,7 +25,7 @@ import android.view.View import androidx.appcompat.app.ActionBarDrawerToggle import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.FragmentManager -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 @@ -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/keyboard_resizeable/KeyboardResizeableFragment.kt b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt index ed13fbf..e43d405 100644 --- a/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt +++ b/navigation-base/src/main/java/ru/touchin/roboswag/navigation_base/keyboard_resizeable/KeyboardResizeableFragment.kt @@ -6,7 +6,7 @@ import android.os.Parcelable import android.view.View import androidx.annotation.LayoutRes import androidx.lifecycle.LifecycleObserver -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 @@ -21,7 +21,7 @@ abstract class KeyboardResizeableFragment Date: Thu, 9 Jul 2020 17:45:01 +0300 Subject: [PATCH 085/154] wrong override, tried to check equality with boolean type when wrapping with navigation container was requested --- .../BottomNavigationController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index babcf94..92d5ec8 100644 --- 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 @@ -38,8 +38,8 @@ class BottomNavigationController( if (wrapWithNavigationContainer) { super.isTabClass(tab, fragment) } else { - (fragment as ViewControllerFragment<*, *>).viewControllerClass - } === tab.cls + (fragment as ViewControllerFragment<*, *>).viewControllerClass === tab.cls + } } From e45558e3b7c737d2e7860e9a0a547e1a0b9fc226 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Thu, 9 Jul 2020 18:50:53 +0300 Subject: [PATCH 086/154] make cicerone tuner not responsible for creating navigator --- .../navigation_cicerone/CiceroneTuner.kt | 24 ++----------------- .../navigation_cicerone/flow/FlowFragment.kt | 19 ++++++++++----- 2 files changed, 15 insertions(+), 28 deletions(-) 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 index 2f2d1c9..ba525d3 100644 --- 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 @@ -1,23 +1,16 @@ package ru.touchin.roboswag.navigation_cicerone -import androidx.annotation.IdRes -import androidx.fragment.app.FragmentActivity -import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent +import ru.terrakok.cicerone.Navigator import ru.terrakok.cicerone.NavigatorHolder -import ru.terrakok.cicerone.android.support.SupportAppNavigator class CiceroneTuner( - private val activity: FragmentActivity, private val navigatorHolder: NavigatorHolder, - @IdRes private val fragmentContainerId: Int, - private val fragmentManager: FragmentManager? = null + private val navigator: Navigator ) : LifecycleObserver { - val navigator by lazy(this::createNavigator) - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun addNavigator() { navigatorHolder.setNavigator(navigator) @@ -28,17 +21,4 @@ class CiceroneTuner( navigatorHolder.removeNavigator() } - private fun createNavigator() = if (fragmentManager != null) { - SupportAppNavigator( - activity, - fragmentManager, - fragmentContainerId - ) - } else { - SupportAppNavigator( - activity, - fragmentContainerId - ) - } - } 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 index 221e72a..8a90fd7 100644 --- 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 @@ -3,9 +3,12 @@ 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 @@ -34,12 +37,7 @@ abstract class FlowFragment : Fragment(R.layout.fragment_flow) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycle.addObserver( - CiceroneTuner( - activity = requireActivity(), - navigatorHolder = navigatorHolder, - fragmentContainerId = R.id.flow_parent, - fragmentManager = childFragmentManager - ) + CiceroneTuner(navigatorHolder = navigatorHolder, navigator = createNavigator()) ) requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { @@ -49,5 +47,14 @@ abstract class FlowFragment : Fragment(R.layout.fragment_flow) { }) } + open fun createNavigator(): Navigator = SupportAppNavigator( + requireActivity(), + childFragmentManager, + getFragmentContainerId() + ) + + @IdRes + protected fun getFragmentContainerId(): Int = R.id.flow_parent + abstract fun getLaunchScreen(): SupportAppScreen } From 141f86f5338309589b7095ea0f6a51caef53cc6e Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Thu, 9 Jul 2020 18:52:44 +0300 Subject: [PATCH 087/154] fix replace deprecation in UiUtils.kt --- .../java/ru/touchin/roboswag/components/utils/UiUtils.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 9ea02be..c18332a 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 @@ -99,13 +99,13 @@ object UiUtils { */ @Deprecated( message = "Use extension instead", - replaceWith = ReplaceWith("sizeInDp.toPixels()") + replaceWith = ReplaceWith("sizeInDp.px") ) fun dpToPixels(context: Context, sizeInDp: Float): Float = sizeInDp.px @Deprecated( message = "Use extension instead", - replaceWith = ReplaceWith("pixels.toDp()") + replaceWith = ReplaceWith("pixels.px") ) fun pixelsToDp(context: Context, pixels: Int): Int = pixels.dp @@ -122,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(activity: Activity): Int = 56f.px.toInt() /** * Returns status bar (on top where system info is) common height. From 2c208751457a9e4761beecaf88a7831b4bf2e5b2 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Thu, 9 Jul 2020 18:55:14 +0300 Subject: [PATCH 088/154] fix replace deprecation in uiutils 2 --- .../main/java/ru/touchin/roboswag/components/utils/UiUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c18332a..79f1783 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 @@ -105,7 +105,7 @@ object UiUtils { @Deprecated( message = "Use extension instead", - replaceWith = ReplaceWith("pixels.px") + replaceWith = ReplaceWith("pixels.dp") ) fun pixelsToDp(context: Context, pixels: Int): Int = pixels.dp From 75add732ec807744a400a4f99e6898fd58659fab Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 9 Jul 2020 21:14:31 +0300 Subject: [PATCH 089/154] fixed static --- .../ru/touchin/roboswag/components/utils/MetricExtensions.kt | 2 +- .../main/java/ru/touchin/roboswag/components/utils/UiUtils.kt | 4 ++-- .../ru/touchin/roboswag/components/utils/ViewExtensions.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 index 0f627f3..ff91a66 100644 --- a/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/MetricExtensions.kt @@ -53,4 +53,4 @@ val Float.px: Float * @return Size in dp. */ val Int.dp: Int - get() = (this / Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file + 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 9ea02be..477d864 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 @@ -75,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. @@ -101,12 +99,14 @@ object UiUtils { message = "Use extension instead", replaceWith = ReplaceWith("sizeInDp.toPixels()") ) + @Suppress("detekt.UnusedPrivateMember") fun dpToPixels(context: Context, sizeInDp: Float): Float = sizeInDp.px @Deprecated( message = "Use extension instead", replaceWith = ReplaceWith("pixels.toDp()") ) + @Suppress("detekt.UnusedPrivateMember") fun pixelsToDp(context: Context, pixels: Int): Int = pixels.dp } 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 index 041f926..9c65ac7 100644 --- a/utils/src/main/java/ru/touchin/roboswag/components/utils/ViewExtensions.kt +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/ViewExtensions.kt @@ -45,4 +45,4 @@ fun View.showSoftInput() { requestFocus() val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) -} \ No newline at end of file +} From eb697e1614cf9e2db36f233c16cdcf5735b1acb1 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Fri, 10 Jul 2020 13:21:58 +0300 Subject: [PATCH 090/154] fixed code review --- .../main/java/ru/touchin/roboswag/components/utils/UiUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 79f1783..4a2fe30 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 @@ -122,7 +122,7 @@ object UiUtils { * @param activity Activity of action bar; * @return Height of action bar. */ - fun getActionBarHeight(activity: Activity): Int = 56f.px.toInt() + fun getActionBarHeight(activity: Activity): Int = 56.px /** * Returns status bar (on top where system info is) common height. From 3e0f50e0bdc55b85a384ca51ae2e09c74689e744 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sun, 12 Jul 2020 21:07:27 +0500 Subject: [PATCH 091/154] Added device utils extensions --- .../ru/touchin/templates/DeviceUtils.java | 60 +++---------------- .../templates/DeviceUtilsExtensions.kt | 52 ++++++++++++++++ 2 files changed, 60 insertions(+), 52 deletions(-) create mode 100644 utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt diff --git a/utils/src/main/java/ru/touchin/templates/DeviceUtils.java b/utils/src/main/java/ru/touchin/templates/DeviceUtils.java index 4a48f10..b4663bf 100644 --- a/utils/src/main/java/ru/touchin/templates/DeviceUtils.java +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtils.java @@ -19,15 +19,10 @@ 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. @@ -40,28 +35,10 @@ public final class DeviceUtils { * @param context Application context * @return Active network type {@link NetworkType} */ + @Deprecated @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; - } + return DeviceUtilsExtensionsKt.getNetworkType(context); } /** @@ -70,36 +47,15 @@ public final class DeviceUtils { * @param context Application context * @return true if network connected, false otherwise. */ + @Deprecated public static boolean isNetworkConnected(@NonNull final Context context) { - return getNetworkType(context) != NetworkType.NONE; + return DeviceUtilsExtensionsKt.isNetworkConnected(context); } + @Deprecated @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; - } + static NetworkType getMobileNetworkType(@NonNull final NetworkInfo info) { + return DeviceUtilsExtensionsKt.getMobileNetworkType(info); } private DeviceUtils() { 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..2259cf3 --- /dev/null +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt @@ -0,0 +1,52 @@ +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 android.telephony.TelephonyManager +import ru.touchin.templates.DeviceUtils.NetworkType + +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 + } +} + +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 + } From b254a07eb505a27c9a6fa16f2b29d790fa317493 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 13 Jul 2020 19:55:47 +0500 Subject: [PATCH 092/154] added biometric extension --- .../touchin/templates/DeviceUtilsExtensions.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt index 2259cf3..f840b19 100644 --- a/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt @@ -4,10 +4,14 @@ 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.net.NetworkInfo +import android.os.Build import android.os.Process import android.telephony.TelephonyManager +import androidx.annotation.RequiresPermission +import androidx.core.hardware.fingerprint.FingerprintManagerCompat import ru.touchin.templates.DeviceUtils.NetworkType fun Context.isNetworkConnected(): Boolean = getNetworkType() != NetworkType.NONE @@ -50,3 +54,16 @@ fun getMobileNetworkType(info: NetworkInfo): NetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN -> NetworkType.UNKNOWN else -> NetworkType.UNKNOWN } + + +@RequiresPermission(anyOf = [Manifest.permission.USE_FINGERPRINT, Manifest.permission.USE_BIOMETRIC]) +fun Context.canAuthenticateWithBiometrics(): Boolean { + return 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 + } +} From c62a905120dd1aaaee80fa2cd5295444135722d1 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Wed, 15 Jul 2020 12:28:55 +0300 Subject: [PATCH 093/154] fix static analysis --- .../main/java/ru/touchin/roboswag/components/utils/UiUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 14eee85..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 @@ -122,7 +122,7 @@ object UiUtils { * @param activity Activity of action bar; * @return Height of action bar. */ - fun getActionBarHeight(activity: Activity): Int = 56.px + fun getActionBarHeight(): Int = 56.px /** * Returns status bar (on top where system info is) common height. From 948538e273dd597a5b561fbff54350de2803c476 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Wed, 15 Jul 2020 12:43:25 +0300 Subject: [PATCH 094/154] fixed static analysis --- .../templates/DeviceUtilsExtensions.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt index f840b19..9c1eb24 100644 --- a/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt @@ -10,6 +10,7 @@ import android.net.NetworkInfo import android.os.Build import android.os.Process import android.telephony.TelephonyManager +import androidx.annotation.RequiresApi import androidx.annotation.RequiresPermission import androidx.core.hardware.fingerprint.FingerprintManagerCompat import ru.touchin.templates.DeviceUtils.NetworkType @@ -56,14 +57,14 @@ fun getMobileNetworkType(info: NetworkInfo): NetworkType = } +@RequiresApi(Build.VERSION_CODES.M) +@Suppress("InlinedApi") @RequiresPermission(anyOf = [Manifest.permission.USE_FINGERPRINT, Manifest.permission.USE_BIOMETRIC]) -fun Context.canAuthenticateWithBiometrics(): Boolean { - return 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 - } +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 } From 8afda9e4052a60ae68170005836acf9165878273 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 28 Jul 2020 15:07:21 +0500 Subject: [PATCH 095/154] added extensions --- .../roboswag/components/utils/ContextExtensions.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 utils/src/main/java/ru/touchin/roboswag/components/utils/ContextExtensions.kt 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) From 61d7a4a0754e20bdfd2dfa641e1075c0dd3908e1 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 30 Jul 2020 13:41:12 +0500 Subject: [PATCH 096/154] removed distinct --- .../java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 71ef368..0f08467 100644 --- 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 @@ -1,6 +1,7 @@ package ru.touchin.roboswag.mvi_arch.core import android.os.Parcelable +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.Transformations @@ -36,7 +37,7 @@ abstract class MviViewModel protected val currentState: State get() = state.value ?: initialState From c6e9b718ae86d308eaab9745bad8a4fa84831f98 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 30 Jul 2020 13:41:12 +0500 Subject: [PATCH 097/154] Revert "removed distinct" This reverts commit 61d7a4a0754e20bdfd2dfa641e1075c0dd3908e1. --- .../java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 0f08467..71ef368 100644 --- 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 @@ -1,7 +1,6 @@ package ru.touchin.roboswag.mvi_arch.core import android.os.Parcelable -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.Transformations @@ -37,7 +36,7 @@ abstract class MviViewModel + internal val state = Transformations.distinctUntilChanged(_state) protected val currentState: State get() = state.value ?: initialState From abf95d4abb7738db97f82a764dcee8e3b38d45e8 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 20 Aug 2020 14:17:39 +0500 Subject: [PATCH 098/154] fixed state race --- .../main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 71ef368..d3df82c 100644 --- 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 @@ -39,7 +39,7 @@ abstract class MviViewModel Date: Thu, 20 Aug 2020 15:09:17 +0500 Subject: [PATCH 099/154] fixed static --- .../ru/touchin/templates/DeviceUtils.java | 110 ----------------- .../java/ru/touchin/templates/DeviceUtils.kt | 115 ++++++++++++++++++ .../templates/DeviceUtilsExtensions.kt | 26 +--- 3 files changed, 116 insertions(+), 135 deletions(-) delete mode 100644 utils/src/main/java/ru/touchin/templates/DeviceUtils.java create mode 100644 utils/src/main/java/ru/touchin/templates/DeviceUtils.kt 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 b4663bf..0000000 --- a/utils/src/main/java/ru/touchin/templates/DeviceUtils.java +++ /dev/null @@ -1,110 +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.content.Context; -import android.net.NetworkInfo; - -import androidx.annotation.NonNull; - -/** - * 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} - */ - @Deprecated - @NonNull - public static NetworkType getNetworkType(@NonNull final Context context) { - return DeviceUtilsExtensionsKt.getNetworkType(context); - } - - /** - * Detects if some network connected. - * - * @param context Application context - * @return true if network connected, false otherwise. - */ - @Deprecated - public static boolean isNetworkConnected(@NonNull final Context context) { - return DeviceUtilsExtensionsKt.isNetworkConnected(context); - } - - @Deprecated - @NonNull - static NetworkType getMobileNetworkType(@NonNull final NetworkInfo info) { - return DeviceUtilsExtensionsKt.getMobileNetworkType(info); - } - - 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 index 9c1eb24..ef213a9 100644 --- a/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtilsExtensions.kt @@ -6,14 +6,13 @@ import android.content.Context import android.content.pm.PackageManager import android.hardware.biometrics.BiometricManager import android.net.ConnectivityManager -import android.net.NetworkInfo import android.os.Build import android.os.Process -import android.telephony.TelephonyManager 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 @@ -34,29 +33,6 @@ fun Context.getNetworkType(): NetworkType { } } -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 - } - - @RequiresApi(Build.VERSION_CODES.M) @Suppress("InlinedApi") @RequiresPermission(anyOf = [Manifest.permission.USE_FINGERPRINT, Manifest.permission.USE_BIOMETRIC]) From 5711adcd0ad7dfca71b47a1f2679182b962a4cc6 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 20 Aug 2020 20:52:45 +0500 Subject: [PATCH 100/154] added loading view --- views/build.gradle | 16 +++++ .../ru/touchin/widget/LoadingContentView.kt | 68 ++++++++++++++++++ .../res/drawable/light_button_background.xml | 9 +++ views/src/main/res/font/roboto.ttf | Bin 0 -> 171676 bytes views/src/main/res/font/roboto_medium.ttf | Bin 0 -> 172064 bytes views/src/main/res/layout/progress_view.xml | 64 +++++++++++++++++ views/src/main/res/values/attrs.xml | 4 ++ views/src/main/res/values/colors.xml | 6 ++ views/src/main/res/values/styles.xml | 13 ++++ 9 files changed, 180 insertions(+) create mode 100644 views/src/main/java/ru/touchin/widget/LoadingContentView.kt create mode 100644 views/src/main/res/drawable/light_button_background.xml create mode 100644 views/src/main/res/font/roboto.ttf create mode 100644 views/src/main/res/font/roboto_medium.ttf create mode 100644 views/src/main/res/layout/progress_view.xml create mode 100644 views/src/main/res/values/colors.xml diff --git a/views/build.gradle b/views/build.gradle index 623cb8b..4b38a04 100644 --- a/views/build.gradle +++ b/views/build.gradle @@ -1,9 +1,19 @@ apply from: "../android-configs/lib-config.gradle" +apply plugin: 'kotlin-android' + +android { + buildFeatures { + viewBinding true + } +} dependencies { implementation project(":utils") + implementation project(":kotlin-extensions") implementation project(":logging") + implementation "androidx.annotation:annotation" + implementation "androidx.core:core" implementation "com.google.android.material:material" constraints { @@ -13,4 +23,10 @@ dependencies { } } } + implementation "androidx.core:core-ktx:1.3.1" + implementation "org.jetbrains.kotlin:kotlin-stdlib" +} + +repositories { + mavenCentral() } diff --git a/views/src/main/java/ru/touchin/widget/LoadingContentView.kt b/views/src/main/java/ru/touchin/widget/LoadingContentView.kt new file mode 100644 index 0000000..1222a70 --- /dev/null +++ b/views/src/main/java/ru/touchin/widget/LoadingContentView.kt @@ -0,0 +1,68 @@ +package ru.touchin.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.components.views.R +import ru.touchin.roboswag.components.views.databinding.ProgressViewBinding +import kotlin.properties.Delegates + +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/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 0000000000000000000000000000000000000000..2c97eeadffe1a34bd67d3ff1c3887fd53e22c2ca GIT binary patch literal 171676 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 ZcAgz$Pp~K2ZnnEU$(~HkLK9l*e*jr~ZsGs{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1a7f3b0bba45b7470a4240c3ec67595eeeb02192 GIT binary patch literal 172064 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

h}nbI!7$fg`g%jGMHsI6;dmGA6LaTaO;a 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 NSDLFxS=xl2`X5i?+06g| literal 0 HcmV?d00001 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 06d1d83..bf3eacc 100644 --- a/views/src/main/res/values/attrs.xml +++ b/views/src/main/res/values/attrs.xml @@ -30,4 +30,8 @@ + + + + diff --git a/views/src/main/res/values/colors.xml b/views/src/main/res/values/colors.xml new file mode 100644 index 0000000..9c0944f --- /dev/null +++ b/views/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #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 + + + + + + + From 42e81a694d25ac775c6f086f188261df448a4959 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 20 Aug 2020 20:56:57 +0500 Subject: [PATCH 101/154] removed dep --- views/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/views/build.gradle b/views/build.gradle index 4b38a04..662d92a 100644 --- a/views/build.gradle +++ b/views/build.gradle @@ -12,7 +12,6 @@ dependencies { implementation project(":kotlin-extensions") implementation project(":logging") - implementation "androidx.annotation:annotation" implementation "androidx.core:core" implementation "com.google.android.material:material" From d961fbd29a971cfc04bb13e0b495994338e8161c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 21 Aug 2020 12:37:45 +0500 Subject: [PATCH 102/154] fixed review --- views/build.gradle | 7 ++++++- .../src/main/java/ru/touchin/widget/LoadingContentView.kt | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/views/build.gradle b/views/build.gradle index 662d92a..686aadd 100644 --- a/views/build.gradle +++ b/views/build.gradle @@ -14,6 +14,7 @@ dependencies { implementation "androidx.core:core" implementation "com.google.android.material:material" + implementation "androidx.core:core-ktx" constraints { implementation("com.google.android.material:material") { @@ -21,8 +22,12 @@ dependencies { require '1.0.0' } } + implementation("androidx.core:core-ktx") { + version { + require '1.3.1' + } + } } - implementation "androidx.core:core-ktx:1.3.1" implementation "org.jetbrains.kotlin:kotlin-stdlib" } diff --git a/views/src/main/java/ru/touchin/widget/LoadingContentView.kt b/views/src/main/java/ru/touchin/widget/LoadingContentView.kt index 1222a70..f079bf5 100644 --- a/views/src/main/java/ru/touchin/widget/LoadingContentView.kt +++ b/views/src/main/java/ru/touchin/widget/LoadingContentView.kt @@ -10,6 +10,7 @@ import ru.touchin.roboswag.components.views.R import ru.touchin.roboswag.components.views.databinding.ProgressViewBinding import kotlin.properties.Delegates +//TODO make customizable views list and views style class LoadingContentView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, From 4b38e3fcd3f4cdbffc5bff4d34eb004cdeb6c3b8 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 21 Aug 2020 12:46:46 +0500 Subject: [PATCH 103/154] fixed review --- views/build.gradle | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/views/build.gradle b/views/build.gradle index 686aadd..e76deb5 100644 --- a/views/build.gradle +++ b/views/build.gradle @@ -12,7 +12,6 @@ dependencies { implementation project(":kotlin-extensions") implementation project(":logging") - implementation "androidx.core:core" implementation "com.google.android.material:material" implementation "androidx.core:core-ktx" @@ -27,8 +26,12 @@ dependencies { require '1.3.1' } } + implementation("org.jetbrains.kotlin:kotlin-stdlib") { + version { + require '1.3.0' + } + } } - implementation "org.jetbrains.kotlin:kotlin-stdlib" } repositories { From af7e505bc54794b01a231d0f7618cc8fddaa02b6 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2020 18:06:08 +0500 Subject: [PATCH 104/154] Amount edittext first version --- .../widget/AmountWithDecimalEditText.kt | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt new file mode 100644 index 0000000..50b955a --- /dev/null +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt @@ -0,0 +1,151 @@ +package ru.touchin.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.widget.doOnTextChanged +import ru.touchin.roboswag.components.views.R +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +class AmountWithDecimalEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int = R.attr.editTextStyle +) : AppCompatEditText(context, attrs, defStyleAttr) { + + companion object { + + private const val COMMON_MONEY_MASK = "###,##0" + private const val DEFAULT_DECIMAL_SEPARATOR = "," + private const val GROUPING_SEPARATOR = ' ' + private const val DEFAULT_DECIMAL_PART_LENGTH = 2 + private val hardcodedSymbols = listOf(GROUPING_SEPARATOR) + private val possibleDecimalSeparators = listOf(",", ".") + } + + private var textBefore = "" + private var isTextWasArtificiallyChanged = true + + var decimalSeparator = DEFAULT_DECIMAL_SEPARATOR + set(value) { + if (!possibleDecimalSeparators.contains(value)) + throw Exception("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") + field = value + } + + + var decimalPartLength = DEFAULT_DECIMAL_PART_LENGTH + var isSeparatorCutInvalidDecimalLength = false + + init { + doOnTextChanged { text, _, _, _ -> + if (isTextWasArtificiallyChanged) { + isTextWasArtificiallyChanged = false + val cursorPos = selectionStart + var text = text.toString() + possibleDecimalSeparators.forEach { + text = text.replace(it, decimalSeparator) + } + + if (text == decimalSeparator || text.count { it == decimalSeparator[0] } > 1) { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + return@doOnTextChanged + } + + if (text.take(2) == "00") { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + return@doOnTextChanged + } + + if (text.length >= 2 && text[0] == '0' && text[1] != decimalSeparator[0]) { + setText(text[1].toString()) + setSelection(max(cursorPos - 1, 0)) + return@doOnTextChanged + } + + + val decimalPartLength_ = text.split(decimalSeparator).getOrNull(1)?.length + if (!isSeparatorCutInvalidDecimalLength && decimalPartLength_ != null && decimalPartLength_ > decimalPartLength) { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + return@doOnTextChanged + } + + val textAfter = if (text.isNotEmpty()) { + text.withoutFormatting().formatMoney(decimalPartLength_) + } else "" + + if (!isTextErased(textBefore, textAfter)) { + val diff = textAfter.length - textBefore.length - 1 + setText(textAfter) + setSelection(min(cursorPos + diff, textAfter.length)) + } else { + if (!textBefore.contains(decimalSeparator) + && textAfter.contains(decimalSeparator) + ) { + setText(textAfter) + setSelection( + min( + textAfter.length, + textAfter.indexOf(decimalSeparator) + 1 + ) + ) + return@doOnTextChanged + } + val diff = textBefore.length - textAfter.length + if (diff == 0) { + setText(textAfter) + setSelection(min(cursorPos, textAfter.length)) + } else { + setText(textAfter) + setSelection(max(cursorPos - diff + 1, 0)) + } + + } + } else { + textBefore = text.toString() + isTextWasArtificiallyChanged = true + } + } + } + + + fun getTextWithoutFormatting() = text.toString().withoutFormatting() + + private fun String.withoutFormatting(): String { + var result = this + hardcodedSymbols.forEach { result = this.replace(it.toString(), "") } + return result + } + + private fun isTextErased(textBefore: String, textAfter: String) = + textAfter.length <= textBefore.length + + private fun String.formatMoney(decimalPartLength_: Int?): String { + var mask = COMMON_MONEY_MASK + if (decimalPartLength_ != null && decimalPartLength != 0) mask += "." + "0".repeat( + min( + decimalPartLength_, + decimalPartLength + ) + ) + + val formatter = DecimalFormat(mask) + formatter.decimalFormatSymbols = DecimalFormatSymbols().also { + it.decimalSeparator = decimalSeparator[0] + it.groupingSeparator = GROUPING_SEPARATOR + } + return formatter.format(this.replace(",", ".").toDouble().floor()) + } + + private fun Double.floor() = + (this * 10.toDouble().pow(decimalPartLength)).toLong() / 10.toDouble() + .pow(decimalPartLength) + +} From c720619c3719872b6a481a0dcf06dc2b67cce585 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2020 21:54:42 +0500 Subject: [PATCH 105/154] improved past --- .../widget/AmountWithDecimalEditText.kt | 98 +++++++++++++------ 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt index 50b955a..54899b1 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt @@ -7,14 +7,15 @@ import androidx.core.widget.doOnTextChanged import ru.touchin.roboswag.components.views.R import java.text.DecimalFormat import java.text.DecimalFormatSymbols +import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.math.pow class AmountWithDecimalEditText @JvmOverloads constructor( - context: Context, - attrs: AttributeSet?, - defStyleAttr: Int = R.attr.editTextStyle + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int = R.attr.editTextStyle ) : AppCompatEditText(context, attrs, defStyleAttr) { companion object { @@ -52,28 +53,43 @@ class AmountWithDecimalEditText @JvmOverloads constructor( } if (text == decimalSeparator || text.count { it == decimalSeparator[0] } > 1) { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + } return@doOnTextChanged } if (text.take(2) == "00") { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + } return@doOnTextChanged } if (text.length >= 2 && text[0] == '0' && text[1] != decimalSeparator[0]) { - setText(text[1].toString()) - setSelection(max(cursorPos - 1, 0)) + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setTextWhichWasPasted(text) + setSelection(max(cursorPos - 1, 0)) + } return@doOnTextChanged } - val decimalPartLength_ = text.split(decimalSeparator).getOrNull(1)?.length if (!isSeparatorCutInvalidDecimalLength && decimalPartLength_ != null && decimalPartLength_ > decimalPartLength) { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + } return@doOnTextChanged } @@ -87,15 +103,10 @@ class AmountWithDecimalEditText @JvmOverloads constructor( setSelection(min(cursorPos + diff, textAfter.length)) } else { if (!textBefore.contains(decimalSeparator) - && textAfter.contains(decimalSeparator) + && textAfter.contains(decimalSeparator) ) { setText(textAfter) - setSelection( - min( - textAfter.length, - textAfter.indexOf(decimalSeparator) + 1 - ) - ) + setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) return@doOnTextChanged } val diff = textBefore.length - textAfter.length @@ -115,6 +126,28 @@ class AmountWithDecimalEditText @JvmOverloads constructor( } } + private fun setTextWhichWasPasted(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) + setText(result) + setSelection(result.length) + } + fun getTextWithoutFormatting() = text.toString().withoutFormatting() @@ -124,28 +157,33 @@ class AmountWithDecimalEditText @JvmOverloads constructor( return result } + private fun String.prepareForDoubleCast(): String { + var result = this + possibleDecimalSeparators.forEach { + result = result.replace(it, ".") + } + return result.withoutFormatting() + } + private fun isTextErased(textBefore: String, textAfter: String) = - textAfter.length <= textBefore.length + textAfter.length <= textBefore.length private fun String.formatMoney(decimalPartLength_: Int?): String { var mask = COMMON_MONEY_MASK - if (decimalPartLength_ != null && decimalPartLength != 0) mask += "." + "0".repeat( - min( - decimalPartLength_, - decimalPartLength - ) - ) + if (decimalPartLength_ != null && decimalPartLength != 0) { + mask += "." + "0".repeat(min(decimalPartLength_, decimalPartLength)) + } val formatter = DecimalFormat(mask) formatter.decimalFormatSymbols = DecimalFormatSymbols().also { it.decimalSeparator = decimalSeparator[0] it.groupingSeparator = GROUPING_SEPARATOR } - return formatter.format(this.replace(",", ".").toDouble().floor()) + return formatter.format(this.prepareForDoubleCast().toDouble().floor()) } private fun Double.floor() = - (this * 10.toDouble().pow(decimalPartLength)).toLong() / 10.toDouble() - .pow(decimalPartLength) + (this * 10.toDouble().pow(decimalPartLength)).toLong() / 10.toDouble() + .pow(decimalPartLength) -} +} \ No newline at end of file From 57466ea21426a7d92a32043c79ffec999d0d7af7 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2020 22:04:19 +0500 Subject: [PATCH 106/154] added error catcher --- .../widget/AmountWithDecimalEditText.kt | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt index 54899b1..0b94d90 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt @@ -97,6 +97,11 @@ class AmountWithDecimalEditText @JvmOverloads constructor( text.withoutFormatting().formatMoney(decimalPartLength_) } else "" + if (textAfter.isEmpty()) { + setText("") + return@doOnTextChanged + } + if (!isTextErased(textBefore, textAfter)) { val diff = textAfter.length - textBefore.length - 1 setText(textAfter) @@ -169,17 +174,21 @@ class AmountWithDecimalEditText @JvmOverloads constructor( textAfter.length <= textBefore.length private fun String.formatMoney(decimalPartLength_: Int?): String { - var mask = COMMON_MONEY_MASK - if (decimalPartLength_ != null && decimalPartLength != 0) { - mask += "." + "0".repeat(min(decimalPartLength_, decimalPartLength)) - } + try { + var mask = COMMON_MONEY_MASK + if (decimalPartLength_ != null && decimalPartLength != 0) { + mask += "." + "0".repeat(min(decimalPartLength_, decimalPartLength)) + } - val formatter = DecimalFormat(mask) - formatter.decimalFormatSymbols = DecimalFormatSymbols().also { - it.decimalSeparator = decimalSeparator[0] - it.groupingSeparator = GROUPING_SEPARATOR + val formatter = DecimalFormat(mask) + formatter.decimalFormatSymbols = DecimalFormatSymbols().also { + it.decimalSeparator = decimalSeparator[0] + it.groupingSeparator = GROUPING_SEPARATOR + } + return formatter.format(this.prepareForDoubleCast().toDouble().floor()) + } catch (e: Throwable) { + return "" } - return formatter.format(this.prepareForDoubleCast().toDouble().floor()) } private fun Double.floor() = From 817f41da3ed25a1ec15ddeb75f2f61a741bdd89f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2020 22:07:41 +0500 Subject: [PATCH 107/154] added global catch --- .../widget/AmountWithDecimalEditText.kt | 163 +++++++++--------- 1 file changed, 79 insertions(+), 84 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt index 0b94d90..8dedf59 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt @@ -47,82 +47,81 @@ class AmountWithDecimalEditText @JvmOverloads constructor( if (isTextWasArtificiallyChanged) { isTextWasArtificiallyChanged = false val cursorPos = selectionStart - var text = text.toString() - possibleDecimalSeparators.forEach { - text = text.replace(it, decimalSeparator) - } - - if (text == decimalSeparator || text.count { it == decimalSeparator[0] } > 1) { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) + try { + var text = text.toString() + possibleDecimalSeparators.forEach { + text = text.replace(it, decimalSeparator) } - return@doOnTextChanged - } - if (text.take(2) == "00") { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) - } - return@doOnTextChanged - } - - if (text.length >= 2 && text[0] == '0' && text[1] != decimalSeparator[0]) { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - setTextWhichWasPasted(text) - setSelection(max(cursorPos - 1, 0)) - } - return@doOnTextChanged - } - - val decimalPartLength_ = text.split(decimalSeparator).getOrNull(1)?.length - if (!isSeparatorCutInvalidDecimalLength && decimalPartLength_ != null && decimalPartLength_ > decimalPartLength) { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) - } - return@doOnTextChanged - } - - val textAfter = if (text.isNotEmpty()) { - text.withoutFormatting().formatMoney(decimalPartLength_) - } else "" - - if (textAfter.isEmpty()) { - setText("") - return@doOnTextChanged - } - - if (!isTextErased(textBefore, textAfter)) { - val diff = textAfter.length - textBefore.length - 1 - setText(textAfter) - setSelection(min(cursorPos + diff, textAfter.length)) - } else { - if (!textBefore.contains(decimalSeparator) - && textAfter.contains(decimalSeparator) - ) { - setText(textAfter) - setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) + if (text == decimalSeparator || text.count { it == decimalSeparator[0] } > 1) { + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + } return@doOnTextChanged } - val diff = textBefore.length - textAfter.length - if (diff == 0) { - setText(textAfter) - setSelection(min(cursorPos, textAfter.length)) - } else { - setText(textAfter) - setSelection(max(cursorPos - diff + 1, 0)) + + if (text.take(2) == "00") { + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + } + return@doOnTextChanged } + if (text.length >= 2 && text[0] == '0' && text[1] != decimalSeparator[0]) { + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setTextWhichWasPasted(text) + setSelection(max(cursorPos - 1, 0)) + } + return@doOnTextChanged + } + + val decimalPartLength_ = text.split(decimalSeparator).getOrNull(1)?.length + if (!isSeparatorCutInvalidDecimalLength && decimalPartLength_ != null && decimalPartLength_ > decimalPartLength) { + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + setText(textBefore) + setSelection(max(cursorPos - 1, 0)) + } + return@doOnTextChanged + } + + val textAfter = if (text.isNotEmpty()) { + text.withoutFormatting().formatMoney(decimalPartLength_) + } else "" + + if (!isTextErased(textBefore, textAfter)) { + val diff = textAfter.length - textBefore.length - 1 + setText(textAfter) + setSelection(min(cursorPos + diff, textAfter.length)) + } else { + if (!textBefore.contains(decimalSeparator) + && textAfter.contains(decimalSeparator) + ) { + setText(textAfter) + setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) + return@doOnTextChanged + } + val diff = textBefore.length - textAfter.length + if (diff == 0) { + setText(textAfter) + setSelection(min(cursorPos, textAfter.length)) + } else { + setText(textAfter) + setSelection(max(cursorPos - diff + 1, 0)) + } + + } + } catch (e: Throwable) { + setText("") } } else { textBefore = text.toString() @@ -174,21 +173,17 @@ class AmountWithDecimalEditText @JvmOverloads constructor( textAfter.length <= textBefore.length private fun String.formatMoney(decimalPartLength_: Int?): String { - try { - var mask = COMMON_MONEY_MASK - if (decimalPartLength_ != null && decimalPartLength != 0) { - mask += "." + "0".repeat(min(decimalPartLength_, decimalPartLength)) - } - - val formatter = DecimalFormat(mask) - formatter.decimalFormatSymbols = DecimalFormatSymbols().also { - it.decimalSeparator = decimalSeparator[0] - it.groupingSeparator = GROUPING_SEPARATOR - } - return formatter.format(this.prepareForDoubleCast().toDouble().floor()) - } catch (e: Throwable) { - return "" + var mask = COMMON_MONEY_MASK + if (decimalPartLength_ != null && decimalPartLength != 0) { + mask += "." + "0".repeat(min(decimalPartLength_, decimalPartLength)) } + + val formatter = DecimalFormat(mask) + formatter.decimalFormatSymbols = DecimalFormatSymbols().also { + it.decimalSeparator = decimalSeparator[0] + it.groupingSeparator = GROUPING_SEPARATOR + } + return formatter.format(this.prepareForDoubleCast().toDouble().floor()) } private fun Double.floor() = From b484c6dd482ca03babbf5aa94fe0cddab16d5fc3 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 22 Aug 2020 22:08:38 +0500 Subject: [PATCH 108/154] codestyle --- .../ru/touchin/widget/AmountWithDecimalEditText.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt index 8dedf59..ff14056 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt @@ -26,22 +26,21 @@ class AmountWithDecimalEditText @JvmOverloads constructor( private const val DEFAULT_DECIMAL_PART_LENGTH = 2 private val hardcodedSymbols = listOf(GROUPING_SEPARATOR) private val possibleDecimalSeparators = listOf(",", ".") + } - private var textBefore = "" - private var isTextWasArtificiallyChanged = true - var decimalSeparator = DEFAULT_DECIMAL_SEPARATOR set(value) { if (!possibleDecimalSeparators.contains(value)) throw Exception("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") field = value } - - var decimalPartLength = DEFAULT_DECIMAL_PART_LENGTH var isSeparatorCutInvalidDecimalLength = false + private var textBefore = "" + private var isTextWasArtificiallyChanged = true + init { doOnTextChanged { text, _, _, _ -> if (isTextWasArtificiallyChanged) { @@ -130,6 +129,8 @@ class AmountWithDecimalEditText @JvmOverloads constructor( } } + fun getTextWithoutFormatting() = text.toString().withoutFormatting() + private fun setTextWhichWasPasted(text: String) { var result = "" var decimalLength = -1 @@ -152,9 +153,6 @@ class AmountWithDecimalEditText @JvmOverloads constructor( setSelection(result.length) } - - fun getTextWithoutFormatting() = text.toString().withoutFormatting() - private fun String.withoutFormatting(): String { var result = this hardcodedSymbols.forEach { result = this.replace(it.toString(), "") } From 09720b291925900e05e401d813b35e6d4c24f19f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 25 Aug 2020 13:03:08 +0500 Subject: [PATCH 109/154] update amount field --- ...tText.kt => AmountWithDecimalDecorator.kt} | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) rename views/src/main/java/ru/touchin/widget/{AmountWithDecimalEditText.kt => AmountWithDecimalDecorator.kt} (69%) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt similarity index 69% rename from views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt rename to views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index ff14056..a52119f 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalEditText.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -1,10 +1,7 @@ package ru.touchin.widget -import android.content.Context -import android.util.AttributeSet import androidx.appcompat.widget.AppCompatEditText import androidx.core.widget.doOnTextChanged -import ru.touchin.roboswag.components.views.R import java.text.DecimalFormat import java.text.DecimalFormatSymbols import kotlin.math.abs @@ -12,11 +9,10 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.pow -class AmountWithDecimalEditText @JvmOverloads constructor( - context: Context, - attrs: AttributeSet?, - defStyleAttr: Int = R.attr.editTextStyle -) : AppCompatEditText(context, attrs, defStyleAttr) { +@Suppress("detekt.LabeledExpression") +class AmountWithDecimalDecorator( + val editText: AppCompatEditText +) { companion object { @@ -26,13 +22,14 @@ class AmountWithDecimalEditText @JvmOverloads constructor( private const val DEFAULT_DECIMAL_PART_LENGTH = 2 private val hardcodedSymbols = listOf(GROUPING_SEPARATOR) private val possibleDecimalSeparators = listOf(",", ".") - + } var decimalSeparator = DEFAULT_DECIMAL_SEPARATOR set(value) { - if (!possibleDecimalSeparators.contains(value)) - throw Exception("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") + if (!possibleDecimalSeparators.contains(value)) { + throw IllegalArgumentException("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") + } field = value } var decimalPartLength = DEFAULT_DECIMAL_PART_LENGTH @@ -42,10 +39,11 @@ class AmountWithDecimalEditText @JvmOverloads constructor( private var isTextWasArtificiallyChanged = true init { - doOnTextChanged { text, _, _, _ -> + @Suppress("detekt.TooGenericExceptionCaught") + editText.doOnTextChanged { text, _, _, _ -> if (isTextWasArtificiallyChanged) { isTextWasArtificiallyChanged = false - val cursorPos = selectionStart + val cursorPos = editText.selectionStart try { var text = text.toString() possibleDecimalSeparators.forEach { @@ -56,8 +54,8 @@ class AmountWithDecimalEditText @JvmOverloads constructor( if (abs(textBefore.length - text.length) > 1) { setTextWhichWasPasted(text) } else { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) + editText.setText(textBefore) + editText.setSelection(max(cursorPos - 1, 0)) } return@doOnTextChanged } @@ -66,8 +64,8 @@ class AmountWithDecimalEditText @JvmOverloads constructor( if (abs(textBefore.length - text.length) > 1) { setTextWhichWasPasted(text) } else { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) + editText.setText(textBefore) + editText.setSelection(max(cursorPos - 1, 0)) } return@doOnTextChanged } @@ -77,50 +75,51 @@ class AmountWithDecimalEditText @JvmOverloads constructor( setTextWhichWasPasted(text) } else { setTextWhichWasPasted(text) - setSelection(max(cursorPos - 1, 0)) + editText.setSelection(max(cursorPos - 1, 0)) } return@doOnTextChanged } - val decimalPartLength_ = text.split(decimalSeparator).getOrNull(1)?.length - if (!isSeparatorCutInvalidDecimalLength && decimalPartLength_ != null && decimalPartLength_ > decimalPartLength) { + val currentDecimalPartLength = text.split(decimalSeparator).getOrNull(1)?.length + if (!isSeparatorCutInvalidDecimalLength && currentDecimalPartLength != null + && currentDecimalPartLength > decimalPartLength) { if (abs(textBefore.length - text.length) > 1) { setTextWhichWasPasted(text) } else { - setText(textBefore) - setSelection(max(cursorPos - 1, 0)) + editText.setText(textBefore) + editText.setSelection(max(cursorPos - 1, 0)) } return@doOnTextChanged } val textAfter = if (text.isNotEmpty()) { - text.withoutFormatting().formatMoney(decimalPartLength_) + text.withoutFormatting().formatMoney(currentDecimalPartLength) } else "" if (!isTextErased(textBefore, textAfter)) { val diff = textAfter.length - textBefore.length - 1 - setText(textAfter) - setSelection(min(cursorPos + diff, textAfter.length)) + editText.setText(textAfter) + editText.setSelection(min(cursorPos + diff, textAfter.length)) } else { if (!textBefore.contains(decimalSeparator) && textAfter.contains(decimalSeparator) ) { - setText(textAfter) - setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) + editText.setText(textAfter) + editText.setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) return@doOnTextChanged } val diff = textBefore.length - textAfter.length if (diff == 0) { - setText(textAfter) - setSelection(min(cursorPos, textAfter.length)) + editText.setText(textAfter) + editText.setSelection(min(cursorPos, textAfter.length)) } else { - setText(textAfter) - setSelection(max(cursorPos - diff + 1, 0)) + editText.setText(textAfter) + editText.setSelection(max(cursorPos - diff + 1, 0)) } } } catch (e: Throwable) { - setText("") + editText.setText("") } } else { textBefore = text.toString() @@ -129,7 +128,7 @@ class AmountWithDecimalEditText @JvmOverloads constructor( } } - fun getTextWithoutFormatting() = text.toString().withoutFormatting() + fun getTextWithoutFormatting() = editText.text.toString().withoutFormatting() private fun setTextWhichWasPasted(text: String) { var result = "" @@ -149,8 +148,8 @@ class AmountWithDecimalEditText @JvmOverloads constructor( index++ } result = result.formatMoney(decimalPartLength) - setText(result) - setSelection(result.length) + editText.setText(result) + editText.setSelection(result.length) } private fun String.withoutFormatting(): String { @@ -170,10 +169,10 @@ class AmountWithDecimalEditText @JvmOverloads constructor( private fun isTextErased(textBefore: String, textAfter: String) = textAfter.length <= textBefore.length - private fun String.formatMoney(decimalPartLength_: Int?): String { + private fun String.formatMoney(currentDecimalPartLength: Int?): String { var mask = COMMON_MONEY_MASK - if (decimalPartLength_ != null && decimalPartLength != 0) { - mask += "." + "0".repeat(min(decimalPartLength_, decimalPartLength)) + if (currentDecimalPartLength != null && decimalPartLength != 0) { + mask += "." + "0".repeat(min(currentDecimalPartLength, decimalPartLength)) } val formatter = DecimalFormat(mask) @@ -188,4 +187,4 @@ class AmountWithDecimalEditText @JvmOverloads constructor( (this * 10.toDouble().pow(decimalPartLength)).toLong() / 10.toDouble() .pow(decimalPartLength) -} \ No newline at end of file +} From bb15c71f935942347b063074e087143c056d803b Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Tue, 25 Aug 2020 11:24:06 +0300 Subject: [PATCH 110/154] GroupItemDecoration GroupItemDecoration --- recyclerview-adapters/build.gradle | 7 ++ .../touchin/decorators/GroupItemDecoration.kt | 88 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 recyclerview-adapters/src/main/java/ru/touchin/decorators/GroupItemDecoration.kt diff --git a/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle index f733886..0b09710 100644 --- a/recyclerview-adapters/build.gradle +++ b/recyclerview-adapters/build.gradle @@ -7,11 +7,18 @@ dependencies { implementation "androidx.recyclerview:recyclerview" + 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/java/ru/touchin/decorators/GroupItemDecoration.kt b/recyclerview-adapters/src/main/java/ru/touchin/decorators/GroupItemDecoration.kt new file mode 100644 index 0000000..5d55918 --- /dev/null +++ b/recyclerview-adapters/src/main/java/ru/touchin/decorators/GroupItemDecoration.kt @@ -0,0 +1,88 @@ +package ru.touchin.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)) { + val groupViewHolder = viewHoldersPool[adapterPosition] + ?: onCreateViewHolder(parent).also { viewHoldersPool[adapterPosition] = it } + onBindViewHolder(adapterPosition, groupViewHolder) + val groupView = groupViewHolder.view + val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams + val widthSpec = ViewGroup.getChildMeasureSpec( + View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY), + parent.paddingLeft + parent.paddingRight + layoutParams.leftMargin + layoutParams.rightMargin, + groupView.layoutParams.width + ) + val heightSpec = ViewGroup.getChildMeasureSpec( + View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.EXACTLY), + parent.paddingTop + parent.paddingBottom + layoutParams.topMargin + layoutParams.bottomMargin, + groupView.layoutParams.height + ) + 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 + } + } else { + viewHoldersPool.remove(adapterPosition) + } + } + + @Suppress("detekt.NestedBlockDepth") + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + var invalidate = false + for (child in parent.children) { + val adapterPosition = parent.getChildAdapterPosition(child) + val groupView = viewHoldersPool[adapterPosition]?.view + if (predicate(adapterPosition)) { + if (groupView != null) { + val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams + parent.getDecoratedBoundsWithMargins(child, bounds) + canvas.save() + 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) + } + groupView.draw(canvas) + canvas.restore() + } else { + invalidate = true + break + } + } else if (groupView != null) { + invalidate = true + break + } + } + if (invalidate) { + parent.invalidateItemDecorations() + } + } + + open class ViewHolder(val view: View) { + fun findViewById(@IdRes resId: Int): T = view.findViewById(resId) + } + +} From 23634ac08be73ef7f5f1ae249cbd43bea95e6e7c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 25 Aug 2020 13:56:18 +0500 Subject: [PATCH 111/154] fixed review --- .../widget/AmountWithDecimalDecorator.kt | 99 +++++++++---------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index a52119f..eefaf86 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -1,6 +1,6 @@ package ru.touchin.widget -import androidx.appcompat.widget.AppCompatEditText +import android.widget.EditText import androidx.core.widget.doOnTextChanged import java.text.DecimalFormat import java.text.DecimalFormatSymbols @@ -11,7 +11,7 @@ import kotlin.math.pow @Suppress("detekt.LabeledExpression") class AmountWithDecimalDecorator( - val editText: AppCompatEditText + val editText: EditText ) { companion object { @@ -50,45 +50,22 @@ class AmountWithDecimalDecorator( text = text.replace(it, decimalSeparator) } - if (text == decimalSeparator || text.count { it == decimalSeparator[0] } > 1) { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - editText.setText(textBefore) - editText.setSelection(max(cursorPos - 1, 0)) - } - return@doOnTextChanged - } - - if (text.take(2) == "00") { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - editText.setText(textBefore) - editText.setSelection(max(cursorPos - 1, 0)) - } + if (text == decimalSeparator || text.count { it == decimalSeparator[0] } > 1 || text.take(2) == "00") { + setTextWhenNewInputIncorrect(text, cursorPos) return@doOnTextChanged } if (text.length >= 2 && text[0] == '0' && text[1] != decimalSeparator[0]) { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - setTextWhichWasPasted(text) + setTextWhichWasPasted(text) + if (abs(textBefore.length - text.length) <= 1) { editText.setSelection(max(cursorPos - 1, 0)) } return@doOnTextChanged } val currentDecimalPartLength = text.split(decimalSeparator).getOrNull(1)?.length - if (!isSeparatorCutInvalidDecimalLength && currentDecimalPartLength != null - && currentDecimalPartLength > decimalPartLength) { - if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) - } else { - editText.setText(textBefore) - editText.setSelection(max(cursorPos - 1, 0)) - } + if (isDecimalPathTooLong(currentDecimalPartLength)) { + setTextWhenNewInputIncorrect(text, cursorPos) return@doOnTextChanged } @@ -97,26 +74,9 @@ class AmountWithDecimalDecorator( } else "" if (!isTextErased(textBefore, textAfter)) { - val diff = textAfter.length - textBefore.length - 1 - editText.setText(textAfter) - editText.setSelection(min(cursorPos + diff, textAfter.length)) + onTextErased(textAfter, cursorPos) } else { - if (!textBefore.contains(decimalSeparator) - && textAfter.contains(decimalSeparator) - ) { - editText.setText(textAfter) - editText.setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) - return@doOnTextChanged - } - val diff = textBefore.length - textAfter.length - if (diff == 0) { - editText.setText(textAfter) - editText.setSelection(min(cursorPos, textAfter.length)) - } else { - editText.setText(textAfter) - editText.setSelection(max(cursorPos - diff + 1, 0)) - } - + onTextInserted(textAfter, cursorPos) } } catch (e: Throwable) { editText.setText("") @@ -128,7 +88,44 @@ class AmountWithDecimalDecorator( } } - fun getTextWithoutFormatting() = editText.text.toString().withoutFormatting() + fun getTextWithoutFormatting(): String = editText.text.toString().withoutFormatting() + + private fun setTextWhenNewInputIncorrect(text: String, cursorPos: Int) { + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + editText.setText(textBefore) + editText.setSelection(max(cursorPos - 1, 0)) + } + } + + private fun onTextErased(textAfter: String, cursorPos: Int) { + val diff = textAfter.length - textBefore.length - 1 + editText.setText(textAfter) + editText.setSelection(min(cursorPos + diff, textAfter.length)) + } + + private fun onTextInserted(textAfter: String, cursorPos: Int) { + if (!textBefore.contains(decimalSeparator) + && textAfter.contains(decimalSeparator) + ) { + editText.setText(textAfter) + editText.setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) + return + } + val diff = textBefore.length - textAfter.length + if (diff == 0) { + editText.setText(textAfter) + editText.setSelection(min(cursorPos, textAfter.length)) + } else { + editText.setText(textAfter) + editText.setSelection(max(cursorPos - diff + 1, 0)) + } + } + + private fun isDecimalPathTooLong(currentDecimalPartLength: Int?) = + !isSeparatorCutInvalidDecimalLength && currentDecimalPartLength != null + && currentDecimalPartLength > decimalPartLength private fun setTextWhichWasPasted(text: String) { var result = "" From 28ab74db9bdb2204edeb6edb02a2b176a36c96d9 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 25 Aug 2020 14:15:19 +0500 Subject: [PATCH 112/154] fixe review --- .../touchin/widget/AmountWithDecimalDecorator.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index eefaf86..d267464 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -56,10 +56,7 @@ class AmountWithDecimalDecorator( } if (text.length >= 2 && text[0] == '0' && text[1] != decimalSeparator[0]) { - setTextWhichWasPasted(text) - if (abs(textBefore.length - text.length) <= 1) { - editText.setSelection(max(cursorPos - 1, 0)) - } + setTextWithHeadZero(text, cursorPos) return@doOnTextChanged } @@ -90,6 +87,15 @@ class AmountWithDecimalDecorator( fun getTextWithoutFormatting(): String = editText.text.toString().withoutFormatting() + private fun setTextWithHeadZero(text: String, cursorPos: Int) { + if (abs(textBefore.length - text.length) > 1) { + setTextWhichWasPasted(text) + } else { + editText.setText(text.substring(1, text.length)) + editText.setSelection(max(cursorPos - 1, 0)) + } + } + private fun setTextWhenNewInputIncorrect(text: String, cursorPos: Int) { if (abs(textBefore.length - text.length) > 1) { setTextWhichWasPasted(text) From 55b070cae23c229a1e3719f90d7ad4720b2bb09c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 25 Aug 2020 15:23:45 +0500 Subject: [PATCH 113/154] upd functional --- .../ru/touchin/widget/AmountWithDecimalDecorator.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index d267464..c6aeb86 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -17,7 +17,7 @@ class AmountWithDecimalDecorator( companion object { private const val COMMON_MONEY_MASK = "###,##0" - private const val DEFAULT_DECIMAL_SEPARATOR = "," + private const val DEFAULT_DECIMAL_SEPARATOR = "." private const val GROUPING_SEPARATOR = ' ' private const val DEFAULT_DECIMAL_PART_LENGTH = 2 private val hardcodedSymbols = listOf(GROUPING_SEPARATOR) @@ -34,6 +34,7 @@ class AmountWithDecimalDecorator( } var decimalPartLength = DEFAULT_DECIMAL_PART_LENGTH var isSeparatorCutInvalidDecimalLength = false + var onTextChanged: (text: String) -> Unit = {} private var textBefore = "" private var isTextWasArtificiallyChanged = true @@ -81,11 +82,13 @@ class AmountWithDecimalDecorator( } else { textBefore = text.toString() isTextWasArtificiallyChanged = true + onTextChanged(text.toString()) } } } - fun getTextWithoutFormatting(): String = editText.text.toString().withoutFormatting() + fun getTextWithoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String = + textBefore.withoutFormatting(decimalSeparatorToReplace) private fun setTextWithHeadZero(text: String, cursorPos: Int) { if (abs(textBefore.length - text.length) > 1) { @@ -155,9 +158,10 @@ class AmountWithDecimalDecorator( editText.setSelection(result.length) } - private fun String.withoutFormatting(): String { + 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 } From aa1da93d29e8644f3b1169155e1bb62073910dbe Mon Sep 17 00:00:00 2001 From: stanislav Date: Tue, 25 Aug 2020 13:29:56 +0300 Subject: [PATCH 114/154] replaced package name --- .../java/ru/touchin/roboswag/pagination/PaginationAdapter.kt | 4 ++-- .../ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt | 2 +- recyclerview-adapters/src/main/AndroidManifest.xml | 2 +- .../recyclerview_adapters}/adapters/AdapterDelegate.java | 2 +- .../recyclerview_adapters}/adapters/DelegatesManager.kt | 2 +- .../recyclerview_adapters}/adapters/DelegationListAdapter.kt | 2 +- .../recyclerview_adapters}/adapters/ItemAdapterDelegate.java | 2 +- .../adapters/OffsetAdapterUpdateCallback.kt | 2 +- .../adapters/PositionAdapterDelegate.java | 2 +- .../recyclerview_adapters}/adapters/SimpleDataObserver.kt | 2 +- .../recyclerview_adapters}/decorators/GroupItemDecoration.kt | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/adapters/AdapterDelegate.java (98%) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/adapters/DelegatesManager.kt (97%) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/adapters/DelegationListAdapter.kt (98%) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/adapters/ItemAdapterDelegate.java (98%) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/adapters/OffsetAdapterUpdateCallback.kt (93%) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/adapters/PositionAdapterDelegate.java (97%) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/adapters/SimpleDataObserver.kt (92%) rename recyclerview-adapters/src/main/java/ru/touchin/{ => roboswag/recyclerview_adapters}/decorators/GroupItemDecoration.kt (98%) diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt index 13b8d37..8a0cb9a 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt @@ -3,8 +3,8 @@ package ru.touchin.roboswag.pagination import android.annotation.SuppressLint import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import ru.touchin.adapters.AdapterDelegate -import ru.touchin.adapters.DelegationListAdapter +import ru.touchin.roboswag.recyclerview_adapters.adapters.AdapterDelegate +import ru.touchin.roboswag.recyclerview_adapters.adapters.DelegationListAdapter import ru.touchin.mvi_test.core_ui.pagination.ProgressItem class PaginationAdapter( diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt index 093ddfe..322cfd1 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt @@ -2,7 +2,7 @@ package ru.touchin.roboswag.pagination import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import ru.touchin.adapters.ItemAdapterDelegate +import ru.touchin.roboswag.recyclerview_adapters.adapters.ItemAdapterDelegate import ru.touchin.mvi_arch.core_pagination.R import ru.touchin.mvi_test.core_ui.pagination.ProgressItem import ru.touchin.roboswag.components.utils.UiUtils 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 98% 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 a47fbe6..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,4 +1,4 @@ -package ru.touchin.adapters +package ru.touchin.roboswag.recyclerview_adapters.adapters import android.view.ViewGroup import androidx.recyclerview.widget.AsyncDifferConfig 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-adapters/src/main/java/ru/touchin/decorators/GroupItemDecoration.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt similarity index 98% rename from recyclerview-adapters/src/main/java/ru/touchin/decorators/GroupItemDecoration.kt rename to recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt index 5d55918..105970f 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/decorators/GroupItemDecoration.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt @@ -1,4 +1,4 @@ -package ru.touchin.decorators +package ru.touchin.roboswag.recyclerview_adapters.decorators import android.graphics.Canvas import android.graphics.Rect From 294975d565355302e647d2b409678299a1bd4d62 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Tue, 25 Aug 2020 13:43:47 +0300 Subject: [PATCH 115/154] create fun getHeightChildMeasureSpec(), getWidthChildMeasureSpec(), obtainViewHolder() create fun getHeightChildMeasureSpec(), getWidthChildMeasureSpec(), obtainViewHolder(), translateCanvasByOrientation() --- .../decorators/GroupItemDecoration.kt | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt index 105970f..232643c 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt @@ -24,21 +24,12 @@ class GroupItemDecoration( override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val adapterPosition = parent.getChildAdapterPosition(view) if (predicate(adapterPosition)) { - val groupViewHolder = viewHoldersPool[adapterPosition] - ?: onCreateViewHolder(parent).also { viewHoldersPool[adapterPosition] = it } + val groupViewHolder = obtainViewHolder(adapterPosition, parent) onBindViewHolder(adapterPosition, groupViewHolder) val groupView = groupViewHolder.view val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams - val widthSpec = ViewGroup.getChildMeasureSpec( - View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY), - parent.paddingLeft + parent.paddingRight + layoutParams.leftMargin + layoutParams.rightMargin, - groupView.layoutParams.width - ) - val heightSpec = ViewGroup.getChildMeasureSpec( - View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.EXACTLY), - parent.paddingTop + parent.paddingBottom + layoutParams.topMargin + layoutParams.bottomMargin, - groupView.layoutParams.height - ) + 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) { @@ -52,7 +43,7 @@ class GroupItemDecoration( @Suppress("detekt.NestedBlockDepth") override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { - var invalidate = false + var isInvalidated = false for (child in parent.children) { val adapterPosition = parent.getChildAdapterPosition(child) val groupView = viewHoldersPool[adapterPosition]?.view @@ -61,26 +52,49 @@ class GroupItemDecoration( val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams parent.getDecoratedBoundsWithMargins(child, bounds) canvas.save() - 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) - } + translateCanvasByOrientation(canvas, parent, layoutParams) groupView.draw(canvas) canvas.restore() } else { - invalidate = true + isInvalidated = true break } } else if (groupView != null) { - invalidate = true + isInvalidated = true break } } - if (invalidate) { + if (isInvalidated) { parent.invalidateItemDecorations() } } + 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) = viewHoldersPool[adapterPosition] + ?: onCreateViewHolder(parent).also { viewHoldersPool[adapterPosition] = it } + + private fun getHeightChildMeasureSpec(parent: RecyclerView, layoutParams: ViewGroup.MarginLayoutParams, groupView: View): Int { + return 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 { + return 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) } From 7c38dd037904efe044bbcb4678f80910576d667e Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 25 Aug 2020 16:12:55 +0500 Subject: [PATCH 116/154] fixed review --- .../main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index c6aeb86..a9c806a 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -77,7 +77,8 @@ class AmountWithDecimalDecorator( onTextInserted(textAfter, cursorPos) } } catch (e: Throwable) { - editText.setText("") + editText.setText(textBefore) + editText.setSelection(textBefore.length) } } else { textBefore = text.toString() From e5c8cad4a15192cbb4703e49264f3cbc149fddd5 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Tue, 25 Aug 2020 14:25:53 +0300 Subject: [PATCH 117/154] replaced else if to when, calculateOutRectPosition() replaced else if to when, calculateOutRectPosition() --- .../decorators/GroupItemDecoration.kt | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt index 232643c..38a26a9 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt @@ -24,18 +24,7 @@ class GroupItemDecoration( override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val adapterPosition = parent.getChildAdapterPosition(view) if (predicate(adapterPosition)) { - 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 - } + calculateOutRectPosition(adapterPosition, parent, outRect) } else { viewHoldersPool.remove(adapterPosition) } @@ -44,24 +33,27 @@ class GroupItemDecoration( @Suppress("detekt.NestedBlockDepth") override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { var isInvalidated = false - for (child in parent.children) { + loop@ for (child in parent.children) { val adapterPosition = parent.getChildAdapterPosition(child) val groupView = viewHoldersPool[adapterPosition]?.view - if (predicate(adapterPosition)) { - if (groupView != null) { - val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams - parent.getDecoratedBoundsWithMargins(child, bounds) - canvas.save() - translateCanvasByOrientation(canvas, parent, layoutParams) - groupView.draw(canvas) - canvas.restore() - } else { - isInvalidated = true - break + when { + predicate(adapterPosition) -> { + if (groupView != null) { + val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams + parent.getDecoratedBoundsWithMargins(child, bounds) + canvas.save() + translateCanvasByOrientation(canvas, parent, layoutParams) + groupView.draw(canvas) + canvas.restore() + } else { + isInvalidated = true + break@loop + } + } + groupView != null -> { + isInvalidated = true + break@loop } - } else if (groupView != null) { - isInvalidated = true - break } } if (isInvalidated) { @@ -69,6 +61,21 @@ class GroupItemDecoration( } } + 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 translateCanvasByOrientation(canvas: Canvas, parent: RecyclerView, layoutParams: ViewGroup.MarginLayoutParams) { when (orientation) { RecyclerView.VERTICAL -> canvas.translate(parent.paddingLeft.toFloat() + layoutParams.leftMargin, bounds.top.toFloat()) From 4a48d2ceef8fc5329658580b94588c9f337d5784 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Wed, 26 Aug 2020 18:14:30 +0300 Subject: [PATCH 118/154] Paginator error handle mod Paginator error handle mod --- .../touchin/roboswag/pagination/ErrorItem.kt | 3 ++ .../roboswag/pagination/PaginationAdapter.kt | 15 +++++++-- .../roboswag/pagination/PaginationView.kt | 1 - .../touchin/roboswag/pagination/Paginator.kt | 31 +++++++++++++++---- .../pagination/ProgressAdapterDelegate.kt | 1 - .../roboswag/pagination/ProgressItem.kt | 2 +- 6 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 pagination/src/main/java/ru/touchin/roboswag/pagination/ErrorItem.kt 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 index 13b8d37..202790a 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationAdapter.kt @@ -5,7 +5,6 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import ru.touchin.adapters.AdapterDelegate import ru.touchin.adapters.DelegationListAdapter -import ru.touchin.mvi_test.core_ui.pagination.ProgressItem class PaginationAdapter( private val nextPageCallback: () -> Unit, @@ -27,8 +26,12 @@ class PaginationAdapter( delegate.forEach(this::addDelegate) } - fun update(data: List, isPageProgress: Boolean) { - submitList(data + listOfNotNull(ProgressItem.takeIf { isPageProgress })) + 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) { @@ -36,4 +39,10 @@ class PaginationAdapter( 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 index 7311245..b206ce4 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt @@ -7,7 +7,6 @@ import android.widget.FrameLayout import androidx.recyclerview.widget.StaggeredGridLayoutManager import ru.touchin.extensions.setOnRippleClickListener import ru.touchin.mvi_arch.core_pagination.databinding.ViewPaginationBinding -import ru.touchin.mvi_test.core_ui.pagination.Paginator // TODO: add an errorview with empty state and error text class PaginationView @JvmOverloads constructor( diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt index 7081d8e..a20340e 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -1,4 +1,4 @@ -package ru.touchin.mvi_test.core_ui.pagination +package ru.touchin.roboswag.pagination import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest @@ -9,7 +9,7 @@ import ru.touchin.roboswag.mvi_arch.marker.StateChange import ru.touchin.roboswag.mvi_arch.marker.ViewState class Paginator( - private val showError: (Error) -> Unit, + private val errorHandleMod: ErrorHandleMod, private val loadPage: suspend (Int) -> List ) : Store(State.Empty) { @@ -41,6 +41,11 @@ class Paginator( 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) { @@ -97,12 +102,26 @@ class Paginator( when (currentState) { is State.EmptyProgress -> State.EmptyError(change.error) is State.Refresh<*> -> { - showError(Error.RefreshFailed) - State.Data(currentState.pageCount, currentState.data) + when (errorHandleMod) { + is ErrorHandleMod.Alert -> { + errorHandleMod.showError(Error.RefreshFailed) + State.Data(currentState.pageCount, currentState.data) + } + is ErrorHandleMod.ErrorItem -> { + State.Data(currentState.pageCount, currentState.data + ErrorItem) + } + } } is State.NewPageProgress<*> -> { - showError(Error.NewPageFailed) - State.Data(currentState.pageCount, currentState.data) + when (errorHandleMod) { + is ErrorHandleMod.Alert -> { + errorHandleMod.showError(Error.NewPageFailed) + State.Data(currentState.pageCount, currentState.data) + } + is ErrorHandleMod.ErrorItem -> { + State.Data(currentState.pageCount, currentState.data + ErrorItem) + } + } } else -> currentState }.only() diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt index 093ddfe..edc29c2 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressAdapterDelegate.kt @@ -4,7 +4,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import ru.touchin.adapters.ItemAdapterDelegate import ru.touchin.mvi_arch.core_pagination.R -import ru.touchin.mvi_test.core_ui.pagination.ProgressItem import ru.touchin.roboswag.components.utils.UiUtils class ProgressAdapterDelegate : ItemAdapterDelegate() { diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt index 7e46cec..ffcc5fb 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/ProgressItem.kt @@ -1,3 +1,3 @@ -package ru.touchin.mvi_test.core_ui.pagination +package ru.touchin.roboswag.pagination object ProgressItem From 85092fb4ae1bb292e276d97ff80bce1aaf89433f Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Wed, 26 Aug 2020 19:05:58 +0300 Subject: [PATCH 119/154] formatting GroupItemDecoration formatting GroupItemDecoration --- .../decorators/GroupItemDecoration.kt | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt index 38a26a9..67470b0 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt @@ -39,12 +39,7 @@ class GroupItemDecoration( when { predicate(adapterPosition) -> { if (groupView != null) { - val layoutParams = groupView.layoutParams as ViewGroup.MarginLayoutParams - parent.getDecoratedBoundsWithMargins(child, bounds) - canvas.save() - translateCanvasByOrientation(canvas, parent, layoutParams) - groupView.draw(canvas) - canvas.restore() + onDrawGroupView(groupView, parent, child, canvas) } else { isInvalidated = true break@loop @@ -76,6 +71,15 @@ class GroupItemDecoration( } } + 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()) @@ -83,24 +87,28 @@ class GroupItemDecoration( } } - private fun obtainViewHolder(adapterPosition: Int, parent: RecyclerView) = viewHoldersPool[adapterPosition] + 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 { - return ViewGroup.getChildMeasureSpec( - View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.EXACTLY), - parent.paddingTop + parent.paddingBottom + layoutParams.topMargin + layoutParams.bottomMargin, - groupView.layoutParams.height - ) - } + 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 { - return ViewGroup.getChildMeasureSpec( - View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY), - parent.paddingLeft + parent.paddingRight + layoutParams.leftMargin + layoutParams.rightMargin, - groupView.layoutParams.width - ) - } + 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) From 313481500aa50e30ac896d2d08af6942e4903c4e Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Wed, 26 Aug 2020 20:21:04 +0300 Subject: [PATCH 120/154] return lost commit --- .../touchin/roboswag/pagination/PaginationView.kt | 14 +++++++------- .../ru/touchin/roboswag/pagination/Paginator.kt | 14 +++++++++++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt index b206ce4..e31018f 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt @@ -48,25 +48,25 @@ class PaginationView @JvmOverloads constructor( when (state) { is Paginator.State.Empty -> { - adapter.update(emptyList(), false) + adapter.update(emptyList(), PaginationAdapter.UpdateState.Common) } is Paginator.State.EmptyProgress -> { - adapter.update(emptyList(), false) + adapter.update(emptyList(), PaginationAdapter.UpdateState.Common) } is Paginator.State.EmptyError -> { - adapter.update(emptyList(), false) + adapter.update(emptyList(), PaginationAdapter.UpdateState.Common) } is Paginator.State.Data<*> -> { - adapter.update(state.data as List, false) + adapter.update(state.data as List, PaginationAdapter.UpdateState.Common) } is Paginator.State.Refresh<*> -> { - adapter.update(state.data as List, false) + adapter.update(state.data as List, PaginationAdapter.UpdateState.Common) } is Paginator.State.NewPageProgress<*> -> { - adapter.update(state.data as List, true) + adapter.update(state.data as List, PaginationAdapter.UpdateState.Progress) } is Paginator.State.FullData<*> -> { - adapter.update(state.data as List, false) + 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 index a20340e..164cad6 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -30,7 +30,7 @@ class Paginator( object Empty : State() object EmptyProgress : State() data class EmptyError(val error: Throwable) : State() - data class Data(val pageCount: Int = 0, val data: List) : 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() @@ -108,7 +108,11 @@ class Paginator( State.Data(currentState.pageCount, currentState.data) } is ErrorHandleMod.ErrorItem -> { - State.Data(currentState.pageCount, currentState.data + ErrorItem) + State.Data( + pageCount = currentState.pageCount, + data = currentState.data, + error = change.error + ) } } } @@ -119,7 +123,11 @@ class Paginator( State.Data(currentState.pageCount, currentState.data) } is ErrorHandleMod.ErrorItem -> { - State.Data(currentState.pageCount, currentState.data + ErrorItem) + State.Data( + pageCount = currentState.pageCount, + data = currentState.data, + error = change.error + ) } } } From 66189db56ad6fbb62180f160735587c809472792 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 11:20:34 +0500 Subject: [PATCH 121/154] fixed review --- .../widget/AmountWithDecimalDecorator.kt | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index a9c806a..6978501 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -11,7 +11,10 @@ import kotlin.math.pow @Suppress("detekt.LabeledExpression") class AmountWithDecimalDecorator( - val editText: EditText + val editText: EditText, + val decimalSeparator: String = DEFAULT_DECIMAL_SEPARATOR, + val decimalPartLength: Int = DEFAULT_DECIMAL_PART_LENGTH, + val isSeparatorCutInvalidDecimalLength: Boolean = false ) { companion object { @@ -25,56 +28,51 @@ class AmountWithDecimalDecorator( } - var decimalSeparator = DEFAULT_DECIMAL_SEPARATOR - set(value) { - if (!possibleDecimalSeparators.contains(value)) { - throw IllegalArgumentException("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") - } - field = value - } - var decimalPartLength = DEFAULT_DECIMAL_PART_LENGTH - var isSeparatorCutInvalidDecimalLength = false var onTextChanged: (text: String) -> Unit = {} private var textBefore = "" private var isTextWasArtificiallyChanged = true init { + if (!possibleDecimalSeparators.contains(decimalSeparator)) { + throw IllegalArgumentException("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") + } + @Suppress("detekt.TooGenericExceptionCaught") editText.doOnTextChanged { text, _, _, _ -> if (isTextWasArtificiallyChanged) { isTextWasArtificiallyChanged = false - val cursorPos = editText.selectionStart + val cursorPosition = editText.selectionStart try { - var text = text.toString() + var currentText = text.toString() possibleDecimalSeparators.forEach { - text = text.replace(it, decimalSeparator) + currentText = currentText.replace(it, decimalSeparator) } - if (text == decimalSeparator || text.count { it == decimalSeparator[0] } > 1 || text.take(2) == "00") { - setTextWhenNewInputIncorrect(text, cursorPos) + if (isTextFormatIncorrect(currentText)) { + setTextWhenNewInputIncorrect(currentText, cursorPosition) return@doOnTextChanged } - if (text.length >= 2 && text[0] == '0' && text[1] != decimalSeparator[0]) { - setTextWithHeadZero(text, cursorPos) + if (isTextHasHeadZero(currentText)) { + setTextWithHeadZero(currentText, cursorPosition) return@doOnTextChanged } - val currentDecimalPartLength = text.split(decimalSeparator).getOrNull(1)?.length + val currentDecimalPartLength = currentText.split(decimalSeparator).getOrNull(1)?.length if (isDecimalPathTooLong(currentDecimalPartLength)) { - setTextWhenNewInputIncorrect(text, cursorPos) + setTextWhenNewInputIncorrect(currentText, cursorPosition) return@doOnTextChanged } - val textAfter = if (text.isNotEmpty()) { - text.withoutFormatting().formatMoney(currentDecimalPartLength) + val textAfter = if (currentText.isNotEmpty()) { + currentText.withoutFormatting().formatMoney(currentDecimalPartLength) } else "" if (!isTextErased(textBefore, textAfter)) { - onTextErased(textAfter, cursorPos) + onTextErased(textAfter, cursorPosition) } else { - onTextInserted(textAfter, cursorPos) + onTextInserted(textAfter, cursorPosition) } } catch (e: Throwable) { editText.setText(textBefore) @@ -91,6 +89,12 @@ class AmountWithDecimalDecorator( fun getTextWithoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String = textBefore.withoutFormatting(decimalSeparatorToReplace) + 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, cursorPos: Int) { if (abs(textBefore.length - text.length) > 1) { setTextWhichWasPasted(text) @@ -166,7 +170,7 @@ class AmountWithDecimalDecorator( return result } - private fun String.prepareForDoubleCast(): String { + private fun String.replaceSeparatorsToDot(): String { var result = this possibleDecimalSeparators.forEach { result = result.replace(it, ".") @@ -188,7 +192,7 @@ class AmountWithDecimalDecorator( it.decimalSeparator = decimalSeparator[0] it.groupingSeparator = GROUPING_SEPARATOR } - return formatter.format(this.prepareForDoubleCast().toDouble().floor()) + return formatter.format(this.replaceSeparatorsToDot().toDouble().floor()) } private fun Double.floor() = From 784fd3d8f3d47a40594a597b9825e763611ba735 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Thu, 27 Aug 2020 09:57:58 +0300 Subject: [PATCH 122/154] fix when block in PaginationView fix when block in PaginationView --- .../java/ru/touchin/roboswag/pagination/PaginationView.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt index e31018f..d6d686a 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/PaginationView.kt @@ -47,13 +47,7 @@ class PaginationView @JvmOverloads constructor( adapter.fullData = state === Paginator.State.Empty || state is Paginator.State.FullData<*> when (state) { - is Paginator.State.Empty -> { - adapter.update(emptyList(), PaginationAdapter.UpdateState.Common) - } - is Paginator.State.EmptyProgress -> { - adapter.update(emptyList(), PaginationAdapter.UpdateState.Common) - } - is Paginator.State.EmptyError -> { + is Paginator.State.EmptyError, Paginator.State.Empty, Paginator.State.EmptyProgress -> { adapter.update(emptyList(), PaginationAdapter.UpdateState.Common) } is Paginator.State.Data<*> -> { From 948abae5f8ca733d6ba5187de41a55a48b37533b Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Thu, 27 Aug 2020 12:00:04 +0300 Subject: [PATCH 123/154] reformat GroupItemDecoration reformat GroupItemDecoration --- .../decorators/GroupItemDecoration.kt | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt index 67470b0..8cc38bf 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt @@ -31,28 +31,19 @@ class GroupItemDecoration( } @Suppress("detekt.NestedBlockDepth") - override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { - var isInvalidated = false - loop@ for (child in parent.children) { - val adapterPosition = parent.getChildAdapterPosition(child) - val groupView = viewHoldersPool[adapterPosition]?.view - when { - predicate(adapterPosition) -> { - if (groupView != null) { - onDrawGroupView(groupView, parent, child, canvas) - } else { - isInvalidated = true - break@loop - } - } - groupView != null -> { - isInvalidated = true - break@loop - } + 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 } - } - if (isInvalidated) { + } else if (groupView != null) { parent.invalidateItemDecorations() + return } } From 6ee18672350246744946c4dd7b17bb13355e1b7c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 14:17:11 +0500 Subject: [PATCH 124/154] fixed review --- .../widget/AmountWithDecimalDecorator.kt | 110 +++++++++--------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index 6978501..b30cb41 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -9,7 +9,6 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.pow -@Suppress("detekt.LabeledExpression") class AmountWithDecimalDecorator( val editText: EditText, val decimalSeparator: String = DEFAULT_DECIMAL_SEPARATOR, @@ -20,7 +19,8 @@ class AmountWithDecimalDecorator( companion object { private const val COMMON_MONEY_MASK = "###,##0" - private const val DEFAULT_DECIMAL_SEPARATOR = "." + 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 val hardcodedSymbols = listOf(GROUPING_SEPARATOR) @@ -38,57 +38,59 @@ class AmountWithDecimalDecorator( throw IllegalArgumentException("Not allowed decimal separator. Supports only: $possibleDecimalSeparators") } - @Suppress("detekt.TooGenericExceptionCaught") - editText.doOnTextChanged { text, _, _, _ -> - if (isTextWasArtificiallyChanged) { - isTextWasArtificiallyChanged = false - val cursorPosition = editText.selectionStart - try { - var currentText = text.toString() - possibleDecimalSeparators.forEach { - currentText = currentText.replace(it, decimalSeparator) - } - - if (isTextFormatIncorrect(currentText)) { - setTextWhenNewInputIncorrect(currentText, cursorPosition) - return@doOnTextChanged - } - - if (isTextHasHeadZero(currentText)) { - setTextWithHeadZero(currentText, cursorPosition) - return@doOnTextChanged - } - - val currentDecimalPartLength = currentText.split(decimalSeparator).getOrNull(1)?.length - if (isDecimalPathTooLong(currentDecimalPartLength)) { - setTextWhenNewInputIncorrect(currentText, cursorPosition) - return@doOnTextChanged - } - - val textAfter = if (currentText.isNotEmpty()) { - currentText.withoutFormatting().formatMoney(currentDecimalPartLength) - } else "" - - if (!isTextErased(textBefore, textAfter)) { - onTextErased(textAfter, cursorPosition) - } else { - onTextInserted(textAfter, cursorPosition) - } - } catch (e: Throwable) { - editText.setText(textBefore) - editText.setSelection(textBefore.length) - } - } else { - textBefore = text.toString() - isTextWasArtificiallyChanged = true - onTextChanged(text.toString()) - } - } + editText.doOnTextChanged { text, _, _, _ -> doOnTextChanged(text.toString()) } } fun getTextWithoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String = textBefore.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) + } + + if (isTextFormatIncorrect(currentText)) { + setTextWhenNewInputIncorrect(currentText, cursorPosition) + return + } + + if (isTextHasHeadZero(currentText)) { + setTextWithHeadZero(currentText, cursorPosition) + return + } + + val currentDecimalPartLength = currentText.split(decimalSeparator).getOrNull(1)?.length + if (isDecimalPathTooLong(currentDecimalPartLength)) { + setTextWhenNewInputIncorrect(currentText, cursorPosition) + return + } + + val formattedText = if (currentText.isNotEmpty()) { + currentText.withoutFormatting().formatMoney(currentDecimalPartLength) + } else "" + + if (!isTextErased(textBefore, formattedText)) { + onTextErased(formattedText, cursorPosition) + } else { + onNewUserInput(formattedText, cursorPosition) + } + } catch (e: Throwable) { + editText.setText(textBefore) + editText.setSelection(textBefore.length) + } + } else { + textBefore = text + isTextWasArtificiallyChanged = true + onTextChanged(text) + } + } + private fun isTextFormatIncorrect(currentText: String) = currentText == decimalSeparator || currentText.count { it == decimalSeparator[0] } > 1 || currentText.take(2) == "00" @@ -97,7 +99,7 @@ class AmountWithDecimalDecorator( private fun setTextWithHeadZero(text: String, cursorPos: Int) { if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) + setTextWhichWasInserted(text) } else { editText.setText(text.substring(1, text.length)) editText.setSelection(max(cursorPos - 1, 0)) @@ -106,7 +108,7 @@ class AmountWithDecimalDecorator( private fun setTextWhenNewInputIncorrect(text: String, cursorPos: Int) { if (abs(textBefore.length - text.length) > 1) { - setTextWhichWasPasted(text) + setTextWhichWasInserted(text) } else { editText.setText(textBefore) editText.setSelection(max(cursorPos - 1, 0)) @@ -119,7 +121,7 @@ class AmountWithDecimalDecorator( editText.setSelection(min(cursorPos + diff, textAfter.length)) } - private fun onTextInserted(textAfter: String, cursorPos: Int) { + private fun onNewUserInput(textAfter: String, cursorPos: Int) { if (!textBefore.contains(decimalSeparator) && textAfter.contains(decimalSeparator) ) { @@ -141,7 +143,7 @@ class AmountWithDecimalDecorator( !isSeparatorCutInvalidDecimalLength && currentDecimalPartLength != null && currentDecimalPartLength > decimalPartLength - private fun setTextWhichWasPasted(text: String) { + private fun setTextWhichWasInserted(text: String) { var result = "" var decimalLength = -1 var index = 0 @@ -173,7 +175,7 @@ class AmountWithDecimalDecorator( private fun String.replaceSeparatorsToDot(): String { var result = this possibleDecimalSeparators.forEach { - result = result.replace(it, ".") + result = result.replace(it, DOT_SYMBOL) } return result.withoutFormatting() } @@ -184,7 +186,7 @@ class AmountWithDecimalDecorator( private fun String.formatMoney(currentDecimalPartLength: Int?): String { var mask = COMMON_MONEY_MASK if (currentDecimalPartLength != null && decimalPartLength != 0) { - mask += "." + "0".repeat(min(currentDecimalPartLength, decimalPartLength)) + mask += DOT_SYMBOL + "0".repeat(min(currentDecimalPartLength, decimalPartLength)) } val formatter = DecimalFormat(mask) From b83bf97a6768a28fb39d3c32ac4119c53347e86d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 14:41:35 +0500 Subject: [PATCH 125/154] fixed static --- .../main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index b30cb41..dc2f644 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -44,7 +44,7 @@ class AmountWithDecimalDecorator( fun getTextWithoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String = textBefore.withoutFormatting(decimalSeparatorToReplace) - @Suppress("detekt.TooGenericExceptionCaught") + @Suppress("detekt.TooGenericExceptionCaught", "detekt.LongMethod") private fun doOnTextChanged(text: String) { if (isTextWasArtificiallyChanged) { isTextWasArtificiallyChanged = false From b66cd421a855a084c1df1cf4150d1464ddf4c4aa Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 14:43:17 +0500 Subject: [PATCH 126/154] reduce method size --- .../java/ru/touchin/widget/AmountWithDecimalDecorator.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index dc2f644..bd95453 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -44,16 +44,14 @@ class AmountWithDecimalDecorator( fun getTextWithoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String = textBefore.withoutFormatting(decimalSeparatorToReplace) - @Suppress("detekt.TooGenericExceptionCaught", "detekt.LongMethod") + @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) - } + possibleDecimalSeparators.forEach { currentText = currentText.replace(it, decimalSeparator) } if (isTextFormatIncorrect(currentText)) { setTextWhenNewInputIncorrect(currentText, cursorPosition) From 7b58d602c9cd070cb27d9366cdb7195f9079eeae Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 15:05:52 +0500 Subject: [PATCH 127/154] renamed packages --- pagination/src/main/res/layout/view_pagination.xml | 4 ++-- .../roboswag/{components => }/views/MaterialLoadingBar.java | 3 ++- .../{components => }/views/MaterialProgressDrawable.java | 2 +- .../roboswag/{components => }/views/TypefacedEditText.java | 6 ++++-- .../roboswag/{components => }/views/TypefacedTextView.java | 6 ++++-- .../{components => }/views/internal/AttributesUtils.java | 0 .../views}/widget/AmountWithDecimalDecorator.kt | 2 +- .../{ => roboswag/views}/widget/LoadingContentView.kt | 2 +- .../ru/touchin/{ => roboswag/views}/widget/Switcher.java | 2 +- 9 files changed, 16 insertions(+), 11 deletions(-) rename views/src/main/java/ru/touchin/roboswag/{components => }/views/MaterialLoadingBar.java (98%) rename views/src/main/java/ru/touchin/roboswag/{components => }/views/MaterialProgressDrawable.java (99%) rename views/src/main/java/ru/touchin/roboswag/{components => }/views/TypefacedEditText.java (98%) rename views/src/main/java/ru/touchin/roboswag/{components => }/views/TypefacedTextView.java (99%) rename views/src/main/java/ru/touchin/roboswag/{components => }/views/internal/AttributesUtils.java (100%) rename views/src/main/java/ru/touchin/{ => roboswag/views}/widget/AmountWithDecimalDecorator.kt (99%) rename views/src/main/java/ru/touchin/{ => roboswag/views}/widget/LoadingContentView.kt (98%) rename views/src/main/java/ru/touchin/{ => roboswag/views}/widget/Switcher.java (98%) diff --git a/pagination/src/main/res/layout/view_pagination.xml b/pagination/src/main/res/layout/view_pagination.xml index b022ded..a930418 100644 --- a/pagination/src/main/res/layout/view_pagination.xml +++ b/pagination/src/main/res/layout/view_pagination.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -28,6 +28,6 @@ android:layout_height="match_parent" android:clipToPadding="false" /> - + 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..83b5cfa 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; @@ -30,6 +30,7 @@ import android.util.AttributeSet; import android.util.TypedValue; import ru.touchin.roboswag.components.utils.UiUtils; +import ru.touchin.roboswag.components.views.R; /** * Created by Ilia Kurtov on 07/12/2016. 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/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..5253fdf 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.components.views.BuildConfig; +import ru.touchin.roboswag.components.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..8eaaf4d 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.components.views.BuildConfig; +import ru.touchin.roboswag.components.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 100% 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 diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt similarity index 99% rename from views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt rename to views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt index bd95453..f8e3c52 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -1,4 +1,4 @@ -package ru.touchin.widget +package ru.touchin.roboswag.views.widget import android.widget.EditText import androidx.core.widget.doOnTextChanged diff --git a/views/src/main/java/ru/touchin/widget/LoadingContentView.kt b/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt similarity index 98% rename from views/src/main/java/ru/touchin/widget/LoadingContentView.kt rename to views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt index f079bf5..0409fb3 100644 --- a/views/src/main/java/ru/touchin/widget/LoadingContentView.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt @@ -1,4 +1,4 @@ -package ru.touchin.widget +package ru.touchin.roboswag.views.widget import android.content.Context import android.util.AttributeSet 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 98% 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..dd0ab2a 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; From b9360231bef224f873492ddd571f1ea5745547b2 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 15:06:06 +0500 Subject: [PATCH 128/154] renamed package --- .../ru/touchin/roboswag/views/internal/AttributesUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/src/main/java/ru/touchin/roboswag/views/internal/AttributesUtils.java b/views/src/main/java/ru/touchin/roboswag/views/internal/AttributesUtils.java index abe7169..16f09cd 100644 --- a/views/src/main/java/ru/touchin/roboswag/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; From 55d71bf96bda7c0a749cda09f32bb75137e32224 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 15:39:47 +0500 Subject: [PATCH 129/154] renaming --- .../widget/AmountWithDecimalDecorator.kt | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt index b30cb41..b874431 100644 --- a/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/widget/AmountWithDecimalDecorator.kt @@ -30,7 +30,7 @@ class AmountWithDecimalDecorator( var onTextChanged: (text: String) -> Unit = {} - private var textBefore = "" + private var previousInputtedText = "" private var isTextWasArtificiallyChanged = true init { @@ -42,7 +42,7 @@ class AmountWithDecimalDecorator( } fun getTextWithoutFormatting(decimalSeparatorToReplace: String = decimalSeparator): String = - textBefore.withoutFormatting(decimalSeparatorToReplace) + previousInputtedText.withoutFormatting(decimalSeparatorToReplace) @Suppress("detekt.TooGenericExceptionCaught") private fun doOnTextChanged(text: String) { @@ -75,72 +75,75 @@ class AmountWithDecimalDecorator( currentText.withoutFormatting().formatMoney(currentDecimalPartLength) } else "" - if (!isTextErased(textBefore, formattedText)) { - onTextErased(formattedText, cursorPosition) + if (isTextErased(previousInputtedText, formattedText)) { + setTextAfterUserErase(formattedText, cursorPosition) } else { - onNewUserInput(formattedText, cursorPosition) + setTextAfterUserInput(formattedText, cursorPosition) } } catch (e: Throwable) { - editText.setText(textBefore) - editText.setSelection(textBefore.length) + editText.setText(previousInputtedText) + editText.setSelection(previousInputtedText.length) } } else { - textBefore = text + previousInputtedText = text isTextWasArtificiallyChanged = true onTextChanged(text) } } private fun isTextFormatIncorrect(currentText: String) = - currentText == decimalSeparator || currentText.count { it == decimalSeparator[0] } > 1 || currentText.take(2) == "00" + 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, cursorPos: Int) { - if (abs(textBefore.length - text.length) > 1) { + 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(cursorPos - 1, 0)) + editText.setSelection(max(cursorPosition - 1, 0)) } } - private fun setTextWhenNewInputIncorrect(text: String, cursorPos: Int) { - if (abs(textBefore.length - text.length) > 1) { + private fun setTextWhenNewInputIncorrect(text: String, cursorPosition: Int) { + if (abs(previousInputtedText.length - text.length) > 1) { setTextWhichWasInserted(text) } else { - editText.setText(textBefore) - editText.setSelection(max(cursorPos - 1, 0)) + editText.setText(previousInputtedText) + editText.setSelection(max(cursorPosition - 1, 0)) } } - private fun onTextErased(textAfter: String, cursorPos: Int) { - val diff = textAfter.length - textBefore.length - 1 - editText.setText(textAfter) - editText.setSelection(min(cursorPos + diff, textAfter.length)) + private fun setTextAfterUserErase(formattedText: String, cursorPosition: Int) { + val diff = formattedText.length - previousInputtedText.length - 1 + editText.setText(formattedText) + editText.setSelection(min(cursorPosition + diff, formattedText.length)) } - private fun onNewUserInput(textAfter: String, cursorPos: Int) { - if (!textBefore.contains(decimalSeparator) - && textAfter.contains(decimalSeparator) + private fun setTextAfterUserInput(formattedText: String, cursorPosition: Int) { + if (!previousInputtedText.contains(decimalSeparator) + && formattedText.contains(decimalSeparator) ) { - editText.setText(textAfter) - editText.setSelection(min(textAfter.length, textAfter.indexOf(decimalSeparator) + 1)) + editText.setText(formattedText) + editText.setSelection(min(formattedText.length, formattedText.indexOf(decimalSeparator) + 1)) return } - val diff = textBefore.length - textAfter.length + val diff = previousInputtedText.length - formattedText.length if (diff == 0) { - editText.setText(textAfter) - editText.setSelection(min(cursorPos, textAfter.length)) + editText.setText(formattedText) + editText.setSelection(min(cursorPosition, formattedText.length)) } else { - editText.setText(textAfter) - editText.setSelection(max(cursorPos - diff + 1, 0)) + editText.setText(formattedText) + editText.setSelection(max(cursorPosition - diff + 1, 0)) } } private fun isDecimalPathTooLong(currentDecimalPartLength: Int?) = - !isSeparatorCutInvalidDecimalLength && currentDecimalPartLength != null + !isSeparatorCutInvalidDecimalLength + && currentDecimalPartLength != null && currentDecimalPartLength > decimalPartLength private fun setTextWhichWasInserted(text: String) { @@ -180,8 +183,8 @@ class AmountWithDecimalDecorator( return result.withoutFormatting() } - private fun isTextErased(textBefore: String, textAfter: String) = - textAfter.length <= textBefore.length + private fun isTextErased(textBefore: String, formattedText: String) = + formattedText.length <= textBefore.length private fun String.formatMoney(currentDecimalPartLength: Int?): String { var mask = COMMON_MONEY_MASK From 0cde16ec0a60f37cae6de17b03912bcf32393f26 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 27 Aug 2020 18:48:15 +0500 Subject: [PATCH 130/154] Replaced functions --- .../roboswag/views/widget/AmountWithDecimalDecorator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 24435d0..bf0675b 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -115,13 +115,13 @@ class AmountWithDecimalDecorator( } } - private fun setTextAfterUserErase(formattedText: String, cursorPosition: Int) { + 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 setTextAfterUserInput(formattedText: String, cursorPosition: Int) { + private fun setTextAfterUserErase(formattedText: String, cursorPosition: Int) { if (!previousInputtedText.contains(decimalSeparator) && formattedText.contains(decimalSeparator) ) { From 21c35b299425837adcceb65b56d403402d859d80 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Fri, 28 Aug 2020 16:07:44 +0300 Subject: [PATCH 131/154] addOnBackPressedCallback addOnBackPressedCallback --- .../roboswag/mvi_arch/core/MviFragment.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 index 2cf6857..08a7df3 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -103,6 +104,22 @@ abstract class MviFragment( viewModel.dispatchAction(action) } + protected fun addOnBackPressedCallback(actionProvider: () -> Action) { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + dispatchAction(actionProvider) + } + }) + } + + 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. * From d6aa8fad79d63ff41b95d0936e9040c27f10ccb7 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Fri, 28 Aug 2020 16:25:01 +0300 Subject: [PATCH 132/154] replaced common code to actionProvider.invoke() replaced common code to actionProvider.invoke() --- .../java/ru/touchin/roboswag/mvi_arch/core/MviFragment.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 index 08a7df3..80e7758 100644 --- 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 @@ -105,11 +105,7 @@ abstract class MviFragment( } protected fun addOnBackPressedCallback(actionProvider: () -> Action) { - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - dispatchAction(actionProvider) - } - }) + addOnBackPressedCallback(actionProvider.invoke()) } protected fun addOnBackPressedCallback(action: Action) { From 565b9df0ddc9c9a706ebeac797392176ce140392 Mon Sep 17 00:00:00 2001 From: stanislav Date: Tue, 1 Sep 2020 00:40:00 +0300 Subject: [PATCH 133/154] DividerItemDecoration --- views/src/main/AndroidManifest.xml | 2 +- .../views/decorators/DividerItemDecoration.kt | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 views/src/main/java/ru/touchin/roboswag/views/decorators/DividerItemDecoration.kt 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/views/decorators/DividerItemDecoration.kt b/views/src/main/java/ru/touchin/roboswag/views/decorators/DividerItemDecoration.kt new file mode 100644 index 0000000..c2f28de --- /dev/null +++ b/views/src/main/java/ru/touchin/roboswag/views/decorators/DividerItemDecoration.kt @@ -0,0 +1,74 @@ +package ru.touchin.roboswag.views.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 + +} From 5b09479c884aeaba380bdf8499cccdd075fcfac8 Mon Sep 17 00:00:00 2001 From: stanislav Date: Tue, 1 Sep 2020 00:40:30 +0300 Subject: [PATCH 134/154] BottomDividerItemDecoration --- .../decorators/BottomDividerItemDecoration.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 views/src/main/java/ru/touchin/roboswag/views/decorators/BottomDividerItemDecoration.kt diff --git a/views/src/main/java/ru/touchin/roboswag/views/decorators/BottomDividerItemDecoration.kt b/views/src/main/java/ru/touchin/roboswag/views/decorators/BottomDividerItemDecoration.kt new file mode 100644 index 0000000..bc14997 --- /dev/null +++ b/views/src/main/java/ru/touchin/roboswag/views/decorators/BottomDividerItemDecoration.kt @@ -0,0 +1,30 @@ +package ru.touchin.roboswag.views.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() + +} From 1832d5a6ba1aac3add09620aba5cbecb3a3589db Mon Sep 17 00:00:00 2001 From: stanislav Date: Tue, 1 Sep 2020 00:41:01 +0300 Subject: [PATCH 135/154] replaced GroupItemDecoration --- .../touchin/roboswag/views}/decorators/GroupItemDecoration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters => views/src/main/java/ru/touchin/roboswag/views}/decorators/GroupItemDecoration.kt (98%) diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt b/views/src/main/java/ru/touchin/roboswag/views/decorators/GroupItemDecoration.kt similarity index 98% rename from recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt rename to views/src/main/java/ru/touchin/roboswag/views/decorators/GroupItemDecoration.kt index 8cc38bf..f4dbc03 100644 --- a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/recyclerview_adapters/decorators/GroupItemDecoration.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/decorators/GroupItemDecoration.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.recyclerview_adapters.decorators +package ru.touchin.roboswag.views.decorators import android.graphics.Canvas import android.graphics.Rect From 3ba5ba62c5bb637fca0a1852455b0da7c9eaf299 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Tue, 1 Sep 2020 00:53:32 +0300 Subject: [PATCH 136/154] updated imports --- .../java/ru/touchin/roboswag/views/MaterialLoadingBar.java | 1 - .../java/ru/touchin/roboswag/views/TypefacedEditText.java | 4 ++-- .../java/ru/touchin/roboswag/views/TypefacedTextView.java | 4 ++-- .../ru/touchin/roboswag/views/widget/LoadingContentView.kt | 4 ++-- .../main/java/ru/touchin/roboswag/views/widget/Switcher.java | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/views/src/main/java/ru/touchin/roboswag/views/MaterialLoadingBar.java b/views/src/main/java/ru/touchin/roboswag/views/MaterialLoadingBar.java index 83b5cfa..1b495b2 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/MaterialLoadingBar.java +++ b/views/src/main/java/ru/touchin/roboswag/views/MaterialLoadingBar.java @@ -30,7 +30,6 @@ import android.util.AttributeSet; import android.util.TypedValue; import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.components.views.R; /** * Created by Ilia Kurtov on 07/12/2016. diff --git a/views/src/main/java/ru/touchin/roboswag/views/TypefacedEditText.java b/views/src/main/java/ru/touchin/roboswag/views/TypefacedEditText.java index 5253fdf..3d38246 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/TypefacedEditText.java +++ b/views/src/main/java/ru/touchin/roboswag/views/TypefacedEditText.java @@ -40,8 +40,8 @@ import java.util.ArrayList; import java.util.List; import ru.touchin.defaults.DefaultTextWatcher; -import ru.touchin.roboswag.components.views.BuildConfig; -import ru.touchin.roboswag.components.views.R; +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/views/TypefacedTextView.java b/views/src/main/java/ru/touchin/roboswag/views/TypefacedTextView.java index 8eaaf4d..ea849ad 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/TypefacedTextView.java +++ b/views/src/main/java/ru/touchin/roboswag/views/TypefacedTextView.java @@ -34,8 +34,8 @@ import java.util.ArrayList; import java.util.List; import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.components.views.BuildConfig; -import ru.touchin.roboswag.components.views.R; +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/views/widget/LoadingContentView.kt b/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt index 0409fb3..72c5f71 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/LoadingContentView.kt @@ -6,8 +6,8 @@ import android.view.LayoutInflater import androidx.core.content.withStyledAttributes import ru.touchin.extensions.observable import ru.touchin.extensions.setOnRippleClickListener -import ru.touchin.roboswag.components.views.R -import ru.touchin.roboswag.components.views.databinding.ProgressViewBinding +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 diff --git a/views/src/main/java/ru/touchin/roboswag/views/widget/Switcher.java b/views/src/main/java/ru/touchin/roboswag/views/widget/Switcher.java index dd0ab2a..949f993 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/Switcher.java +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/Switcher.java @@ -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 { From ae8491bb4605434896722d78c414bcb2ff3b6dc4 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Tue, 1 Sep 2020 13:39:24 +0300 Subject: [PATCH 137/154] replaced decorators to recyclerview_decorators replaced decorators to recyclerview_decorators --- recyclerview-decorators/.gitignore | 1 + recyclerview-decorators/build.gradle | 32 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 1 + .../decorators/BottomDividerItemDecoration.kt | 2 +- .../decorators/DividerItemDecoration.kt | 2 +- .../decorators/GroupItemDecoration.kt | 2 +- 6 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 recyclerview-decorators/.gitignore create mode 100644 recyclerview-decorators/build.gradle create mode 100644 recyclerview-decorators/src/main/AndroidManifest.xml rename {views/src/main/java/ru/touchin/roboswag/views => recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators}/decorators/BottomDividerItemDecoration.kt (95%) rename {views/src/main/java/ru/touchin/roboswag/views => recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators}/decorators/DividerItemDecoration.kt (97%) rename {views/src/main/java/ru/touchin/roboswag/views => recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators}/decorators/GroupItemDecoration.kt (98%) 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/views/src/main/java/ru/touchin/roboswag/views/decorators/BottomDividerItemDecoration.kt b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/BottomDividerItemDecoration.kt similarity index 95% rename from views/src/main/java/ru/touchin/roboswag/views/decorators/BottomDividerItemDecoration.kt rename to recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/BottomDividerItemDecoration.kt index bc14997..e3599d1 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/decorators/BottomDividerItemDecoration.kt +++ b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/BottomDividerItemDecoration.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.views.decorators +package ru.touchin.roboswag.recyclerview_decorators.decorators import android.content.Context import android.graphics.Rect diff --git a/views/src/main/java/ru/touchin/roboswag/views/decorators/DividerItemDecoration.kt b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/DividerItemDecoration.kt similarity index 97% rename from views/src/main/java/ru/touchin/roboswag/views/decorators/DividerItemDecoration.kt rename to recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/DividerItemDecoration.kt index c2f28de..386dd77 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/decorators/DividerItemDecoration.kt +++ b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/DividerItemDecoration.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.views.decorators +package ru.touchin.roboswag.recyclerview_decorators.decorators import android.content.Context import android.graphics.Canvas diff --git a/views/src/main/java/ru/touchin/roboswag/views/decorators/GroupItemDecoration.kt b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/GroupItemDecoration.kt similarity index 98% rename from views/src/main/java/ru/touchin/roboswag/views/decorators/GroupItemDecoration.kt rename to recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/GroupItemDecoration.kt index f4dbc03..6ff4d29 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/decorators/GroupItemDecoration.kt +++ b/recyclerview-decorators/src/main/java/ru/touchin/roboswag/recyclerview_decorators/decorators/GroupItemDecoration.kt @@ -1,4 +1,4 @@ -package ru.touchin.roboswag.views.decorators +package ru.touchin.roboswag.recyclerview_decorators.decorators import android.graphics.Canvas import android.graphics.Rect From d9fddd8276605f96b3e6fce66d12b5e1326f1cba Mon Sep 17 00:00:00 2001 From: Bas9312 Date: Thu, 3 Sep 2020 15:38:59 +0500 Subject: [PATCH 138/154] play-services-location updated to more actual version --- livedata-location/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livedata-location/build.gradle b/livedata-location/build.gradle index b61b9ce..b454b52 100644 --- a/livedata-location/build.gradle +++ b/livedata-location/build.gradle @@ -10,7 +10,7 @@ dependencies { constraints { implementation("com.google.android.gms:play-services-location") { version { - require '1.0.0' + require '17.0.0' } } } From 6ef642fd1cf52e71b665c5213f366b0e6861d7dc Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Tue, 8 Sep 2020 18:47:24 +0300 Subject: [PATCH 139/154] fix gradle scripts for new modules --- android-plugins/build.gradle.kts | 18 ---- .../kotlin/plugins/lib-settings.gradle.kts | 23 ----- .../gradle-plugins/android_app.properties | 1 - mvi-arch/build.gradle | 86 +++++++++++++++++++ mvi-arch/build.gradle.kts | 13 --- navigation-cicerone/build.gradle | 27 ++++++ navigation-cicerone/build.gradle.kts | 10 --- pagination/build.gradle | 29 +++++++ pagination/build.gradle.kts | 12 --- 9 files changed, 142 insertions(+), 77 deletions(-) delete mode 100644 android-plugins/build.gradle.kts delete mode 100644 android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts delete mode 100644 android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties create mode 100644 mvi-arch/build.gradle delete mode 100644 mvi-arch/build.gradle.kts create mode 100644 navigation-cicerone/build.gradle delete mode 100644 navigation-cicerone/build.gradle.kts create mode 100644 pagination/build.gradle delete mode 100644 pagination/build.gradle.kts diff --git a/android-plugins/build.gradle.kts b/android-plugins/build.gradle.kts deleted file mode 100644 index b044b2e..0000000 --- a/android-plugins/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - `kotlin-dsl` - `kotlin-dsl-precompiled-script-plugins` -} - -repositories { - jcenter() - google() -} - -dependencies { - implementation("com.android.tools.build:gradle:4.0.0") - - implementation("com.android.tools.build:gradle-api:4.0.0") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61") - -} diff --git a/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts b/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts deleted file mode 100644 index 016b271..0000000 --- a/android-plugins/src/main/kotlin/plugins/lib-settings.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id("com.android.application") apply false - id("kotlin-android") - id("kotlin-android-extensions") -} - -android { - compileSdkVersion(29) - - defaultConfig { - minSdkVersion(21) - targetSdkVersion(29) - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } -} diff --git a/android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties b/android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties deleted file mode 100644 index e5d4100..0000000 --- a/android-plugins/src/main/resources/META-INF/gradle-plugins/android_app.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=plugins.AndroidAppPlugin diff --git a/mvi-arch/build.gradle b/mvi-arch/build.gradle new file mode 100644 index 0000000..5306a90 --- /dev/null +++ b/mvi-arch/build.gradle @@ -0,0 +1,86 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + implementation project(":navigation-base") + implementation project(":lifecycle") + implementation project(":kotlin-extensions") + + 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") + implementation("com.github.valeryponomarenko.componentsmanager:androidx") + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android") + + def fragmentVersion = "1.2.1" + def lifecycleVersion = "2.2.0" + def coroutinesVersion = "1.3.7" + + 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("2.27") + } + } + 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/build.gradle.kts b/mvi-arch/build.gradle.kts deleted file mode 100644 index 99febc6..0000000 --- a/mvi-arch/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG) -} - -dependencies { - androidX() - fragment() - lifecycle() - - dagger() - - coroutines() -} diff --git a/navigation-cicerone/build.gradle b/navigation-cicerone/build.gradle new file mode 100644 index 0000000..a8a659d --- /dev/null +++ b/navigation-cicerone/build.gradle @@ -0,0 +1,27 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + implementation project(":navigation-base") + + implementation("ru.terrakok.cicerone:cicerone") + implementation("androidx.fragment:fragment") + implementation("com.google.dagger:dagger") + + constraints { + implementation("ru.terrakok.cicerone:cicerone") { + version { + require("5.1.0") + } + } + implementation("androidx.fragment:fragment") { + version { + require("1.2.1") + } + } + implementation("com.google.dagger:dagger") { + version { + require("2.27") + } + } + } +} diff --git a/navigation-cicerone/build.gradle.kts b/navigation-cicerone/build.gradle.kts deleted file mode 100644 index a4dc829..0000000 --- a/navigation-cicerone/build.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG) -} - -dependencies { - implementationModule(Module.RoboSwag.NAVIGATION_BASE) - implementation(Library.CICERONE) - fragment() - dagger(withAssistedInject = false) -} diff --git a/pagination/build.gradle b/pagination/build.gradle new file mode 100644 index 0000000..98e100c --- /dev/null +++ b/pagination/build.gradle @@ -0,0 +1,29 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + implementation project(":mvi-arch") + implementation project(":recyclerview-adapters") + implementation project(":utils") + + implementation("com.google.android.material:material") + implementation("androidx.swiperefreshlayout:swiperefreshlayout") + implementation("androidx.recyclerview:recyclerview") + + 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") + } + } + } +} diff --git a/pagination/build.gradle.kts b/pagination/build.gradle.kts deleted file mode 100644 index 53f07c6..0000000 --- a/pagination/build.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - id(Plugins.ANDROID_LIB_PLUGIN_WITH_DEFAULT_CONFIG) -} - -dependencies { - mvi() - materialDesign() - recyclerView() - implementationModule(Module.RoboSwag.KOTLIN_EXTENSIONS) - implementationModule(Module.RoboSwag.VIEWS) - implementationModule(Module.RoboSwag.UTILS) -} From a5fb2743f661206e7f1e344f0061de4a92bd5d4f Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Wed, 9 Sep 2020 11:58:37 +0300 Subject: [PATCH 140/154] added max integer path logic added max integer path logic --- .../views/widget/AmountWithDecimalDecorator.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 index bf0675b..d814e7a 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -13,6 +13,7 @@ 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 ) { @@ -23,6 +24,7 @@ class AmountWithDecimalDecorator( 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(",", ".") @@ -52,6 +54,12 @@ class AmountWithDecimalDecorator( try { var currentText = text possibleDecimalSeparators.forEach { currentText = currentText.replace(it, decimalSeparator) } + val currentIntegerPathLength = currentText.withoutFormatting().split(decimalSeparator)[0].length + + if (isIntegerPathToLong(currentIntegerPathLength)) { + setTextWhenNewInputIncorrect(currentText, cursorPosition) + return + } if (isTextFormatIncorrect(currentText)) { setTextWhenNewInputIncorrect(currentText, cursorPosition) @@ -89,6 +97,8 @@ class AmountWithDecimalDecorator( } } + private fun isIntegerPathToLong(currentIntegerPathLength: Int) = currentIntegerPathLength > integerPartLength + private fun isTextFormatIncorrect(currentText: String) = currentText == decimalSeparator || currentText.count { it == decimalSeparator[0] } > 1 From 23c8a987c1600c31c80c9303e5e1d84d4850af55 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Wed, 9 Sep 2020 12:31:48 +0300 Subject: [PATCH 141/154] added paginator full state if itemList < pageSize added paginator full state if itemList < pageSize --- .../main/java/ru/touchin/roboswag/pagination/Paginator.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt index 164cad6..dc681b9 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -10,7 +10,8 @@ import ru.touchin.roboswag.mvi_arch.marker.ViewState class Paginator( private val errorHandleMod: ErrorHandleMod, - private val loadPage: suspend (Int) -> List + private val loadPage: suspend (Int) -> List, + private val pageSize: Int ) : Store(State.Empty) { sealed class Change : StateChange { @@ -89,7 +90,7 @@ class Paginator( } } is State.NewPageProgress<*> -> { - if (items.isEmpty()) { + if (items.isEmpty() || items.size < pageSize) { State.FullData(currentState.pageCount, currentState.data) } else { State.Data(currentState.pageCount + 1, currentState.data + items) From f2c972d69f6fc0b9437c1739aca9b28e8749bd7a Mon Sep 17 00:00:00 2001 From: stanislav Date: Wed, 9 Sep 2020 12:46:29 +0300 Subject: [PATCH 142/154] removed extra condition in paginator --- .../src/main/java/ru/touchin/roboswag/pagination/Paginator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt index dc681b9..53b808a 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -90,7 +90,7 @@ class Paginator( } } is State.NewPageProgress<*> -> { - if (items.isEmpty() || items.size < pageSize) { + if (items.size < pageSize) { State.FullData(currentState.pageCount, currentState.data) } else { State.Data(currentState.pageCount + 1, currentState.data + items) From aa8fceb34223588ed48e3d798b47d3ca770fda2a Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Wed, 9 Sep 2020 16:26:19 +0300 Subject: [PATCH 143/154] fix memory leaks --- .../navigation_cicerone/flow/FlowFragment.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 index 8a90fd7..08903be 100644 --- 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 @@ -34,17 +34,19 @@ abstract class FlowFragment : Fragment(R.layout.fragment_flow) { 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(object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - router.exit() - } - }) + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, exitRouterOnBackPressed) } open fun createNavigator(): Navigator = SupportAppNavigator( From b373086c8a253b346381be248bcf5cebb298505c Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Wed, 9 Sep 2020 16:50:32 +0300 Subject: [PATCH 144/154] fix errors from wrong gradle setup in new modules --- android-configs/common-config.gradle | 8 ++++++++ base-map/build.gradle | 5 ----- bottom-navigation-fragment/build.gradle | 2 -- bottom-navigation-viewcontroller/build.gradle | 2 -- google-map/build.gradle | 2 -- kotlin-extensions/build.gradle | 2 -- lifecycle-rx/build.gradle | 2 -- lifecycle-viewcontroller/build.gradle | 2 -- lifecycle/build.gradle | 2 -- livedata-location/build.gradle | 2 -- navigation-base/build.gradle | 2 -- navigation-cicerone/build.gradle | 11 ++++++++++- navigation-viewcontroller/build.gradle | 2 -- pagination/build.gradle | 17 +++++++++++++++++ recyclerview-adapters/build.gradle | 2 -- rx-extensions/build.gradle | 1 - utils/build.gradle | 1 - yandex-map/build.gradle | 2 -- 18 files changed, 35 insertions(+), 32 deletions(-) diff --git a/android-configs/common-config.gradle b/android-configs/common-config.gradle index 4949440..7871c2e 100644 --- a/android-configs/common-config.gradle +++ b/android-configs/common-config.gradle @@ -23,4 +23,12 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" } diff --git a/base-map/build.gradle b/base-map/build.gradle index 28cb33f..abacdd7 100644 --- a/base-map/build.gradle +++ b/base-map/build.gradle @@ -1,6 +1 @@ apply from: "../android-configs/lib-config.gradle" - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib" - -} diff --git a/bottom-navigation-fragment/build.gradle b/bottom-navigation-fragment/build.gradle index 7be765e..70d5eea 100644 --- a/bottom-navigation-fragment/build.gradle +++ b/bottom-navigation-fragment/build.gradle @@ -4,8 +4,6 @@ dependencies { implementation project(":navigation-base") implementation project(":bottom-navigation-base") - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.core:core-ktx" implementation "androidx.appcompat:appcompat" diff --git a/bottom-navigation-viewcontroller/build.gradle b/bottom-navigation-viewcontroller/build.gradle index ab6dfc9..c4d828d 100644 --- a/bottom-navigation-viewcontroller/build.gradle +++ b/bottom-navigation-viewcontroller/build.gradle @@ -5,8 +5,6 @@ dependencies { implementation project(":navigation-viewcontroller") implementation project(":bottom-navigation-base") - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.core:core-ktx" implementation "androidx.appcompat:appcompat" diff --git a/google-map/build.gradle b/google-map/build.gradle index 5dbe162..18e0af5 100644 --- a/google-map/build.gradle +++ b/google-map/build.gradle @@ -3,8 +3,6 @@ apply from: "../android-configs/lib-config.gradle" dependencies { api project(":base-map") - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "com.google.android.gms:play-services-maps" constraints { diff --git a/kotlin-extensions/build.gradle b/kotlin-extensions/build.gradle index 9a33a8e..007e067 100644 --- a/kotlin-extensions/build.gradle +++ b/kotlin-extensions/build.gradle @@ -1,8 +1,6 @@ apply from: "../android-configs/lib-config.gradle" dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.recyclerview:recyclerview" constraints { diff --git a/lifecycle-rx/build.gradle b/lifecycle-rx/build.gradle index fb335b2..f78e445 100644 --- a/lifecycle-rx/build.gradle +++ b/lifecycle-rx/build.gradle @@ -5,8 +5,6 @@ dependencies { api project(":logging") api project(":lifecycle") - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.appcompat:appcompat" implementation "androidx.lifecycle:lifecycle-extensions" diff --git a/lifecycle-viewcontroller/build.gradle b/lifecycle-viewcontroller/build.gradle index 22d42c4..e76fa52 100644 --- a/lifecycle-viewcontroller/build.gradle +++ b/lifecycle-viewcontroller/build.gradle @@ -6,8 +6,6 @@ dependencies { compileOnly "javax.inject:javax.inject:1" - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.appcompat:appcompat" implementation "androidx.fragment:fragment" diff --git a/lifecycle/build.gradle b/lifecycle/build.gradle index 6538e89..ce6fc3e 100644 --- a/lifecycle/build.gradle +++ b/lifecycle/build.gradle @@ -3,8 +3,6 @@ apply from: "../android-configs/lib-config.gradle" dependencies { compileOnly "javax.inject:javax.inject:1" - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.appcompat:appcompat" implementation "androidx.fragment:fragment" diff --git a/livedata-location/build.gradle b/livedata-location/build.gradle index b454b52..15afa97 100644 --- a/livedata-location/build.gradle +++ b/livedata-location/build.gradle @@ -3,8 +3,6 @@ apply from: "../android-configs/lib-config.gradle" dependencies { api project(":lifecycle") - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "com.google.android.gms:play-services-location" constraints { diff --git a/navigation-base/build.gradle b/navigation-base/build.gradle index 2b9ae58..ef03f06 100644 --- a/navigation-base/build.gradle +++ b/navigation-base/build.gradle @@ -14,8 +14,6 @@ dependencies { implementation 'net.danlew:android.joda' - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.appcompat:appcompat" implementation "androidx.fragment:fragment" diff --git a/navigation-cicerone/build.gradle b/navigation-cicerone/build.gradle index a8a659d..fda8578 100644 --- a/navigation-cicerone/build.gradle +++ b/navigation-cicerone/build.gradle @@ -1,4 +1,5 @@ apply from: "../android-configs/lib-config.gradle" +apply plugin: 'kotlin-kapt' dependencies { implementation project(":navigation-base") @@ -6,6 +7,9 @@ dependencies { implementation("ru.terrakok.cicerone:cicerone") implementation("androidx.fragment:fragment") implementation("com.google.dagger:dagger") + kapt("com.google.dagger:dagger-compiler") + + def daggerVersion = "2.27" constraints { implementation("ru.terrakok.cicerone:cicerone") { @@ -20,7 +24,12 @@ dependencies { } implementation("com.google.dagger:dagger") { version { - require("2.27") + require(daggerVersion) + } + } + kapt("com.google.dagger:dagger-compiler") { + version { + require(daggerVersion) } } } diff --git a/navigation-viewcontroller/build.gradle b/navigation-viewcontroller/build.gradle index 05bbe39..95448fe 100644 --- a/navigation-viewcontroller/build.gradle +++ b/navigation-viewcontroller/build.gradle @@ -11,8 +11,6 @@ dependencies { implementation 'net.danlew:android.joda' - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.appcompat:appcompat" implementation("com.crashlytics.sdk.android:crashlytics") diff --git a/pagination/build.gradle b/pagination/build.gradle index 98e100c..4458f16 100644 --- a/pagination/build.gradle +++ b/pagination/build.gradle @@ -4,11 +4,18 @@ 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 { @@ -25,5 +32,15 @@ dependencies { 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/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle index 0b09710..8b8971b 100644 --- a/recyclerview-adapters/build.gradle +++ b/recyclerview-adapters/build.gradle @@ -3,8 +3,6 @@ apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(':kotlin-extensions') - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "androidx.recyclerview:recyclerview" implementation "androidx.core:core-ktx" diff --git a/rx-extensions/build.gradle b/rx-extensions/build.gradle index bb4e059..3e4f58f 100644 --- a/rx-extensions/build.gradle +++ b/rx-extensions/build.gradle @@ -4,7 +4,6 @@ dependencies { implementation project(":utils") implementation project(":logging") - implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "io.reactivex.rxjava2:rxjava" constraints { diff --git a/utils/build.gradle b/utils/build.gradle index f8763bd..2a77fe4 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -3,7 +3,6 @@ apply from: "../android-configs/lib-config.gradle" dependencies { implementation "androidx.core:core" implementation "androidx.annotation:annotation" - implementation "org.jetbrains.kotlin:kotlin-stdlib" constraints { implementation("androidx.core:core") { diff --git a/yandex-map/build.gradle b/yandex-map/build.gradle index 88830e4..0f4b147 100644 --- a/yandex-map/build.gradle +++ b/yandex-map/build.gradle @@ -3,8 +3,6 @@ apply from: "../android-configs/lib-config.gradle" dependencies { implementation project(":base-map") - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "com.yandex.android:mapkit" constraints { From 06b5dc516dc1869c9829ea0cb41ecc97124d0c2b Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Wed, 9 Sep 2020 19:13:24 +0300 Subject: [PATCH 145/154] fixed crash of release build because of duplicate dagger generated code --- mvi-arch/build.gradle | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mvi-arch/build.gradle b/mvi-arch/build.gradle index 5306a90..b115592 100644 --- a/mvi-arch/build.gradle +++ b/mvi-arch/build.gradle @@ -1,4 +1,5 @@ apply from: "../android-configs/lib-config.gradle" +apply plugin: 'kotlin-kapt' dependencies { implementation project(":navigation-base") @@ -16,6 +17,7 @@ dependencies { 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") @@ -24,6 +26,7 @@ dependencies { 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") { @@ -63,7 +66,12 @@ dependencies { } implementation("com.google.dagger:dagger") { version { - require("2.27") + require(daggerVersion) + } + } + kapt("com.google.dagger:dagger-compiler") { + version { + require(daggerVersion) } } implementation("com.github.valeryponomarenko.componentsmanager:androidx") { From 5b39a9ba1f86c6510a4d80a178830ae585e6cf07 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Thu, 10 Sep 2020 16:07:25 +0300 Subject: [PATCH 146/154] added trim integer path to integerPartLength added trim integer path to integerPartLength --- .../views/widget/AmountWithDecimalDecorator.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 index d814e7a..e7e34ec 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -57,8 +57,7 @@ class AmountWithDecimalDecorator( val currentIntegerPathLength = currentText.withoutFormatting().split(decimalSeparator)[0].length if (isIntegerPathToLong(currentIntegerPathLength)) { - setTextWhenNewInputIncorrect(currentText, cursorPosition) - return + currentText = trimIntegerPath(currentText) } if (isTextFormatIncorrect(currentText)) { @@ -99,6 +98,13 @@ class AmountWithDecimalDecorator( private fun isIntegerPathToLong(currentIntegerPathLength: Int) = currentIntegerPathLength > integerPartLength + private fun trimIntegerPath(currentText: String): String { + val textSplit = currentText.withoutFormatting().split(decimalSeparator) + val integerPath = textSplit[0] + val decimalPath = if (textSplit.size > 1) decimalSeparator + textSplit[1] else "" + return integerPath.take(integerPartLength) + decimalPath + } + private fun isTextFormatIncorrect(currentText: String) = currentText == decimalSeparator || currentText.count { it == decimalSeparator[0] } > 1 From e2a3599ea6b8702f1d0c2f5ed426e1581580fe8f Mon Sep 17 00:00:00 2001 From: stanislav Date: Thu, 10 Sep 2020 16:25:23 +0300 Subject: [PATCH 147/154] fixed names in AmountWithDecimalDecorator.kt --- .../widget/AmountWithDecimalDecorator.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 index e7e34ec..0e3794f 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -54,10 +54,10 @@ class AmountWithDecimalDecorator( try { var currentText = text possibleDecimalSeparators.forEach { currentText = currentText.replace(it, decimalSeparator) } - val currentIntegerPathLength = currentText.withoutFormatting().split(decimalSeparator)[0].length + val currentIntegerPartLength = currentText.withoutFormatting().split(decimalSeparator)[0].length - if (isIntegerPathToLong(currentIntegerPathLength)) { - currentText = trimIntegerPath(currentText) + if (isIntegerPartToLong(currentIntegerPartLength)) { + currentText = trimIntegerPart(currentText) } if (isTextFormatIncorrect(currentText)) { @@ -71,7 +71,7 @@ class AmountWithDecimalDecorator( } val currentDecimalPartLength = currentText.split(decimalSeparator).getOrNull(1)?.length - if (isDecimalPathTooLong(currentDecimalPartLength)) { + if (isDecimalPartTooLong(currentDecimalPartLength)) { setTextWhenNewInputIncorrect(currentText, cursorPosition) return } @@ -96,13 +96,13 @@ class AmountWithDecimalDecorator( } } - private fun isIntegerPathToLong(currentIntegerPathLength: Int) = currentIntegerPathLength > integerPartLength + private fun isIntegerPartToLong(currentIntegerPartLength: Int) = currentIntegerPartLength > integerPartLength - private fun trimIntegerPath(currentText: String): String { - val textSplit = currentText.withoutFormatting().split(decimalSeparator) - val integerPath = textSplit[0] - val decimalPath = if (textSplit.size > 1) decimalSeparator + textSplit[1] else "" - return integerPath.take(integerPartLength) + decimalPath + 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) = @@ -155,7 +155,7 @@ class AmountWithDecimalDecorator( } } - private fun isDecimalPathTooLong(currentDecimalPartLength: Int?) = + private fun isDecimalPartTooLong(currentDecimalPartLength: Int?) = !isSeparatorCutInvalidDecimalLength && currentDecimalPartLength != null && currentDecimalPartLength > decimalPartLength From 78a51d60f8da2c96ed71dcb286a06f6210508599 Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Thu, 10 Sep 2020 16:59:00 +0300 Subject: [PATCH 148/154] added FullState case to pagonator added FullState case to pagonator --- .../ru/touchin/roboswag/pagination/Paginator.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt index 53b808a..1a3d52b 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -76,17 +76,17 @@ class Paginator( val items = change.items when (currentState) { is State.EmptyProgress -> { - if (items.isEmpty()) { - State.Empty - } else { - State.Data(0, items) + when { + items.isEmpty() -> State.Empty + items.size < pageSize -> State.FullData(0, items) + else -> State.Data(0, items) } } is State.Refresh<*> -> { - if (items.isEmpty()) { - State.Empty - } else { - State.Data(0, items) + when { + items.isEmpty() -> State.Empty + items.size < pageSize -> State.FullData(0, items) + else -> State.Data(0, items) } } is State.NewPageProgress<*> -> { From c2ba0ce66f52a2ebd9fc5613845ac4461bb05c9e Mon Sep 17 00:00:00 2001 From: stanislav Date: Fri, 11 Sep 2020 12:23:10 +0300 Subject: [PATCH 149/154] paginator fix in NewPageProgress case State.FullData --- .../src/main/java/ru/touchin/roboswag/pagination/Paginator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt index 1a3d52b..1393444 100644 --- a/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt +++ b/pagination/src/main/java/ru/touchin/roboswag/pagination/Paginator.kt @@ -91,7 +91,7 @@ class Paginator( } is State.NewPageProgress<*> -> { if (items.size < pageSize) { - State.FullData(currentState.pageCount, currentState.data) + State.FullData(currentState.pageCount, currentState.data + items) } else { State.Data(currentState.pageCount + 1, currentState.data + items) } From 93ea90101984e199d58e484b99d87644eb49b2bc Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Mon, 14 Sep 2020 12:20:43 +0300 Subject: [PATCH 150/154] amount with decimal decorator round amount with decimal decorator round --- .../roboswag/views/widget/AmountWithDecimalDecorator.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 0e3794f..281b9c6 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -8,6 +8,7 @@ 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, @@ -214,8 +215,6 @@ class AmountWithDecimalDecorator( return formatter.format(this.replaceSeparatorsToDot().toDouble().floor()) } - private fun Double.floor() = - (this * 10.toDouble().pow(decimalPartLength)).toLong() / 10.toDouble() - .pow(decimalPartLength) + private fun Double.floor() = (this * 10f.pow(decimalPartLength)).roundToLong() / 10f.pow(decimalPartLength) } From 965f74f08bb4d82f03e327c9d4fba63787ecd1fc Mon Sep 17 00:00:00 2001 From: Stanisalv Date: Mon, 14 Sep 2020 12:43:30 +0300 Subject: [PATCH 151/154] add TODO in AmountWithDecimalDecorator add TODO in AmountWithDecimalDecorator --- .../touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt | 1 + 1 file changed, 1 insertion(+) 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 index 281b9c6..c14a52f 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -215,6 +215,7 @@ class AmountWithDecimalDecorator( return formatter.format(this.replaceSeparatorsToDot().toDouble().floor()) } + // TODO make it simple private fun Double.floor() = (this * 10f.pow(decimalPartLength)).roundToLong() / 10f.pow(decimalPartLength) } From eb32192b34d449dc5318da09b34284525d083c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=91=D0=B0=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D0=BA=D0=B8=D0=B8=CC=86?= Date: Tue, 22 Sep 2020 11:38:10 +0300 Subject: [PATCH 152/154] added mediators for logging --- mvi-arch/build.gradle | 3 ++ .../roboswag/mvi_arch/core/MviViewModel.kt | 31 +++++++++++- .../touchin/roboswag/mvi_arch/core/Store.kt | 13 +++++ .../mvi_arch/mediator/LoggingMediator.kt | 49 +++++++++++++++++++ .../roboswag/mvi_arch/mediator/Mediator.kt | 18 +++++++ .../mvi_arch/mediator/MediatorStore.kt | 25 ++++++++++ 6 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/LoggingMediator.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/Mediator.kt create mode 100644 mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/mediator/MediatorStore.kt diff --git a/mvi-arch/build.gradle b/mvi-arch/build.gradle index b115592..6059357 100644 --- a/mvi-arch/build.gradle +++ b/mvi-arch/build.gradle @@ -5,6 +5,7 @@ dependencies { implementation project(":navigation-base") implementation project(":lifecycle") implementation project(":kotlin-extensions") + implementation project(":logging") implementation("androidx.core:core-ktx") implementation("androidx.appcompat:appcompat") @@ -23,6 +24,8 @@ dependencies { 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" 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 index d3df82c..a04d309 100644 --- 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 @@ -1,12 +1,19 @@ 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. @@ -33,6 +40,12 @@ abstract class MviViewModel(mediatorStore::onNewState) + + init { + viewModelScope.launch { + state.observeForever(stateMediatorObserver) + } + } + + @CallSuper + open fun dispatchAction(action: Action) { + mediatorStore.onAction(action) + } + + 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 index 56eef64..5655d64 100644 --- 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 @@ -14,9 +14,12 @@ 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 @@ -32,6 +35,12 @@ abstract class Store> = mutableListOf() + private val mediatorStore = MediatorStore( + listOfNotNull( + LoggingMediator(this::class.simpleName!!).takeIf { BuildConfig.DEBUG } + ) + ) + init { storeScope.launch { effects @@ -43,10 +52,13 @@ abstract class Store @@ -55,6 +67,7 @@ abstract class Store 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) } + } +} From 6bce581a8db1678492cf06679d0330bc0977e906 Mon Sep 17 00:00:00 2001 From: Maxim Bachinsky Date: Wed, 23 Sep 2020 13:16:59 +0300 Subject: [PATCH 153/154] fixed code review --- .../main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt | 1 + 1 file changed, 1 insertion(+) 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 index a04d309..0654c0e 100644 --- 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 @@ -67,6 +67,7 @@ abstract class MviViewModel Date: Mon, 5 Oct 2020 14:28:29 +0300 Subject: [PATCH 154/154] float to double --- .../touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c14a52f..526f5a1 100644 --- a/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt +++ b/views/src/main/java/ru/touchin/roboswag/views/widget/AmountWithDecimalDecorator.kt @@ -216,6 +216,6 @@ class AmountWithDecimalDecorator( } // TODO make it simple - private fun Double.floor() = (this * 10f.pow(decimalPartLength)).roundToLong() / 10f.pow(decimalPartLength) + private fun Double.floor() = (this * 10.0.pow(decimalPartLength)).roundToLong() / 10.0.pow(decimalPartLength) }