Skip to content

Commit 6d5e981

Browse files
committed
Initial attempt to use Jetpack Navigation in shared KMP code
1 parent 26ade4c commit 6d5e981

File tree

6 files changed

+242
-6
lines changed

6 files changed

+242
-6
lines changed

composeApp/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ kotlin {
7777
implementation(libs.voyager)
7878

7979
implementation(libs.kmpObservableViewModel)
80+
implementation(libs.androidx.lifecycle.viewmodel.compose)
81+
implementation(libs.androidx.navigation.compose)
8082

8183
implementation(libs.koalaplot)
8284
implementation(libs.treemap.chart)
+220-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,238 @@
1+
import androidx.compose.foundation.layout.Column
2+
import androidx.compose.foundation.layout.Spacer
3+
import androidx.compose.foundation.layout.fillMaxHeight
4+
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.foundation.layout.size
7+
import androidx.compose.foundation.layout.wrapContentSize
8+
import androidx.compose.foundation.rememberScrollState
9+
import androidx.compose.foundation.verticalScroll
10+
import androidx.compose.material.icons.Icons
11+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
12+
import androidx.compose.material3.CenterAlignedTopAppBar
13+
import androidx.compose.material3.CircularProgressIndicator
14+
import androidx.compose.material3.ExperimentalMaterial3Api
15+
import androidx.compose.material3.Icon
16+
import androidx.compose.material3.IconButton
117
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.Scaffold
19+
import androidx.compose.material3.Text
220
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.LaunchedEffect
22+
import androidx.compose.runtime.collectAsState
23+
import androidx.compose.runtime.getValue
24+
import androidx.compose.ui.Alignment
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.graphics.Color
27+
import androidx.compose.ui.text.style.TextAlign
28+
import androidx.compose.ui.unit.dp
29+
import androidx.navigation.compose.NavHost
30+
import androidx.navigation.compose.composable
31+
import androidx.navigation.compose.rememberNavController
32+
import androidx.navigation.toRoute
333
import cafe.adriel.voyager.navigator.Navigator
434
import dev.johnoreilly.climatetrace.di.commonModule
35+
import dev.johnoreilly.climatetrace.remote.Country
536
import dev.johnoreilly.climatetrace.ui.ClimateTraceScreen
37+
import dev.johnoreilly.climatetrace.ui.CountryAssetEmissionsInfoTreeMapChart
38+
import dev.johnoreilly.climatetrace.ui.CountryListView
39+
import dev.johnoreilly.climatetrace.ui.SectorEmissionsPieChart
40+
import dev.johnoreilly.climatetrace.ui.YearSelector
41+
import dev.johnoreilly.climatetrace.ui.toPercent
42+
import dev.johnoreilly.climatetrace.viewmodel.CountryDetailsUIState
43+
import dev.johnoreilly.climatetrace.viewmodel.CountryDetailsViewModel
44+
import dev.johnoreilly.climatetrace.viewmodel.CountryListUIState
45+
import dev.johnoreilly.climatetrace.viewmodel.CountryListViewModel
46+
import kotlinx.serialization.Serializable
647
import org.jetbrains.compose.ui.tooling.preview.Preview
748
import org.koin.compose.KoinApplication
49+
import org.koin.compose.koinInject
850

951

1052
@Preview
1153
@Composable
12-
fun App() {
54+
fun AppVoyagerNav() {
1355
KoinApplication(application = {
1456
modules(commonModule())
1557
}) {
1658
MaterialTheme {
1759
Navigator(screen = ClimateTraceScreen())
1860
}
1961
}
20-
}
62+
}
63+
64+
@Serializable
65+
object CountryList
66+
67+
@Composable
68+
fun AppJetpackBav() {
69+
KoinApplication(application = {
70+
modules(commonModule())
71+
}) {
72+
MaterialTheme {
73+
val navController = rememberNavController()
74+
75+
NavHost(
76+
navController = navController,
77+
startDestination = CountryList,
78+
) {
79+
composable<CountryList> {
80+
CountryListScreenJetpackNav { country ->
81+
navController.navigate(country)
82+
}
83+
}
84+
composable<Country> { backStackEntry ->
85+
val country: Country = backStackEntry.toRoute()
86+
CountryInfoDetailedViewJetpackNav(country, popBack = { navController.popBackStack() })
87+
}
88+
}
89+
}
90+
}
91+
}
92+
93+
94+
@OptIn(ExperimentalMaterial3Api::class)
95+
@Composable
96+
fun CountryListScreenJetpackNav(countrySelected: (country: Country) -> Unit) {
97+
val viewModel = koinInject<CountryListViewModel>()
98+
val viewState by viewModel.viewState.collectAsState()
99+
100+
Scaffold(
101+
topBar = {
102+
CenterAlignedTopAppBar(title = {
103+
Text("ClimateTraceKMP")
104+
}
105+
)
106+
}
107+
) {
108+
Column(Modifier.padding(it)) {
109+
when (val state = viewState) {
110+
is CountryListUIState.Loading -> {
111+
Column(
112+
modifier = Modifier.fillMaxSize().fillMaxHeight()
113+
.wrapContentSize(Alignment.Center)
114+
) {
115+
CircularProgressIndicator()
116+
}
117+
}
118+
119+
is CountryListUIState.Error -> {}
120+
is CountryListUIState.Success -> {
121+
CountryListView(state.countryList, null, countrySelected)
122+
}
123+
}
124+
}
125+
}
126+
}
127+
128+
129+
@Composable
130+
fun CountryInfoDetailedViewJetpackNav(
131+
country: Country,
132+
popBack: () -> Unit
133+
) {
134+
val countryDetailsViewModel: CountryDetailsViewModel = koinInject()
135+
val countryDetailsViewState by countryDetailsViewModel.viewState.collectAsState()
136+
137+
LaunchedEffect(country) {
138+
countryDetailsViewModel.setCountry(country)
139+
}
140+
141+
val viewState = countryDetailsViewState
142+
when (viewState) {
143+
CountryDetailsUIState.NoCountrySelected -> {
144+
Column(
145+
modifier = Modifier.fillMaxSize()
146+
.wrapContentSize(Alignment.Center)
147+
) {
148+
Text(text = "No Country Selected.", style = MaterialTheme.typography.titleMedium)
149+
}
150+
}
151+
is CountryDetailsUIState.Loading -> {
152+
Column(
153+
modifier = Modifier.fillMaxSize()
154+
.wrapContentSize(Alignment.Center)
155+
) {
156+
CircularProgressIndicator()
157+
}
158+
}
159+
is CountryDetailsUIState.Error -> { Text("Error") }
160+
is CountryDetailsUIState.Success -> {
161+
CountryInfoDetailedViewSuccessJetpackNav(viewState, popBack) {
162+
countryDetailsViewModel.setYear(it)
163+
}
164+
}
165+
}
166+
}
167+
168+
169+
@OptIn(ExperimentalMaterial3Api::class)
170+
@Composable
171+
fun CountryInfoDetailedViewSuccessJetpackNav(viewState: CountryDetailsUIState.Success, popBack: () -> Unit, onYearSelected: (String) -> Unit) {
172+
173+
Scaffold(
174+
topBar = {
175+
CenterAlignedTopAppBar(
176+
title = { Text(viewState.country.name) },
177+
navigationIcon = {
178+
IconButton(onClick = { popBack() }) {
179+
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
180+
}
181+
}
182+
)
183+
}
184+
) {
185+
186+
Column(
187+
modifier = Modifier
188+
.verticalScroll(rememberScrollState())
189+
.fillMaxSize()
190+
.padding(16.dp),
191+
horizontalAlignment = Alignment.CenterHorizontally
192+
) {
193+
194+
Text(
195+
text = viewState.country.name,
196+
style = MaterialTheme.typography.titleLarge,
197+
textAlign = TextAlign.Center
198+
)
199+
200+
Spacer(modifier = Modifier.size(16.dp))
201+
202+
val year = viewState.year
203+
val countryAssetEmissionsList = viewState.countryAssetEmissionsList
204+
val countryEmissionInfo = viewState.countryEmissionInfo
205+
206+
YearSelector(year, onYearSelected)
207+
countryEmissionInfo?.let {
208+
val co2 = (countryEmissionInfo.emissions.co2 / 1_000_000).toInt()
209+
val percentage =
210+
(countryEmissionInfo.emissions.co2 / countryEmissionInfo.worldEmissions.co2).toPercent(
211+
2
212+
)
213+
214+
Text(text = "co2 = $co2 Million Tonnes ($year)")
215+
Text(text = "rank = ${countryEmissionInfo.rank} ($percentage)")
216+
217+
Spacer(modifier = Modifier.size(16.dp))
218+
219+
val filteredCountryAssetEmissionsList =
220+
countryAssetEmissionsList.filter { it.sector != null }
221+
if (filteredCountryAssetEmissionsList.isNotEmpty()) {
222+
SectorEmissionsPieChart(countryAssetEmissionsList)
223+
Spacer(modifier = Modifier.size(32.dp))
224+
CountryAssetEmissionsInfoTreeMapChart(countryAssetEmissionsList)
225+
} else {
226+
Spacer(modifier = Modifier.size(16.dp))
227+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
228+
Text(
229+
"Invalid data",
230+
style = MaterialTheme.typography.titleMedium.copy(color = Color.Red),
231+
textAlign = TextAlign.Center
232+
)
233+
}
234+
}
235+
}
236+
}
237+
}
238+
}

