mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
1c86f3f4bf | |||
6d999be590 | |||
e715772dbf | |||
63eb7b7ea8 | |||
b07683b815 | |||
96e97d1691 | |||
261d8827e3 | |||
c3156f2e41 | |||
8c08801460 | |||
aaf1299da7 | |||
a411355b4f | |||
eba41066b4 | |||
f295dff8a2 | |||
a16815143c | |||
6ff3f6ae42 | |||
84071881af | |||
7cccf7e56e | |||
2516d5e381 | |||
cdec8bac75 | |||
fa30aae194 | |||
eb959a3135 | |||
24033e0cac | |||
71f9a505e0 | |||
979b8f017b |
12
.github/workflows/build.yml
vendored
Normal file
12
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: Regular build
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew build
|
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,5 +1,62 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.5.12
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `Android`
|
||||||
|
* Extension `View#changeVisibility` has been fixed
|
||||||
|
* `Android`
|
||||||
|
* `RecyclerView`
|
||||||
|
* Default adapter got `dataCountFlow` property
|
||||||
|
* New subtype of adapter based on `StateFlow`: `StateFlowBasedRecyclerViewAdapter`
|
||||||
|
|
||||||
|
## 0.5.11
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* Fixes in `WriteOneToManyRepo#add`
|
||||||
|
* `Exposed`:
|
||||||
|
* Fixes in `ExposedOneToManyKeyValueRepo#add`
|
||||||
|
|
||||||
|
## 0.5.10
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Core KTX`: `1.3.2` -> `1.5.0`
|
||||||
|
* `AndroidX Recycler`: `1.2.0` -> `1.2.1`
|
||||||
|
* `AppCompat`: `1.2.0` -> `1.3.0`
|
||||||
|
* `Android`
|
||||||
|
* `RecyclerView`:
|
||||||
|
* `data` of `RecyclerViewAdapter` became an abstract field
|
||||||
|
* New function `RecyclerViewAdapter`
|
||||||
|
* `Common`:
|
||||||
|
* New extension `View#changeVisibility`
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* `WriteOneToManyRepo` got new function `clearWithValue`
|
||||||
|
* `Android`:
|
||||||
|
* New extension `SQLiteDatabase#selectDistinct`
|
||||||
|
* Fixes in `OneToManyAndroidRepo`
|
||||||
|
* `Ktor`
|
||||||
|
* `Server`
|
||||||
|
* All elements in configurators became a `fun interface`
|
||||||
|
* `Pagination`
|
||||||
|
* New function `doForAllWithCurrentPaging`
|
||||||
|
|
||||||
|
## 0.5.9
|
||||||
|
|
||||||
|
* `Repos`
|
||||||
|
* `Common`
|
||||||
|
* `OneToManyAndroidRepo` got new primary constructor
|
||||||
|
|
||||||
|
## 0.5.8
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* New extension `Iterable#firstNotNull`
|
||||||
|
* `Coroutines`
|
||||||
|
* New extension `Flow#firstNotNull`
|
||||||
|
* New extensions `CoroutineContext#LinkedSupervisorJob`, `CoroutineScope#LinkedSupervisorJob` and
|
||||||
|
`CoroutineScope#LinkedSupervisorScope`
|
||||||
|
|
||||||
## 0.5.7
|
## 0.5.7
|
||||||
|
|
||||||
* `Pagination`
|
* `Pagination`
|
||||||
|
@@ -35,9 +35,9 @@ class ActionViewHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ActionsRecyclerViewAdapter(
|
class ActionsRecyclerViewAdapter(
|
||||||
data: List<AlertAction>,
|
override val data: List<AlertAction>,
|
||||||
private val dialogInterfaceGetter: () -> DialogInterface
|
private val dialogInterfaceGetter: () -> DialogInterface
|
||||||
) : RecyclerViewAdapter<AlertAction>(data) {
|
) : RecyclerViewAdapter<AlertAction>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
||||||
parent, dialogInterfaceGetter
|
parent, dialogInterfaceGetter
|
||||||
)
|
)
|
||||||
|
@@ -11,6 +11,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
|
@@ -1,12 +1,21 @@
|
|||||||
package dev.inmo.micro_utils.android.recyclerview
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
|
||||||
abstract class RecyclerViewAdapter<T>(
|
abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||||
val data: List<T>
|
protected abstract val data: List<T>
|
||||||
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
|
||||||
|
private val _dataCountState by lazy {
|
||||||
|
MutableStateFlow<Int>(data.size)
|
||||||
|
}
|
||||||
|
val dataCountState: StateFlow<Int> by lazy {
|
||||||
|
_dataCountState.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
var emptyView: View? = null
|
var emptyView: View? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
object : RecyclerView.AdapterDataObserver() {
|
object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount)
|
super.onItemRangeChanged(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount, payload)
|
super.onItemRangeChanged(positionStart, itemCount, payload)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
super.onChanged()
|
super.onChanged()
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeRemoved(positionStart, itemCount)
|
super.onItemRangeRemoved(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
super.onItemRangeInserted(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
|
|
||||||
private fun checkEmpty() {
|
private fun checkEmpty() {
|
||||||
emptyView ?. let {
|
emptyView ?. let {
|
||||||
if (data.isEmpty()) {
|
if (dataCountState.value == 0) {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
it.visibility = View.GONE
|
it.visibility = View.GONE
|
||||||
@@ -66,3 +81,11 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> RecyclerViewAdapter(
|
||||||
|
data: List<T>,
|
||||||
|
onCreateViewHolder: (parent: ViewGroup, viewType: Int) -> AbstractViewHolder<T>
|
||||||
|
) = object : RecyclerViewAdapter<T>() {
|
||||||
|
override val data: List<T> = data
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<T> = onCreateViewHolder(parent, viewType)
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.Diff
|
||||||
|
import dev.inmo.micro_utils.common.PreviewFeature
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
@PreviewFeature("This feature in preview state and may contains different bugs. " +
|
||||||
|
"Besides, this feature can be changed in future in non-compatible way")
|
||||||
|
abstract class StateFlowBasedRecyclerViewAdapter<T>(
|
||||||
|
listeningScope: CoroutineScope,
|
||||||
|
dataState: StateFlow<List<T>>
|
||||||
|
) : RecyclerViewAdapter<T>() {
|
||||||
|
override var data: List<T> = emptyList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
dataState.onEach {
|
||||||
|
try {
|
||||||
|
val diffForRemoves = Diff(data, it)
|
||||||
|
val removedIndexes = diffForRemoves.removed.map { it.index }
|
||||||
|
val leftRemove = removedIndexes.toMutableList()
|
||||||
|
data = data.filterIndexed { i, _ ->
|
||||||
|
if (i in leftRemove) {
|
||||||
|
leftRemove.remove(i)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
removedIndexes.sortedDescending().forEach {
|
||||||
|
notifyItemRemoved(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val diffAddsAndReplaces = Diff(data, it)
|
||||||
|
data = it
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
diffAddsAndReplaces.replaced.forEach { (from, to) ->
|
||||||
|
notifyItemMoved(from.index, to.index)
|
||||||
|
}
|
||||||
|
diffAddsAndReplaces.added.forEach {
|
||||||
|
notifyItemInserted(it.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// currently do nothing
|
||||||
|
}
|
||||||
|
}.launchIn(listeningScope)
|
||||||
|
}
|
||||||
|
}
|
@@ -16,7 +16,7 @@ package dev.inmo.micro_utils.common
|
|||||||
AnnotationTarget.TYPEALIAS,
|
AnnotationTarget.TYPEALIAS,
|
||||||
AnnotationTarget.TYPE_PARAMETER
|
AnnotationTarget.TYPE_PARAMETER
|
||||||
)
|
)
|
||||||
annotation class PreviewFeature
|
annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases")
|
||||||
|
|
||||||
@RequiresOptIn(
|
@RequiresOptIn(
|
||||||
"This thing is marked as warned. See message of warn to get more info",
|
"This thing is marked as warned. See message of warn to get more info",
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!
|
@@ -33,3 +33,15 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
|||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) {
|
||||||
|
if (show) {
|
||||||
|
show()
|
||||||
|
} else {
|
||||||
|
if (goneOnHide) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!
|
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
fun CoroutineContext.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
|
||||||
|
fun CoroutineScope.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = coroutineContext.LinkedSupervisorJob(additionalContext)
|
||||||
|
|
||||||
|
fun CoroutineScope.LinkedSupervisorScope(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = CoroutineScope(
|
||||||
|
coroutineContext + LinkedSupervisorJob(additionalContext)
|
||||||
|
)
|
@@ -22,9 +22,9 @@ uuidVersion=0.3.0
|
|||||||
|
|
||||||
# ANDROID
|
# ANDROID
|
||||||
|
|
||||||
core_ktx_version=1.3.2
|
core_ktx_version=1.5.0
|
||||||
androidx_recycler_version=1.2.0
|
androidx_recycler_version=1.2.1
|
||||||
appcompat_version=1.2.0
|
appcompat_version=1.3.0
|
||||||
|
|
||||||
android_minSdkVersion=19
|
android_minSdkVersion=19
|
||||||
android_compileSdkVersion=30
|
android_compileSdkVersion=30
|
||||||
@@ -45,5 +45,5 @@ dokka_version=1.4.32
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.5.7
|
version=0.5.12
|
||||||
android_code_version=48
|
android_code_version=53
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
data class ApplicationCachingHeadersConfigurator(
|
data class ApplicationCachingHeadersConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(CachingHeaders) {
|
install(CachingHeaders) {
|
||||||
|
@@ -10,17 +10,18 @@ import kotlinx.serialization.Serializable
|
|||||||
class ApplicationRoutingConfigurator(
|
class ApplicationRoutingConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun Route.invoke() }
|
fun interface Element { operator fun Route.invoke() }
|
||||||
|
private val rootInstaller = Element {
|
||||||
|
elements.forEach {
|
||||||
|
it.apply { invoke() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
try {
|
featureOrNull(Routing) ?.apply {
|
||||||
feature(Routing)
|
rootInstaller.apply { invoke() }
|
||||||
} catch (e: IllegalStateException) {
|
} ?: install(Routing) {
|
||||||
install(Routing) {
|
rootInstaller.apply { invoke() }
|
||||||
elements.forEach {
|
|
||||||
it.apply { invoke() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
class ApplicationSessionsConfigurator(
|
class ApplicationSessionsConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun Sessions.Configuration.invoke() }
|
fun interface Element { operator fun Sessions.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(Sessions) {
|
install(Sessions) {
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
class StatusPagesConfigurator(
|
class StatusPagesConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun StatusPages.Configuration.invoke() }
|
fun interface Element { operator fun StatusPages.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(StatusPages) {
|
install(StatusPages) {
|
||||||
|
@@ -33,3 +33,8 @@ suspend fun <T> doAllWithCurrentPaging(
|
|||||||
block
|
block
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> doForAllWithCurrentPaging(
|
||||||
|
initialPagination: Pagination = FirstPagePagination(),
|
||||||
|
block: suspend (Pagination) -> PaginationResult<T>
|
||||||
|
) = doAllWithCurrentPaging(initialPagination, block)
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.repos
|
package dev.inmo.micro_utils.repos
|
||||||
|
|
||||||
import dev.inmo.micro_utils.pagination.*
|
import dev.inmo.micro_utils.pagination.*
|
||||||
|
import dev.inmo.micro_utils.pagination.utils.doForAllWithCurrentPaging
|
||||||
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
import dev.inmo.micro_utils.pagination.utils.getAllWithNextPaging
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
|
|||||||
suspend fun remove(toRemove: Map<Key, List<Value>>)
|
suspend fun remove(toRemove: Map<Key, List<Value>>)
|
||||||
|
|
||||||
suspend fun clear(k: Key)
|
suspend fun clear(k: Key)
|
||||||
|
suspend fun clearWithValue(v: Value)
|
||||||
|
|
||||||
suspend fun set(toSet: Map<Key, List<Value>>) {
|
suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||||
toSet.keys.forEach { key -> clear(key) }
|
toSet.keys.forEach { key -> clear(key) }
|
||||||
@@ -87,7 +89,19 @@ suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.set(
|
|||||||
k: Key, vararg v: Value
|
k: Key, vararg v: Value
|
||||||
) = set(k, v.toList())
|
) = set(k, v.toList())
|
||||||
|
|
||||||
interface OneToManyKeyValueRepo<Key, Value> : ReadOneToManyKeyValueRepo<Key, Value>, WriteOneToManyKeyValueRepo<Key, Value>
|
interface OneToManyKeyValueRepo<Key, Value> : ReadOneToManyKeyValueRepo<Key, Value>, WriteOneToManyKeyValueRepo<Key, Value> {
|
||||||
|
override suspend fun clearWithValue(v: Value) {
|
||||||
|
doWithPagination {
|
||||||
|
val keysResult = keys(v, it)
|
||||||
|
|
||||||
|
if (keysResult.results.isNotEmpty()) {
|
||||||
|
remove(keysResult.results.map { it to listOf(v) })
|
||||||
|
}
|
||||||
|
|
||||||
|
keysResult.currentPageIfNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
typealias KeyValuesRepo<Key,Value> = OneToManyKeyValueRepo<Key, Value>
|
typealias KeyValuesRepo<Key,Value> = OneToManyKeyValueRepo<Key, Value>
|
||||||
|
|
||||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
||||||
|
@@ -114,6 +114,7 @@ open class MapperWriteOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
|
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
|
||||||
|
override suspend fun clearWithValue(v: FromValue) = to.clearWithValue(v.toOutValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
@@ -74,6 +74,19 @@ fun SQLiteDatabase.select(
|
|||||||
table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
|
table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun SQLiteDatabase.selectDistinct(
|
||||||
|
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(
|
||||||
|
true, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
|
||||||
|
)
|
||||||
|
|
||||||
fun makePlaceholders(count: Int): String {
|
fun makePlaceholders(count: Int): String {
|
||||||
return (0 until count).joinToString { "?" }
|
return (0 until count).joinToString { "?" }
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,7 @@ package dev.inmo.micro_utils.repos.onetomany
|
|||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import dev.inmo.micro_utils.common.mapNotNullA
|
import dev.inmo.micro_utils.common.mapNotNullA
|
||||||
import dev.inmo.micro_utils.pagination.FirstPagePagination
|
import dev.inmo.micro_utils.pagination.*
|
||||||
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.pagination.utils.reverse
|
||||||
import dev.inmo.micro_utils.repos.*
|
import dev.inmo.micro_utils.repos.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -20,10 +17,14 @@ private val internalSerialFormat = Json {
|
|||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typealias KeyValuesAndroidRepo<Key, Value> = OneToManyAndroidRepo<Key, Value>
|
||||||
|
|
||||||
class OneToManyAndroidRepo<Key, Value>(
|
class OneToManyAndroidRepo<Key, Value>(
|
||||||
private val tableName: String,
|
private val tableName: String,
|
||||||
private val keySerializer: KSerializer<Key>,
|
private val keyAsString: Key.() -> String,
|
||||||
private val valueSerializer: KSerializer<Value>,
|
private val valueAsString: Value.() -> String,
|
||||||
|
private val keyFromString: String.() -> Key,
|
||||||
|
private val valueFromString: String.() -> Value,
|
||||||
private val helper: SQLiteOpenHelper
|
private val helper: SQLiteOpenHelper
|
||||||
) : OneToManyKeyValueRepo<Key, Value> {
|
) : OneToManyKeyValueRepo<Key, Value> {
|
||||||
private val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
|
private val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
|
||||||
@@ -34,12 +35,9 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
override val onDataCleared: Flow<Key> = _onDataCleared.asSharedFlow()
|
override val onDataCleared: Flow<Key> = _onDataCleared.asSharedFlow()
|
||||||
|
|
||||||
private val idColumnName = "id"
|
private val idColumnName = "id"
|
||||||
|
private val idColumnArray = arrayOf(idColumnName)
|
||||||
private val valueColumnName = "value"
|
private val valueColumnName = "value"
|
||||||
|
private val valueColumnArray = arrayOf(valueColumnName)
|
||||||
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 {
|
init {
|
||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
@@ -57,12 +55,23 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
for ((k, values) in toAdd) {
|
for ((k, values) in toAdd) {
|
||||||
values.forEach { v ->
|
values.forEach { v ->
|
||||||
|
val kAsString = k.keyAsString()
|
||||||
|
val vAsString = v.valueAsString()
|
||||||
|
val isThere = select(tableName,
|
||||||
|
null,
|
||||||
|
"$idColumnName=? AND $valueColumnName=?",
|
||||||
|
arrayOf(kAsString, vAsString),
|
||||||
|
limit = limitClause(1)
|
||||||
|
).use { it.moveToFirst() }
|
||||||
|
if (isThere) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
insert(
|
insert(
|
||||||
tableName,
|
tableName,
|
||||||
null,
|
null,
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
idColumnName to k.asId(),
|
idColumnName to k.keyAsString(),
|
||||||
valueColumnName to v.asValue()
|
valueColumnName to v.valueAsString()
|
||||||
)
|
)
|
||||||
).also {
|
).also {
|
||||||
if (it != -1L) {
|
if (it != -1L) {
|
||||||
@@ -77,7 +86,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
|
|
||||||
override suspend fun clear(k: Key) {
|
override suspend fun clear(k: Key) {
|
||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
delete(tableName, "$idColumnName=?", arrayOf(k.asId()))
|
delete(tableName, "$idColumnName=?", arrayOf(k.keyAsString()))
|
||||||
}.also {
|
}.also {
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
_onDataCleared.emit(k)
|
_onDataCleared.emit(k)
|
||||||
@@ -88,7 +97,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
override suspend fun set(toSet: Map<Key, List<Value>>) {
|
override suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||||
val (clearedKeys, inserted) = helper.blockingWritableTransaction {
|
val (clearedKeys, inserted) = helper.blockingWritableTransaction {
|
||||||
toSet.mapNotNull { (k, _) ->
|
toSet.mapNotNull { (k, _) ->
|
||||||
if (delete(tableName, "$idColumnName=?", arrayOf(k.asId())) > 0) {
|
if (delete(tableName, "$idColumnName=?", arrayOf(k.keyAsString())) > 0) {
|
||||||
k
|
k
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -98,7 +107,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
insert(
|
insert(
|
||||||
tableName,
|
tableName,
|
||||||
null,
|
null,
|
||||||
contentValuesOf(idColumnName to k.asId(), valueColumnName to v.asValue())
|
contentValuesOf(idColumnName to k.keyAsString(), valueColumnName to v.valueAsString())
|
||||||
)
|
)
|
||||||
k to v
|
k to v
|
||||||
}
|
}
|
||||||
@@ -109,7 +118,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun contains(k: Key): Boolean = helper.blockingReadableTransaction {
|
override suspend fun contains(k: Key): Boolean = helper.blockingReadableTransaction {
|
||||||
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = firstPageWithOneElementPagination.limitClause()).use {
|
||||||
it.count > 0
|
it.count > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,14 +127,14 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
select(
|
select(
|
||||||
tableName,
|
tableName,
|
||||||
selection = "$idColumnName=? AND $valueColumnName=?",
|
selection = "$idColumnName=? AND $valueColumnName=?",
|
||||||
selectionArgs = arrayOf(k.asId(), v.asValue()),
|
selectionArgs = arrayOf(k.keyAsString(), v.valueAsString()),
|
||||||
limit = FirstPagePagination(1).limitClause()
|
limit = FirstPagePagination(1).limitClause()
|
||||||
).use {
|
).use {
|
||||||
it.count > 0
|
it.count > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun count(): Long =helper.blockingReadableTransaction {
|
override suspend fun count(): Long = helper.blockingReadableTransaction {
|
||||||
select(
|
select(
|
||||||
tableName
|
tableName
|
||||||
).use {
|
).use {
|
||||||
@@ -134,7 +143,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
}.toLong()
|
}.toLong()
|
||||||
|
|
||||||
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
|
override suspend fun count(k: Key): Long = helper.blockingReadableTransaction {
|
||||||
select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
|
selectDistinct(tableName, columns = valueColumnArray, selection = "$idColumnName=?", selectionArgs = arrayOf(k.keyAsString()), limit = FirstPagePagination(1).limitClause()).use {
|
||||||
it.count
|
it.count
|
||||||
}
|
}
|
||||||
}.toLong()
|
}.toLong()
|
||||||
@@ -144,18 +153,25 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
reversed: Boolean
|
reversed: Boolean
|
||||||
): PaginationResult<Value> = count(k).let { count ->
|
): PaginationResult<Value> = count(k).let { count ->
|
||||||
|
if (pagination.firstIndex >= count) {
|
||||||
|
return@let emptyList<Value>().createPaginationResult(
|
||||||
|
pagination,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||||
helper.blockingReadableTransaction {
|
helper.blockingReadableTransaction {
|
||||||
select(
|
select(
|
||||||
tableName,
|
tableName,
|
||||||
|
valueColumnArray,
|
||||||
selection = "$idColumnName=?",
|
selection = "$idColumnName=?",
|
||||||
selectionArgs = arrayOf(k.asId()),
|
selectionArgs = arrayOf(k.keyAsString()),
|
||||||
limit = resultPagination.limitClause()
|
limit = resultPagination.limitClause()
|
||||||
).use { c ->
|
).use { c ->
|
||||||
mutableListOf<Value>().also {
|
mutableListOf<Value>().also {
|
||||||
if (c.moveToFirst()) {
|
if (c.moveToFirst()) {
|
||||||
do {
|
do {
|
||||||
it.add(c.getString(valueColumnName).asValue())
|
it.add(c.getString(valueColumnName).valueFromString())
|
||||||
} while (c.moveToNext())
|
} while (c.moveToNext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,16 +186,23 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
reversed: Boolean
|
reversed: Boolean
|
||||||
): PaginationResult<Key> = count().let { count ->
|
): PaginationResult<Key> = count().let { count ->
|
||||||
|
if (pagination.firstIndex >= count) {
|
||||||
|
return@let emptyList<Key>().createPaginationResult(
|
||||||
|
pagination,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||||
helper.blockingReadableTransaction {
|
helper.blockingReadableTransaction {
|
||||||
select(
|
selectDistinct(
|
||||||
tableName,
|
tableName,
|
||||||
|
idColumnArray,
|
||||||
limit = resultPagination.limitClause()
|
limit = resultPagination.limitClause()
|
||||||
).use { c ->
|
).use { c ->
|
||||||
mutableListOf<Key>().also {
|
mutableListOf<Key>().also {
|
||||||
if (c.moveToFirst()) {
|
if (c.moveToFirst()) {
|
||||||
do {
|
do {
|
||||||
it.add(c.getString(idColumnName).asKey())
|
it.add(c.getString(idColumnName).keyFromString())
|
||||||
} while (c.moveToNext())
|
} while (c.moveToNext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,16 +220,17 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
): PaginationResult<Key> = count().let { count ->
|
): PaginationResult<Key> = count().let { count ->
|
||||||
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
|
||||||
helper.blockingReadableTransaction {
|
helper.blockingReadableTransaction {
|
||||||
select(
|
selectDistinct(
|
||||||
tableName,
|
tableName,
|
||||||
|
idColumnArray,
|
||||||
selection = "$valueColumnName=?",
|
selection = "$valueColumnName=?",
|
||||||
selectionArgs = arrayOf(v.asValue()),
|
selectionArgs = arrayOf(v.valueAsString()),
|
||||||
limit = resultPagination.limitClause()
|
limit = resultPagination.limitClause()
|
||||||
).use { c ->
|
).use { c ->
|
||||||
mutableListOf<Key>().also {
|
mutableListOf<Key>().also {
|
||||||
if (c.moveToFirst()) {
|
if (c.moveToFirst()) {
|
||||||
do {
|
do {
|
||||||
it.add(c.getString(idColumnName).asKey())
|
it.add(c.getString(idColumnName).keyFromString())
|
||||||
} while (c.moveToNext())
|
} while (c.moveToNext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +245,7 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
helper.blockingWritableTransaction {
|
helper.blockingWritableTransaction {
|
||||||
toRemove.flatMap { (k, vs) ->
|
toRemove.flatMap { (k, vs) ->
|
||||||
vs.mapNotNullA { v ->
|
vs.mapNotNullA { v ->
|
||||||
if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.asId(), v.asValue())) > 0) {
|
if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.keyAsString(), v.valueAsString())) > 0) {
|
||||||
k to v
|
k to v
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -233,3 +257,24 @@ class OneToManyAndroidRepo<Key, Value>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <Key, Value> OneToManyAndroidRepo(
|
||||||
|
tableName: String,
|
||||||
|
keySerializer: KSerializer<Key>,
|
||||||
|
valueSerializer: KSerializer<Value>,
|
||||||
|
helper: SQLiteOpenHelper
|
||||||
|
) = OneToManyAndroidRepo(
|
||||||
|
tableName,
|
||||||
|
{ internalSerialFormat.encodeToString(keySerializer, this) },
|
||||||
|
{ internalSerialFormat.encodeToString(valueSerializer, this) },
|
||||||
|
{ internalSerialFormat.decodeFromString(keySerializer, this) },
|
||||||
|
{ internalSerialFormat.decodeFromString(valueSerializer, this) },
|
||||||
|
helper
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <Key, Value> KeyValuesAndroidRepo(
|
||||||
|
tableName: String,
|
||||||
|
keySerializer: KSerializer<Key>,
|
||||||
|
valueSerializer: KSerializer<Value>,
|
||||||
|
helper: SQLiteOpenHelper
|
||||||
|
) = OneToManyAndroidRepo(tableName, keySerializer, valueSerializer, helper)
|
||||||
|
@@ -31,6 +31,9 @@ open class ExposedOneToManyKeyValueRepo<Key, Value>(
|
|||||||
transaction(database) {
|
transaction(database) {
|
||||||
toAdd.keys.flatMap { k ->
|
toAdd.keys.flatMap { k ->
|
||||||
toAdd[k] ?.mapNotNull { v ->
|
toAdd[k] ?.mapNotNull { v ->
|
||||||
|
if (select { keyColumn.eq(k).and(valueColumn.eq(v)) }.limit(1).count() > 0) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
insertIgnore {
|
insertIgnore {
|
||||||
it[keyColumn] = k
|
it[keyColumn] = k
|
||||||
it[valueColumn] = v
|
it[valueColumn] = v
|
||||||
|
@@ -86,6 +86,12 @@ class MapWriteOneToManyKeyValueRepo<Key, Value>(
|
|||||||
override suspend fun clear(k: Key) {
|
override suspend fun clear(k: Key) {
|
||||||
map.remove(k) ?.also { _onDataCleared.emit(k) }
|
map.remove(k) ?.also { _onDataCleared.emit(k) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun clearWithValue(v: Value) {
|
||||||
|
map.forEach { (k, values) ->
|
||||||
|
if (values.remove(v)) _onValueRemoved.emit(k to v)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapOneToManyKeyValueRepo<Key, Value>(
|
class MapOneToManyKeyValueRepo<Key, Value>(
|
||||||
|
@@ -67,6 +67,15 @@ class KtorWriteOneToManyKeyValueRepo<Key, Value> (
|
|||||||
Unit.serializer(),
|
Unit.serializer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun clearWithValue(v: Value) = unifiedRequester.unipost(
|
||||||
|
buildStandardUrl(
|
||||||
|
baseUrl,
|
||||||
|
clearWithValueRoute,
|
||||||
|
),
|
||||||
|
BodyPair(valueSerializer, v),
|
||||||
|
Unit.serializer(),
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun set(toSet: Map<Key, List<Value>>) = unifiedRequester.unipost(
|
override suspend fun set(toSet: Map<Key, List<Value>>) = unifiedRequester.unipost(
|
||||||
buildStandardUrl(
|
buildStandardUrl(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@@ -75,4 +84,4 @@ class KtorWriteOneToManyKeyValueRepo<Key, Value> (
|
|||||||
BodyPair(keyValueMapSerializer, toSet),
|
BodyPair(keyValueMapSerializer, toSet),
|
||||||
Unit.serializer(),
|
Unit.serializer(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -14,4 +14,5 @@ const val onDataClearedRoute = "onDataCleared"
|
|||||||
const val addRoute = "add"
|
const val addRoute = "add"
|
||||||
const val removeRoute = "remove"
|
const val removeRoute = "remove"
|
||||||
const val clearRoute = "clear"
|
const val clearRoute = "clear"
|
||||||
const val setRoute = "set"
|
const val clearWithValueRoute = "clearWithValue"
|
||||||
|
const val setRoute = "set"
|
||||||
|
@@ -72,6 +72,17 @@ fun <Key, Value> Route.configureOneToManyWriteKeyValueRepoRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post(clearWithValueRoute) {
|
||||||
|
unifiedRouter.apply {
|
||||||
|
val v = uniload(valueSerializer)
|
||||||
|
|
||||||
|
unianswer(
|
||||||
|
Unit.serializer(),
|
||||||
|
originalRepo.clearWithValue(v),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
post(setRoute) {
|
post(setRoute) {
|
||||||
unifiedRouter.apply {
|
unifiedRouter.apply {
|
||||||
val obj = uniload(keyValueMapSerializer)
|
val obj = uniload(keyValueMapSerializer)
|
||||||
|
Reference in New Issue
Block a user