Merge pull request #14 from InsanusMokrassar/0.3.1

0.3.1
This commit is contained in:
InsanusMokrassar 2020-11-10 19:50:57 +06:00 committed by GitHub
commit 776a3af497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1196 additions and 24 deletions

1
.gitignore vendored
View File

@ -10,5 +10,6 @@ build/
out/ out/
secret.gradle secret.gradle
local.properties
publishing.sh publishing.sh

View File

@ -1,5 +1,41 @@
# Changelog # 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 ## 0.3.0
All deprecations has been removed All deprecations has been removed

View File

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

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>

View File

@ -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<T>(
inflater: LayoutInflater,
container: ViewGroup?,
viewId: Int,
onViewInflated: ((View) -> Unit)? = null
) : AbstractViewHolder<T>(
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)
}

View File

@ -0,0 +1,10 @@
package dev.inmo.micro_utils.android.recyclerview
import android.view.View
import androidx.recyclerview.widget.RecyclerView
abstract class AbstractViewHolder<in T>(
view: View
) : RecyclerView.ViewHolder(view) {
abstract fun onBind(item: T)
}

View File

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

View File

@ -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<Int> {
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<Int>.mapLeftItems(
crossinline countGetter: () -> Int
): Flow<Int> = map { countGetter() - it }
inline fun Flow<Int>.mapRequireFilling(
minimalLeftItems: Int,
crossinline countGetter: () -> Int
): Flow<Int> = mapLeftItems(countGetter).mapNotNull {
if (it < minimalLeftItems) {
it
} else {
null
}
}
inline fun RecyclerView.mapRequireFilling(
minimalLeftItems: Int,
completingScope: CoroutineScope,
crossinline countGetter: () -> Int
): Flow<Int> = lastVisibleItemFlow(completingScope).mapRequireFilling(minimalLeftItems, countGetter)

View File

@ -0,0 +1,68 @@
package dev.inmo.micro_utils.android.recyclerview
import android.view.View
import androidx.recyclerview.widget.RecyclerView
abstract class RecyclerViewAdapter<T>(
val data: List<T>
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
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<T>, position: Int) {
holder.onBind(data[position])
}
private fun checkEmpty() {
emptyView ?. let {
if (data.isEmpty()) {
it.visibility = View.VISIBLE
} else {
it.visibility = View.GONE
}
}
}
}

View File

@ -1,16 +1,20 @@
buildscript { buildscript {
repositories { repositories {
mavenLocal()
jcenter() jcenter()
google()
mavenCentral() mavenCentral()
mavenLocal()
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_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 "com.github.breadmoirai:github-release:$github_release_plugin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
} }
} }
@ -19,6 +23,7 @@ allprojects {
mavenLocal() mavenLocal()
jcenter() jcenter()
mavenCentral() mavenCentral()
google()
maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" }
} }
} }

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.common"/>

View File

@ -0,0 +1,7 @@
package dev.inmo.micro_utils.common
@Suppress("UNCHECKED_CAST", "SimplifiableCall")
inline fun <T, R> Iterable<T>.mapNotNullA(transform: (T) -> R?): List<R> = map(transform).filter { it != null } as List<R>
@Suppress("UNCHECKED_CAST", "SimplifiableCall")
inline fun <T, R> Array<T>.mapNotNullA(mapper: (T) -> R?): List<R> = map(mapper).filter { it != null } as List<R>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@ -12,5 +14,10 @@ kotlin {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
} }
} }
androidMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
}
}
} }
} }

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.coroutines"/>

View File

@ -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 <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext(
Dispatchers.Main,
block
)

31
defaultAndroidSettings Normal file
View File

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

View File

@ -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 { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" 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 { repositories {
mavenLocal() mavenLocal()
jcenter() jcenter()
google()
mavenCentral() mavenCentral()
} }
@ -30,6 +19,7 @@ kotlin {
browser() browser()
nodejs() nodejs()
} }
android {}
sourceSets { sourceSets {
commonMain { commonMain {
@ -41,6 +31,57 @@ kotlin {
it != project it != project
&& it.hasProperty("kotlin") && it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") } && 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 api it
} }
@ -84,5 +125,11 @@ tasks.dokkaHtml {
named("jvmMain") { named("jvmMain") {
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain")) sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
} }
named("androidMain") {
sourceRoots.setFrom(findSourcesWithName("androidMain", "commonMain"))
} }
} }
}
apply from: "$defaultAndroidSettingsPresetPath"

