mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-10-04 14:49:30 +00:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
67c595b440 | |||
830b7aee56 | |||
1890608cb3 | |||
bd396959a9 | |||
d5fe19f0a5 | |||
46bfb09415 | |||
a60cb596d1 | |||
6f9d5e2d5f | |||
80bc226ee1 | |||
12e37184e1 | |||
25e9345d02 | |||
ccc4d030c3 | |||
90c0817b6d | |||
527f7bbafe | |||
765a32729f | |||
de783f77a2 | |||
de4c8d104c | |||
88c8c28f45 | |||
57b36826d1 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.13
|
||||
|
||||
* `Common`
|
||||
* `Android`
|
||||
* Add expand/collapse functionality for horizontal expand/collapse
|
||||
|
||||
## 0.4.12
|
||||
|
||||
* `Coroutines`
|
||||
* `JVM`
|
||||
* Update `launchSynchronously` signature
|
||||
* `Selector`
|
||||
* Project created
|
||||
|
||||
## 0.4.11
|
||||
|
||||
* `Common`
|
||||
* Add `clamp` function
|
||||
|
||||
## 0.4.10
|
||||
|
||||
* `Versions`:
|
||||
|
37
README.md
37
README.md
@@ -1 +1,36 @@
|
||||
# MicroUtils
|
||||
# MicroUtils
|
||||
|
||||
This is a library with collection of tools for working in Kotlin environment. First of all, this library collection is oriented to use next technologies:
|
||||
|
||||
* [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines)
|
||||
* [`Kotlin Serialization`](https://github.com/Kotlin/kotlinx.serialization)
|
||||
* [`Kotlin Exposed`](https://github.com/JetBrains/Exposed)
|
||||
* [`Ktor`](https://ktor.io)
|
||||
|
||||
<details>
|
||||
<summary> <b>Android environment</b> </summary>
|
||||
|
||||
You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUtils/blob/master/gradle.properties#L24-L34">properties file</a> to get information about current project dependencies, compile and build tools for `Android` target.
|
||||
|
||||
</details>
|
||||
|
||||
## Projects
|
||||
|
||||
* `common` contains common tools for platform which usually are absent out-of-the-box when you starting project
|
||||
* `selector` contains tools to use `Selector` interface with things like `RecyclerView` in android or other selection needs
|
||||
* `coroutines` is a module for `Kotlin Coroutines` with different things like subscribing on flows (`onEach` + `launchIn` shortcut :) )
|
||||
* `ktor` is a set of modules for `client`s and `server`s
|
||||
* `mime_types` is NOT lightweight set of `MimeType`s with a lot of different objected and serializable (with `Kotlin Serialization`) mime types
|
||||
* `pagination` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for lightweight pagination
|
||||
* `repos` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for `KeyValue`/`OneToMany`/`CRUD` repos created to be able to exclude some heavy dependencies when you need some simple and lightweight typical repositories
|
||||
|
||||
## Complex modules structure
|
||||
|
||||
Most of complex modules are built with next hierarchy:
|
||||
|
||||
* `common` submodule for `API` things which are common for all platforms
|
||||
* `exposed` submodule contains realizations for exposed tables
|
||||
* `ktor` submodule is usually unavailable directly, because it contains its own submodules for clients and servers
|
||||
* `common` part contains routes which are common for clients and servers
|
||||
* `client` submodule contains clients which are usually using `UnifiedRequester` to make requests using routes from `ktor/common` module and some internal logic of requests
|
||||
* `server` submodule (in most cases `JVM`-only) contains some extensions for `Route` instances which usually will give opportunity to proxy internet requests from `ktor/client` realization to some proxy object
|
||||
|
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T {
|
||||
return when {
|
||||
this < min -> min
|
||||
this > max -> max
|
||||
else -> this
|
||||
}
|
||||
}
|
@@ -5,23 +5,44 @@ import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.Transformation
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expand(
|
||||
private fun View.performExpand(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
onMeasured: View.() -> Unit,
|
||||
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||
) {
|
||||
measure(targetWidth, targetHeight)
|
||||
val measuredHeight: Int = measuredHeight
|
||||
layoutParams.height = 0
|
||||
visibility = View.VISIBLE
|
||||
onMeasured()
|
||||
show()
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
super.applyTransformation(interpolatedTime, t)
|
||||
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
|
||||
onPerformAnimation(interpolatedTime, t)
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean = true
|
||||
}
|
||||
|
||||
a.duration = duration
|
||||
startAnimation(a)
|
||||
}
|
||||
|
||||
private fun View.performCollapse(
|
||||
duration: Long = 500,
|
||||
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||
) {
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
if (interpolatedTime == 1f) {
|
||||
gone()
|
||||
} else {
|
||||
onPerformAnimation(interpolatedTime, t)
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean {
|
||||
return true
|
||||
}
|
||||
@@ -32,27 +53,58 @@ fun View.expand(
|
||||
startAnimation(a)
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expand(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
) {
|
||||
var measuredHeight = 0
|
||||
performExpand(
|
||||
duration,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
{
|
||||
measuredHeight = this.measuredHeight
|
||||
}
|
||||
) { interpolatedTime, _ ->
|
||||
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expandHorizontally(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
) {
|
||||
var measuredWidth = 0
|
||||
performExpand(
|
||||
duration,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
{
|
||||
measuredWidth = this.measuredWidth
|
||||
}
|
||||
) { interpolatedTime, _ ->
|
||||
layoutParams.width = if (interpolatedTime == 1f) targetWidth else (measuredWidth * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.collapse(duration: Long = 500) {
|
||||
val initialHeight: Int = measuredHeight
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
if (interpolatedTime == 1f) {
|
||||
visibility = View.GONE
|
||||
} else {
|
||||
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean {
|
||||
return true
|
||||
}
|
||||
performCollapse(duration) { interpolatedTime, _ ->
|
||||
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
a.duration = duration
|
||||
|
||||
startAnimation(a)
|
||||
@PreviewFeature
|
||||
fun View.collapseHorizontally(duration: Long = 500) {
|
||||
val initialWidth: Int = measuredWidth
|
||||
performCollapse(duration) { interpolatedTime, _ ->
|
||||
layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
@@ -74,3 +126,15 @@ fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||
collapse(duration)
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true in case of expanding
|
||||
*/
|
||||
@PreviewFeature
|
||||
fun View.toggleExpandHorizontallyState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||
expandHorizontally(duration)
|
||||
true
|
||||
} else {
|
||||
collapseHorizontally(duration)
|
||||
false
|
||||
}
|
||||
|
@@ -2,12 +2,12 @@ package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun <T> launchSynchronously(scope: CoroutineScope = CoroutineScope(Dispatchers.Default), block: suspend CoroutineScope.() -> T): T {
|
||||
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
||||
var throwable: Throwable? = null
|
||||
var result: T? = null
|
||||
val objectToSynchronize = java.lang.Object()
|
||||
val launchCallback = {
|
||||
scope.launch {
|
||||
launch {
|
||||
safely(
|
||||
{
|
||||
throwable = it
|
||||
@@ -26,3 +26,5 @@ fun <T> launchSynchronously(scope: CoroutineScope = CoroutineScope(Dispatchers.D
|
||||
}
|
||||
throw throwable ?: return result!!
|
||||
}
|
||||
|
||||
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
||||
|
@@ -40,5 +40,5 @@ dokka_version=1.4.20
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.4.10
|
||||
android_code_version=14
|
||||
version=0.4.13
|
||||
android_code_version=17
|
||||
|
17
selector/common/build.gradle
Normal file
17
selector/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,168 @@
|
||||
package dev.inmo.micro_utils.selector
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
||||
/**
|
||||
* Unified interface which can be used in any system which require some selection functionality
|
||||
*/
|
||||
interface Selector<T> {
|
||||
val selectedItems: List<T>
|
||||
val itemSelected: SharedFlow<T>
|
||||
val itemUnselected: SharedFlow<T>
|
||||
|
||||
suspend fun toggleSelection(element: T)
|
||||
suspend fun forceSelect(element: T)
|
||||
suspend fun forceDeselect(element: T)
|
||||
suspend fun clearSelection()
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline operator fun <T> Selector<T>.contains(element: T) = selectedItems.contains(element)
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> Selector<T>.nothingSelected(): Boolean = selectedItems.isEmpty()
|
||||
suspend inline fun <T> Selector<T>.toggleSelection(elements: List<T>) = elements.forEach { toggleSelection(it) }
|
||||
suspend inline fun <T> Selector<T>.forceSelect(elements: List<T>) = elements.forEach { forceSelect(it) }
|
||||
suspend inline fun <T> Selector<T>.forceDeselect(elements: List<T>) = elements.forEach { forceDeselect(it) }
|
||||
suspend inline fun <T> Selector<T>.toggleSelection(firstElement: T, vararg elements: T) = toggleSelection(listOf(firstElement) + elements.toList())
|
||||
suspend inline fun <T> Selector<T>.forceSelect(firstElement: T, vararg elements: T) = forceSelect(listOf(firstElement) + elements.toList())
|
||||
suspend inline fun <T> Selector<T>.forceDeselect(firstElement: T, vararg elements: T) = forceDeselect(listOf(firstElement) + elements.toList())
|
||||
|
||||
/**
|
||||
* Realization of [Selector] with one or without selected element. This realization will always have empty
|
||||
* [selectedItems] when nothing selected and one element in [selectedItems] when something selected. Contains
|
||||
* [selectedItem] value for simple access to currently selected item.
|
||||
*
|
||||
* On calling of [toggleSelection] previous selection will be erased and [itemUnselected] will emit this element.
|
||||
*
|
||||
* @param safeChanges Set to false to disable using of [mutex] for synchronizing changes on [toggleSelection]
|
||||
*/
|
||||
class SingleSelector<T>(
|
||||
selectedItem: T? = null,
|
||||
safeChanges: Boolean = true
|
||||
) : Selector<T> {
|
||||
var selectedItem: T? = selectedItem
|
||||
private set
|
||||
override val selectedItems: List<T>
|
||||
get() = selectedItem ?.let { listOf(it) } ?: emptyList()
|
||||
|
||||
private val _itemSelected = MutableSharedFlow<T>()
|
||||
override val itemSelected: SharedFlow<T> = _itemSelected.asSharedFlow()
|
||||
private val _itemUnselected = MutableSharedFlow<T>()
|
||||
override val itemUnselected: SharedFlow<T> = _itemUnselected.asSharedFlow()
|
||||
|
||||
private val mutex = if (safeChanges) {
|
||||
Mutex()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override suspend fun forceDeselect(element: T) {
|
||||
mutex ?.lock()
|
||||
if (selectedItem == element) {
|
||||
selectedItem = null
|
||||
_itemUnselected.emit(element)
|
||||
}
|
||||
mutex ?.unlock()
|
||||
}
|
||||
|
||||
override suspend fun forceSelect(element: T) {
|
||||
mutex ?.lock()
|
||||
if (selectedItem != element) {
|
||||
selectedItem = element
|
||||
_itemSelected.emit(element)
|
||||
}
|
||||
mutex ?.unlock()
|
||||
}
|
||||
|
||||
override suspend fun toggleSelection(element: T) {
|
||||
mutex ?.lock()
|
||||
if (selectedItem == element) {
|
||||
selectedItem = null
|
||||
_itemUnselected.emit(element)
|
||||
} else {
|
||||
val previouslySelected = selectedItem
|
||||
selectedItem = null
|
||||
if (previouslySelected != null) {
|
||||
_itemUnselected.emit(previouslySelected)
|
||||
}
|
||||
selectedItem = element
|
||||
_itemSelected.emit(element)
|
||||
}
|
||||
mutex ?.unlock()
|
||||
}
|
||||
|
||||
override suspend fun clearSelection() {
|
||||
selectedItem ?.let { forceDeselect(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Realization of [Selector] with multiple selected elements. On calling of [toggleSelection] this realization will select passed element OR deselect it if it is already in
|
||||
* [selectedItems]
|
||||
*
|
||||
* @param safeChanges Set to false to disable using of [mutex] for synchronizing changes on [toggleSelection]
|
||||
*/
|
||||
class MultipleSelector<T>(
|
||||
selectedItems: List<T> = emptyList(),
|
||||
safeChanges: Boolean = true
|
||||
) : Selector<T> {
|
||||
private val _selectedItems: MutableList<T> = selectedItems.toMutableList()
|
||||
override val selectedItems: List<T> = _selectedItems
|
||||
|
||||
private val _itemSelected = MutableSharedFlow<T>()
|
||||
override val itemSelected: SharedFlow<T> = _itemSelected.asSharedFlow()
|
||||
private val _itemUnselected = MutableSharedFlow<T>()
|
||||
override val itemUnselected: SharedFlow<T> = _itemUnselected.asSharedFlow()
|
||||
|
||||
private val mutex = if (safeChanges) {
|
||||
Mutex()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override suspend fun forceDeselect(element: T) {
|
||||
mutex ?.lock()
|
||||
if (_selectedItems.remove(element)) {
|
||||
_itemUnselected.emit(element)
|
||||
}
|
||||
mutex ?.unlock()
|
||||
}
|
||||
|
||||
override suspend fun forceSelect(element: T) {
|
||||
mutex ?.lock()
|
||||
if (element !in _selectedItems && _selectedItems.add(element)) {
|
||||
_itemSelected.emit(element)
|
||||
}
|
||||
mutex ?.unlock()
|
||||
}
|
||||
|
||||
override suspend fun toggleSelection(element: T) {
|
||||
mutex ?.lock()
|
||||
if (_selectedItems.remove(element)) {
|
||||
_itemUnselected.emit(element)
|
||||
} else {
|
||||
_selectedItems.add(element)
|
||||
_itemSelected.emit(element)
|
||||
}
|
||||
mutex ?.unlock()
|
||||
}
|
||||
|
||||
override suspend fun clearSelection() {
|
||||
mutex ?.lock()
|
||||
val preSelectedItems = _selectedItems.toList()
|
||||
_selectedItems.clear()
|
||||
preSelectedItems.forEach { _itemUnselected.emit(it) }
|
||||
mutex ?.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("FunctionName", "NOTHING_TO_INLINE")
|
||||
inline fun <T> Selector(
|
||||
multiple: Boolean,
|
||||
safeChanges: Boolean = true
|
||||
): Selector<T> = if (multiple) {
|
||||
MultipleSelector(safeChanges = safeChanges)
|
||||
} else {
|
||||
SingleSelector(safeChanges = safeChanges)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.selector
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* @return Returned [SharedFlow] will emit true when [element] has been selected in [this] [Selector] and will emit
|
||||
* false when this [element] was deselected
|
||||
*
|
||||
* @see [Selector]
|
||||
* @see [Selector.itemSelected]
|
||||
* @see [Selector.itemUnselected]
|
||||
*/
|
||||
fun <T> Selector<T>.itemSelectionFlow(element: T, scope: CoroutineScope): SharedFlow<Boolean> = MutableSharedFlow<Boolean>().apply {
|
||||
itemSelected.onEach { if (it == element) emit(true) }.launchIn(scope)
|
||||
itemUnselected.onEach { if (it == element) emit(false) }.launchIn(scope)
|
||||
}.asSharedFlow()
|
1
selector/common/src/main/AndroidManifest.xml
Normal file
1
selector/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.selector"/>
|
@@ -2,6 +2,7 @@ rootProject.name='micro_utils'
|
||||
|
||||
String[] includes = [
|
||||
":common",
|
||||
":selector:common",
|
||||
":pagination:common",
|
||||
":pagination:exposed",
|
||||
":pagination:ktor:common",
|
||||
|
Reference in New Issue
Block a user