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(libs.voyager.tab)
implementation(projects.resources) implementation(projects.resources)
implementation(projects.coreArchitecture)
} }
} }
val commonTest by getting { 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 package com.github.diegoberaldin.raccoonforlemmy.feature_home
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.Module
import org.koin.dsl.module
val homeTabModule = module { expect val homeTabModule: Module
factoryOf(::HomeScreenModel)
} expect fun getHomeScreenModel(): HomeScreenModel

View File

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

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

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home 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.KoinComponent
import org.koin.core.component.inject 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 actual fun getHomeScreenModel() = HomeScreenModelHelper.model

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.tab) implementation(libs.voyager.tab)
implementation(projects.resources) implementation(projects.resources)
implementation(projects.coreArchitecture)
} }
} }
val commonTest by getting { 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 package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.Module
import org.koin.dsl.module
val inboxTabModule = module { expect val inboxTabModule: Module
factoryOf(::InboxScreenModel)
} expect fun getInboxScreenModel(): InboxScreenModel

View File

@ -1,9 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox package com.github.diegoberaldin.raccoonforlemmy.feature_inbox
import cafe.adriel.voyager.core.model.ScreenModel 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.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions 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.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@ -36,6 +37,8 @@ object InboxTab : Tab {
@Composable @Composable
override fun Content() { override fun Content() {
val model = rememberScreenModel { getInboxScreenModel() } val model = rememberScreenModel { getInboxScreenModel() }
model.bindToLifecycle(key)
Column(modifier = Modifier.padding(4.dp)) { Column(modifier = Modifier.padding(4.dp)) {
Text( Text(
text = "Inbox content" text = "Inbox content"

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_inbox 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.KoinComponent
import org.koin.core.component.inject 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 actual fun getInboxScreenModel() = InboxScreenModelHelper.model

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.tab) implementation(libs.voyager.tab)
implementation(projects.resources) implementation(projects.resources)
implementation(projects.coreArchitecture)
} }
} }
val commonTest by getting { 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 package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.Module
import org.koin.dsl.module
val profileTabModule = module { expect val profileTabModule: Module
factoryOf(::ProfileScreenModel)
} expect fun getProfileScreenModel(): ProfileScreenModel

View File

@ -1,9 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile package com.github.diegoberaldin.raccoonforlemmy.feature_profile
import cafe.adriel.voyager.core.model.ScreenModel 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.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions 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.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@ -36,6 +37,8 @@ object ProfileTab : Tab {
@Composable @Composable
override fun Content() { override fun Content() {
val model = rememberScreenModel { getProfileScreenModel() } val model = rememberScreenModel { getProfileScreenModel() }
model.bindToLifecycle(key)
Column(modifier = Modifier.padding(4.dp)) { Column(modifier = Modifier.padding(4.dp)) {
Text( Text(
text = "Profile content" text = "Profile content"

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile 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.KoinComponent
import org.koin.core.component.inject 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 actual fun getProfileScreenModel() = ProfileScreenModelHelper.model

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.tab) implementation(libs.voyager.tab)
implementation(projects.resources) implementation(projects.resources)
implementation(projects.coreArchitecture)
} }
} }
val commonTest by getting { 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 package com.github.diegoberaldin.raccoonforlemmy.feature_search
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.Module
import org.koin.dsl.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 package com.github.diegoberaldin.raccoonforlemmy.feature_search
import cafe.adriel.voyager.core.model.ScreenModel 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.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions 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.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@ -36,6 +37,8 @@ object SearchTab : Tab {
@Composable @Composable
override fun Content() { override fun Content() {
val model = rememberScreenModel { getSearchScreenModel() } val model = rememberScreenModel { getSearchScreenModel() }
model.bindToLifecycle(key)
Column(modifier = Modifier.padding(4.dp)) { Column(modifier = Modifier.padding(4.dp)) {
Text( Text(
text = "Search content" text = "Search content"

View File

@ -1,7 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_search 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.KoinComponent
import org.koin.core.component.inject 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 actual fun getSearchScreenModel() = SearchScreenModelHelper.model

View File

@ -46,7 +46,7 @@ kotlin {
implementation(projects.coreAppearance) implementation(projects.coreAppearance)
implementation(projects.corePreferences) implementation(projects.corePreferences)
implementation(projects.coreArchitecture)
implementation(projects.resources) 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 package com.github.diegoberaldin.raccoonforlemmy.feature_settings
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.Module
import org.koin.dsl.module
val settingsTabModule = module { expect val settingsTabModule: Module
factoryOf(::SettingsScreenModel)
} 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 cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.data.ThemeState import com.github.diegoberaldin.raccoonforlemmy.core_appearance.data.ThemeState
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.repository.ThemeRepository 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.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -17,29 +14,31 @@ import kotlinx.coroutines.launch
class SettingsScreenModel( class SettingsScreenModel(
private val themeRepository: ThemeRepository, private val themeRepository: ThemeRepository,
private val keyStore: TemporaryKeyStore, private val keyStore: TemporaryKeyStore,
) : ScreenModel { private val mvi: DefaultMviModel<SettingsScreenMviModel.Intent, SettingsScreenMviModel.UiState, SettingsScreenMviModel.Effect>,
private val _uiState = MutableStateFlow(SettingsScreenUiState()) ) : ScreenModel,
val uiState = _uiState.asStateFlow() MviModel<SettingsScreenMviModel.Intent, SettingsScreenMviModel.UiState, SettingsScreenMviModel.Effect> by mvi {
val scope = CoroutineScope(SupervisorJob())
init {
override fun onStarted() {
mvi.onStarted()
themeRepository.state.onEach { themeRepository.state.onEach {
val isDarkTheme = when (themeRepository.state.value) { val isDarkTheme = when (themeRepository.state.value) {
ThemeState.Dark -> true ThemeState.Dark -> true
else -> false else -> false
} }
_uiState.getAndUpdate { it.copy(darkTheme = isDarkTheme) } mvi.updateState { it.copy(darkTheme = isDarkTheme) }
}.launchIn(scope)// TODO: is this running forever? }.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) themeRepository.changeTheme(if (value) ThemeState.Dark else ThemeState.Light)
scope.launch { mvi.scope.launch {
keyStore.save(KeyStoreKeys.EnableDarkTheme, value) 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.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions 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.MR
import com.github.diegoberaldin.raccoonforlemmy.resources.getLanguageRepository
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
object SettingsTab : Tab { object SettingsTab : Tab {
@ -43,6 +43,8 @@ object SettingsTab : Tab {
@Composable @Composable
override fun Content() { override fun Content() {
val model = rememberScreenModel { getSettingsScreenModel() } val model = rememberScreenModel { getSettingsScreenModel() }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState() val uiState by model.uiState.collectAsState()
Column(modifier = Modifier.padding(4.dp)) { Column(modifier = Modifier.padding(4.dp)) {
@ -56,7 +58,7 @@ object SettingsTab : Tab {
Checkbox( Checkbox(
checked = uiState.darkTheme, checked = uiState.darkTheme,
onCheckedChange = { 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-appearance")
include(":core-preferences") include(":core-preferences")
include(":resources") include(":resources")
include(":core-architecture")