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