From 7d0dc2680f6734bd475567c9e1e8b55051029fca Mon Sep 17 00:00:00 2001 From: Dmitri Chernysh Date: Sun, 5 Jan 2025 01:12:11 +0200 Subject: [PATCH] Save app settings to Proto DataStore (#30) * Added a new module to sore app settings * Refactoring * Save the dark mode setting * + new module for main activity * Changes dark mode in the main activity based on the settings --- app/build.gradle.kts | 2 + .../com/mobiledevpro/app/Application.kt | 3 +- .../com/mobiledevpro/app/MainActivity.kt | 28 ++++++++- .../kotlin/com/mobiledevpro/app/di/Module.kt | 40 ++++++++++++ .../coroutines/BaseCoroutinesFLowUseCase.kt | 2 +- .../coroutines/BaseCoroutinesUseCase.kt | 2 +- .../mobiledevpro/domain/model/UserProfile.kt | 4 +- .../navigation/screen/UserProfileScreenNav.kt | 3 + .../kotlin/com/mobiledevpro/ui/theme/Theme.kt | 9 +-- .../domain/usecase/GetChatListUseCase.kt | 2 +- feature/main/.gitignore | 1 + feature/main/build.gradle.kts | 7 +++ feature/main/consumer-rules.pro | 0 feature/main/proguard-rules.pro | 21 +++++++ feature/main/src/main/AndroidManifest.xml | 2 + .../main/view/state/MainUIState.kt | 30 +++++++++ .../main/view/vm/MainViewModel.kt | 49 +++++++++++++++ .../domain/usecase/GetPeopleListUseCase.kt | 2 +- .../domain/usecase/GetPeopleProfileUseCase.kt | 2 +- feature/settings_core/.gitignore | 1 + feature/settings_core/build.gradle.kts | 33 ++++++++++ feature/settings_core/consumer-rules.pro | 0 feature/settings_core/proguard-rules.pro | 21 +++++++ .../src/main/AndroidManifest.xml | 2 + .../core/datastore/AppSettingSerializer.kt | 60 ++++++++++++++++++ .../core/datastore/AppSettingsManager.kt | 27 ++++++++ .../core/datastore/ImplAppSettingsManager.kt | 41 ++++++++++++ .../settings/core/model/Settings.kt | 11 ++++ .../core/usecase/GetAppSettingsUseCase.kt | 45 ++++++++++++++ .../core/usecase/UpdateAppSettingsUseCase.kt | 51 +++++++++++++++ .../src/main/proto/app_settings.proto | 8 +++ feature/user_profile/build.gradle.kts | 4 ++ .../mobiledevpro/user/profile/di/Module.kt | 4 ++ .../domain/usecase/GetUserProfileUseCase.kt | 2 +- .../user/profile/view/ProfileScreen.kt | 62 +++++++------------ .../profile/view/state/UserProfileUIState.kt | 4 +- .../user/profile/view/vm/ProfileViewModel.kt | 39 +++++++++++- gradle/libs.versions.toml | 5 ++ settings.gradle.kts | 2 + 39 files changed, 570 insertions(+), 61 deletions(-) create mode 100644 app/src/main/kotlin/com/mobiledevpro/app/di/Module.kt create mode 100644 feature/main/.gitignore create mode 100644 feature/main/build.gradle.kts create mode 100644 feature/main/consumer-rules.pro create mode 100644 feature/main/proguard-rules.pro create mode 100644 feature/main/src/main/AndroidManifest.xml create mode 100644 feature/main/src/main/kotlin/com/mobiledevpro/main/view/state/MainUIState.kt create mode 100644 feature/main/src/main/kotlin/com/mobiledevpro/main/view/vm/MainViewModel.kt create mode 100644 feature/settings_core/.gitignore create mode 100644 feature/settings_core/build.gradle.kts create mode 100644 feature/settings_core/consumer-rules.pro create mode 100644 feature/settings_core/proguard-rules.pro create mode 100644 feature/settings_core/src/main/AndroidManifest.xml create mode 100644 feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingSerializer.kt create mode 100644 feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingsManager.kt create mode 100644 feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/ImplAppSettingsManager.kt create mode 100644 feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/model/Settings.kt create mode 100644 feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/GetAppSettingsUseCase.kt create mode 100644 feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/UpdateAppSettingsUseCase.kt create mode 100644 feature/settings_core/src/main/proto/app_settings.proto diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a0b2d49..499d73b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -135,6 +135,8 @@ dependencies { androidTestImplementation(libs.firebase.crashlytics) { exclude(group = "com.google.firebase", module = "firebase-crashlytics") } + + implementation(projects.feature.main) } diff --git a/app/src/main/kotlin/com/mobiledevpro/app/Application.kt b/app/src/main/kotlin/com/mobiledevpro/app/Application.kt index ff58657..6fb5b5e 100644 --- a/app/src/main/kotlin/com/mobiledevpro/app/Application.kt +++ b/app/src/main/kotlin/com/mobiledevpro/app/Application.kt @@ -20,6 +20,7 @@ package com.mobiledevpro.app import android.app.Application import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.mobiledevpro.app.di.coreModule import com.mobiledevpro.apptemplate.compose.BuildConfig import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -36,7 +37,7 @@ class App : Application() { // Reference Android context androidContext(this@App) // Load common modules (feature modules will be loaded on demand) - // modules(myAppModules) + modules(coreModule) } //Stop sending crashes and analytics in debug mode diff --git a/app/src/main/kotlin/com/mobiledevpro/app/MainActivity.kt b/app/src/main/kotlin/com/mobiledevpro/app/MainActivity.kt index b6b9c4e..880a4fc 100644 --- a/app/src/main/kotlin/com/mobiledevpro/app/MainActivity.kt +++ b/app/src/main/kotlin/com/mobiledevpro/app/MainActivity.kt @@ -4,16 +4,30 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mobiledevpro.app.di.mainModule import com.mobiledevpro.app.ui.MainApp +import com.mobiledevpro.di.koinScope +import com.mobiledevpro.main.view.state.MainUIState +import com.mobiledevpro.main.view.vm.MainViewModel import com.mobiledevpro.ui.theme.AppTheme -import com.mobiledevpro.ui.theme.darkModeState import org.koin.compose.KoinContext +import org.koin.core.context.loadKoinModules +import kotlin.getValue class MainActivity : ComponentActivity() { + private val viewModel: MainViewModel by koinScope().inject() + + init { + loadKoinModules(mainModule) + } + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) @@ -22,9 +36,17 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { - val darkModeState by darkModeState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + var darkMode by remember { mutableStateOf(true) } + + if (uiState is MainUIState.Success) { + (uiState as MainUIState.Success).settings.let { + darkMode = it.darkMode + } + } - AppTheme(darkTheme = darkModeState) { + AppTheme(darkTheme = darkMode) { KoinContext { MainApp() } diff --git a/app/src/main/kotlin/com/mobiledevpro/app/di/Module.kt b/app/src/main/kotlin/com/mobiledevpro/app/di/Module.kt new file mode 100644 index 0000000..e8d945f --- /dev/null +++ b/app/src/main/kotlin/com/mobiledevpro/app/di/Module.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 | Dmitri Chernysh | https://github.com/dmitriy-chernysh + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.app.di + +import com.mobiledevpro.app.MainActivity +import com.mobiledevpro.main.view.vm.MainViewModel +import com.mobiledevpro.settings.core.datastore.AppSettingsManager +import com.mobiledevpro.settings.core.datastore.ImplAppSettingsManager +import com.mobiledevpro.settings.core.usecase.GetAppSettingsUseCase +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.scopedOf +import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val coreModule = module { + singleOf(::ImplAppSettingsManager) { bind() } +} + +val mainModule = module { + scope { + viewModelOf(::MainViewModel) + scopedOf(::GetAppSettingsUseCase) + } +} diff --git a/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesFLowUseCase.kt b/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesFLowUseCase.kt index 51af5ec..10d916f 100644 --- a/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesFLowUseCase.kt +++ b/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesFLowUseCase.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.withContext * Created on Sep 12, 2022. * */ -abstract class BaseCoroutinesFLowUseCase( +abstract class BaseCoroutinesFLowUseCase( executionDispatcher: CoroutineDispatcher ) : BaseUseCase(executionDispatcher) { diff --git a/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesUseCase.kt b/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesUseCase.kt index ac2ad41..74fcc36 100644 --- a/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesUseCase.kt +++ b/core/coroutines/src/main/kotlin/com/mobiledevpro/coroutines/BaseCoroutinesUseCase.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.withContext * Created on Sep 12, 2022. * */ -abstract class BaseCoroutinesUseCase( +abstract class BaseCoroutinesUseCase( executionDispatcher: CoroutineDispatcher ) : BaseUseCase(executionDispatcher) { diff --git a/core/domain/src/main/kotlin/com/mobiledevpro/domain/model/UserProfile.kt b/core/domain/src/main/kotlin/com/mobiledevpro/domain/model/UserProfile.kt index 0116ae4..ecade50 100644 --- a/core/domain/src/main/kotlin/com/mobiledevpro/domain/model/UserProfile.kt +++ b/core/domain/src/main/kotlin/com/mobiledevpro/domain/model/UserProfile.kt @@ -27,8 +27,8 @@ import android.net.Uri */ data class UserProfile( - val name : String, - val nickname: String, + val name: String = "", + val nickname: String = "", val status: Boolean = false, val photo : Uri = Uri.EMPTY ) diff --git a/core/navigation/src/main/kotlin/com/mobiledevpro/navigation/screen/UserProfileScreenNav.kt b/core/navigation/src/main/kotlin/com/mobiledevpro/navigation/screen/UserProfileScreenNav.kt index 576487a..9483cfc 100644 --- a/core/navigation/src/main/kotlin/com/mobiledevpro/navigation/screen/UserProfileScreenNav.kt +++ b/core/navigation/src/main/kotlin/com/mobiledevpro/navigation/screen/UserProfileScreenNav.kt @@ -38,6 +38,9 @@ fun NavGraphBuilder.userProfileScreen(onNavigateTo: (Screen) -> Unit) { ProfileScreen( state = viewModel.uiState, + onDarkModeSwitched = { isDarkMode -> + viewModel.onDarkModeSwitched(isDarkMode) + }, onNavigateToSubscription = { onNavigateTo(Screen.Subscription) } diff --git a/core/ui/src/main/kotlin/com/mobiledevpro/ui/theme/Theme.kt b/core/ui/src/main/kotlin/com/mobiledevpro/ui/theme/Theme.kt index ef5567a..3e69177 100644 --- a/core/ui/src/main/kotlin/com/mobiledevpro/ui/theme/Theme.kt +++ b/core/ui/src/main/kotlin/com/mobiledevpro/ui/theme/Theme.kt @@ -17,9 +17,6 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow private val LightColorScheme = lightColorScheme( primary = md_theme_light_primary, @@ -134,8 +131,4 @@ fun AppTheme( content() } } -} - -//TODO: it's temporary implementation. Dark mode value should be saved into preferences. -val _darkModeState = MutableStateFlow(true) -val darkModeState: StateFlow = _darkModeState.asStateFlow() \ No newline at end of file +} \ No newline at end of file diff --git a/feature/chat_list/src/main/kotlin/com/mobiledevpro/chatlist/domain/usecase/GetChatListUseCase.kt b/feature/chat_list/src/main/kotlin/com/mobiledevpro/chatlist/domain/usecase/GetChatListUseCase.kt index c6b9406..f794532 100644 --- a/feature/chat_list/src/main/kotlin/com/mobiledevpro/chatlist/domain/usecase/GetChatListUseCase.kt +++ b/feature/chat_list/src/main/kotlin/com/mobiledevpro/chatlist/domain/usecase/GetChatListUseCase.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.flowOf class GetChatListUseCase( -) : BaseCoroutinesFLowUseCase, None>(Dispatchers.IO) { +) : BaseCoroutinesFLowUseCase>(Dispatchers.IO) { override suspend fun buildUseCaseFlow(params: None?): Flow> = flowOf(fakeChatList) diff --git a/feature/main/.gitignore b/feature/main/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/main/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts new file mode 100644 index 0000000..4539ac4 --- /dev/null +++ b/feature/main/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("feature-module") +} + +dependencies { + api(projects.feature.settingsCore) +} \ No newline at end of file diff --git a/feature/main/consumer-rules.pro b/feature/main/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/feature/main/proguard-rules.pro b/feature/main/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/feature/main/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml new file mode 100644 index 0000000..568741e --- /dev/null +++ b/feature/main/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/feature/main/src/main/kotlin/com/mobiledevpro/main/view/state/MainUIState.kt b/feature/main/src/main/kotlin/com/mobiledevpro/main/view/state/MainUIState.kt new file mode 100644 index 0000000..c89b7bd --- /dev/null +++ b/feature/main/src/main/kotlin/com/mobiledevpro/main/view/state/MainUIState.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 | Dmitri Chernysh | https://mobile-dev.pro + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.main.view.state + +import com.mobiledevpro.settings.core.model.Settings +import com.mobiledevpro.ui.state.UIState + + +sealed interface MainUIState : UIState { + data object Empty : MainUIState + + data class Success( + val settings: Settings + ) : MainUIState +} \ No newline at end of file diff --git a/feature/main/src/main/kotlin/com/mobiledevpro/main/view/vm/MainViewModel.kt b/feature/main/src/main/kotlin/com/mobiledevpro/main/view/vm/MainViewModel.kt new file mode 100644 index 0000000..46aa72e --- /dev/null +++ b/feature/main/src/main/kotlin/com/mobiledevpro/main/view/vm/MainViewModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 | Dmitri Chernysh | https://mobile-dev.pro + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.main.view.vm + +import androidx.lifecycle.viewModelScope +import com.mobiledevpro.main.view.state.MainUIState +import com.mobiledevpro.settings.core.usecase.GetAppSettingsUseCase +import com.mobiledevpro.ui.vm.BaseViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.onSuccess + + +class MainViewModel( + private val getAppSettingsUseCase: GetAppSettingsUseCase +) : BaseViewModel() { + + override fun initUIState(): MainUIState = MainUIState.Empty + + init { + observeSettings() + } + + private fun observeSettings() { + viewModelScope.launch { + getAppSettingsUseCase.execute() + .collectLatest { result -> + result.onSuccess { settings -> + _uiState.value = MainUIState.Success(settings) + } + } + } + } +} \ No newline at end of file diff --git a/feature/people_list/src/main/kotlin/com/mobiledevpro/peoplelist/domain/usecase/GetPeopleListUseCase.kt b/feature/people_list/src/main/kotlin/com/mobiledevpro/peoplelist/domain/usecase/GetPeopleListUseCase.kt index 9337a25..8c9e92e 100644 --- a/feature/people_list/src/main/kotlin/com/mobiledevpro/peoplelist/domain/usecase/GetPeopleListUseCase.kt +++ b/feature/people_list/src/main/kotlin/com/mobiledevpro/peoplelist/domain/usecase/GetPeopleListUseCase.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.flowOf class GetPeopleListUseCase( -) : BaseCoroutinesFLowUseCase, None>(Dispatchers.IO) { +) : BaseCoroutinesFLowUseCase>(Dispatchers.IO) { override suspend fun buildUseCaseFlow(params: None?): Flow> { Log.d("testing", "buildUseCaseFlow: Thread - ${Thread.currentThread().name}") diff --git a/feature/people_profile/src/main/kotlin/com/mobiledevpro/people/profile/domain/usecase/GetPeopleProfileUseCase.kt b/feature/people_profile/src/main/kotlin/com/mobiledevpro/people/profile/domain/usecase/GetPeopleProfileUseCase.kt index dd3646f..22dbe8b 100644 --- a/feature/people_profile/src/main/kotlin/com/mobiledevpro/people/profile/domain/usecase/GetPeopleProfileUseCase.kt +++ b/feature/people_profile/src/main/kotlin/com/mobiledevpro/people/profile/domain/usecase/GetPeopleProfileUseCase.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.flowOf class GetPeopleProfileUseCase( -) : BaseCoroutinesFLowUseCase(Dispatchers.IO) { +) : BaseCoroutinesFLowUseCase(Dispatchers.IO) { override suspend fun buildUseCaseFlow(params: Int?): Flow = params?.let { peopleProfileId -> diff --git a/feature/settings_core/.gitignore b/feature/settings_core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/settings_core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/settings_core/build.gradle.kts b/feature/settings_core/build.gradle.kts new file mode 100644 index 0000000..4798b89 --- /dev/null +++ b/feature/settings_core/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("feature-module") + alias(libs.plugins.google.protobuf) +} + + +dependencies { + api(libs.data.store) + api(libs.protobuf.kotlin) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}" + } + + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + /* + create("kotlin") { + option("lite") + } + + */ + } + } + } + +} \ No newline at end of file diff --git a/feature/settings_core/consumer-rules.pro b/feature/settings_core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/feature/settings_core/proguard-rules.pro b/feature/settings_core/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/feature/settings_core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/settings_core/src/main/AndroidManifest.xml b/feature/settings_core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..568741e --- /dev/null +++ b/feature/settings_core/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingSerializer.kt b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingSerializer.kt new file mode 100644 index 0000000..6f3529b --- /dev/null +++ b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingSerializer.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2025 | Dmitri Chernysh | https://github.com/dmitriy-chernysh + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.settings.core.datastore + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.core.Serializer +import androidx.datastore.dataStore +import com.google.protobuf.InvalidProtocolBufferException +import com.mobiledevpro.settings.AppSettings +import java.io.InputStream +import java.io.OutputStream + + +/** + * Serializer for Proto DataStore + * + */ +object SettingsSerializer : Serializer { + override val defaultValue: AppSettings = + AppSettings.getDefaultInstance() + .toBuilder() + .setDarkMode(true) + .build() + + override suspend fun readFrom(input: InputStream): AppSettings { + return try { + AppSettings.parseFrom(input) + } catch (e: InvalidProtocolBufferException) { + Log.e("SETTINGS", "Cannot read proto. Create default. ${e.localizedMessage}") + defaultValue + } + } + + override suspend fun writeTo( + t: AppSettings, + output: OutputStream + ) = t.writeTo(output) +} + +val Context.appSettingsDataStore: DataStore by dataStore( + fileName = "app_settings.pb", + serializer = SettingsSerializer +) \ No newline at end of file diff --git a/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingsManager.kt b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingsManager.kt new file mode 100644 index 0000000..c74aa5e --- /dev/null +++ b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/AppSettingsManager.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 | Dmitri Chernysh | https://github.com/dmitriy-chernysh + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.settings.core.datastore + +import com.mobiledevpro.settings.AppSettings +import kotlinx.coroutines.flow.Flow + +interface AppSettingsManager { + fun get(): Flow + + suspend fun setDarkMode(isDarkMode: Boolean) +} \ No newline at end of file diff --git a/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/ImplAppSettingsManager.kt b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/ImplAppSettingsManager.kt new file mode 100644 index 0000000..90c2c71 --- /dev/null +++ b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/datastore/ImplAppSettingsManager.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 | Dmitri Chernysh | https://github.com/dmitriy-chernysh + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.settings.core.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import com.mobiledevpro.settings.AppSettings +import kotlinx.coroutines.flow.Flow + +class ImplAppSettingsManager( + appContext: Context +) : AppSettingsManager { + + private val appSettings: DataStore = appContext.appSettingsDataStore + + override fun get(): Flow = appSettings.data + + override suspend fun setDarkMode(isDarkMode: Boolean) { + appSettings.updateData { + it.toBuilder() + .setDarkMode(isDarkMode) + .build() + } + } + +} \ No newline at end of file diff --git a/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/model/Settings.kt b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/model/Settings.kt new file mode 100644 index 0000000..08bf10c --- /dev/null +++ b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/model/Settings.kt @@ -0,0 +1,11 @@ +package com.mobiledevpro.settings.core.model + +/** + * App Settings + * + * Created on Jan 03, 2025. + * + */ +data class Settings( + val darkMode: Boolean = true +) diff --git a/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/GetAppSettingsUseCase.kt b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/GetAppSettingsUseCase.kt new file mode 100644 index 0000000..1f8c707 --- /dev/null +++ b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/GetAppSettingsUseCase.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025 | Dmitri Chernysh | https://github.com/dmitriy-chernysh + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.settings.core.usecase + +import com.mobiledevpro.coroutines.BaseCoroutinesFLowUseCase +import com.mobiledevpro.coroutines.None +import com.mobiledevpro.settings.core.datastore.AppSettingsManager +import com.mobiledevpro.settings.core.model.Settings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Use case to get the general settings of the app + * + * Created on Jan 03, 2025. + * + */ +class GetAppSettingsUseCase( + private val settingsManager: AppSettingsManager +) : BaseCoroutinesFLowUseCase(Dispatchers.IO) { + + override suspend fun buildUseCaseFlow(params: None?): Flow = + settingsManager.get() + .map { appSettings -> + Settings( + darkMode = appSettings.darkMode + ) + } +} \ No newline at end of file diff --git a/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/UpdateAppSettingsUseCase.kt b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/UpdateAppSettingsUseCase.kt new file mode 100644 index 0000000..4614f10 --- /dev/null +++ b/feature/settings_core/src/main/kotlin/com/mobiledevpro/settings/core/usecase/UpdateAppSettingsUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025 | Dmitri Chernysh | https://github.com/dmitriy-chernysh + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.mobiledevpro.settings.core.usecase + +import com.mobiledevpro.coroutines.BaseCoroutinesUseCase +import com.mobiledevpro.coroutines.None +import com.mobiledevpro.settings.AppSettings +import com.mobiledevpro.settings.core.datastore.AppSettingsManager +import com.mobiledevpro.settings.core.model.Settings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first + +/** + * Update general settings of the app + * + * Created on Jan 03, 2025. + * + */ +class UpdateAppSettingsUseCase( + private val settingsManager: AppSettingsManager +) : BaseCoroutinesUseCase(Dispatchers.IO) { + + override suspend fun buildUseCase(params: Settings?): None = + params?.let { settings -> + settingsManager.get() + .first() + .let { existingSettings: AppSettings -> + + if (existingSettings.darkMode != settings.darkMode) + settingsManager.setDarkMode(settings.darkMode) + + } + None() + } ?: throw RuntimeException("App Settings not found") + +} \ No newline at end of file diff --git a/feature/settings_core/src/main/proto/app_settings.proto b/feature/settings_core/src/main/proto/app_settings.proto new file mode 100644 index 0000000..12274c3 --- /dev/null +++ b/feature/settings_core/src/main/proto/app_settings.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "com.mobiledevpro.settings"; +option java_multiple_files = true; + +message AppSettings { + bool dark_mode = 1; +} \ No newline at end of file diff --git a/feature/user_profile/build.gradle.kts b/feature/user_profile/build.gradle.kts index d5d59fc..0bf8c6a 100644 --- a/feature/user_profile/build.gradle.kts +++ b/feature/user_profile/build.gradle.kts @@ -1,3 +1,7 @@ plugins { id("feature-module") +} + +dependencies { + implementation(projects.feature.settingsCore) } \ No newline at end of file diff --git a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/di/Module.kt b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/di/Module.kt index 217a426..d9238e4 100644 --- a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/di/Module.kt +++ b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/di/Module.kt @@ -17,6 +17,8 @@ */ package com.mobiledevpro.user.profile.di +import com.mobiledevpro.settings.core.usecase.GetAppSettingsUseCase +import com.mobiledevpro.settings.core.usecase.UpdateAppSettingsUseCase import com.mobiledevpro.user.profile.domain.usecase.GetUserProfileUseCase import com.mobiledevpro.user.profile.view.vm.ProfileViewModel import org.koin.core.module.dsl.scopedOf @@ -35,5 +37,7 @@ val featureUserProfileModule = module { scope { viewModelOf(::ProfileViewModel) scopedOf(::GetUserProfileUseCase) + scopedOf(::GetAppSettingsUseCase) + scopedOf(::UpdateAppSettingsUseCase) } } \ No newline at end of file diff --git a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/domain/usecase/GetUserProfileUseCase.kt b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/domain/usecase/GetUserProfileUseCase.kt index 610bc40..e7638ac 100644 --- a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/domain/usecase/GetUserProfileUseCase.kt +++ b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/domain/usecase/GetUserProfileUseCase.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.flowOf class GetUserProfileUseCase( -) : BaseCoroutinesFLowUseCase(Dispatchers.IO) { +) : BaseCoroutinesFLowUseCase(Dispatchers.IO) { override suspend fun buildUseCaseFlow(params: None?): Flow = flowOf(fakeUser) diff --git a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/ProfileScreen.kt b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/ProfileScreen.kt index d0ab13d..b07147c 100644 --- a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/ProfileScreen.kt +++ b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/ProfileScreen.kt @@ -17,9 +17,7 @@ */ package com.mobiledevpro.user.profile.view -import android.net.Uri import android.util.Log -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -39,16 +37,16 @@ import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.boundsInParent import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -65,42 +63,33 @@ import com.mobiledevpro.ui.component.ProfilePictureSize import com.mobiledevpro.ui.component.ScreenBackground import com.mobiledevpro.ui.component.SettingsButton import com.mobiledevpro.ui.theme.AppTheme -import com.mobiledevpro.ui.theme._darkModeState -import com.mobiledevpro.ui.theme.darkModeState import com.mobiledevpro.user.profile.view.state.UserProfileUIState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update import com.mobiledevpro.ui.R as RUi @Composable fun ProfileScreen( state: StateFlow, + onDarkModeSwitched: (Boolean) -> Unit, onNavigateToSubscription: () -> Unit ) { Log.d("navigation", "ProfileScreen:") val uiState by state.collectAsStateWithLifecycle() - val context = LocalContext.current - - val backgroundBoxTopOffset = remember { mutableStateOf(0) } - val darkModeOn = remember { mutableStateOf(darkModeState.value) } - - val user : UserProfile = when(uiState) { - is UserProfileUIState.Success -> (uiState as UserProfileUIState.Success).userProfile - is UserProfileUIState.Fail -> { - LaunchedEffect(Unit) { - Toast.makeText( - context, - (uiState as UserProfileUIState.Fail).throwable.localizedMessage, - Toast.LENGTH_LONG - ).show() - } - UserProfile("", "", false, Uri.EMPTY) + var backgroundBoxTopOffset by remember { mutableIntStateOf(0) } + var darkModeOn by remember { mutableStateOf(true) } + var userProfile by remember { mutableStateOf(UserProfile()) } + + if (uiState is UserProfileUIState.Success) { + (uiState as UserProfileUIState.Success).userProfile?.let { + userProfile = it } - else -> UserProfile("", "", false, Uri.EMPTY) + (uiState as UserProfileUIState.Success).settings?.let { + darkModeOn = it.darkMode + } } ScreenBackground( @@ -113,7 +102,7 @@ fun ProfileScreen( //Background with rounded top-corners Box( modifier = Modifier - .offset { IntOffset(0, backgroundBoxTopOffset.value) } + .offset { IntOffset(0, backgroundBoxTopOffset) } .clip(RoundedCornerShape(topStart = 48.dp, topEnd = 48.dp)) .background(color = MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp)) ) @@ -134,7 +123,7 @@ fun ProfileScreen( ) { ProfilePicture( - photoUri = user.photo, + photoUri = userProfile.photo, onlineStatus = true, size = ProfilePictureSize.LARGE, modifier = Modifier @@ -142,25 +131,21 @@ fun ProfileScreen( .align(Alignment.CenterHorizontally) .onGloballyPositioned { val rect = it.boundsInParent() - backgroundBoxTopOffset.value = + backgroundBoxTopOffset = rect.topCenter.y.toInt() + (rect.bottomCenter.y - rect.topCenter.y).toInt() / 2 } ) ProfileContent( - userName = user.name, - subName = user.nickname, - isOnline = user.status, + userName = userProfile.name, + subName = userProfile.nickname, + isOnline = userProfile.status, alignment = Alignment.CenterHorizontally, modifier = Modifier .padding(8.dp) .align(Alignment.CenterHorizontally) ) - - - - Column( modifier = Modifier .fillMaxHeight() @@ -170,13 +155,11 @@ fun ProfileScreen( LabeledDarkModeSwitch( label = "Dark mode", - checked = darkModeOn.value, + checked = darkModeOn, onCheckedChanged = { isDark -> Log.d("main", "ProfileScreen: dark $isDark") - darkModeOn.value = isDark - _darkModeState.update { - isDark - } + darkModeOn = isDark + onDarkModeSwitched(isDark) }) Divider() @@ -211,6 +194,7 @@ fun ProfileScreenPreview() { AppTheme(darkTheme = true) { ProfileScreen( state = MutableStateFlow(UserProfileUIState.Success(fakeUser)), + onDarkModeSwitched = {}, onNavigateToSubscription = {} ) } diff --git a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/state/UserProfileUIState.kt b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/state/UserProfileUIState.kt index 5c35b69..4a8d3aa 100644 --- a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/state/UserProfileUIState.kt +++ b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/state/UserProfileUIState.kt @@ -18,6 +18,7 @@ package com.mobiledevpro.user.profile.view.state import com.mobiledevpro.domain.model.UserProfile +import com.mobiledevpro.settings.core.model.Settings import com.mobiledevpro.ui.state.UIState /** @@ -30,7 +31,8 @@ sealed interface UserProfileUIState : UIState { data object Empty : UserProfileUIState - data class Success(val userProfile: UserProfile) : UserProfileUIState + data class Success(val userProfile: UserProfile? = null, val settings: Settings? = null) : + UserProfileUIState data class Fail(val throwable: Throwable) : UserProfileUIState } \ No newline at end of file diff --git a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/vm/ProfileViewModel.kt b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/vm/ProfileViewModel.kt index e882d5b..21a0429 100644 --- a/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/vm/ProfileViewModel.kt +++ b/feature/user_profile/src/main/kotlin/com/mobiledevpro/user/profile/view/vm/ProfileViewModel.kt @@ -19,6 +19,9 @@ package com.mobiledevpro.user.profile.view.vm import android.util.Log import androidx.lifecycle.viewModelScope +import com.mobiledevpro.settings.core.model.Settings +import com.mobiledevpro.settings.core.usecase.GetAppSettingsUseCase +import com.mobiledevpro.settings.core.usecase.UpdateAppSettingsUseCase import com.mobiledevpro.ui.vm.BaseViewModel import com.mobiledevpro.user.profile.domain.usecase.GetUserProfileUseCase import com.mobiledevpro.user.profile.view.state.UserProfileUIState @@ -27,7 +30,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class ProfileViewModel( - private val getUserProfileUseCase: GetUserProfileUseCase + private val getUserProfileUseCase: GetUserProfileUseCase, + private val getAppSettingsUseCase: GetAppSettingsUseCase, + private val updateAppSettingsUseCase: UpdateAppSettingsUseCase ) : BaseViewModel() { override fun initUIState(): UserProfileUIState = UserProfileUIState.Empty @@ -35,6 +40,21 @@ class ProfileViewModel( init { Log.d("UI", "ProfileViewModel init") observeUserProfile() + observeAppSettings() + } + + fun onDarkModeSwitched(isDarkMode: Boolean) { + viewModelScope.launch { + val appSettings: Settings? = if (uiState.value is UserProfileUIState.Success) { + (uiState.value as UserProfileUIState.Success).settings + } else { + null + } + + appSettings?.let { setting -> + updateAppSettingsUseCase.execute(setting.copy(darkMode = isDarkMode)) + } + } } private fun observeUserProfile() { @@ -57,4 +77,21 @@ class ProfileViewModel( } } } + + private fun observeAppSettings() { + viewModelScope.launch { + getAppSettingsUseCase.execute() + .collectLatest { result -> + result.onSuccess { settings -> + Log.d("settings", "observeAppSettings: dark mode = ${settings.darkMode}") + _uiState.update { + if (it is UserProfileUIState.Success) + it.copy(settings = settings) + else + UserProfileUIState.Success(settings = settings) + } + } + } + } + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 827324a..a145dc9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,8 @@ kotlin-serialization = "1.4.1" google-services = "4.4.2" plugin-crashlytics = "3.0.2" plugin-performance = "1.4.2" +plugin-protobuf = "0.9.4" + # Libs android-core-ktx = "1.15.0" @@ -28,6 +30,7 @@ coil = "2.7.0" material3 = "1.3.1" koin = "4.0.0" data-store = "1.1.1" +protobuf = "3.25.5" koin-compose = "4.0.0" coroutines-android = "1.9.0" workmanager = "2.10.0" @@ -62,6 +65,7 @@ google-services = { id = "com.google.gms.google-services", version.ref = "google crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "plugin-crashlytics" } performance-monitor = { id = "com.google.firebase.firebase-perf", version.ref = "plugin-performance" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } # Compose Compiler plugin was added in Kotlin 2.0 +google-protobuf = { id = "com.google.protobuf", version.ref = "plugin-protobuf" } [libraries] @@ -77,6 +81,7 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" } gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "android-gradle" } data-store = { module = "androidx.datastore:datastore-preferences", version.ref = "data-store" } +protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines-android" } workmanager = { module = "androidx.work:work-runtime-ktx", version.ref = "workmanager" } desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugaring" } diff --git a/settings.gradle.kts b/settings.gradle.kts index dc11b64..4fe615e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,3 +33,5 @@ include(":feature:people") include(":core:di") include(":core:util") include(":core:analytics") +include(":feature:settings_core") +include(":feature:main")