View File

@ -1,3 +0,0 @@
dokka_version=1.4.0
org.gradle.jvmargs=-Xmx1024m

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.dokka"/>

View File

@ -21,6 +21,9 @@ allprojects {
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization" mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject" mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject"
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject"
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle" publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle" publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"

View File

@ -3,6 +3,9 @@ org.gradle.parallel=true
kotlin.js.generate.externals=true kotlin.js.generate.externals=true
kotlin.incremental=true kotlin.incremental=true
kotlin.incremental.js=true kotlin.incremental.js=true
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx4096m
kotlin_version=1.4.10 kotlin_version=1.4.10
kotlin_coroutines_version=1.4.1 kotlin_coroutines_version=1.4.1
@ -18,5 +21,25 @@ github_release_plugin_version=2.2.12
uuidVersion=0.2.2 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 group=dev.inmo
version=0.3.0 version=0.3.1
android_code_version=1

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.ktor.client"/>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.ktor.common"/>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.mime_types"/>

24
mppAndroidProject Normal file
View File

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

View File

@ -9,6 +9,9 @@ kotlin {
browser() browser()
nodejs() nodejs()
} }
android {
publishLibraryVariants("release")
}
sourceSets { sourceSets {
commonMain { commonMain {
@ -34,5 +37,14 @@ kotlin {
implementation kotlin('test-junit') 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"

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.pagination.common"/>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.pagination.ktor.common"/>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@ -15,5 +17,13 @@ kotlin {
api "com.benasher44:uuid:$uuidVersion" 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")
}
}
} }
} }

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.repos.common"/>

View File

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

View File

@ -0,0 +1,8 @@
package dev.inmo.micro_utils.repos
import androidx.core.content.contentValuesOf
@Suppress("UNCHECKED_CAST", "SimplifiableCall")
fun contentValuesOfNotNull(vararg pairs: Pair<String, Any?>?) = contentValuesOf(
*(pairs.filter { it != null } as List<Pair<String, Any?>>).toTypedArray()
)

View File

@ -0,0 +1,21 @@
package dev.inmo.micro_utils.repos
import android.database.Cursor
inline fun <T> Cursor.map(
block: (Cursor) -> T
): List<T> {
val result = mutableListOf<T>()
if (moveToFirst()) {
do {
result.add(block(this))
} while (moveToNext())
}
return result
}
fun Cursor.firstOrNull(): Cursor? = if (moveToFirst()) {
this
} else {
null
}

View File

@ -0,0 +1,6 @@
package dev.inmo.micro_utils.repos
import kotlinx.coroutines.newSingleThreadContext
import kotlin.coroutines.CoroutineContext
val DatabaseCoroutineContext: CoroutineContext = newSingleThreadContext("db-context")

View File

@ -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<String, ColumnType>
) = "create table $tableName (${columnsToTypes.joinToString(", ") { "${it.first} ${it.second}" }});"
fun SQLiteDatabase.createTable(
tableName: String,
vararg columnsToTypes: Pair<String, ColumnType>,
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<String>? = null,
selection: String? = null,
selectionArgs: Array<String>? = 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 { "\"?\"" }
}

View File

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

View File

@ -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 <T> 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 <T> SQLiteDatabase.inlineTransaction(block: SQLiteDatabase.() -> T): T {
return when {
inTransaction() -> block()
else -> {
beginTransaction()
try {
block().also {
setTransactionSuccessful()
}
} finally {
endTransaction()
}
}
}
}

View File

@ -0,0 +1,7 @@
package dev.inmo.micro_utils.repos
val internalId = "_id"
val internalIdType = ColumnType.Numeric.INTEGER(
autoincrement = true,
primaryKey = true
)

View File

@ -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<T> (
val callback: suspend SQLiteDatabase.() -> T,
val continuation: Continuation<T>
) {
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 <T> writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block)
suspend fun <T> readableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.readableTransaction(block)
}
suspend fun <T> SQLiteOpenHelper.writableTransaction(block: suspend SQLiteDatabase.() -> T): T {
return writableDatabase.transaction(block)
}
suspend fun <T> SQLiteOpenHelper.readableTransaction(block: suspend SQLiteDatabase.() -> T): T {
return readableDatabase.transaction(block)
}

View File

