Skip to content

Commit dc652e9

Browse files
committed
refactor: Modularize project and introduce convention plugins
This commit refactors the project into a multi-module architecture, separating concerns into `core` and `feature` modules. This modularization improves scalability, maintainability, and build times by clearly defining dependencies and responsibilities. Gradle convention plugins have been introduced to standardize module configurations and streamline the build process. ### Core Changes: - **feat(project)!**: Migrated from a monolithic `composeApp` to a multi-module architecture. Source code has been moved into new `core` and `feature` modules (e.g., `core/data`, `feature/settings/presentation`). - **feat(build)**: Introduced Gradle convention plugins (`kmp.library`, `kmp.feature`, `kmp.ui`, `kmp.ui.feature`) to standardize module configurations for Kotlin Multiplatform. - **feat(di)**: Centralized and modularized Koin dependency injection. Each feature now has its own DI module, improving separation of concerns. - **refactor(core)**: Moved shared components, models, and utilities from `composeApp` into the `core` module, splitting them into `core:data`, `core:domain`, and `core:presentation` layers. - **refactor(navigation)**: Migrated from a custom navigation implementation to `androidx.navigation:navigation-compose` for more robust and standardized navigation handling. - **feat(i18n)**: Organized string resources into per-feature directories for better localization management, adding translations for Chinese, Turkish, and Russian. ### Detailed Changes: - **refactor(data)!**: Relocated all data-layer components (DAOs, DTOs, repositories, services) from `composeApp` to their respective `core` or `feature` data modules. - **refactor(domain)!**: Moved domain models and use cases to the `core:domain` and feature-specific domain modules. - **refactor(ui)!**: Migrated presentation-layer components (Composables, ViewModels, UI state) to `core:presentation` or feature-specific presentation modules. This includes themes, components like `RepositoryCard`, and utilities. - **refactor(services)**: Relocated platform-specific implementations like `Downloader`, `Installer`, and `BrowserHelper` to the `core:data` module. - **feat(build)**: Enabled `-Xexpect-actual-classes` and `-Xmulti-dollar-interpolation` compiler arguments. - **deps**: Upgraded `navigation-compose` to `2.9.1`.
1 parent d04cd1f commit dc652e9

315 files changed

