chore(architecture): introduce MVI architecture

This commit is contained in:
Diego Beraldin 2023-07-22 22:07:55 +02:00
parent 0772d6bf00
commit e56784050a
49 changed files with 563 additions and 121 deletions

View File

@ -0,0 +1,54 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
alias(libs.plugins.compose)
alias(libs.plugins.native.cocoapods)
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
targetHierarchy.default()
android {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
version = "1.0"
ios.deploymentTarget = "14.1"
framework {
baseName = "core-architecture"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
android {
namespace = "com.github.diegoberaldin.raccoonforlemmy.core_architecture"
compileSdk = 33
defaultConfig {
minSdk = 26
}
}

View File

@ -0,0 +1,42 @@
Pod::Spec.new do |spec|
spec.name = 'core-architecture'
spec.version = '1.0'
spec.homepage = 'Link to the Shared Module homepage'
spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
spec.authors = ''
spec.license = ''
spec.summary = 'Some description for the Shared Module'
spec.vendored_frameworks = "build/cocoapods/framework/core-architecture.framework"
spec.libraries = "c++"
spec.module_name = "#{spec.name}_umbrella"
spec.ios.deployment_target = '14.1'
spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':core-architecture',
'PRODUCT_MODULE_NAME' => 'core-architecture',
}
spec.script_phases = [
{
:name => 'Build core-architecture',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$COCOAPODS_SKIP_KOTLIN_BUILD" ]; then
echo "Skipping Gradle build task invocation due to COCOAPODS_SKIP_KOTLIN_BUILD environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration=$CONFIGURATION
SCRIPT
}
]
end

View File

@ -0,0 +1,39 @@
Pod::Spec.new do |spec|
spec.name = 'core_architecture'
spec.version = '1.0'
spec.homepage = 'Link to the Shared Module homepage'
spec.source = { :http=> ''}
spec.authors = ''
spec.license = ''
spec.summary = 'Some description for the Shared Module'
spec.vendored_frameworks = 'build/cocoapods/framework/core-architecture.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '14.1'
spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':core-architecture',
'PRODUCT_MODULE_NAME' => 'core-architecture',
}
spec.script_phases = [
{
:name => 'Build core_architecture',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
SCRIPT
}
]
end

View File

@ -0,0 +1,59 @@
package com.github.diegoberaldin.raccoonforlemmy.core_architecture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
/**
* Basic implementation of the MVI model. This is useful to easily implement the interface by delegation,
* minimizing the amount of code that is needed when integrating the MVI pattern.
* The [updateState] and [emitEffect] methods are shortcuts to easily update the UI state and emit a side effect.
*
* @param Intent class of intents
* @param State class of UI state
* @param Effect class of effects
* @constructor Create [DefaultMviModel]
*
* @param initialState initial UI state
*/
class DefaultMviModel<Intent, State, Effect>(
initialState: State,
) : MviModel<Intent, State, Effect> {
override val uiState = MutableStateFlow(initialState)
override val effects = MutableSharedFlow<Effect>()
lateinit var scope: CoroutineScope
/**
* Emit an effect (event).
*
* @param value Value
*/
suspend fun emitEffect(value: Effect) {
effects.emit(value)
}
/**
* Update the UI state.
*
* @param block Block
*/
inline fun updateState(block: (State) -> State) {
uiState.update { block(uiState.value) }
}
override fun reduce(intent: Intent) {
// Noop
}
override fun onStarted() {
scope = CoroutineScope(SupervisorJob())
}
override fun onDisposed() {
scope.cancel()
}
}

View File

@ -0,0 +1,38 @@
package com.github.diegoberaldin.raccoonforlemmy.core_architecture
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Model contract for Model-View-Intent architecture.
*/
interface MviModel<Intent, State, Effect> {
/**
* Representation of the state holder's state for the view to consume.
*/
val uiState: StateFlow<State>
/**
* One-shot events generated by the state holder.
*/
val effects: SharedFlow<Effect>
/**
* Reduce a view intent updating the [uiState] accordingly.
*
* @param intent View intent to process
*/
fun reduce(intent: Intent)
/**
* To be called whenever the view component becomes visible to start listening events,
* initialize the coroutine scope, etc.
*/
fun onStarted()
/**
* To be called wheneer the view component is not visible any more to cancel ongoing operations.
*/
fun onDisposed()
}

View File

