Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Main] 다크 모드 기능을 데이터로 관리한다 #226

Merged
merged 9 commits into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ configureHiltAndroid()

dependencies {
implementation(project(":core:model"))
implementation(project(":core:data"))
implementation(project(":core:designsystem"))
implementation(project(":core:domain"))
implementation(project(":core:navigation"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.droidknights.app2023.configureCoroutineAndroid
import com.droidknights.app2023.configureHiltAndroid
import com.droidknights.app2023.configureKotest
import com.droidknights.app2023.configureKotlinAndroid

Expand All @@ -10,3 +11,4 @@ plugins {
configureKotlinAndroid()
configureKotest()
configureCoroutineAndroid()
configureHiltAndroid()
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ android {

dependencies {
implementation(projects.core.model)
implementation(projects.core.datastore)

implementation(libs.retrofit.core)
implementation(libs.retrofit.kotlin.serialization)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import com.droidknights.app2023.core.data.api.fake.AssetsGithubRawApi
import com.droidknights.app2023.core.data.repository.ContributorRepository
import com.droidknights.app2023.core.data.repository.DefaultContributorRepository
import com.droidknights.app2023.core.data.repository.DefaultSessionRepository
import com.droidknights.app2023.core.data.repository.DefaultSettingsRepository
import com.droidknights.app2023.core.data.repository.DefaultSponsorRepository
import com.droidknights.app2023.core.data.repository.SessionRepository
import com.droidknights.app2023.core.data.repository.SettingsRepository
import com.droidknights.app2023.core.data.repository.SponsorRepository
import dagger.Binds
import dagger.Module
Expand All @@ -26,6 +28,11 @@ internal abstract class DataModule {
repository: DefaultContributorRepository,
): ContributorRepository

@Binds
abstract fun bindsSettingsRepository(
repository: DefaultSettingsRepository,
): SettingsRepository

@InstallIn(SingletonComponent::class)
@Module
internal object FakeModule {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.droidknights.app2023.core.data.repository

import com.droidknights.app2023.core.datastore.SettingsPreferencesDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

internal class DefaultSettingsRepository @Inject constructor(
private val preferencesDataSource: SettingsPreferencesDataSource
) : SettingsRepository {

override fun getIsDarkTheme(): Flow<Boolean> =
preferencesDataSource.settingsData.map { settingsData -> settingsData.isDarkTheme }

override suspend fun updateIsDarkTheme(isDarkTheme: Boolean) {
preferencesDataSource.updateIsDarkTheme(isDarkTheme)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.droidknights.app2023.core.data.repository

import kotlinx.coroutines.flow.Flow

interface SettingsRepository {

fun getIsDarkTheme(): Flow<Boolean>

suspend fun updateIsDarkTheme(isDarkTheme: Boolean)
}
1 change: 1 addition & 0 deletions core/datastore/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
13 changes: 13 additions & 0 deletions core/datastore/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
id("droidknights.android.library")
}

android {
namespace = "com.droidknights.app2023.core.datastore"
}

dependencies {
testImplementation(libs.junit4)
testImplementation(libs.kotlin.test)
implementation(libs.androidx.datastore)
}
2 changes: 2 additions & 0 deletions core/datastore/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.droidknights.app2023.core.datastore

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import com.droidknights.app2023.core.datastore.model.SettingsData
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class SettingsPreferencesDataSource @Inject constructor(
private val dataStore: DataStore<Preferences>
) {
object PreferencesKey {
val IS_DARK_THEME = booleanPreferencesKey("IS_DARK_THEME")
}

val settingsData = dataStore.data.map { preferences ->
SettingsData(
isDarkTheme = preferences[PreferencesKey.IS_DARK_THEME] ?: false
)
}

suspend fun updateIsDarkTheme(isDarkTheme: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKey.IS_DARK_THEME] = isDarkTheme
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.droidknights.app2023.core.datastore.di

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
private const val DATASTORE_NAME = "SETTINGS_PREFERENCES"
private val Context.dataStore by preferencesDataStore(DATASTORE_NAME)

@Provides
@Singleton
fun provideSettingsDataStore(
@ApplicationContext context: Context
): DataStore<Preferences> = context.dataStore
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.droidknights.app2023.core.datastore.model

data class SettingsData(
val isDarkTheme: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.droidknights.app2023.core.datastore

import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import io.kotest.core.spec.style.StringSpec
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import org.junit.rules.TemporaryFolder

internal class SettingsPreferencesDataSourceTest : StringSpec() {
wisemuji marked this conversation as resolved.
Show resolved Hide resolved

private lateinit var testDispatcher: TestDispatcher
private lateinit var tempFolder: TemporaryFolder
private lateinit var dataSource: SettingsPreferencesDataSource

init {
beforeSpec {
testDispatcher = StandardTestDispatcher()
tempFolder = TemporaryFolder.builder().assureDeletion().build()
dataSource = SettingsPreferencesDataSource(
PreferenceDataStoreFactory.create(
scope = CoroutineScope(testDispatcher),
produceFile = { tempFolder.newFile("SETTINGS_PREFERENCES_TEST") }
)
)
}

afterSpec {
tempFolder.delete()
}

"isDarkTheme 초기상태 테스트".config(true) {
CoroutineScope(testDispatcher).launch {
// Given - 초기상태

// When - dataSource 의 초기 SettingsData 값 조회
val settingsData = dataSource.settingsData.first()

// Then - SettingsData.isDarkTheme 값이 false 이어야 한다
assert(settingsData.isDarkTheme == false)
}
}

"isDarkTheme 저장 및 조회 테스트" {
CoroutineScope(testDispatcher).launch {
// Given - isDarkTheme is true
dataSource.updateIsDarkTheme(true)

// When - isDarkTheme 을 true 로 업데이트 후 SettingsData 값 조회
val settingsData = dataSource.settingsData.first()

// Then - SettingsData.isDarkTheme 값이 true 이어야 한다
assert(settingsData.isDarkTheme == true)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
package com.droidknights.app2023.feature.main

import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.droidknights.app2023.core.designsystem.theme.KnightsTheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private var isDarkTheme by mutableStateOf(false)
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

WindowCompat.setDecorFitsSystemWindows(window, false)

setContent {
val isDarkTheme by viewModel.isDarkTheme.collectAsStateWithLifecycle(false, this)
KnightsTheme(darkTheme = isDarkTheme) {
MainScreen()
MainScreen(
onChangeDarkTheme = { isDarkTheme -> viewModel.updateIsDarkTheme(isDarkTheme) }
)
}
}
}

private fun isNightModeEnabled(uiMode: Int): Boolean {
val currentNightMode = uiMode and Configuration.UI_MODE_NIGHT_MASK
return currentNightMode == Configuration.UI_MODE_NIGHT_YES
}

// FIXME : configurationChanges를 사용하지 않고 깜빡이지 않게 테마를 바꾸는 방법 찾기
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
isDarkTheme = isNightModeEnabled(newConfig.uiMode)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import com.droidknights.app2023.feature.session.navigation.sessionNavGraph
import com.droidknights.app2023.feature.setting.navigation.settingNavGraph

@Composable
internal fun MainScreen(navigator: MainNavigator = rememberMainNavigator()) {
internal fun MainScreen(
navigator: MainNavigator = rememberMainNavigator(),
onChangeDarkTheme: (Boolean) -> Unit
) {
Scaffold(
content = { padding ->
Box(
Expand All @@ -60,6 +63,7 @@ internal fun MainScreen(navigator: MainNavigator = rememberMainNavigator()) {
)
settingNavGraph(
padding = padding,
onChangeDarkTheme = onChangeDarkTheme
)

bookmarkNavGraph()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.droidknights.app2023.feature.main

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droidknights.app2023.core.data.repository.SettingsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
) : ViewModel() {
val isDarkTheme = settingsRepository.getIsDarkTheme()

fun updateIsDarkTheme(isDarkTheme: Boolean) = viewModelScope.launch {
settingsRepository.updateIsDarkTheme(isDarkTheme)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.droidknights.app2023.feature.setting

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -33,28 +32,27 @@ import com.droidknights.app2023.core.designsystem.theme.KnightsTheme
import com.droidknights.app2023.core.designsystem.theme.LocalDarkTheme

@Composable
internal fun SettingScreen(padding: PaddingValues) {
internal fun SettingScreen(
padding: PaddingValues,
onChangeDarkTheme: (Boolean) -> Unit
) {
Column(
Modifier
.padding(padding)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
LightDarkThemeCard()
LightDarkThemeCard(
onChangeDarkTheme = onChangeDarkTheme
)
}
}

@Composable
private fun LightDarkThemeCard(darkTheme: Boolean = LocalDarkTheme.current) {
val changeDarkTheme: (Boolean) -> Unit = {
val mode = if (it) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(mode)
}

private fun LightDarkThemeCard(
onChangeDarkTheme: (Boolean) -> Unit,
darkTheme: Boolean = LocalDarkTheme.current
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
KnightsCard {
Column {
Expand All @@ -76,14 +74,14 @@ private fun LightDarkThemeCard(darkTheme: Boolean = LocalDarkTheme.current) {
selected = !darkTheme,
titleRes = R.string.light_mode,
imageRes = R.drawable.img_light_mode,
onClick = { changeDarkTheme(false) },
onClick = { onChangeDarkTheme(false) },
modifier = cardModifier,
)
ThemeCard(
selected = darkTheme,
titleRes = R.string.dark_mode,
imageRes = R.drawable.img_dark_mode,
onClick = { changeDarkTheme(true) },
onClick = { onChangeDarkTheme(true) },
modifier = cardModifier,
)
}
Expand Down Expand Up @@ -136,6 +134,6 @@ private fun ThemeCard(
@Composable
private fun SettingScreenPreview() {
KnightsTheme {
SettingScreen(PaddingValues(0.dp))
SettingScreen(PaddingValues(0.dp)) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ fun NavController.navigateSetting(navOptions: NavOptions) {

fun NavGraphBuilder.settingNavGraph(
padding: PaddingValues,
onChangeDarkTheme: (Boolean) -> Unit
) {
composable(route = SettingRoute.route) {
SettingScreen(padding)
SettingScreen(padding, onChangeDarkTheme)
}
}

Expand Down
Loading