Skip to content

Commit

Permalink
Create StateFlowWrapper for multiplatform use (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandregpereira authored Feb 14, 2024
1 parent 06ab763 commit ebf4db0
Show file tree
Hide file tree
Showing 66 changed files with 654 additions and 911 deletions.
13 changes: 13 additions & 0 deletions core/flow/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
kotlin("multiplatform")
}

configureJvmTargets()

kotlin {
sourceSets {
commonMain.dependencies {
api(libs.kotlin.coroutines.core)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package br.alexandregpereira.flow

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class StateFlowWrapper<T : Any>(
private val origin: StateFlow<T>,
private val coroutineScope: CoroutineScope,
) : StateFlow<T> by origin {

fun subscribe(block: (T) -> Unit) {
origin.onEach(block).launchIn(coroutineScope)
}
}

class SharedFlowWrapper<T : Any>(
private val origin: SharedFlow<T>,
private val coroutineScope: CoroutineScope,
) : SharedFlow<T> by origin {

fun subscribe(block: (T) -> Unit) {
origin.onEach(block).launchIn(coroutineScope)
}
}

fun <T : Any> StateFlow<T>.wrap(
coroutineScope: CoroutineScope
): StateFlowWrapper<T> = StateFlowWrapper(
origin = this,
coroutineScope = coroutineScope
)

fun <T : Any> SharedFlow<T>.wrap(
coroutineScope: CoroutineScope
): SharedFlowWrapper<T> = SharedFlowWrapper(
origin = this,
coroutineScope = coroutineScope
)
9 changes: 2 additions & 7 deletions core/state-holder/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,8 @@ configureJvmTargets()

kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.kotlin.coroutines.core)
}
}
if (isMac()) {
val iosMain by getting
commonMain.dependencies {
api(project(":core:flow"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,44 @@

package br.alexandregpereira.hunter.state

import br.alexandregpereira.flow.SharedFlowWrapper
import br.alexandregpereira.flow.wrap
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow

interface ActionHandler<Action> {
interface ActionHandler<Action : Any> {

val action: SharedFlow<Action>
val action: SharedFlowWrapper<Action>
}

interface MutableActionHandler<Action> : ActionHandler<Action> {
interface MutableActionHandler<Action : Any> : ActionHandler<Action> {

fun sendAction(action: Action)

fun onActionHandlerClose()
}

fun <Action> MutableActionHandler(): MutableActionHandler<Action> {
fun <Action : Any> MutableActionHandler(): MutableActionHandler<Action> {
return MutableActionHandlerImpl()
}

private class MutableActionHandlerImpl<Action> : MutableActionHandler<Action> {
private class MutableActionHandlerImpl<Action : Any> : MutableActionHandler<Action> {

private val coroutineScope = MainScope()

private val _action = MutableSharedFlow<Action>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val action: SharedFlow<Action> = _action
override val action: SharedFlowWrapper<Action> = _action.wrap(coroutineScope)

override fun sendAction(action: Action) {
_action.tryEmit(action)
}

override fun onActionHandlerClose() {
coroutineScope.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,78 +16,35 @@

package br.alexandregpereira.hunter.state

import br.alexandregpereira.flow.StateFlowWrapper
import br.alexandregpereira.flow.wrap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

abstract class UiModel<State>(
abstract class UiModel<State : Any>(
initialState: State
) : StateHolder<State> {

val scope = CoroutineScope(
protected val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Main.immediate
)

private val _state = MutableStateFlow(initialState)
override val state: StateFlow<State> = _state
override val state: StateFlowWrapper<State> = _state.wrap(scope)

protected fun setState(block: State.() -> State) {
_state.value = state.value.block()
_state.value = _state.value.block()
}

fun onCleared() {
open fun onCleared() {
scope.cancel()
}
}

interface StateHolder<State> {
interface StateHolder<State : Any> {

val state: StateFlow<State>
}

interface MutableStateHolder<State>: StateHolder<State> {

fun setState(block: State.() -> State)
}

fun <State> MutableStateHolder(initialState: State): MutableStateHolder<State> {
return DefaultStateHolderImpl(initialState)
}

fun <State> MutableStateHolder(
stateRecovery: StateRecovery<State>
): MutableStateHolder<State> {
return DefaultStateHolderWithRecovery(stateRecovery)
}

private class DefaultStateHolderImpl<State>(
initialState: State
): MutableStateHolder<State> {

private val _state = MutableStateFlow(initialState)
override val state: StateFlow<State> = _state

override fun setState(block: State.() -> State) {
_state.value = state.value.block()
}
}

private class DefaultStateHolderWithRecovery<State>(
private val stateRecovery: StateRecovery<State>
): MutableStateHolder<State> {

private val stateHolderDelegate = DefaultStateHolderImpl(stateRecovery.getState())

override val state: StateFlow<State> = stateHolderDelegate.state

override fun setState(block: State.() -> State) {
stateHolderDelegate.setState {
block().also {
stateRecovery.saveState(it)
}
}
}
val state: StateFlowWrapper<State>
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package br.alexandregpereira.hunter.monster.compendium.domain

import kotlin.native.ObjCName

@ObjCName(name = "MonsterCompendiumError", exact = true)
sealed class MonsterCompendiumError(cause: Throwable? = null) : Throwable(cause) {
class NoMonsterError(cause: Throwable? = null) : MonsterCompendiumError(cause)
class UnknownError(cause: Throwable? = null) : MonsterCompendiumError(cause)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data class MonsterPreviewFolder(
val index: String,
val name: String = "",
val type: MonsterPreviewFolderType = MonsterPreviewFolderType.ABERRATION,
val challengeRating: Float = 0f,
val challengeRating: String = "",
val imageUrl: String = "",
val backgroundColorLight: String,
val backgroundColorDark: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal fun List<MonsterEntity>.asDomainMonsterPreviewFolderEntity(): List<Mons
index = index,
name = name,
type = MonsterPreviewFolderType.valueOf(type),
challengeRating = challengeRating,
challengeRating = challengeRating.getChallengeRatingFormatted(),
imageUrl = imageUrl,
backgroundColorLight = backgroundColorLight,
backgroundColorDark = backgroundColorDark,
Expand All @@ -49,3 +49,12 @@ internal fun List<MonsterEntity>.asDomainMonsterPreviewFolderEntity(): List<Mons
}
}
}

private fun Float.getChallengeRatingFormatted(): String {
return if (this < 1) {
val value = 1 / this
"1/${value.toInt()}"
} else {
this.toInt().toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ data class Monster(
) {

val xp: Int = challengeRatingToXp()

val challengeRatingFormatted: String = challengeRating.getChallengeRatingFormatted()
}

private fun Float.getChallengeRatingFormatted(): String {
return if (this < 1) {
val value = 1 / this
"1/${value.toInt()}"
} else {
this.toInt().toString()
}
}

data class MonsterImageData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,8 @@ import br.alexandregpereira.hunter.event.monster.detail.MonsterDetailEvent.OnVis
import br.alexandregpereira.hunter.event.monster.detail.MonsterDetailEventDispatcher
import br.alexandregpereira.hunter.folder.preview.event.FolderPreviewEvent
import br.alexandregpereira.hunter.folder.preview.event.FolderPreviewEventDispatcher
import br.alexandregpereira.hunter.state.ScopeManager
import br.alexandregpereira.hunter.state.StateHolder
import br.alexandregpereira.hunter.state.UiModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
Expand All @@ -45,12 +42,7 @@ class FolderDetailStateHolder internal constructor(
private val monsterDetailEventDispatcher: MonsterDetailEventDispatcher,
private val dispatcher: CoroutineDispatcher,
private val analytics: FolderDetailAnalytics,
) : ScopeManager(), StateHolder<FolderDetailState> {

private val _state: MutableStateFlow<FolderDetailState> = MutableStateFlow(
stateRecovery.getState()
)
override val state: StateFlow<FolderDetailState> = _state
) : UiModel<FolderDetailState>(stateRecovery.getState()) {

init {
observeEvents()
Expand Down Expand Up @@ -119,8 +111,4 @@ class FolderDetailStateHolder internal constructor(
}
.launchIn(scope)
}

private fun setState(block: FolderDetailState.() -> FolderDetailState) {
_state.value = state.value.block()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ import br.alexandregpereira.hunter.event.folder.insert.FolderInsertEvent.Show
import br.alexandregpereira.hunter.event.folder.insert.FolderInsertResult.OnMonsterRemoved
import br.alexandregpereira.hunter.event.folder.insert.FolderInsertResult.OnSaved
import br.alexandregpereira.hunter.localization.AppLocalization
import br.alexandregpereira.hunter.state.ScopeManager
import br.alexandregpereira.hunter.state.StateHolder
import br.alexandregpereira.hunter.state.UiModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
Expand All @@ -44,10 +41,8 @@ class FolderInsertStateHolder internal constructor(
private val dispatcher: CoroutineDispatcher,
private val analytics: FolderInsertAnalytics,
private val appLocalization: AppLocalization,
) : ScopeManager(), StateHolder<FolderInsertState> {
) : UiModel<FolderInsertState>(stateRecovery.getState()) {

private val _state = MutableStateFlow(stateRecovery.getState())
override val state: StateFlow<FolderInsertState> = _state
private val strings: FolderInsertStrings
get() = getFolderInsertStrings(appLocalization.getLanguage())

Expand Down Expand Up @@ -157,8 +152,4 @@ class FolderInsertStateHolder internal constructor(
}
}.launchIn(scope)
}

private fun setState(block: FolderInsertState.() -> FolderInsertState) {
_state.value = state.value.block()
}
}
1 change: 1 addition & 0 deletions feature/monster-compendium/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ android {
}

dependencies {
implementation project(':core:localization')
implementation project(':feature:folder-preview:event')
implementation project(':feature:monster-detail:event')
implementation project(':feature:monster-registration:event')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import br.alexandregpereira.hunter.monster.compendium.MonsterCompendiumViewAction.GoToCompendiumIndex
import br.alexandregpereira.hunter.monster.compendium.state.MonsterCompendiumAction.GoToCompendiumIndex
import br.alexandregpereira.hunter.monster.compendium.ui.MonsterCompendiumScreen
import org.koin.androidx.compose.koinViewModel

Expand Down
Loading

0 comments on commit ebf4db0

Please sign in to comment.