Skip to content

Commit ad17379

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

File tree

6 files changed

+240
-6
lines changed

6 files changed

+240
-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)
+218-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,236 @@
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
646
import org.jetbrains.compose.ui.tooling.preview.Preview
747
import org.koin.compose.KoinApplication
48+
import org.koin.compose.koinInject
849

950

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

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)