Lines changed: 35486 additions & 3582 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/KotlinMultiplatform.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal fun Project.configureKotlinMultiplatform() {
1616
extensions.configure<KotlinMultiplatformExtension> {
1717
compilerOptions {
1818
freeCompilerArgs.add("-Xexpect-actual-classes")
19+
freeCompilerArgs.add("-Xmulti-dollar-interpolation")
1920
freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
2021
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
2122
}

build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/PathUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fun Project.pathToPackageName(): String {
99
.replace("-", "_")
1010
.lowercase()
1111

12-
return "zed.rainxch${relativePackageName }"
12+
return "zed.rainxch${relativePackageName}"
1313
}
1414

1515
fun Project.pathToResourcePrefix(): String {

composeApp/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,14 @@ kotlin {
5656
implementation(projects.feature.starred.data)
5757
implementation(projects.feature.starred.presentation)
5858

59-
6059
implementation(libs.jetbrains.compose.navigation)
6160
implementation(libs.bundles.koin.common)
61+
implementation(libs.liquid)
62+
implementation(libs.jetbrains.compose.material.icons.extended)
6263

6364
implementation(compose.runtime)
6465
implementation(compose.foundation)
65-
implementation(compose.material3)
66+
implementation(libs.jetbrains.compose.material3)
6667
implementation(compose.ui)
6768
implementation(compose.components.resources)
6869
implementation(compose.components.uiToolingPreview)

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,14 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
1010

1111
class MainActivity : ComponentActivity() {
1212
override fun onCreate(savedInstanceState: Bundle?) {
13-
var shouldShowSplashScreen = true
14-
15-
installSplashScreen().setKeepOnScreenCondition {
16-
shouldShowSplashScreen
17-
}
13+
installSplashScreen()
1814

1915
enableEdgeToEdge()
2016

2117
super.onCreate(savedInstanceState)
2218

2319
setContent {
24-
App(
25-
onAuthenticationChecked = {
26-
shouldShowSplashScreen = false
27-
}
28-
)
20+
App()
2921
}
3022
}
3123
}

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt

Lines changed: 0 additions & 71 deletions
This file was deleted.

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/domain/AndroidPlatform.kt

Lines changed: 0 additions & 13 deletions
This file was deleted.

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/utils/isLiquidTopbarEnabled.android.kt

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,27 @@
11
package zed.rainxch.githubstore
22

33
import androidx.compose.foundation.isSystemInDarkTheme
4-
import androidx.compose.foundation.layout.Box
5-
import androidx.compose.foundation.layout.fillMaxSize
6-
import androidx.compose.material3.CircularWavyProgressIndicator
74
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
85
import androidx.compose.runtime.Composable
9-
import androidx.compose.runtime.LaunchedEffect
106
import androidx.compose.runtime.getValue
11-
import androidx.compose.runtime.mutableStateListOf
12-
import androidx.compose.runtime.saveable.rememberSerializable
13-
import androidx.compose.ui.Alignment
14-
import androidx.compose.ui.Modifier
157
import androidx.lifecycle.compose.collectAsStateWithLifecycle
16-
import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer
8+
import androidx.navigation.compose.rememberNavController
179
import org.jetbrains.compose.ui.tooling.preview.Preview
1810
import org.koin.compose.viewmodel.koinViewModel
19-
import zed.rainxch.githubstore.app.state.components.RateLimitDialog
11+
import zed.rainxch.core.presentation.theme.GithubStoreTheme
12+
import zed.rainxch.core.presentation.utils.ApplyAndroidSystemBars
2013
import zed.rainxch.githubstore.app.navigation.AppNavigation
2114
import zed.rainxch.githubstore.app.navigation.GithubStoreGraph
22-
import zed.rainxch.githubstore.core.presentation.theme.GithubStoreTheme
23-
import zed.rainxch.githubstore.core.presentation.utils.ApplyAndroidSystemBars
15+
import zed.rainxch.githubstore.app.state.components.RateLimitDialog
2416

2517
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
2618
@Composable
2719
@Preview
28-
fun App(
29-
onAuthenticationChecked: () -> Unit = { },
30-
) {
20+
fun App() {
3121
val viewModel: MainViewModel = koinViewModel()
3222
val state by viewModel.state.collectAsStateWithLifecycle()
3323

34-
val navBackStack = rememberSerializable(
35-
serializer = SnapshotStateListSerializer<GithubStoreGraph>()
36-
) {
37-
mutableStateListOf(GithubStoreGraph.HomeScreen)
38-
24+
val navBackStack = rememberNavController()
3925

4026
GithubStoreTheme(
4127
fontTheme = state.currentFontTheme,
@@ -45,24 +31,6 @@ fun App(
4531
) {
4632
ApplyAndroidSystemBars(state.isDarkTheme)
4733

48-
LaunchedEffect(state.isCheckingAuth) {
49-
if (!state.isCheckingAuth) {
50-
onAuthenticationChecked()
51-
}
52-
}
53-
54-
if (state.isCheckingAuth) {
55-
Box(
56-
modifier = Modifier.fillMaxSize(),
57-
contentAlignment = Alignment.Center
58-
) {
59-
CircularWavyProgressIndicator()
60-
}
61-
62-
return@GithubStoreTheme
63-
}
64-
65-
6634
if (state.showRateLimitDialog && state.rateLimitInfo != null) {
6735
RateLimitDialog(
6836
rateLimitInfo = state.rateLimitInfo,
@@ -73,14 +41,13 @@ fun App(
7341
onSignIn = {
7442
viewModel.onAction(MainAction.DismissRateLimitDialog)
7543

76-
navBackStack.clear()
77-
navBackStack.add(GithubStoreGraph.AuthenticationScreen)
44+
navBackStack.navigate(GithubStoreGraph.AuthenticationScreen)
7845
}
7946
)
8047
}
8148

8249
AppNavigation(
83-
navBackStack = navBackStack
50+
navController = navBackStack
8451
)
8552
}
8653
}

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package zed.rainxch.githubstore
22

3+
import zed.rainxch.core.domain.model.AppTheme
4+
import zed.rainxch.core.domain.model.FontTheme
35
import zed.rainxch.core.domain.model.RateLimitInfo
4-
import zed.rainxch.githubstore.core.presentation.model.AppTheme
5-
import zed.rainxch.githubstore.core.presentation.model.FontTheme
66

77
data class MainState(
88
val isLoggedIn: Boolean = false,

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt

Lines changed: 9 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,20 @@ import androidx.lifecycle.viewModelScope
55
import kotlinx.coroutines.Dispatchers
66
import kotlinx.coroutines.flow.MutableStateFlow
77
import kotlinx.coroutines.flow.asStateFlow
8-
import kotlinx.coroutines.flow.distinctUntilChanged
9-
import kotlinx.coroutines.flow.drop
10-
import kotlinx.coroutines.flow.first
118
import kotlinx.coroutines.flow.update
129
import kotlinx.coroutines.launch
13-
import zed.rainxch.core.data.data_source.TokenDataSource
14-
import zed.rainxch.core.domain.logging.GitHubStoreLogger
15-
import zed.rainxch.core.domain.model.Platform
1610
import zed.rainxch.core.domain.repository.AuthenticationState
1711
import zed.rainxch.core.domain.repository.InstalledAppsRepository
1812
import zed.rainxch.core.domain.repository.RateLimitRepository
1913
import zed.rainxch.core.domain.repository.ThemesRepository
20-
import zed.rainxch.core.domain.system.PackageMonitor
14+
import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase
2115

2216
class MainViewModel(
2317
private val themesRepository: ThemesRepository,
24-
private val appStateManager: AppStateManager,
25-
private val packageMonitor: PackageMonitor,
2618
private val installedAppsRepository: InstalledAppsRepository,
2719
private val authenticationState: AuthenticationState,
2820
private val rateLimitRepository: RateLimitRepository,
29-
private val platform: Platform,
30-
private val logger: GitHubStoreLogger
21+
private val syncUseCase: SyncInstalledAppsUseCase
3122
) : ViewModel() {
3223

3324
private val _state = MutableStateFlow(MainState())
@@ -39,6 +30,10 @@ class MainViewModel(
3930
.isUserLoggedIn()
4031
.collect { isLoggedIn ->
4132
_state.update { it.copy(isLoggedIn = isLoggedIn) }
33+
34+
if (isLoggedIn) {
35+
rateLimitRepository.clear()
36+
}
4237
}
4338
}
4439

@@ -92,60 +87,16 @@ class MainViewModel(
9287
}
9388

9489
viewModelScope.launch(Dispatchers.IO) {
95-
try {
96-
val installedPackageNames = packageMonitor.getAllInstalledPackageNames()
97-
98-
val appsInDb = installedAppsRepository.getAllInstalledApps().first()
99-
100-
appsInDb.forEach { app ->
101-
if (!installedPackageNames.contains(app.packageName)) {
102-
logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" }
103-
installedAppsRepository.deleteInstalledApp(app.packageName)
104-
} else if (app.installedVersionName == null) { // Migrate only if new fields unset
105-
if (platform.type == Platform.ANDROID) {
106-
val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)
107-
if (systemInfo != null) {
108-
installedAppsRepository.updateApp(app.copy(
109-
installedVersionName = systemInfo.versionName,
110-
installedVersionCode = systemInfo.versionCode,
111-
latestVersionName = systemInfo.versionName,
112-
latestVersionCode = systemInfo.versionCode
113-
))
114-
logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
115-
} else {
116-
installedAppsRepository.updateApp(app.copy(
117-
installedVersionName = app.installedVersion,
118-
installedVersionCode = 0L,
119-
latestVersionName = app.installedVersion,
120-
latestVersionCode = 0L
121-
))
122-
logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" }
123-
}
124-
} else {
125-
installedAppsRepository.updateApp(app.copy(
126-
installedVersionName = app.installedVersion,
127-
installedVersionCode = 0L,
128-
latestVersionName = app.installedVersion,
129-
latestVersionCode = 0L
130-
))
131-
logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" }
132-
}
133-
}
134-
}
135-
136-
logger.d { "Robust system existence sync and data migration completed" }
137-
} catch (e: Exception) {
138-
logger.e { "Failed to sync existence or migrate data: ${e.message}" }
90+
syncUseCase().onSuccess {
91+
installedAppsRepository.checkAllForUpdates()
13992
}
140-
141-
installedAppsRepository.checkAllForUpdates()
14293
}
14394
}
14495

14596
fun onAction(action: MainAction) {
14697
when (action) {
14798
MainAction.DismissRateLimitDialog -> {
148-
appStateManager.dismissRateLimitDialog()
99+
_state.update { it.copy(showRateLimitDialog = false) }
149100
}
150101
}
151102
}

0 commit comments

Comments
 (0)