@ -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> T.asId: String
get() = (this as? String) ?: this!!.toString()
abstract class AbstractAndroidCRUDRepo<ObjectType, IdType>(
protected val helper: StandardSQLHelper
) : ReadStandardCRUDRepo<ObjectType, IdType> {
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<ObjectType> {
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<ObjectType>().createPaginationResult(pagination, 0)
}
}
}
}
}

View File

@ -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<ObjectType, IdType, InputValueType>(
helper: StandardSQLHelper
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
AbstractAndroidCRUDRepo<ObjectType, IdType>(helper) {
protected val newObjectsChannel = BroadcastChannel<ObjectType>(64)
protected val updateObjectsChannel = BroadcastChannel<ObjectType>(64)
protected val deleteObjectsIdsChannel = BroadcastChannel<IdType>(64)
override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asFlow()
override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asFlow()
override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asFlow()
protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues
override suspend fun create(values: List<InputValueType>): List<ObjectType> {
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<IdType>) {
val deleted = mutableListOf<IdType>()
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<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType> {
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() } }
}

View File

@ -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<String, KeyValueStore<*>>()
fun <T : Any> Context.keyValueStore(
name: String = "default",
cacheValues: Boolean = false
): StandardKeyValueRepo<String, T> {
return cache.getOrPut(name) {
KeyValueStore<T>(this, name, cacheValues)
} as KeyValueStore<T>
}
class KeyValueStore<T : Any> internal constructor (
c: Context,
preferencesName: String,
useCache: Boolean = false
) : SharedPreferences.OnSharedPreferenceChangeListener, StandardKeyValueRepo<String, T> {
private val sharedPreferences = c.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
private val cachedData = if (useCache) {
mutableMapOf<String, Any>()
} else {
null
}
private val onNewValueChannel = BroadcastChannel<Pair<String, T>>(Channel.BUFFERED)
private val onValueRemovedChannel = BroadcastChannel<String>(Channel.BUFFERED)
override val onNewValue: Flow<Pair<String, T>> = onNewValueChannel.asFlow()
override val onValueRemoved: Flow<String> = 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<T> {
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<String> {
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<String, T>) {
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<String>) {
sharedPreferences.edit {
toUnset.forEach { remove(it) }
}
toUnset.forEach { onValueRemovedChannel.send(it) }
}
}

View File

@ -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<Key, Value>(
private val tableName: String,
private val keySerializer: KSerializer<Key>,
private val valueSerializer: KSerializer<Value>,
private val helper: SQLiteOpenHelper
) : OneToManyKeyValueRepo<Key, Value> {
private val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
private val _onValueRemoved: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
override val onValueRemoved: Flow<Pair<Key, Value>> = _onValueRemoved.asSharedFlow()
private val _onDataCleared = MutableSharedFlow<Key>()
override val onDataCleared: Flow<Key> = _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<Key, List<Value>>) {
val added = mutableListOf<Pair<Key, Value>>()
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<Value> = 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<Value>().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<Key> = count().let { count ->
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
helper.readableTransaction {
select(
tableName,
limit = resultPagination.limitClause()
).use { c ->
mutableListOf<Key>().also {
if (c.moveToFirst()) {
do {
it.add(c.getString(idColumnName).asKey())
} while (c.moveToNext())
}
}
}
}.createPaginationResult(
pagination,
count
)
}
override suspend fun remove(toRemove: Map<Key, List<Value>>) {
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)
}
}
}

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.repos.inmemory"/>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.repos.ktor.client"/>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
id "kotlin-android-extensions"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.repos.ktor.common"/>

View File

@ -17,13 +17,18 @@ String[] includes = [
":ktor:common", ":ktor:common",
":ktor:client", ":ktor:client",
":coroutines", ":coroutines",
":android:recyclerview",
":dokka" ":dokka"
] ]
includes.each { includes.each { originalName ->
include it String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replaceAll(":", File.separator)}"
ProjectDescriptor project = project(it) String projectName = "${rootProject.name}${originalName.replaceAll(":", ".")}"
project.name = rootProject.name + project.projectDir.absolutePath.replace("${rootDir.absolutePath}", "").replace(File.separator, ".") String projectIdentifier = ":${projectName}"
include projectIdentifier
ProjectDescriptor project = project(projectIdentifier)
project.name = projectName
project.projectDir = new File(projectDirectory)
} }