Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
133bd0f
feat: Add core-datastore module
hmzgtl16 Nov 24, 2025
5824f0a
feat: Integrate Proto DataStore for user preferences
hmzgtl16 Nov 24, 2025
f533fff
feat: Create new common module and move dispatcher logic
hmzgtl16 Nov 24, 2025
7a8e6d1
feat: Implement Proto DataStore for user preferences
hmzgtl16 Nov 24, 2025
b26c79b
feat: Add SettingsRepository
hmzgtl16 Nov 24, 2025
9bfb734
feat: Add SettingsRepository binding to DataModule
hmzgtl16 Nov 24, 2025
53efdcd
feat: Add settings screen and navigation
hmzgtl16 Nov 25, 2025
1a4c85f
test: Add tests for core:datastore module
hmzgtl16 Nov 27, 2025
827e835
test: Update FakeSettingsRepository to emit default data
hmzgtl16 Nov 27, 2025
12b339d
test: Add unit tests for SettingsRepository
hmzgtl16 Nov 27, 2025
be5f393
refactor: Remove dynamic color preference
hmzgtl16 Nov 27, 2025
52610eb
refactor: Rename SettingsRepository to UserDataRepository and remove …
hmzgtl16 Nov 27, 2025
e02cf2d
chore: Add license headers and apply formatting
hmzgtl16 Nov 27, 2025
f31c996
refactor: Simplify Settings screen UI and logic
hmzgtl16 Nov 27, 2025
1c24446
feat: Add dynamic theme based on user preferences
hmzgtl16 Nov 27, 2025
7ca9ac9
test: Update HomeRepositoryImplTest to include onLastPageReached
hmzgtl16 Nov 27, 2025
bcc0520
test: Update HomeRepositoryImplTest to include onLastPageReached
hmzgtl16 Nov 27, 2025
48f02c1
refactor: Rename SettingsRepository to UserDataRepository
hmzgtl16 Nov 27, 2025
df76f43
refactor: Add license headers and apply code formatting
hmzgtl16 Nov 29, 2025
43c6e97
Merge branch 'main' into main
hmzgtl16 Nov 29, 2025
1c1dd94
Merge branch 'main' into main
hmzgtl16 Dec 13, 2025
c6a40db
Merge branch 'main' into main
skydoves Jan 9, 2026
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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ dependencies {
// features
implementation(projects.feature.home)
implementation(projects.feature.details)
implementation(projects.feature.settings)

// cores
implementation(projects.core.data)
implementation(projects.core.model)
implementation(projects.core.designsystem)
implementation(projects.core.navigation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,47 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.skydoves.pokedex.compose.ui.PokedexMain
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

private val viewModel: MainActivityViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
val splashScreen = installSplashScreen()
enableEdgeToEdge()
super.onCreate(savedInstanceState)

var uiState: MainActivityUiState by mutableStateOf(value = MainActivityUiState.Loading)

lifecycleScope.launch {
lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
viewModel.userData
.onEach { uiState = it }
.collect()
}
}

splashScreen.setKeepOnScreenCondition { uiState.shouldKeepSplashScreen() }

setContent {
PokedexMain()
PokedexMain(
darkTheme = (uiState.shouldUseDarkTheme(isSystemDarkTheme = isSystemInDarkTheme())),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Designed and developed by 2024 skydoves (Jaewoong Eum)
*
* 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.skydoves.pokedex.compose

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.skydoves.pokedex.compose.MainActivityUiState.Loading
import com.skydoves.pokedex.compose.core.data.repository.userdata.UserDataRepository
import com.skydoves.pokedex.compose.core.model.UiTheme
import com.skydoves.pokedex.compose.core.model.UserData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@HiltViewModel
class MainActivityViewModel @Inject constructor(
userDataRepository: UserDataRepository,
) : ViewModel() {

val userData = userDataRepository.userData
.map(MainActivityUiState::Success)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading,
)
}

sealed interface MainActivityUiState {
data object Loading : MainActivityUiState
data class Success(val userData: UserData) : MainActivityUiState {
fun shouldUseDarkTheme(isSystemDarkTheme: Boolean): Boolean = when (userData.uiTheme) {
UiTheme.FOLLOW_SYSTEM -> isSystemDarkTheme
UiTheme.DARK -> true
UiTheme.LIGHT -> false
}
}
}

fun MainActivityUiState.shouldKeepSplashScreen() = this is Loading

fun MainActivityUiState.shouldUseDarkTheme(isSystemDarkTheme: Boolean) = when (this) {
Loading -> isSystemDarkTheme
is MainActivityUiState.Success -> shouldUseDarkTheme(isSystemDarkTheme = isSystemDarkTheme)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.scene.DialogSceneStrategy
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import androidx.navigation3.ui.NavDisplay
import com.skydoves.compose.stability.runtime.TraceRecomposition
Expand All @@ -33,12 +34,14 @@ import com.skydoves.pokedex.compose.core.navigation.PokedexNavigatorImpl
import com.skydoves.pokedex.compose.core.navigation.PokedexScreen
import com.skydoves.pokedex.compose.feature.details.PokedexDetails
import com.skydoves.pokedex.compose.feature.home.PokedexHome
import com.skydoves.pokedex.compose.feature.settings.PokedexSettings

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
@TraceRecomposition
fun PokedexNavHost() {
val backStack = rememberNavBackStack(PokedexScreen.Home)
val dialogStrategy = remember { DialogSceneStrategy<NavKey>() }
val navigator = remember(backStack) { PokedexNavigatorImpl(backStack) }

CompositionLocalProvider(
Expand All @@ -48,6 +51,7 @@ fun PokedexNavHost() {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategy = dialogStrategy,
entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator()),
entryProvider = entryProvider<NavKey> {
entry<PokedexScreen.Home> {
Expand All @@ -64,6 +68,12 @@ fun PokedexNavHost() {
pokemon = screen.pokemon,
)
}

entry<PokedexScreen.Settings>(
metadata = DialogSceneStrategy.dialog(),
) {
PokedexSettings()
}
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import com.skydoves.pokedex.compose.navigation.PokedexNavHost

@Composable
@TraceRecomposition
fun PokedexMain() {
PokedexTheme {
fun PokedexMain(darkTheme: Boolean) {
PokedexTheme(darkTheme = darkTheme) {
PokedexNavHost()
}
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlinx.serialization) apply false
alias(libs.plugins.ksp) apply false
Expand Down
1 change: 1 addition & 0 deletions core/common/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
12 changes: 12 additions & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.ksp)
}

dependencies {
implementation(libs.hilt.core)
implementation(libs.kotlinx.coroutines.core)

ksp(libs.hilt.compiler)
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.skydoves.pokedex.compose.core.network
package com.skydoves.pokedex.compose.core.common.network

import javax.inject.Qualifier
import kotlin.annotation.AnnotationRetention.RUNTIME
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.skydoves.pokedex.compose.core.common.network

import javax.inject.Qualifier

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class PokedexAppScope()
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.skydoves.pokedex.compose.core.common.network.di

import com.skydoves.pokedex.compose.core.common.network.Dispatcher
import com.skydoves.pokedex.compose.core.common.network.PokedexAppDispatchers
import com.skydoves.pokedex.compose.core.common.network.PokedexAppScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
internal object CoroutineScopesModule {

@Provides
@Singleton
@PokedexAppScope
fun providesCoroutineScope(
@Dispatcher(PokedexAppDispatchers.IO) dispatcher: CoroutineDispatcher,
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.skydoves.pokedex.compose.core.common.network.di

import com.skydoves.pokedex.compose.core.common.network.Dispatcher
import com.skydoves.pokedex.compose.core.common.network.PokedexAppDispatchers
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

@Module
@InstallIn(SingletonComponent::class)
internal object DispatchersModule {

@Provides
@Dispatcher(PokedexAppDispatchers.IO)
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
}
2 changes: 2 additions & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
api(projects.core.model)
implementation(projects.core.network)
implementation(projects.core.database)
implementation(projects.core.datastore)
testImplementation(projects.core.test)

// kotlinx
Expand All @@ -47,4 +48,5 @@ dependencies {
testImplementation(libs.androidx.test.core)
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.protobuf.kotlin.lite)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import com.skydoves.pokedex.compose.core.data.repository.details.DetailsReposito
import com.skydoves.pokedex.compose.core.data.repository.details.DetailsRepositoryImpl
import com.skydoves.pokedex.compose.core.data.repository.home.HomeRepository
import com.skydoves.pokedex.compose.core.data.repository.home.HomeRepositoryImpl
import com.skydoves.pokedex.compose.core.data.repository.userdata.UserDataRepository
import com.skydoves.pokedex.compose.core.data.repository.userdata.UserDataRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -34,4 +36,7 @@ internal interface DataModule {

@Binds
fun bindsDetailRepository(detailsRepositoryImpl: DetailsRepositoryImpl): DetailsRepository

@Binds
fun bindsUserDataRepository(userDataRepositoryImpl: UserDataRepositoryImpl): UserDataRepository
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ package com.skydoves.pokedex.compose.core.data.repository.details

import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.skydoves.pokedex.compose.core.common.network.Dispatcher
import com.skydoves.pokedex.compose.core.common.network.PokedexAppDispatchers
import com.skydoves.pokedex.compose.core.database.PokemonInfoDao
import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asDomain
import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asEntity
import com.skydoves.pokedex.compose.core.model.PokemonInfo
import com.skydoves.pokedex.compose.core.network.Dispatcher
import com.skydoves.pokedex.compose.core.network.PokedexAppDispatchers
import com.skydoves.pokedex.compose.core.network.model.PokemonErrorResponse
import com.skydoves.pokedex.compose.core.network.model.mapper.ErrorResponseMapper
import com.skydoves.pokedex.compose.core.network.service.PokedexClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ package com.skydoves.pokedex.compose.core.data.repository.home

import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.skydoves.pokedex.compose.core.common.network.Dispatcher
import com.skydoves.pokedex.compose.core.common.network.PokedexAppDispatchers
import com.skydoves.pokedex.compose.core.database.PokemonDao
import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asDomain
import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asEntity
import com.skydoves.pokedex.compose.core.model.Pokemon
import com.skydoves.pokedex.compose.core.network.Dispatcher
import com.skydoves.pokedex.compose.core.network.PokedexAppDispatchers
import com.skydoves.pokedex.compose.core.network.service.PokedexClient
import com.skydoves.sandwich.ApiResponse
import com.skydoves.sandwich.message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,18 @@
* limitations under the License.
*/

package com.skydoves.pokedex.compose.core.network.di
package com.skydoves.pokedex.compose.core.data.repository.userdata

import com.skydoves.pokedex.compose.core.network.Dispatcher
import com.skydoves.pokedex.compose.core.network.PokedexAppDispatchers
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import com.skydoves.pokedex.compose.core.model.UiTheme
import com.skydoves.pokedex.compose.core.model.UserData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

@Module
@InstallIn(SingletonComponent::class)
internal object DispatchersModule {
class FakeUserDataRepository : UserDataRepository {
override val userData: Flow<UserData> = flowOf(
UserData(uiTheme = UiTheme.FOLLOW_SYSTEM),
)

@Provides
@Dispatcher(PokedexAppDispatchers.IO)
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
override suspend fun setUiTheme(uiTheme: UiTheme) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Designed and developed by 2024 skydoves (Jaewoong Eum)
*
* 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.skydoves.pokedex.compose.core.data.repository.userdata

import com.skydoves.pokedex.compose.core.model.UiTheme
import com.skydoves.pokedex.compose.core.model.UserData
import kotlinx.coroutines.flow.Flow

interface UserDataRepository {

val userData: Flow<UserData>

suspend fun setUiTheme(uiTheme: UiTheme)
}
Loading