Skip to content

Commit a9126b7

Browse files
committed
type safe navigation added
1 parent ef661aa commit a9126b7

35 files changed

+110
-117
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ on each platform.
2626
* Declarative UI with Jetpack Compose
2727
* Shared UI components across Android and desktop.
2828
* Material Design and Material Design 3 support.
29-
* Smooth Navigation & State Management:
30-
* Simple navigation using Jetpack Compose Navigation.
29+
* Type-Safe Navigation & State Management:
30+
* Type-Safe navigation using Jetpack Compose Navigation.
3131
* MVVM Architecture:
3232
* Model-View-ViewModel pattern for separation of concerns.
3333
* ViewModel management for UI-related data.
@@ -54,8 +54,8 @@ on each platform.
5454
layer over SQLite to allow for more robust database access while harnessing the full power of SQLite.
5555
- [DataStore](https://developer.android.com/kotlin/multiplatform/datastore) - The DataStore library stores data
5656
asynchronously, consistently, and transactionally, overcoming some of the drawbacks of SharedPreferences
57-
- [Navigation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html) Navigation is a
58-
key part of UI applications that allows users to move between different application screens.
57+
- [Navigation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html) - Navigation is
58+
a key part of UI applications that allows users to move between different application screens.
5959
- [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) - Library support for Kotlin coroutines with
6060
multiplatform support.
6161
- [Common ViewModel](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-viewmodel.html) The Android

composeApp/src/androidMain/kotlin/com/coding/meet/newsapp/MainActivity.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class MainActivity : ComponentActivity() {
1414
override fun onCreate(savedInstanceState: Bundle?) {
1515
installSplashScreen()
1616
super.onCreate(savedInstanceState)
17-
setActivityProvider { this }
17+
setActivityProvider { this }
1818
setContent {
1919
enableEdgeToEdge(
2020
SystemBarStyle.dark(MaterialTheme.colorScheme.onSurface.toArgb()),

composeApp/src/androidMain/kotlin/com/coding/meet/newsapp/data/datastore/DataStorePreferences.android.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import org.koin.mp.KoinPlatform
88

99
actual fun dataStorePreferences(): DataStore<Preferences> {
1010
val appContext = KoinPlatform.getKoin().get<Application>()
11-
return AppSettings.getDataStore(
11+
return AppSettings.getDataStore(
1212
producePath = {
1313
appContext.filesDir
1414
.resolve(dataStoreFileName)

composeApp/src/androidMain/kotlin/com/coding/meet/newsapp/utils/CommonExpectActual.android.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ actual fun shareLink(url: String) {
2121
val shareIntent = Intent.createChooser(sendIntent, "Share Link")
2222
activityProvider.invoke().startActivity(shareIntent)
2323
}
24-
private var activityProvider : () -> Activity = {
24+
25+
private var activityProvider: () -> Activity = {
2526
throw IllegalArgumentException("Error")
2627
}
2728

28-
fun setActivityProvider(provider :() -> Activity){
29+
fun setActivityProvider(provider: () -> Activity) {
2930
activityProvider = provider
3031
}
32+
3133
actual fun randomUUIDStr(): String {
3234
return UUID.randomUUID().toString()
3335
}

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/data/datastore/DataStorePreferences.kt

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ expect fun dataStorePreferences(): DataStore<Preferences>
1313

1414
object AppSettings {
1515
private lateinit var dataStore: DataStore<Preferences>
16+
1617
@OptIn(InternalCoroutinesApi::class)
1718
private val lock = SynchronizedObject()
1819

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/data/model/Article.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ data class Article(
1717
val description: String?,
1818
@SerialName("publishedAt")
1919
@PrimaryKey(autoGenerate = false)
20-
@ColumnInfo(name="articleId")
20+
@ColumnInfo(name = "articleId")
2121
val publishedAt: String,
2222
@SerialName("source")
2323
val source: Source,

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/data/repository/LocalNewsRepository.kt

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class LocalNewsRepository(
1616
suspend fun deleteArticle(article: Article) {
1717
newsDao.delete(article)
1818
}
19+
1920
suspend fun deleteAllBookmark() {
2021
newsDao.deleteAllBookmark()
2122
}

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/data/repository/OnlineNewsRepository.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import com.coding.meet.newsapp.BuildKonfig.API_KEY
44
import io.ktor.client.*
55
import io.ktor.client.request.*
66
import io.ktor.client.statement.*
7+
78
class OnlineNewsRepository(
8-
private val networkModule : HttpClient
9+
private val networkModule: HttpClient
910
) {
1011

1112
suspend fun getNews(category: String): HttpResponse {

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/di/Koin.kt

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.coding.meet.newsapp.di
22

33
import org.koin.core.context.startKoin
44
import org.koin.dsl.KoinAppDeclaration
5+
56
fun initKoin(appDeclaration: KoinAppDeclaration = {}) =
67
startKoin {
78
appDeclaration()

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/di/NetworkModule.kt

-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import io.ktor.client.plugins.contentnegotiation.*
77
import io.ktor.client.plugins.logging.*
88
import io.ktor.http.*
99
import io.ktor.serialization.kotlinx.json.*
10-
import kotlinx.serialization.ExperimentalSerializationApi
1110
import kotlinx.serialization.json.Json
1211
import org.koin.dsl.module
1312

14-
@OptIn(ExperimentalSerializationApi::class)
1513
val networkModule = module {
1614
single {
1715
HttpClient {

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/theme/Theme.kt

+2
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ fun NewsAppTheme(
3939
Theme.LIGHT_MODE.name -> {
4040
LightColorScheme
4141
}
42+
4243
Theme.DARK_MODE.name -> {
4344
DarkColorScheme
4445
}
46+
4547
else -> {
4648
if (darkTheme) {
4749
DarkColorScheme

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/MainScreen.kt

+8-5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import androidx.navigation.compose.rememberNavController
1919
import com.coding.meet.newsapp.ui.navigation.NavigationItem
2020
import com.coding.meet.newsapp.ui.navigation.NavigationSideBar
2121
import com.coding.meet.newsapp.ui.navigation.NewsBottomNavigation
22-
import com.coding.meet.newsapp.ui.navigation.graphs.RootNavGraph
22+
import com.coding.meet.newsapp.ui.navigation.graphs.NavGraph
2323
import com.coding.meet.newsapp.ui.setting.SettingViewModel
2424
import com.coding.meet.newsapp.utils.navigationItemsLists
2525

@@ -41,7 +41,9 @@ fun MainScreen(settingViewModel: SettingViewModel) {
4141
}
4242
val navigationItem by remember {
4343
derivedStateOf {
44-
navigationItemsLists.find { it.route == currentRoute }
44+
navigationItemsLists.find {
45+
it.route::class.qualifiedName == currentRoute
46+
}
4547
}
4648
}
4749
val isMainScreenVisible by remember(isMediumExpandedWWSC) {
@@ -99,7 +101,7 @@ fun MainScaffold(
99101
) {
100102
Row {
101103
AnimatedVisibility(
102-
modifier = Modifier.background( MaterialTheme.colorScheme.surface),
104+
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
103105
visible = isMediumExpandedWWSC && isMainScreenVisible,
104106
enter = slideInHorizontally(
105107
// Slide in from the left
@@ -131,7 +133,8 @@ fun MainScaffold(
131133
targetOffsetY = { fullHeight -> fullHeight }
132134
)
133135
) {
134-
NewsBottomNavigation(items = navigationItemsLists,
136+
NewsBottomNavigation(
137+
items = navigationItemsLists,
135138
currentRoute = currentRoute,
136139
onItemClick = { currentNavigationItem ->
137140
onItemClick(currentNavigationItem)
@@ -140,7 +143,7 @@ fun MainScaffold(
140143
}
141144
}
142145
) { innerPadding ->
143-
RootNavGraph(
146+
NavGraph(
144147
rootNavController = rootNavController,
145148
innerPadding = innerPadding,
146149
settingViewModel = settingViewModel

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/article_detail/ArticleDetailScreen.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import androidx.compose.animation.core.tween
55
import androidx.compose.foundation.Image
66
import androidx.compose.foundation.layout.*
77
import androidx.compose.foundation.lazy.LazyColumn
8+
import androidx.compose.foundation.shape.RoundedCornerShape
89
import androidx.compose.material.icons.Icons
910
import androidx.compose.material.icons.automirrored.filled.ArrowBack
1011
import androidx.compose.material.icons.filled.Share
1112
import androidx.compose.material3.*
1213
import androidx.compose.runtime.*
1314
import androidx.compose.ui.Alignment
1415
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.draw.clip
1517
import androidx.compose.ui.graphics.graphicsLayer
1618
import androidx.compose.ui.graphics.painter.Painter
1719
import androidx.compose.ui.layout.ContentScale
@@ -104,7 +106,7 @@ fun ArticleDetailScreen(
104106
) {
105107
item {
106108
Box(
107-
modifier = Modifier.fillMaxWidth(),
109+
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(10)),
108110
contentAlignment = Alignment.Center
109111
) {
110112
var imageLoadResult by remember {

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/article_detail/ArticleDetailViewModel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kotlinx.coroutines.IO
1212
import kotlinx.coroutines.launch
1313

1414
class ArticleDetailViewModel(
15-
private val localNewsRepository : LocalNewsRepository
15+
private val localNewsRepository: LocalNewsRepository
1616
) : ViewModel() {
1717

1818
var isBookmarked by mutableStateOf(false)

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/bookmark/BookmarkScreen.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import androidx.navigation.NavController
1414
import com.coding.meet.newsapp.ui.common.ArticleListScreen
1515
import com.coding.meet.newsapp.ui.common.EmptyContent
1616
import com.coding.meet.newsapp.ui.common.ShimmerEffect
17-
import com.coding.meet.newsapp.ui.navigation.SettingRouteScreen
17+
import com.coding.meet.newsapp.ui.navigation.Route
1818
import com.coding.meet.newsapp.utils.navigationItemsLists
1919
import news_kmp_app.composeapp.generated.resources.Res
2020
import news_kmp_app.composeapp.generated.resources.ic_browse
@@ -28,12 +28,12 @@ fun BookmarkScreen(
2828
rootNavController: NavController,
2929
paddingValues: PaddingValues
3030
) {
31-
val bookmarkViewModel = koinViewModel<com.coding.meet.newsapp.ui.bookmark.BookmarkViewModel>()
31+
val bookmarkViewModel = koinViewModel<BookmarkViewModel>()
3232

3333
val uiState by bookmarkViewModel.bookmarkNewsStateFlow.collectAsState()
3434
val originDirection = LocalLayoutDirection.current
3535
Column(
36-
modifier = Modifier.fillMaxSize().padding(
36+
modifier = Modifier.fillMaxSize().padding(
3737
start = paddingValues.calculateStartPadding(originDirection),
3838
end = paddingValues.calculateEndPadding(originDirection),
3939
bottom = paddingValues.calculateBottomPadding(),
@@ -48,7 +48,7 @@ fun BookmarkScreen(
4848
)
4949
}, actions = {
5050
IconButton(onClick = {
51-
rootNavController.navigate(SettingRouteScreen.SettingDetail.route)
51+
rootNavController.navigate(Route.SettingDetail)
5252
}) {
5353
Icon(
5454
imageVector = Icons.Filled.Settings,

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/bookmark/BookmarkViewModel.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import kotlinx.coroutines.flow.catch
1313
import kotlinx.coroutines.launch
1414

1515
class BookmarkViewModel(
16-
private val localNewsRepository : LocalNewsRepository
16+
private val localNewsRepository: LocalNewsRepository
1717
) : ViewModel() {
1818

1919
private val _bookmarkNewsStateFlow =
2020
MutableStateFlow<Resource<List<Article>>>(Resource.Loading)
2121
val bookmarkNewsStateFlow: StateFlow<Resource<List<Article>>>
2222
get() = _bookmarkNewsStateFlow
23+
2324
init {
2425
getArticles()
2526
}

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/common/ArticleListScreen.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import androidx.navigation.NavController
1010
import com.coding.meet.newsapp.data.model.Article
1111
import com.coding.meet.newsapp.theme.cardMinSize
1212
import com.coding.meet.newsapp.theme.mediumPadding
13-
import com.coding.meet.newsapp.ui.navigation.NewsRouteScreen
13+
import com.coding.meet.newsapp.ui.navigation.Route
1414
import com.coding.meet.newsapp.utils.randomUUIDStr
1515
import kotlinx.serialization.encodeToString
1616
import kotlinx.serialization.json.Json
@@ -36,7 +36,7 @@ fun ArticleListScreen(
3636
rootNavController.currentBackStackEntry?.savedStateHandle?.apply {
3737
set("article", articleStr)
3838
}
39-
rootNavController.navigate(NewsRouteScreen.NewsDetail.route)
39+
rootNavController.navigate(Route.NewsDetail)
4040
})
4141
}
4242
}

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/common/EmptyContent.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fun EmptyContent(
2626
message: String,
2727
icon: DrawableResource,
2828
isOnRetryBtnVisible: Boolean = true,
29-
onRetryClick: (() -> Unit) = { }
29+
onRetryClick: (() -> Unit) = { }
3030
) {
3131
Column(
3232
modifier = Modifier.fillMaxSize(),

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/headline/HeadlineScreen.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import com.coding.meet.newsapp.theme.xSmallPadding
1818
import com.coding.meet.newsapp.ui.common.ArticleListScreen
1919
import com.coding.meet.newsapp.ui.common.EmptyContent
2020
import com.coding.meet.newsapp.ui.common.ShimmerEffect
21-
import com.coding.meet.newsapp.ui.navigation.SettingRouteScreen
21+
import com.coding.meet.newsapp.ui.navigation.Route
2222
import com.coding.meet.newsapp.utils.categoryList
2323
import com.coding.meet.newsapp.utils.navigationItemsLists
2424
import news_kmp_app.composeapp.generated.resources.Res
@@ -54,7 +54,7 @@ fun HeadlineScreen(
5454
)
5555
}, actions = {
5656
IconButton(onClick = {
57-
rootNavController.navigate(SettingRouteScreen.SettingDetail.route)
57+
rootNavController.navigate(Route.SettingDetail)
5858
}) {
5959
Icon(
6060
imageVector = Icons.Filled.Settings,

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/navigation/BottomNavigationBar.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package com.coding.meet.newsapp.ui.navigation
2+
23
import androidx.compose.foundation.layout.fillMaxWidth
34
import androidx.compose.material3.*
45
import androidx.compose.runtime.Composable
@@ -17,8 +18,10 @@ fun NewsBottomNavigation(
1718
modifier = Modifier.fillMaxWidth(),
1819
) {
1920
items.forEach { navigationItem ->
21+
val isSelected = navigationItem.route::class.qualifiedName == currentRoute
22+
2023
NavigationBarItem(
21-
selected = currentRoute == navigationItem.route,
24+
selected = isSelected,
2225
onClick = { onItemClick(navigationItem) },
2326
icon = {
2427
Icon(
@@ -29,7 +32,7 @@ fun NewsBottomNavigation(
2932
label = {
3033
Text(
3134
text = stringResource(navigationItem.title),
32-
style = if (navigationItem.route == currentRoute) MaterialTheme.typography.labelLarge
35+
style = if (isSelected) MaterialTheme.typography.labelLarge
3336
else MaterialTheme.typography.labelMedium,
3437
maxLines = 1,
3538
overflow = TextOverflow.Ellipsis

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/navigation/NavigationItem.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ import org.jetbrains.compose.resources.StringResource
66
data class NavigationItem(
77
val icon: DrawableResource,
88
val title: StringResource,
9-
val route : String
9+
val route: Route
1010
)

composeApp/src/commonMain/kotlin/com/coding/meet/newsapp/ui/navigation/NavigationSideBar.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ fun NavigationSideBar(
2121
containerColor = MaterialTheme.colorScheme.surface,
2222
) {
2323
items.forEach { navigationItem ->
24+
val isSelected = navigationItem.route::class.qualifiedName == currentRoute
25+
2426
NavigationRailItem(
2527
modifier = Modifier.padding(vertical = smallPadding),
2628
icon = {
@@ -32,13 +34,13 @@ fun NavigationSideBar(
3234
label = {
3335
Text(
3436
text = stringResource(navigationItem.title),
35-
style = if (navigationItem.route == currentRoute) MaterialTheme.typography.labelLarge
37+
style = if (isSelected) MaterialTheme.typography.labelLarge
3638
else MaterialTheme.typography.labelMedium,
3739
maxLines = 1,
3840
overflow = TextOverflow.Ellipsis
3941
)
4042
},
41-
selected = navigationItem.route == currentRoute,
43+
selected = isSelected,
4244
onClick = { onItemClick(navigationItem) }
4345
)
4446
}
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
package com.coding.meet.newsapp.ui.navigation
22

3+
import kotlinx.serialization.Serializable
34

4-
object Graph {
5-
const val MainScreenGraph = "mainScreenGraph"
6-
}
5+
sealed interface Route {
76

8-
sealed class MainRouteScreen(var route: String) {
7+
@Serializable
8+
data object Headline : Route
99

10-
object Headline : MainRouteScreen("headline")
11-
object Search : MainRouteScreen("search")
12-
object Bookmark : MainRouteScreen("bookmark")
13-
}
14-
sealed class NewsRouteScreen(var route: String) {
15-
object NewsDetail : NewsRouteScreen("newsDetail")
16-
}
10+
@Serializable
11+
data object Search : Route
12+
13+
@Serializable
14+
data object Bookmark : Route
15+
16+
@Serializable
17+
data object NewsDetail : Route
18+
19+
@Serializable
20+
data object SettingDetail : Route
1721

18-
sealed class SettingRouteScreen(var route: String) {
19-
object SettingDetail : SettingRouteScreen("settingDetail")
2022
}

0 commit comments

Comments
 (0)