-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from ikarenkov/ui-adapter-module
Added ui store and convenient dsl to build it
- Loading branch information
Showing
28 changed files
with
697 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
...c/commonMain/kotlin/io/github/ikarenkov/kombucha/DefaultStoreCoroutineExceptionHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package io.github.ikarenkov.kombucha | ||
|
||
import kotlinx.coroutines.CoroutineExceptionHandler | ||
import kotlinx.coroutines.CoroutineName | ||
|
||
fun DefaultStoreCoroutineExceptionHandler() = CoroutineExceptionHandler { context, throwable -> | ||
val storeName = context[CoroutineName] | ||
println("Unhandled error in Coroutine store named \"$storeName\".") | ||
throwable.printStackTrace() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# UI Adapter | ||
|
||
This module contains [UiStore](/src/commonMain/kotlin/io/github/ikarenkov/kombucha/ui/UiStore.kt) that provides functionality: | ||
|
||
1. Convert models to Ui Models | ||
2. Cache ui effects when there is no subscribers and emit cached effects with a first subscription. It can be disable using | ||
parameter `cacheUiEffects = false`. | ||
|
||
You can use [UiStoreBuilder](/src/commonMain/kotlin/io/github/ikarenkov/kombucha/ui/UiStoreBuilder.kt) and function `uiBuilder` for convenient usage | ||
without declaring all 6 generics. [UiStoreBuilder] also provides some build in functions and you can easily extend it using extension fun. | ||
|
||
## Sample | ||
|
||
Take a look to the [sample.feature.ui](../../sample/features/ui) for detailed examples of usage. | ||
|
||
If your UiMsg and UiEff are subclasses of Msg and Eff, you can use following code for simple mapping only UiState | ||
|
||
```kotlin | ||
val store: Store<Msg, State, Eff> = ... | ||
val uiStore = store.uiBuilder().using<Msg.Ext, UiState, Eff.Ext> { state -> | ||
UiState( | ||
state.itemsIds.map { resources.getString(R.string.item_title, it) } | ||
) | ||
} | ||
``` | ||
|
||
Otherwise you can provide your own mappers for UiMsg -> Msg and for Eff -> UiEff | ||
|
||
```kotlin | ||
store.uiBuilder().using<Msg.Ext, UiState, Eff.Ext>( | ||
uiMsgToMsgConverter = { it }, | ||
uiStateConverter = { state -> | ||
UiState( | ||
state.itemsIds.map { resources.getString(R.string.item_title, it) } | ||
) | ||
}, | ||
uiEffConverter = { eff -> | ||
eff as? Eff.Ext | ||
} | ||
) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,23 @@ | ||
plugins { | ||
alias(libs.plugins.kombucha.android.library) | ||
alias(libs.plugins.kombucha.kmp.library) | ||
} | ||
|
||
android.namespace = "io.github.ikarenkov.kombucha.ui_adapter" | ||
tasks.withType<Test> { | ||
useJUnitPlatform() | ||
} | ||
|
||
dependencies { | ||
implementation(libs.kotlinx.coroutines.core) | ||
implementation(projects.kombucha.core) | ||
kotlin { | ||
sourceSets { | ||
commonMain.dependencies { | ||
implementation(libs.kotlinx.coroutines.core) | ||
implementation(projects.kombucha.core) | ||
} | ||
commonTest.dependencies { | ||
implementation(libs.test.kotlin) | ||
implementation(libs.test.coroutines) | ||
} | ||
jvmTest.dependencies { | ||
implementation(libs.test.junit.jupiter) | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
...i-adapter/src/commonMain/kotlin/io/github/ikarenkov/kombucha/ui/CacheWhenNoSubscribers.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package io.github.ikarenkov.kombucha.ui | ||
|
||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.flow.onEach | ||
import kotlinx.coroutines.launch | ||
|
||
/** | ||
* Turn cold flow to hot and caches values only if there are no subscribers. | ||
*/ | ||
@ExperimentalCoroutinesApi | ||
fun <T> Flow<T>.cacheWhenNoSubscribers(scope: CoroutineScope): Flow<T> { | ||
val cache = MutableSharedFlow<T>(replay = Int.MAX_VALUE) | ||
scope.launch { | ||
collect { cache.emit(it) } | ||
} | ||
return cache.onEach { | ||
if (cache.subscriptionCount.value != 0) { | ||
cache.resetReplayCache() | ||
} | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
kombucha/ui-adapter/src/commonMain/kotlin/io/github/ikarenkov/kombucha/ui/UiStore.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package io.github.ikarenkov.kombucha.ui | ||
|
||
import io.github.ikarenkov.kombucha.DefaultStoreCoroutineExceptionHandler | ||
import io.github.ikarenkov.kombucha.store.Store | ||
import io.github.ikarenkov.kombucha.store.StoreScope | ||
import kotlinx.coroutines.CoroutineDispatcher | ||
import kotlinx.coroutines.CoroutineExceptionHandler | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.cancel | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.SharingStarted | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.flow.map | ||
import kotlinx.coroutines.flow.mapNotNull | ||
import kotlinx.coroutines.flow.stateIn | ||
|
||
/** | ||
* Wrapper for store, that allows handle basic UI scenarios: | ||
* 1. Convert models to Ui Models | ||
* 2. Cache ui effects when there is no subscribers and emit cached effects with a first subscription | ||
*/ | ||
class UiStore<UiMsg : Any, UiState : Any, UiEff : Any, Msg : Any, State : Any, Eff : Any>( | ||
private val store: Store<Msg, State, Eff>, | ||
private val uiMsgToMsgConverter: (UiMsg) -> Msg, | ||
private val uiStateConverter: (State) -> UiState, | ||
private val uiEffConverter: (Eff) -> UiEff?, | ||
coroutineExceptionHandler: CoroutineExceptionHandler = DefaultStoreCoroutineExceptionHandler(), | ||
uiDispatcher: CoroutineDispatcher = Dispatchers.Main, | ||
cacheUiEffects: Boolean = true | ||
) : Store<UiMsg, UiState, UiEff> { | ||
|
||
private val coroutineScope = StoreScope( | ||
name = "UiStore for $store", | ||
coroutineExceptionHandler = coroutineExceptionHandler, | ||
coroutineContext = uiDispatcher | ||
) | ||
|
||
override val state: StateFlow<UiState> = store.state | ||
.map { uiStateConverter(it) } | ||
.stateIn( | ||
scope = coroutineScope, | ||
started = SharingStarted.Lazily, | ||
initialValue = uiStateConverter(store.state.value) | ||
) | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class) | ||
override val effects: Flow<UiEff> = | ||
store.effects | ||
.mapNotNull { uiEffConverter(it) } | ||
.let { originalFlow -> | ||
if (cacheUiEffects) { | ||
originalFlow.cacheWhenNoSubscribers(coroutineScope) | ||
} else { | ||
originalFlow | ||
} | ||
} | ||
|
||
override fun accept(msg: UiMsg) { | ||
store.accept(uiMsgToMsgConverter(msg)) | ||
} | ||
|
||
override fun cancel() { | ||
coroutineScope.cancel() | ||
store.cancel() | ||
} | ||
|
||
} |
45 changes: 45 additions & 0 deletions
45
kombucha/ui-adapter/src/commonMain/kotlin/io/github/ikarenkov/kombucha/ui/UiStoreBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package io.github.ikarenkov.kombucha.ui | ||
|
||
import io.github.ikarenkov.kombucha.store.Store | ||
|
||
/** | ||
* Builder that helps to avoid explicit declaration of generic types in [UiStore] and provides different builder functions to build [UiStore]. | ||
* You can | ||
*/ | ||
class UiStoreBuilder<Msg : Any, State : Any, Eff : Any>( | ||
val store: Store<Msg, State, Eff> | ||
) { | ||
|
||
fun <UiMsg : Any, UiState : Any, UiEff : Any> using( | ||
uiMsgToMsgConverter: (UiMsg) -> Msg, | ||
uiStateConverter: (State) -> UiState, | ||
uiEffConverter: (Eff) -> UiEff?, | ||
cacheUiEffects: Boolean = true, | ||
): UiStore<UiMsg, UiState, UiEff, Msg, State, Eff> = UiStore( | ||
store = store, | ||
uiMsgToMsgConverter = uiMsgToMsgConverter, | ||
uiStateConverter = uiStateConverter, | ||
uiEffConverter = uiEffConverter, | ||
cacheUiEffects = cacheUiEffects | ||
) | ||
|
||
/** | ||
* Implementation based on inheritance UiMsg from Msg and UiEff from Eff | ||
*/ | ||
inline fun <UiMsg : Msg, UiState : Any, reified UiEff : Eff> using( | ||
cacheUiEffects: Boolean = true, | ||
noinline uiStateConverter: (State) -> UiState, | ||
): UiStore<UiMsg, UiState, UiEff, Msg, State, Eff> = UiStore( | ||
store = store, | ||
uiMsgToMsgConverter = { it }, | ||
uiStateConverter = uiStateConverter, | ||
uiEffConverter = { it as? UiEff }, | ||
cacheUiEffects = cacheUiEffects | ||
) | ||
|
||
} | ||
|
||
/** | ||
* Dsl builder that helps to avoid full declaration of generic types using regular constructor of [UiStore]. | ||
*/ | ||
fun <Msg : Any, State : Any, Eff : Any> Store<Msg, State, Eff>.uiBuilder() = UiStoreBuilder(this) |
Oops, something went wrong.