@ -0,0 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.core_architecture
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@Composable
fun MviModel<*, *, *>.bindToLifecycle(key: Any = Unit) {
DisposableEffect(key) {
onStarted()
onDispose(::onDisposed)
}
}

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.tab)
implementation(projects.resources)
implementation(projects.coreArchitecture)
}
}
val commonTest by getting {

View File

@ -0,0 +1,18 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.dsl.module
import org.koin.java.KoinJavaComponent.inject
actual val homeTabModule = module {
factory {
HomeScreenModel(
mvi = DefaultMviModel(HomeScreenMviModel.UiState())
)
}
}
actual fun getHomeScreenModel(): HomeScreenModel {
val res: HomeScreenModel by inject(HomeScreenModel::class.java)
return res
}

View File

@ -1,8 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import org.koin.java.KoinJavaComponent.inject
actual fun getHomeScreenModel(): HomeScreenModel {
val res: HomeScreenModel by inject(HomeScreenModel::class.java)
return res
}

View File

@ -1,8 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
import org.koin.core.module.Module
val homeTabModule = module {
factoryOf(::HomeScreenModel)
}
expect val homeTabModule: Module
expect fun getHomeScreenModel(): HomeScreenModel

View File

@ -1,9 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
class HomeScreenModel : ScreenModel {
}
expect fun getHomeScreenModel(): HomeScreenModel
class HomeScreenModel(
private val mvi: DefaultMviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect>,
) : ScreenModel,
MviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect> by mvi {
}

View File

@ -0,0 +1,15 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
interface HomeScreenMviModel :
MviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect> {
sealed interface Intent
data class UiState(
val loading: Boolean = false,
)
sealed interface Effect
}

View File

@ -6,9 +6,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
@ -16,8 +13,8 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.resources.getLanguageRepository
import dev.icerock.moko.resources.compose.stringResource
object HomeTab : Tab {
@ -40,6 +37,8 @@ object HomeTab : Tab {
@Composable
override fun Content() {
val model = rememberScreenModel { getHomeScreenModel() }
model.bindToLifecycle(key)
Column(modifier = Modifier.padding(4.dp)) {
Text(
text = "Posts content"

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
actual val homeTabModule = module {
factory {
HomeScreenModel(
mvi = DefaultMviModel(HomeScreenMviModel.UiState())
)
}
}
actual fun getHomeScreenModel() = HomeScreenModelHelper.model

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.tab)
implementation(projects.resources)
implementation(projects.coreArchitecture)
}
}
val commonTest by getting {

View File

@ -0,0 +1,18 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.dsl.module
import org.koin.java.KoinJavaComponent.inject
actual val inboxTabModule = module {
factory {
InboxScreenModel(
mvi = DefaultMviModel(InboxScreenMviModel.UiState())
)
}
}
actual fun getInboxScreenModel(): InboxScreenModel {
val res: InboxScreenModel by inject(InboxScreenModel::class.java)
return res
}

View File

@ -1,8 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import org.koin.java.KoinJavaComponent.inject
actual fun getInboxScreenModel(): InboxScreenModel {
val res: InboxScreenModel by inject(InboxScreenModel::class.java)
return res
}

View File

@ -1,8 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
import org.koin.core.module.Module
val inboxTabModule = module {
factoryOf(::InboxScreenModel)
}
expect val inboxTabModule: Module
expect fun getInboxScreenModel(): InboxScreenModel

View File

@ -1,9 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
class InboxScreenModel : ScreenModel {
class InboxScreenModel(
private val mvi: DefaultMviModel<InboxScreenMviModel.Intent, InboxScreenMviModel.UiState, InboxScreenMviModel.Effect>,
) : ScreenModel,
MviModel<InboxScreenMviModel.Intent, InboxScreenMviModel.UiState, InboxScreenMviModel.Effect> by mvi {
}
expect fun getInboxScreenModel(): InboxScreenModel

View File

@ -0,0 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
interface InboxScreenMviModel :
MviModel<InboxScreenMviModel.Intent, InboxScreenMviModel.UiState, InboxScreenMviModel.Effect> {
sealed interface Intent
data class UiState(val loading: Boolean = false)
sealed interface Effect
}

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
@ -36,6 +37,8 @@ object InboxTab : Tab {
@Composable
override fun Content() {
val model = rememberScreenModel { getInboxScreenModel() }
model.bindToLifecycle(key)
Column(modifier = Modifier.padding(4.dp)) {
Text(
text = "Inbox content"

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
actual val inboxTabModule = module {
factory {
InboxScreenModel(
mvi = DefaultMviModel(InboxScreenMviModel.UiState())
)
}
}
actual fun getInboxScreenModel() = InboxScreenModelHelper.model

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.tab)
implementation(projects.resources)
implementation(projects.coreArchitecture)
}
}
val commonTest by getting {

View File

@ -0,0 +1,18 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.dsl.module
import org.koin.java.KoinJavaComponent.inject
actual val profileTabModule = module {
factory {
ProfileScreenModel(
mvi = DefaultMviModel(ProfileScreenMviModel.UiState())
)
}
}
actual fun getProfileScreenModel(): ProfileScreenModel {
val res: ProfileScreenModel by inject(ProfileScreenModel::class.java)
return res
}

View File

@ -1,8 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import org.koin.java.KoinJavaComponent.inject
actual fun getProfileScreenModel(): ProfileScreenModel {
val res: ProfileScreenModel by inject(ProfileScreenModel::class.java)
return res
}

View File

@ -1,8 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
import org.koin.core.module.Module
val profileTabModule = module {
factoryOf(::ProfileScreenModel)
}
expect val profileTabModule: Module
expect fun getProfileScreenModel(): ProfileScreenModel

View File

@ -1,9 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
class ProfileScreenModel : ScreenModel {
class ProfileScreenModel(
private val mvi: DefaultMviModel<ProfileScreenMviModel.Intent, ProfileScreenMviModel.UiState, ProfileScreenMviModel.Effect>,
) : ScreenModel,
MviModel<ProfileScreenMviModel.Intent, ProfileScreenMviModel.UiState, ProfileScreenMviModel.Effect> by mvi {
}
expect fun getProfileScreenModel(): ProfileScreenModel

View File

@ -0,0 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
interface ProfileScreenMviModel :
MviModel<ProfileScreenMviModel.Intent, ProfileScreenMviModel.UiState, ProfileScreenMviModel.Effect> {
sealed interface Intent
data class UiState(val loading: Boolean = false)
sealed interface Effect
}

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
@ -36,6 +37,8 @@ object ProfileTab : Tab {
@Composable
override fun Content() {
val model = rememberScreenModel { getProfileScreenModel() }
model.bindToLifecycle(key)
Column(modifier = Modifier.padding(4.dp)) {
Text(
text = "Profile content"

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
actual val profileTabModule = module {
factory {
ProfileScreenModel(
mvi = DefaultMviModel(ProfileScreenMviModel.UiState())
)
}
}
actual fun getProfileScreenModel() = ProfileScreenModelHelper.model

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.tab)
implementation(projects.resources)
implementation(projects.coreArchitecture)
}
}
val commonTest by getting {

View File

@ -0,0 +1,18 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_search
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.dsl.module
import org.koin.java.KoinJavaComponent.inject
actual val searchTabModule = module {
factory {
SearchScreenModel(
mvi = DefaultMviModel(SearchScreenMviModel.UiState())
)
}
}
actual fun getSearchScreenModel(): SearchScreenModel {
val res: SearchScreenModel by inject(SearchScreenModel::class.java)
return res
}

View File

@ -1,8 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_search
import org.koin.java.KoinJavaComponent.inject
actual fun getSearchScreenModel(): SearchScreenModel {
val res: SearchScreenModel by inject(SearchScreenModel::class.java)
return res
}

View File

@ -1,8 +1,9 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_search
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
import org.koin.core.module.Module
expect val searchTabModule: Module
expect fun getSearchScreenModel(): SearchScreenModel
val searchTabModule = module {
factoryOf(::SearchScreenModel)
}

View File

@ -1,9 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_search
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
class SearchScreenModel : ScreenModel {
class SearchScreenModel(
private val mvi: DefaultMviModel<SearchScreenMviModel.Intent, SearchScreenMviModel.UiState, SearchScreenMviModel.Effect>,
) : ScreenModel,
MviModel<SearchScreenMviModel.Intent, SearchScreenMviModel.UiState, SearchScreenMviModel.Effect> by mvi {
}
expect fun getSearchScreenModel(): SearchScreenModel
}

View File

@ -0,0 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_search
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
interface SearchScreenMviModel :
MviModel<SearchScreenMviModel.Intent, SearchScreenMviModel.UiState, SearchScreenMviModel.Effect> {
sealed interface Intent
data class UiState(val loading: Boolean = false)
sealed interface Effect
}

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
@ -36,6 +37,8 @@ object SearchTab : Tab {
@Composable
override fun Content() {
val model = rememberScreenModel { getSearchScreenModel() }
model.bindToLifecycle(key)
Column(modifier = Modifier.padding(4.dp)) {
Text(
text = "Search content"

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_search
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
actual val searchTabModule = module {
factory {
SearchScreenModel(
mvi = DefaultMviModel(SearchScreenMviModel.UiState())
)
}
}
actual fun getSearchScreenModel() = SearchScreenModelHelper.model

View File

@ -46,7 +46,7 @@ kotlin {
implementation(projects.coreAppearance)
implementation(projects.corePreferences)
implementation(projects.coreArchitecture)
implementation(projects.resources)
}
}

View File

@ -0,0 +1,25 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore
import org.koin.dsl.module
import org.koin.java.KoinJavaComponent.get
import org.koin.java.KoinJavaComponent.inject
actual val settingsTabModule = module {
factory {
SettingsScreenModel(
themeRepository = get(ThemeRepository::class.java),
keyStore = get(TemporaryKeyStore::class.java),
mvi = DefaultMviModel(
SettingsScreenMviModel.UiState()
)
)
}
}
actual fun getSettingsScreenModel(): SettingsScreenModel {
val res: SettingsScreenModel by inject(SettingsScreenModel::class.java)
return res
}

View File

@ -1,8 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import org.koin.java.KoinJavaComponent.inject
actual fun getSettingsScreenModel(): SettingsScreenModel {
val res: SettingsScreenModel by inject(SettingsScreenModel::class.java)
return res
}

View File

@ -1,8 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
import org.koin.core.module.Module
val settingsTabModule = module {
factoryOf(::SettingsScreenModel)
}
expect val settingsTabModule: Module
expect fun getSettingsScreenModel(): SettingsScreenModel

View File

@ -3,13 +3,10 @@ package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.data.ThemeState
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -17,29 +14,31 @@ import kotlinx.coroutines.launch
class SettingsScreenModel(
private val themeRepository: ThemeRepository,
private val keyStore: TemporaryKeyStore,
) : ScreenModel {
private val _uiState = MutableStateFlow(SettingsScreenUiState())
val uiState = _uiState.asStateFlow()
val scope = CoroutineScope(SupervisorJob())
init {
private val mvi: DefaultMviModel<SettingsScreenMviModel.Intent, SettingsScreenMviModel.UiState, SettingsScreenMviModel.Effect>,
) : ScreenModel,
MviModel<SettingsScreenMviModel.Intent, SettingsScreenMviModel.UiState, SettingsScreenMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
themeRepository.state.onEach {
val isDarkTheme = when (themeRepository.state.value) {
ThemeState.Dark -> true
else -> false
}
_uiState.getAndUpdate { it.copy(darkTheme = isDarkTheme) }
}.launchIn(scope)// TODO: is this running forever?
mvi.updateState { it.copy(darkTheme = isDarkTheme) }
}.launchIn(mvi.scope)
}
fun setDarkTheme(value: Boolean) {
override fun reduce(intent: SettingsScreenMviModel.Intent) {
when (intent) {
is SettingsScreenMviModel.Intent.EnableDarkMode -> setDarkTheme(intent.value)
}
}
private fun setDarkTheme(value: Boolean) {
themeRepository.changeTheme(if (value) ThemeState.Dark else ThemeState.Light)
scope.launch {
mvi.scope.launch {
keyStore.save(KeyStoreKeys.EnableDarkTheme, value)
}
}
}
expect fun getSettingsScreenModel(): SettingsScreenModel
}

View File

@ -0,0 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
interface SettingsScreenMviModel :
MviModel<SettingsScreenMviModel.Intent, SettingsScreenMviModel.UiState, SettingsScreenMviModel.Effect> {
sealed interface Intent {
data class EnableDarkMode(val value: Boolean) : Intent
}
data class UiState(
val darkTheme: Boolean = false,
)
sealed interface Effect
}

View File

@ -1,5 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_settings
data class SettingsScreenUiState(
val darkTheme: Boolean = false,
)

View File

@ -19,8 +19,8 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.resources.getLanguageRepository
import dev.icerock.moko.resources.compose.stringResource
object SettingsTab : Tab {
@ -43,6 +43,8 @@ object SettingsTab : Tab {
@Composable
override fun Content() {
val model = rememberScreenModel { getSettingsScreenModel() }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
Column(modifier = Modifier.padding(4.dp)) {
@ -56,7 +58,7 @@ object SettingsTab : Tab {
Checkbox(
checked = uiState.darkTheme,
onCheckedChange = {
model.setDarkTheme(it)
model.reduce(SettingsScreenMviModel.Intent.EnableDarkMode(it))
}
)
}

View File

@ -0,0 +1,24 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
actual val settingsTabModule = module {
factory {
SettingsScreenModel(
themeRepository = get(),
keyStore = get(),
mvi = DefaultMviModel(
SettingsScreenMviModel.UiState()
)
)
}
}
actual fun getSettingsScreenModel() = SettingsScreenModelHelper.model
object SettingsScreenModelHelper : KoinComponent {
val model: SettingsScreenModel by inject()
}

View File

@ -1,10 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
actual fun getSettingsScreenModel() = SettingsScreenModelHelper.model
object SettingsScreenModelHelper : KoinComponent {
val model: SettingsScreenModel by inject()
}

View File

@ -31,3 +31,4 @@ include(":core-utils")
include(":core-appearance")
include(":core-preferences")
include(":resources")
include(":core-architecture")