diff --git a/.gitignore b/.gitignore
index 2e84d6fc380..8a04b36aa50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,5 +10,6 @@ build/
out/
secret.gradle
+local.properties
publishing.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59bbbe35d04..e8649aa41f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,41 @@
# Changelog
+## 0.3.1
+
+**ANDROID PACKAGES**
+
+* `Android`:
+ * `RecyclerView`:
+ * Library has been created
+* `Common`
+ * Now available package `dev.inmo:micro_utils.common-android`
+* `Coroutines`
+ * Now available package `dev.inmo:micro_utils.coroutines-android`
+* `Ktor`
+ * `Common`
+ * Now available package `dev.inmo:micro_utils.ktor.common-android`
+ * `Client`
+ * Now available package `dev.inmo:micro_utils.ktor.client-android`
+* `MimeTypes`
+ * Now available package `dev.inmo:micro_utils.mime_types-android`
+* `Pagination`
+ * `Common`
+ * Now available package `dev.inmo:micro_utils.pagination.common-android`
+ * `Ktor`
+ * `Common`
+ * Now available package `dev.inmo:micro_utils.pagination.ktor.common-android`
+* `Repos`
+ * `Common`
+ * Now available package `dev.inmo:micro_utils.repos.common-android`
+ * Now it is possible to use default realizations of repos abstractions natively on android
+ * `Inmemory`
+ * Now available package `dev.inmo:micro_utils.repos.inmemory-android`
+ * `Ktor`
+ * `Common`
+ * Now available package `dev.inmo:micro_utils.repos.ktor.common-android`
+ * `Common`
+ * Now available package `dev.inmo:micro_utils.repos.ktor.client-android`
+
## 0.3.0
All deprecations has been removed
diff --git a/android/recyclerview/build.gradle b/android/recyclerview/build.gradle
new file mode 100644
index 00000000000..c130272d88d
--- /dev/null
+++ b/android/recyclerview/build.gradle
@@ -0,0 +1,23 @@
+plugins {
+ id "org.jetbrains.kotlin.multiplatform"
+ id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
+}
+
+apply from: "$mppAndroidProjectPresetPath"
+
+kotlin {
+ sourceSets {
+ commonMain {
+ dependencies {
+ api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
+ }
+ }
+ androidMain {
+ dependencies {
+ api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
+ }
+ }
+ }
+}
diff --git a/android/recyclerview/src/main/AndroidManifest.xml b/android/recyclerview/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..d08d0502fee
--- /dev/null
+++ b/android/recyclerview/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/AbstractStandardViewHolder.kt b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/AbstractStandardViewHolder.kt
new file mode 100644
index 00000000000..ebcbb481015
--- /dev/null
+++ b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/AbstractStandardViewHolder.kt
@@ -0,0 +1,22 @@
+package dev.inmo.micro_utils.android.recyclerview
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+
+abstract class AbstractStandardViewHolder(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ viewId: Int,
+ onViewInflated: ((View) -> Unit)? = null
+) : AbstractViewHolder(
+ inflater.inflate(viewId, container, false).also {
+ onViewInflated ?.invoke(it)
+ }
+) {
+ constructor(
+ container: ViewGroup,
+ viewId: Int,
+ onViewInflated: ((View) -> Unit)? = null
+ ) : this(LayoutInflater.from(container.context), container, viewId, onViewInflated)
+}
diff --git a/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/AbstractViewHolder.kt b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/AbstractViewHolder.kt
new file mode 100644
index 00000000000..529bbbd4dd3
--- /dev/null
+++ b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/AbstractViewHolder.kt
@@ -0,0 +1,10 @@
+package dev.inmo.micro_utils.android.recyclerview
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+abstract class AbstractViewHolder(
+ view: View
+) : RecyclerView.ViewHolder(view) {
+ abstract fun onBind(item: T)
+}
diff --git a/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/Divider.kt b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/Divider.kt
new file mode 100644
index 00000000000..21b03ca80b0
--- /dev/null
+++ b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/Divider.kt
@@ -0,0 +1,9 @@
+package dev.inmo.micro_utils.android.recyclerview
+
+import android.content.Context
+import android.widget.LinearLayout
+import androidx.recyclerview.widget.DividerItemDecoration
+
+val Context.recyclerViewItemsDecoration
+ get() = DividerItemDecoration(this, LinearLayout.VERTICAL)
+
diff --git a/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/LeftItems.kt b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/LeftItems.kt
new file mode 100644
index 00000000000..fc6d51b864e
--- /dev/null
+++ b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/LeftItems.kt
@@ -0,0 +1,47 @@
+package dev.inmo.micro_utils.android.recyclerview
+
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.*
+
+fun RecyclerView.lastVisibleItemFlow(
+ completingScope: CoroutineScope
+): Flow {
+ val lastVisibleElementFun: () -> Int = (layoutManager as? LinearLayoutManager) ?.let { it::findLastVisibleItemPosition } ?: error("Currently supported only linear layout manager")
+ val lastVisibleFlow = MutableStateFlow(lastVisibleElementFun())
+ addOnScrollListener(
+ object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+ lastVisibleFlow.value = lastVisibleElementFun()
+ }
+ }.also { scrollListener ->
+ lastVisibleFlow.onCompletion {
+ removeOnScrollListener(scrollListener)
+ }.launchIn(completingScope)
+ }
+ )
+ return lastVisibleFlow.asStateFlow()
+}
+
+inline fun Flow.mapLeftItems(
+ crossinline countGetter: () -> Int
+): Flow = map { countGetter() - it }
+
+inline fun Flow.mapRequireFilling(
+ minimalLeftItems: Int,
+ crossinline countGetter: () -> Int
+): Flow = mapLeftItems(countGetter).mapNotNull {
+ if (it < minimalLeftItems) {
+ it
+ } else {
+ null
+ }
+}
+
+inline fun RecyclerView.mapRequireFilling(
+ minimalLeftItems: Int,
+ completingScope: CoroutineScope,
+ crossinline countGetter: () -> Int
+): Flow = lastVisibleItemFlow(completingScope).mapRequireFilling(minimalLeftItems, countGetter)
diff --git a/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/RecyclerViewAdapter.kt b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/RecyclerViewAdapter.kt
new file mode 100644
index 00000000000..96d1020fff6
--- /dev/null
+++ b/android/recyclerview/src/main/kotlin/dev/inmo/micro_utils/android/recyclerview/RecyclerViewAdapter.kt
@@ -0,0 +1,68 @@
+package dev.inmo.micro_utils.android.recyclerview
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+
+abstract class RecyclerViewAdapter(
+ val data: List
+): RecyclerView.Adapter>() {
+ var emptyView: View? = null
+ set(value) {
+ field = value
+ checkEmpty()
+ }
+
+ init {
+ registerAdapterDataObserver(
+ object : RecyclerView.AdapterDataObserver() {
+ override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
+ super.onItemRangeChanged(positionStart, itemCount)
+ checkEmpty()
+ }
+
+ override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
+ super.onItemRangeChanged(positionStart, itemCount, payload)
+ checkEmpty()
+ }
+
+ override fun onChanged() {
+ super.onChanged()
+ checkEmpty()
+ }
+
+ override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
+ super.onItemRangeRemoved(positionStart, itemCount)
+ checkEmpty()
+ }
+
+ override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
+ super.onItemRangeMoved(fromPosition, toPosition, itemCount)
+ checkEmpty()
+ }
+
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ super.onItemRangeInserted(positionStart, itemCount)
+ checkEmpty()
+ }
+ }
+ )
+ checkEmpty()
+ }
+
+ override fun getItemCount(): Int = data.size
+
+ override fun onBindViewHolder(holder: AbstractViewHolder, position: Int) {
+ holder.onBind(data[position])
+ }
+
+ private fun checkEmpty() {
+ emptyView ?. let {
+ if (data.isEmpty()) {
+ it.visibility = View.VISIBLE
+ } else {
+ it.visibility = View.GONE
+ }
+ }
+ }
+}
diff --git a/build.gradle b/build.gradle
index 72868b96ca4..c6d3e1911c6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,16 +1,20 @@
buildscript {
repositories {
- mavenLocal()
jcenter()
+ google()
mavenCentral()
+ mavenLocal()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
+ classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
+ classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
+ classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
}
}
@@ -19,6 +23,7 @@ allprojects {
mavenLocal()
jcenter()
mavenCentral()
+ google()
maven { url "https://kotlin.bintray.com/kotlinx" }
}
}
diff --git a/common/build.gradle b/common/build.gradle
index 9597bd30a20..2ab8e52cd06 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..e62d6f86575
--- /dev/null
+++ b/common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/common/src/main/kotlin/dev/inmo/micro_utils/common/Mapping.kt b/common/src/main/kotlin/dev/inmo/micro_utils/common/Mapping.kt
new file mode 100644
index 00000000000..c9483694bbf
--- /dev/null
+++ b/common/src/main/kotlin/dev/inmo/micro_utils/common/Mapping.kt
@@ -0,0 +1,7 @@
+package dev.inmo.micro_utils.common
+
+@Suppress("UNCHECKED_CAST", "SimplifiableCall")
+inline fun Iterable.mapNotNullA(transform: (T) -> R?): List = map(transform).filter { it != null } as List
+
+@Suppress("UNCHECKED_CAST", "SimplifiableCall")
+inline fun Array.mapNotNullA(mapper: (T) -> R?): List = map(mapper).filter { it != null } as List
diff --git a/coroutines/build.gradle b/coroutines/build.gradle
index 7616133d4f6..19bbfe5aaac 100644
--- a/coroutines/build.gradle
+++ b/coroutines/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -12,5 +14,10 @@ kotlin {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
}
}
+ androidMain {
+ dependencies {
+ api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/coroutines/src/main/AndroidManifest.xml b/coroutines/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..4536ab400ac
--- /dev/null
+++ b/coroutines/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/coroutines/src/main/kotlin/dev/inmo/micro_utils/coroutines/DoInUI.kt b/coroutines/src/main/kotlin/dev/inmo/micro_utils/coroutines/DoInUI.kt
new file mode 100644
index 00000000000..f938e53655f
--- /dev/null
+++ b/coroutines/src/main/kotlin/dev/inmo/micro_utils/coroutines/DoInUI.kt
@@ -0,0 +1,10 @@
+package dev.inmo.micro_utils.coroutines
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+suspend inline fun doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext(
+ Dispatchers.Main,
+ block
+)
diff --git a/defaultAndroidSettings b/defaultAndroidSettings
new file mode 100644
index 00000000000..8e8909ff220
--- /dev/null
+++ b/defaultAndroidSettings
@@ -0,0 +1,31 @@
+android {
+ compileSdkVersion "$android_compileSdkVersion".toInteger()
+ buildToolsVersion "$android_buildToolsVersion"
+
+ defaultConfig {
+ minSdkVersion "$android_minSdkVersion".toInteger()
+ targetSdkVersion "$android_compileSdkVersion".toInteger()
+ versionCode "${android_code_version}".toInteger()
+ versionName "$version"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
+ exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
+ exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8.toString()
+ }
+}
diff --git a/dokka/build.gradle b/dokka/build.gradle
index b619aa90912..fee10660b29 100644
--- a/dokka/build.gradle
+++ b/dokka/build.gradle
@@ -1,26 +1,15 @@
-buildscript {
- repositories {
- mavenLocal()
- jcenter()
- mavenCentral()
- }
-
- dependencies {
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
- classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
- }
-}
-
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
- id "org.jetbrains.dokka" version "$dokka_version"
+ id "com.android.library"
+ id "kotlin-android-extensions"
+ id "org.jetbrains.dokka"
}
repositories {
mavenLocal()
jcenter()
+ google()
mavenCentral()
}
@@ -30,6 +19,7 @@ kotlin {
browser()
nodejs()
}
+ android {}
sourceSets {
commonMain {
@@ -41,6 +31,57 @@ kotlin {
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
+ && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
+ && it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
+ && it.kotlin.sourceSets.any { it.name.contains("androidMain") }
+ ) {
+ api it
+ }
+ }
+ }
+ }
+ jsMain {
+ dependencies {
+ implementation kotlin('stdlib')
+
+ project.parent.subprojects.forEach {
+ if (
+ it != project
+ && it.hasProperty("kotlin")
+ && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
+ && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
+ ) {
+ api it
+ }
+ }
+ }
+ }
+ jvmMain {
+ dependencies {
+ implementation kotlin('stdlib')
+
+ project.parent.subprojects.forEach {
+ if (
+ it != project
+ && it.hasProperty("kotlin")
+ && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
+ && it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
+ ) {
+ api it
+ }
+ }
+ }
+ }
+ androidMain {
+ dependencies {
+ implementation kotlin('stdlib')
+
+ project.parent.subprojects.forEach {
+ if (
+ it != project
+ && it.hasProperty("kotlin")
+ && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
+ && it.kotlin.sourceSets.any { it.name.contains("androidMain") }
) {
api it
}
@@ -84,5 +125,11 @@ tasks.dokkaHtml {
named("jvmMain") {
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
}
+
+ named("androidMain") {
+ sourceRoots.setFrom(findSourcesWithName("androidMain", "commonMain"))
+ }
}
}
+
+apply from: "$defaultAndroidSettingsPresetPath"
diff --git a/dokka/gradle.properties b/dokka/gradle.properties
deleted file mode 100644
index 55021b5f4e7..00000000000
--- a/dokka/gradle.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-dokka_version=1.4.0
-
-org.gradle.jvmargs=-Xmx1024m
diff --git a/dokka/src/main/AndroidManifest.xml b/dokka/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..adef5c13dee
--- /dev/null
+++ b/dokka/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions.gradle b/extensions.gradle
index f71cff3286a..dd5e93145fa 100644
--- a/extensions.gradle
+++ b/extensions.gradle
@@ -21,6 +21,9 @@ allprojects {
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject"
+ mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject"
+
+ defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
diff --git a/gradle.properties b/gradle.properties
index 6d973f0d0d7..d7cce5368b1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,6 +3,9 @@ org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin.incremental.js=true
+android.useAndroidX=true
+android.enableJetifier=true
+org.gradle.jvmargs=-Xmx4096m
kotlin_version=1.4.10
kotlin_coroutines_version=1.4.1
@@ -18,5 +21,25 @@ github_release_plugin_version=2.2.12
uuidVersion=0.2.2
+# ANDROID
+
+core_ktx_version=1.3.2
+androidx_recycler_version=1.1.0
+
+android_minSdkVersion=24
+android_compileSdkVersion=30
+android_buildToolsVersion=30.0.2
+dexcount_version=2.0.0-RC1
+junit_version=4.12
+test_ext_junit_version=1.1.2
+espresso_core=3.3.0
+
+# Dokka
+
+dokka_version=1.4.0
+
+# Project data
+
group=dev.inmo
-version=0.3.0
+version=0.3.1
+android_code_version=1
diff --git a/ktor/client/build.gradle b/ktor/client/build.gradle
index 280a7647124..6983e6f63b4 100644
--- a/ktor/client/build.gradle
+++ b/ktor/client/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/ktor/client/src/main/AndroidManifest.xml b/ktor/client/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..034ad33094d
--- /dev/null
+++ b/ktor/client/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ktor/common/build.gradle b/ktor/common/build.gradle
index 0d6bb922b88..3413a1a4877 100644
--- a/ktor/common/build.gradle
+++ b/ktor/common/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/ktor/common/src/main/AndroidManifest.xml b/ktor/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..9a1797bdc71
--- /dev/null
+++ b/ktor/common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mime_types/build.gradle b/mime_types/build.gradle
index 9597bd30a20..2ab8e52cd06 100644
--- a/mime_types/build.gradle
+++ b/mime_types/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/mime_types/src/main/AndroidManifest.xml b/mime_types/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..be8221df297
--- /dev/null
+++ b/mime_types/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mppAndroidProject b/mppAndroidProject
new file mode 100644
index 00000000000..c9bee90b00f
--- /dev/null
+++ b/mppAndroidProject
@@ -0,0 +1,24 @@
+project.version = "$version"
+project.group = "$group"
+
+apply from: "$publishGradlePath"
+
+kotlin {
+ android {}
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation kotlin('stdlib')
+ }
+ }
+ commonTest {
+ dependencies {
+ implementation kotlin('test-common')
+ implementation kotlin('test-annotations-common')
+ }
+ }
+ }
+}
+
+apply from: "$defaultAndroidSettingsPresetPath"
diff --git a/mppProjectWithSerialization b/mppProjectWithSerialization
index 7ee94f9fec3..f9a1f421547 100644
--- a/mppProjectWithSerialization
+++ b/mppProjectWithSerialization
@@ -9,6 +9,9 @@ kotlin {
browser()
nodejs()
}
+ android {
+ publishLibraryVariants("release")
+ }
sourceSets {
commonMain {
@@ -34,5 +37,14 @@ kotlin {
implementation kotlin('test-junit')
}
}
+ androidTest {
+ dependencies {
+ implementation kotlin('test-junit')
+ implementation "androidx.test.ext:junit:$test_ext_junit_version"
+ implementation "androidx.test.espresso:espresso-core:$espresso_core"
+ }
+ }
}
}
+
+apply from: "$defaultAndroidSettingsPresetPath"
diff --git a/pagination/common/build.gradle b/pagination/common/build.gradle
index 9597bd30a20..2ab8e52cd06 100644
--- a/pagination/common/build.gradle
+++ b/pagination/common/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/pagination/common/src/main/AndroidManifest.xml b/pagination/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..3d678ce1cad
--- /dev/null
+++ b/pagination/common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pagination/ktor/common/build.gradle b/pagination/ktor/common/build.gradle
index dfbe880de3d..18c9967a1ab 100644
--- a/pagination/ktor/common/build.gradle
+++ b/pagination/ktor/common/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/pagination/ktor/common/src/main/AndroidManifest.xml b/pagination/ktor/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..b2896442be7
--- /dev/null
+++ b/pagination/ktor/common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/repos/common/build.gradle b/repos/common/build.gradle
index a36a04e5b95..e78548f9238 100644
--- a/repos/common/build.gradle
+++ b/repos/common/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,5 +17,13 @@ kotlin {
api "com.benasher44:uuid:$uuidVersion"
}
}
+
+ androidMain {
+ dependencies {
+ api "androidx.core:core-ktx:$core_ktx_version"
+ api internalProject("micro_utils.common")
+ api internalProject("micro_utils.coroutines")
+ }
+ }
}
}
\ No newline at end of file
diff --git a/repos/common/src/main/AndroidManifest.xml b/repos/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..66982e66339
--- /dev/null
+++ b/repos/common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ColumnType.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ColumnType.kt
new file mode 100644
index 00000000000..b2026a216f5
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ColumnType.kt
@@ -0,0 +1,46 @@
+package dev.inmo.micro_utils.repos
+
+sealed class ColumnType(
+ typeName: String,
+ nullable: Boolean
+) {
+ open val asType: String = "$typeName${if (!nullable) " not null" else ""}"
+ sealed class Text(
+ nullable: Boolean
+ ) : ColumnType("text", nullable) {
+ object NULLABLE : Text(true)
+ object NOT_NULLABLE : Text(false)
+ }
+ sealed class Numeric(
+ typeName: String,
+ autoincrement: Boolean = false,
+ primaryKey: Boolean = false,
+ nullable: Boolean = false
+ ) : ColumnType(
+ typeName,
+ nullable
+ ) {
+ override val asType: String = "${super.asType}${if (primaryKey) " primary key" else ""}${if (autoincrement) " autoincrement" else ""}"
+
+ class INTEGER(
+ autoincrement: Boolean = false,
+ primaryKey: Boolean = false,
+ nullable: Boolean = false
+ ) : Numeric(
+ "integer",
+ autoincrement,
+ primaryKey,
+ nullable
+ )
+ class DOUBLE(autoincrement: Boolean = false, primaryKey: Boolean = false, nullable: Boolean = false) : Numeric(
+ "double",
+ autoincrement,
+ primaryKey,
+ nullable
+ )
+ }
+
+ override fun toString(): String {
+ return asType
+ }
+}
\ No newline at end of file
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ContentValuesOf.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ContentValuesOf.kt
new file mode 100644
index 00000000000..44b80bd7dc1
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/ContentValuesOf.kt
@@ -0,0 +1,8 @@
+package dev.inmo.micro_utils.repos
+
+import androidx.core.content.contentValuesOf
+
+@Suppress("UNCHECKED_CAST", "SimplifiableCall")
+fun contentValuesOfNotNull(vararg pairs: Pair?) = contentValuesOf(
+ *(pairs.filter { it != null } as List>).toTypedArray()
+)
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/CursorMapping.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/CursorMapping.kt
new file mode 100644
index 00000000000..e74374ac802
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/CursorMapping.kt
@@ -0,0 +1,21 @@
+package dev.inmo.micro_utils.repos
+
+import android.database.Cursor
+
+inline fun Cursor.map(
+ block: (Cursor) -> T
+): List {
+ val result = mutableListOf()
+ if (moveToFirst()) {
+ do {
+ result.add(block(this))
+ } while (moveToNext())
+ }
+ return result
+}
+
+fun Cursor.firstOrNull(): Cursor? = if (moveToFirst()) {
+ this
+} else {
+ null
+}
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt
new file mode 100644
index 00000000000..8a2932eb0fe
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseCoroutineContext.kt
@@ -0,0 +1,6 @@
+package dev.inmo.micro_utils.repos
+
+import kotlinx.coroutines.newSingleThreadContext
+import kotlin.coroutines.CoroutineContext
+
+val DatabaseCoroutineContext: CoroutineContext = newSingleThreadContext("db-context")
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseOperations.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseOperations.kt
new file mode 100644
index 00000000000..b1cfdb28804
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseOperations.kt
@@ -0,0 +1,64 @@
+package dev.inmo.micro_utils.repos
+
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+
+fun createTableQuery(
+ tableName: String,
+ vararg columnsToTypes: Pair
+) = "create table $tableName (${columnsToTypes.joinToString(", ") { "${it.first} ${it.second}" }});"
+
+fun SQLiteDatabase.createTable(
+ tableName: String,
+ vararg columnsToTypes: Pair,
+ onInit: (SQLiteDatabase.() -> Unit)? = null
+): Boolean {
+ val existing = rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName'", null).use {
+ it.count > 0
+ }
+ return if (existing) {
+ false
+ // TODO:: add upgrade opportunity
+ } else {
+ execSQL(createTableQuery(tableName, *columnsToTypes))
+ onInit ?.invoke(this)
+ true
+ }
+}
+
+fun Cursor.getString(columnName: String) = getString(
+ getColumnIndex(columnName)
+)
+
+fun Cursor.getLong(columnName: String) = getLong(
+ getColumnIndex(columnName)
+)
+
+fun Cursor.getInt(columnName: String) = getInt(
+ getColumnIndex(columnName)
+)
+
+fun Cursor.getDouble(columnName: String) = getDouble(
+ getColumnIndex(columnName)
+)
+
+fun SQLiteDatabase.select(
+ table: String,
+ columns: Array? = null,
+ selection: String? = null,
+ selectionArgs: Array? = null,
+ groupBy: String? = null,
+ having: String? = null,
+ orderBy: String? = null,
+ limit: String? = null
+) = query(
+ table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
+)
+
+fun makePlaceholders(count: Int): String {
+ return (0 until count).joinToString { "?" }
+}
+
+fun makeStringPlaceholders(count: Int): String {
+ return (0 until count).joinToString { "\"?\"" }
+}
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabasePaginationExtensions.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabasePaginationExtensions.kt
new file mode 100644
index 00000000000..df72f0b4b61
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabasePaginationExtensions.kt
@@ -0,0 +1,8 @@
+package dev.inmo.micro_utils.repos
+
+import dev.inmo.micro_utils.pagination.Pagination
+import dev.inmo.micro_utils.pagination.firstIndex
+
+fun limitClause(size: Long, since: Long? = null) = "${since ?.let { "$it, " } ?: ""}$size"
+fun limitClause(size: Int, since: Int? = null) = limitClause(size.toLong(), since ?.toLong())
+fun Pagination.limitClause() = limitClause(size, firstIndex)
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt
new file mode 100644
index 00000000000..1ac58f237a5
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/DatabaseTransactions.kt
@@ -0,0 +1,45 @@
+package dev.inmo.micro_utils.repos
+
+import android.database.sqlite.SQLiteDatabase
+import dev.inmo.micro_utils.coroutines.safely
+import kotlinx.coroutines.withContext
+
+suspend fun SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T {
+ return withContext(DatabaseCoroutineContext) {
+ when {
+ inTransaction() -> {
+ block()
+ }
+ else -> {
+ beginTransaction()
+ safely(
+ {
+ endTransaction()
+ throw it
+ }
+ ) {
+ block().also {
+ setTransactionSuccessful()
+ endTransaction()
+ }
+ }
+ }
+ }
+ }
+}
+
+inline fun SQLiteDatabase.inlineTransaction(block: SQLiteDatabase.() -> T): T {
+ return when {
+ inTransaction() -> block()
+ else -> {
+ beginTransaction()
+ try {
+ block().also {
+ setTransactionSuccessful()
+ }
+ } finally {
+ endTransaction()
+ }
+ }
+ }
+}
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/InternalId.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/InternalId.kt
new file mode 100644
index 00000000000..c95db5f7ab9
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/InternalId.kt
@@ -0,0 +1,7 @@
+package dev.inmo.micro_utils.repos
+
+val internalId = "_id"
+val internalIdType = ColumnType.Numeric.INTEGER(
+ autoincrement = true,
+ primaryKey = true
+)
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt
new file mode 100644
index 00000000000..8de4676138a
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/StandardSQLHelper.kt
@@ -0,0 +1,51 @@
+package dev.inmo.micro_utils.repos
+
+import android.content.Context
+import android.database.DatabaseErrorHandler
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+import dev.inmo.micro_utils.coroutines.safely
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+private data class CallbackContinuationPair (
+ val callback: suspend SQLiteDatabase.() -> T,
+ val continuation: Continuation
+) {
+ suspend fun SQLiteDatabase.execute() {
+ safely(
+ {
+ continuation.resumeWithException(it)
+ }
+ ) {
+ continuation.resume(callback())
+ }
+ }
+}
+
+class StandardSQLHelper(
+ context: Context,
+ name: String,
+ factory: SQLiteDatabase.CursorFactory? = null,
+ version: Int = 1,
+ errorHandler: DatabaseErrorHandler? = null
+) {
+ val sqlOpenHelper = object : SQLiteOpenHelper(context, name, factory, version, errorHandler) {
+ override fun onCreate(db: SQLiteDatabase?) {}
+
+ override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
+ }
+
+ suspend fun writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block)
+
+ suspend fun readableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.readableTransaction(block)
+}
+
+suspend fun SQLiteOpenHelper.writableTransaction(block: suspend SQLiteDatabase.() -> T): T {
+ return writableDatabase.transaction(block)
+}
+
+suspend fun SQLiteOpenHelper.readableTransaction(block: suspend SQLiteDatabase.() -> T): T {
+ return readableDatabase.transaction(block)
+}
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractAndroidCRUDRepo.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractAndroidCRUDRepo.kt
new file mode 100644
index 00000000000..bd1109b71ea
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractAndroidCRUDRepo.kt
@@ -0,0 +1,67 @@
+package dev.inmo.micro_utils.repos.crud
+
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import dev.inmo.micro_utils.pagination.Pagination
+import dev.inmo.micro_utils.pagination.PaginationResult
+import dev.inmo.micro_utils.pagination.createPaginationResult
+import dev.inmo.micro_utils.repos.*
+
+val T.asId: String
+ get() = (this as? String) ?: this!!.toString()
+
+abstract class AbstractAndroidCRUDRepo(
+ protected val helper: StandardSQLHelper
+) : ReadStandardCRUDRepo {
+ protected abstract val tableName: String
+ protected abstract val idColumnName: String
+ protected abstract suspend fun Cursor.toObject(): ObjectType
+ protected fun SQLiteDatabase.count(): Long = select(tableName).use {
+ it.count
+ }.toLong()
+
+ override suspend fun contains(id: IdType): Boolean = helper.readableTransaction {
+ select(
+ tableName,
+ null,
+ "$idColumnName=?",
+ arrayOf(id.asId)
+ ).use {
+ it.count > 0
+ }
+ }
+
+ override suspend fun getById(id: IdType): ObjectType? = helper.readableTransaction {
+ select(
+ tableName,
+ selection = "$idColumnName=?",
+ selectionArgs = arrayOf(id.asId),
+ limit = limitClause(1)
+ ).use { c ->
+ if (c.moveToFirst()) {
+ c.toObject()
+ } else {
+ null
+ }
+ }
+ }
+
+ override suspend fun getByPagination(pagination: Pagination): PaginationResult {
+ return helper.readableTransaction {
+ select(
+ tableName,
+ limit = pagination.limitClause()
+ ).use {
+ if (it.moveToFirst()) {
+ val resultList = mutableListOf(it.toObject())
+ while (it.moveToNext()) {
+ resultList.add(it.toObject())
+ }
+ resultList.createPaginationResult(pagination, count())
+ } else {
+ emptyList().createPaginationResult(pagination, 0)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractMutableAndroidCRUDRepo.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractMutableAndroidCRUDRepo.kt
new file mode 100644
index 00000000000..83bab7ad2b9
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/crud/AbstractMutableAndroidCRUDRepo.kt
@@ -0,0 +1,104 @@
+package dev.inmo.micro_utils.repos.crud
+
+import android.content.ContentValues
+import dev.inmo.micro_utils.common.mapNotNullA
+import dev.inmo.micro_utils.repos.*
+import kotlinx.coroutines.channels.BroadcastChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+
+abstract class AbstractMutableAndroidCRUDRepo(
+ helper: StandardSQLHelper
+) : WriteStandardCRUDRepo,
+ AbstractAndroidCRUDRepo(helper) {
+ protected val newObjectsChannel = BroadcastChannel(64)
+ protected val updateObjectsChannel = BroadcastChannel(64)
+ protected val deleteObjectsIdsChannel = BroadcastChannel(64)
+ override val newObjectsFlow: Flow = newObjectsChannel.asFlow()
+ override val updatedObjectsFlow: Flow = updateObjectsChannel.asFlow()
+ override val deletedObjectsIdsFlow: Flow = deleteObjectsIdsChannel.asFlow()
+
+ protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues
+
+ override suspend fun create(values: List): List {
+ val indexes = helper.writableTransaction {
+ values.map {
+ insert(tableName, null, it.asContentValues())
+ }
+ }
+ return helper.readableTransaction {
+ indexes.mapNotNullA { i ->
+ select(
+ tableName,
+ selection = "$internalId=?",
+ selectionArgs = arrayOf(i.toString())
+ ).use { c ->
+ if (c.moveToFirst()) {
+ c.toObject()
+ } else {
+ null
+ }
+ }
+ }
+ }.also {
+ it.forEach {
+ newObjectsChannel.send(it)
+ }
+ }
+ }
+
+ override suspend fun deleteById(ids: List) {
+ val deleted = mutableListOf()
+ helper.writableTransaction {
+ ids.forEach { id ->
+ delete(tableName, "$idColumnName=?", arrayOf(id.asId)).also {
+ if (it > 0) {
+ deleted.add(id)
+ }
+ }
+ }
+ }
+ deleted.forEach {
+ deleteObjectsIdsChannel.send(it)
+ }
+ }
+
+ override suspend fun update(id: IdType, value: InputValueType): ObjectType? {
+ val asContentValues = value.asContentValues(id)
+ if (asContentValues.keySet().isNotEmpty()) {
+ helper.writableTransaction {
+ update(
+ tableName,
+ asContentValues,
+ "$idColumnName=?",
+ arrayOf(id.asId)
+ )
+ }
+ }
+ return getById(id) ?.also {
+ updateObjectsChannel.send(it)
+ }
+ }
+
+ override suspend fun update(values: List>): List {
+ helper.writableTransaction {
+ values.forEach { (id, value) ->
+ update(
+ tableName,
+ value.asContentValues(id),
+ "$idColumnName=?",
+ arrayOf(id.asId)
+ )
+ }
+ }
+ return values.mapNotNullA {
+ getById(it.first)
+ }.also {
+ it.forEach {
+ updateObjectsChannel.send(it)
+ }
+ }
+ }
+
+ override suspend fun count(): Long = helper.readableTransaction { select(tableName).use { it.count.toLong() } }
+}
\ No newline at end of file
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/keyvalue/KeyValueStore.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/keyvalue/KeyValueStore.kt
new file mode 100644
index 00000000000..bcf16a4f7a6
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/keyvalue/KeyValueStore.kt
@@ -0,0 +1,130 @@
+package dev.inmo.micro_utils.repos.keyvalue
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import dev.inmo.micro_utils.pagination.Pagination
+import dev.inmo.micro_utils.pagination.PaginationResult
+import dev.inmo.micro_utils.pagination.utils.paginate
+import dev.inmo.micro_utils.pagination.utils.reverse
+import dev.inmo.micro_utils.repos.StandardKeyValueRepo
+import kotlinx.coroutines.channels.BroadcastChannel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+
+private val cache = HashMap>()
+
+fun Context.keyValueStore(
+ name: String = "default",
+ cacheValues: Boolean = false
+): StandardKeyValueRepo {
+ return cache.getOrPut(name) {
+ KeyValueStore(this, name, cacheValues)
+ } as KeyValueStore
+}
+
+class KeyValueStore internal constructor (
+ c: Context,
+ preferencesName: String,
+ useCache: Boolean = false
+) : SharedPreferences.OnSharedPreferenceChangeListener, StandardKeyValueRepo {
+ private val sharedPreferences = c.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
+
+ private val cachedData = if (useCache) {
+ mutableMapOf()
+ } else {
+ null
+ }
+
+ private val onNewValueChannel = BroadcastChannel>(Channel.BUFFERED)
+ private val onValueRemovedChannel = BroadcastChannel(Channel.BUFFERED)
+
+ override val onNewValue: Flow> = onNewValueChannel.asFlow()
+ override val onValueRemoved: Flow = onValueRemovedChannel.asFlow()
+
+ init {
+ cachedData ?.let {
+ sharedPreferences.all.forEach {
+ if (it.value != null) {
+ cachedData[it.key] = it.value as Any
+ }
+ }
+ sharedPreferences.registerOnSharedPreferenceChangeListener(this)
+ }
+ }
+
+ override fun onSharedPreferenceChanged(sp: SharedPreferences, key: String) {
+ val value = sp.all[key]
+ cachedData ?: return
+ if (value != null) {
+ cachedData[key] = value
+ } else {
+ cachedData.remove(key)
+ }
+ }
+
+ override suspend fun get(k: String): T? {
+ return (cachedData ?. get(k) ?: sharedPreferences.all[k]) as? T
+ }
+
+ override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult {
+ val resultPagination = if (reversed) pagination.reverse(count()) else pagination
+ return sharedPreferences.all.values.paginate(
+ resultPagination
+ ).let {
+ PaginationResult(
+ it.page,
+ it.pagesNumber,
+ it.results.map { it as T }.let { if (reversed) it.reversed() else it },
+ it.size
+ )
+ }
+ }
+
+ override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult {
+ val resultPagination = if (reversed) pagination.reverse(count()) else pagination
+ return sharedPreferences.all.keys.paginate(
+ resultPagination
+ ).let {
+ PaginationResult(
+ it.page,
+ it.pagesNumber,
+ it.results.let { if (reversed) it.reversed() else it },
+ it.size
+ )
+ }
+ }
+
+ override suspend fun contains(key: String): Boolean = sharedPreferences.contains(key)
+
+ override suspend fun count(): Long = sharedPreferences.all.size.toLong()
+
+ override suspend fun set(toSet: Map) {
+ sharedPreferences.edit {
+ toSet.forEach { (k, v) ->
+ when(v) {
+ is Int -> putInt(k, v)
+ is Long -> putLong(k, v)
+ is Float -> putFloat(k, v)
+ is String -> putString(k, v)
+ is Boolean -> putBoolean(k, v)
+ is Set<*> -> putStringSet(k, v.map { (it as? String) ?: it.toString() }.toSet())
+ else -> error(
+ "Currently supported only primitive types and set for SharedPreferences KeyValue repos"
+ )
+ }
+ }
+ }
+ toSet.forEach { (k, v) ->
+ onNewValueChannel.send(k to v)
+ }
+ }
+
+ override suspend fun unset(toUnset: List) {
+ sharedPreferences.edit {
+ toUnset.forEach { remove(it) }
+ }
+ toUnset.forEach { onValueRemovedChannel.send(it) }
+ }
+}
diff --git a/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/onetomany/OneToManyAndroidRepo.kt b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/onetomany/OneToManyAndroidRepo.kt
new file mode 100644
index 00000000000..de4b6f22078
--- /dev/null
+++ b/repos/common/src/main/kotlin/dev/inmo/micro_utils/repos/onetomany/OneToManyAndroidRepo.kt
@@ -0,0 +1,187 @@
+package dev.inmo.micro_utils.repos.onetomany
+
+import android.database.sqlite.SQLiteOpenHelper
+import androidx.core.content.contentValuesOf
+import dev.inmo.micro_utils.common.mapNotNullA
+import dev.inmo.micro_utils.pagination.FirstPagePagination
+import dev.inmo.micro_utils.pagination.Pagination
+import dev.inmo.micro_utils.pagination.PaginationResult
+import dev.inmo.micro_utils.pagination.createPaginationResult
+import dev.inmo.micro_utils.pagination.utils.reverse
+import dev.inmo.micro_utils.repos.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.json.Json
+
+private val internalSerialFormat = Json {
+ ignoreUnknownKeys = true
+}
+
+class OneToManyAndroidRepo(
+ private val tableName: String,
+ private val keySerializer: KSerializer,
+ private val valueSerializer: KSerializer,
+ private val helper: SQLiteOpenHelper
+) : OneToManyKeyValueRepo {
+ private val _onNewValue: MutableSharedFlow> = MutableSharedFlow()
+ override val onNewValue: Flow> = _onNewValue.asSharedFlow()
+ private val _onValueRemoved: MutableSharedFlow> = MutableSharedFlow()
+ override val onValueRemoved: Flow> = _onValueRemoved.asSharedFlow()
+ private val _onDataCleared = MutableSharedFlow()
+ override val onDataCleared: Flow = _onDataCleared.asSharedFlow()
+
+ private val idColumnName = "id"
+ private val valueColumnName = "value"
+
+ private fun Key.asId() = internalSerialFormat.encodeToString(keySerializer, this)
+ private fun Value.asValue() = internalSerialFormat.encodeToString(valueSerializer, this)
+ private fun String.asValue(): Value = internalSerialFormat.decodeFromString(valueSerializer, this)
+ private fun String.asKey(): Key = internalSerialFormat.decodeFromString(keySerializer, this)
+
+ init {
+ runBlocking(DatabaseCoroutineContext) {
+ helper.writableTransaction {
+ createTable(
+ tableName,
+ internalId to internalIdType,
+ idColumnName to ColumnType.Text.NOT_NULLABLE,
+ valueColumnName to ColumnType.Text.NULLABLE
+ )
+ }
+ }
+ }
+
+ override suspend fun add(toAdd: Map>) {
+ val added = mutableListOf>()
+ helper.writableTransaction {
+ toAdd.forEach { (k, values) ->
+ values.forEach { v ->
+ insert(
+ tableName,
+ null,
+ contentValuesOf(
+ idColumnName to k.asId(),
+ valueColumnName to v.asValue()
+ )
+ ).also {
+ if (it != -1L) {
+ added.add(k to v)
+ }
+ }
+ }
+ }
+ }
+ added.forEach { _onNewValue.emit(it) }
+ }
+
+ override suspend fun clear(k: Key) {
+ helper.writableTransaction {
+ delete(tableName, "$idColumnName=?", arrayOf(k.asId()))
+ }.also {
+ if (it > 0) {
+ _onDataCleared.emit(k)
+ }
+ }
+ }
+
+ override suspend fun contains(k: Key): Boolean = helper.readableTransaction {
+ select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
+ it.count > 0
+ }
+ }
+
+ override suspend fun contains(k: Key, v: Value): Boolean = helper.readableTransaction {
+ select(
+ tableName,
+ selection = "$idColumnName=? AND $valueColumnName=?",
+ selectionArgs = arrayOf(k.asId(), v.asValue()),
+ limit = FirstPagePagination(1).limitClause()
+ ).use {
+ it.count > 0
+ }
+ }
+
+ override suspend fun count(): Long =helper.readableTransaction {
+ select(
+ tableName
+ ).use {
+ it.count
+ }
+ }.toLong()
+
+ override suspend fun count(k: Key): Long = helper.readableTransaction {
+ select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
+ it.count
+ }
+ }.toLong()
+
+ override suspend fun get(
+ k: Key,
+ pagination: Pagination,
+ reversed: Boolean
+ ): PaginationResult = count(k).let { count ->
+ val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
+ helper.readableTransaction {
+ select(
+ tableName,
+ selection = "$idColumnName=?",
+ selectionArgs = arrayOf(k.asId()),
+ limit = resultPagination.limitClause()
+ ).use { c ->
+ mutableListOf().also {
+ if (c.moveToFirst()) {
+ do {
+ it.add(c.getString(valueColumnName).asValue())
+ } while (c.moveToNext())
+ }
+ }
+ }
+ }.createPaginationResult(
+ pagination,
+ count
+ )
+ }
+
+ override suspend fun keys(
+ pagination: Pagination,
+ reversed: Boolean
+ ): PaginationResult = count().let { count ->
+ val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
+ helper.readableTransaction {
+ select(
+ tableName,
+ limit = resultPagination.limitClause()
+ ).use { c ->
+ mutableListOf().also {
+ if (c.moveToFirst()) {
+ do {
+ it.add(c.getString(idColumnName).asKey())
+ } while (c.moveToNext())
+ }
+ }
+ }
+ }.createPaginationResult(
+ pagination,
+ count
+ )
+ }
+
+ override suspend fun remove(toRemove: Map>) {
+ helper.writableTransaction {
+ toRemove.flatMap { (k, vs) ->
+ vs.mapNotNullA { v ->
+ if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.asId(), v.asValue())) > 0) {
+ k to v
+ } else {
+ null
+ }
+ }
+ }
+ }.forEach { (k, v) ->
+ _onValueRemoved.emit(k to v)
+ }
+ }
+}
diff --git a/repos/inmemory/build.gradle b/repos/inmemory/build.gradle
index 162e4af700e..2e766c6966f 100644
--- a/repos/inmemory/build.gradle
+++ b/repos/inmemory/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/repos/inmemory/src/main/AndroidManifest.xml b/repos/inmemory/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..63948d05fbf
--- /dev/null
+++ b/repos/inmemory/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/repos/ktor/client/build.gradle b/repos/ktor/client/build.gradle
index 2076deebca3..30fea42d54c 100644
--- a/repos/ktor/client/build.gradle
+++ b/repos/ktor/client/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/repos/ktor/client/src/main/AndroidManifest.xml b/repos/ktor/client/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..a6de7e9b159
--- /dev/null
+++ b/repos/ktor/client/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/repos/ktor/common/build.gradle b/repos/ktor/common/build.gradle
index 506faf0b021..7449b67d2fb 100644
--- a/repos/ktor/common/build.gradle
+++ b/repos/ktor/common/build.gradle
@@ -1,6 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ id "kotlin-android-extensions"
}
apply from: "$mppProjectWithSerializationPresetPath"
diff --git a/repos/ktor/common/src/main/AndroidManifest.xml b/repos/ktor/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..df34e12f6a0
--- /dev/null
+++ b/repos/ktor/common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 92804fc0013..811699ed9eb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,13 +17,18 @@ String[] includes = [
":ktor:common",
":ktor:client",
":coroutines",
+ ":android:recyclerview",
":dokka"
]
-includes.each {
- include it
- ProjectDescriptor project = project(it)
- project.name = rootProject.name + project.projectDir.absolutePath.replace("${rootDir.absolutePath}", "").replace(File.separator, ".")
+includes.each { originalName ->
+ String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replaceAll(":", File.separator)}"
+ String projectName = "${rootProject.name}${originalName.replaceAll(":", ".")}"
+ String projectIdentifier = ":${projectName}"
+ include projectIdentifier
+ ProjectDescriptor project = project(projectIdentifier)
+ project.name = projectName
+ project.projectDir = new File(projectDirectory)
}