composeApp/src/desktopMain/kotlin/main.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import androidx.compose.ui.window.application
66

77
fun main() = application {
88
Window(onCloseRequest = ::exitApplication, title = "ClimateTraceKMP") {
9-
App()
9+
AppJetpackBav()
1010
}
1111
}
1212

1313
@Preview
1414
@Composable
1515
fun AppDesktopPreview() {
16-
App()
16+
AppJetpackBav()
1717
}
1818

composeApp/src/wasmJsMain/kotlin/main.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import androidx.compose.ui.window.CanvasBasedWindow
33

44
@OptIn(ExperimentalComposeUiApi::class)
55
fun main() {
6-
CanvasBasedWindow(canvasElementId = "ComposeTarget") { App() }
6+
CanvasBasedWindow(canvasElementId = "ComposeTarget") { AppJetpackBav() }
77
}

gradle/libs.versions.toml

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ android-minSdk = "24"
1010
android-targetSdk = "34"
1111
androidx-activityCompose = "1.9.0"
1212
compose = "1.6.8"
13-
compose-plugin = "1.6.11"
13+
compose-plugin = "1.7.0-alpha01"
14+
androidx-navigation = "2.8.0-alpha08"
15+
androidx-lifecycle = "2.8.0"
16+
1417
composeWindowSize = "0.5.0"
1518
harawata-appdirs = "1.2.2"
1619
koalaplot = "0.5.3"
@@ -30,6 +33,11 @@ molecule = "2.0.0"
3033
[libraries]
3134
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
3235

36+
androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
37+
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
38+
39+
40+
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
3341
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
3442
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
3543
compose-window-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "composeWindowSize" }

local.properties

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## This file must *NOT* be checked into Version Control Systems,
2+
# as it contains information specific to your local configuration.
3+
#
4+
# Location of the SDK. This is only used by Gradle.
5+
# For customization when using a Version Control System, please read the
6+
# header note.
7+
#Wed Apr 17 18:40:20 CEST 2024
8+
sdk.dir=/Users/johnoreilly/Library/Android/sdk

0 commit comments

Comments
 (0)