diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index b96e093..a7b0299 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -77,6 +77,8 @@ kotlin { implementation(libs.voyager) implementation(libs.kmpObservableViewModel) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.navigation.compose) implementation(libs.koalaplot) implementation(libs.treemap.chart) diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index 928b9e0..eeeecab 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -1,15 +1,57 @@ +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute import cafe.adriel.voyager.navigator.Navigator import dev.johnoreilly.climatetrace.di.commonModule +import dev.johnoreilly.climatetrace.remote.Country import dev.johnoreilly.climatetrace.ui.ClimateTraceScreen +import dev.johnoreilly.climatetrace.ui.CountryAssetEmissionsInfoTreeMapChart +import dev.johnoreilly.climatetrace.ui.CountryListView +import dev.johnoreilly.climatetrace.ui.SectorEmissionsPieChart +import dev.johnoreilly.climatetrace.ui.YearSelector +import dev.johnoreilly.climatetrace.ui.toPercent +import dev.johnoreilly.climatetrace.viewmodel.CountryDetailsUIState +import dev.johnoreilly.climatetrace.viewmodel.CountryDetailsViewModel +import dev.johnoreilly.climatetrace.viewmodel.CountryListUIState +import dev.johnoreilly.climatetrace.viewmodel.CountryListViewModel +import kotlinx.serialization.Serializable import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.KoinApplication +import org.koin.compose.koinInject @Preview @Composable -fun App() { +fun AppVoyagerNav() { KoinApplication(application = { modules(commonModule()) }) { @@ -17,4 +59,180 @@ fun App() { Navigator(screen = ClimateTraceScreen()) } } -} \ No newline at end of file +} + +@Serializable +object CountryList + +@Composable +fun AppJetpackBav() { + KoinApplication(application = { + modules(commonModule()) + }) { + MaterialTheme { + val navController = rememberNavController() + +NavHost( + navController = navController, + startDestination = CountryList, +) { + composable { + CountryListScreenJetpackNav { country -> + navController.navigate(country) + } + } + composable { backStackEntry -> + val country: Country = backStackEntry.toRoute() + CountryInfoDetailedViewJetpackNav(country, popBack = { navController.popBackStack() }) + } +} + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CountryListScreenJetpackNav(countrySelected: (country: Country) -> Unit) { + val viewModel = koinInject() + val viewState by viewModel.viewState.collectAsState() + + Scaffold( + topBar = { + CenterAlignedTopAppBar(title = { + Text("ClimateTraceKMP") + } + ) + } + ) { + Column(Modifier.padding(it)) { + when (val state = viewState) { + is CountryListUIState.Loading -> { + Column( + modifier = Modifier.fillMaxSize().fillMaxHeight() + .wrapContentSize(Alignment.Center) + ) { + CircularProgressIndicator() + } + } + + is CountryListUIState.Error -> {} + is CountryListUIState.Success -> { + CountryListView(state.countryList, null, countrySelected) + } + } + } + } +} + + +@Composable +fun CountryInfoDetailedViewJetpackNav( + country: Country, + popBack: () -> Unit +) { + val countryDetailsViewModel: CountryDetailsViewModel = koinInject() + val countryDetailsViewState by countryDetailsViewModel.viewState.collectAsState() + + LaunchedEffect(country) { + countryDetailsViewModel.setCountry(country) + } + + val viewState = countryDetailsViewState + when (viewState) { + CountryDetailsUIState.NoCountrySelected -> { + Column( + modifier = Modifier.fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { + Text(text = "No Country Selected.", style = MaterialTheme.typography.titleMedium) + } + } + is CountryDetailsUIState.Loading -> { + Column( + modifier = Modifier.fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { + CircularProgressIndicator() + } + } + is CountryDetailsUIState.Error -> { Text("Error") } + is CountryDetailsUIState.Success -> { + CountryInfoDetailedViewSuccessJetpackNav(viewState, popBack) { + countryDetailsViewModel.setYear(it) + } + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CountryInfoDetailedViewSuccessJetpackNav(viewState: CountryDetailsUIState.Success, popBack: () -> Unit, onYearSelected: (String) -> Unit) { + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { Text(viewState.country.name) }, + navigationIcon = { + IconButton(onClick = { popBack() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + ) + } + ) { + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text( + text = viewState.country.name, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.size(16.dp)) + + val year = viewState.year + val countryAssetEmissionsList = viewState.countryAssetEmissionsList + val countryEmissionInfo = viewState.countryEmissionInfo + + YearSelector(year, onYearSelected) + countryEmissionInfo?.let { + val co2 = (countryEmissionInfo.emissions.co2 / 1_000_000).toInt() + val percentage = + (countryEmissionInfo.emissions.co2 / countryEmissionInfo.worldEmissions.co2).toPercent( + 2 + ) + + Text(text = "co2 = $co2 Million Tonnes ($year)") + Text(text = "rank = ${countryEmissionInfo.rank} ($percentage)") + + Spacer(modifier = Modifier.size(16.dp)) + + val filteredCountryAssetEmissionsList = + countryAssetEmissionsList.filter { it.sector != null } + if (filteredCountryAssetEmissionsList.isNotEmpty()) { + SectorEmissionsPieChart(countryAssetEmissionsList) + Spacer(modifier = Modifier.size(32.dp)) + CountryAssetEmissionsInfoTreeMapChart(countryAssetEmissionsList) + } else { + Spacer(modifier = Modifier.size(16.dp)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + "Invalid data", + style = MaterialTheme.typography.titleMedium.copy(color = Color.Red), + textAlign = TextAlign.Center + ) + } + } + } + } + } +} diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt index 87c37a2..4e135ae 100644 --- a/composeApp/src/desktopMain/kotlin/main.kt +++ b/composeApp/src/desktopMain/kotlin/main.kt @@ -6,13 +6,13 @@ import androidx.compose.ui.window.application fun main() = application { Window(onCloseRequest = ::exitApplication, title = "ClimateTraceKMP") { - App() + AppJetpackBav() } } @Preview @Composable fun AppDesktopPreview() { - App() + AppJetpackBav() } diff --git a/composeApp/src/wasmJsMain/kotlin/main.kt b/composeApp/src/wasmJsMain/kotlin/main.kt index b9c0f64..76b2864 100644 --- a/composeApp/src/wasmJsMain/kotlin/main.kt +++ b/composeApp/src/wasmJsMain/kotlin/main.kt @@ -3,5 +3,5 @@ import androidx.compose.ui.window.CanvasBasedWindow @OptIn(ExperimentalComposeUiApi::class) fun main() { - CanvasBasedWindow(canvasElementId = "ComposeTarget") { App() } + CanvasBasedWindow(canvasElementId = "ComposeTarget") { AppJetpackBav() } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc8a98b..d3d37bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,10 @@ android-minSdk = "24" android-targetSdk = "34" androidx-activityCompose = "1.9.0" compose = "1.6.8" -compose-plugin = "1.6.11" +compose-plugin = "1.7.0-alpha01" +androidx-navigation = "2.8.0-alpha08" +androidx-lifecycle = "2.8.0" + composeWindowSize = "0.5.0" harawata-appdirs = "1.2.2" koalaplot = "0.5.3" @@ -30,6 +33,11 @@ molecule = "2.0.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } + + +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } compose-window-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "composeWindowSize" } diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..b18675c --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Wed Apr 17 18:40:20 CEST 2024 +sdk.dir=/Users/johnoreilly/Library/Android/sdk