Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ val abysnerBuildNumber: String by project.properties
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.metro)
alias(libs.plugins.screenshot)
alias(libs.plugins.kover)
id("screenshot-reference-cleanup")
Expand Down Expand Up @@ -116,6 +117,7 @@ screenshotTests {

dependencies {
implementation(project(":composeApp"))
implementation(project(":data"))
implementation(libs.androidx.activity.compose)

screenshotTestImplementation(libs.screenshot.validation.api)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Abysner - Dive planner
* Copyright (C) 2024 Neotech
* Copyright (C) 2024-2026 Neotech
*
* Abysner is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
Expand All @@ -13,20 +13,26 @@
package org.neotech.app.abysner

import android.app.Application
import dev.zacsweers.metro.createGraphFactory
import org.neotech.app.abysner.data.PlatformFileDataSourceImpl
import org.neotech.app.abysner.di.AppComponent
import org.neotech.app.abysner.di.PlatformComponentImpl
import org.neotech.app.abysner.di.create

class AbysnerApplication: Application() {

private lateinit var appComponent: AppComponent

override fun onCreate() {
super.onCreate()
val platformComponent = PlatformComponentImpl::class.create(this.applicationContext)
appComponent = AppComponent::class.create(platformComponent)
// Metro graphs cannot have constructor parameters, and platform source sets cannot extend
// the shared graph with additional bindings (only the reverse direction is supported via
// @GraphExtension it seems). So platform dependencies must be constructed manually and
// passed through a @DependencyGraph.Factory. Currently, there is only one
// (PlatformFileDataSource), but if more platform-specific bindings are added this might get
// messy? Each one needs a factory parameter, a @Provides function in AppComponent, and
// manual construction at every call site (Android, iOS, JVM).
// kotlin-inject avoided this through component inheritance.
appComponent = createGraphFactory<AppComponent.Factory>().create(PlatformFileDataSourceImpl(this.applicationContext))
}

fun appComponent(): AppComponent = appComponent
}

20 changes: 2 additions & 18 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import com.google.devtools.ksp.gradle.KspAATask
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.neotech.gradle.capitalizeFirstCharacter
import java.io.ByteArrayOutputStream

// DMG distribution does not support "-beta", MSI requires at least MAJOR.MINOR.BUILD
Expand All @@ -29,7 +25,7 @@ plugins {
alias(libs.plugins.androidKmpLibrary)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.ksp)
alias(libs.plugins.metro)
alias(libs.plugins.kover)
}

Expand Down Expand Up @@ -114,7 +110,6 @@ kotlin {
commonMain.dependencies {
implementation(project(":domain"))
implementation(project(":data"))
implementation(libs.kotlinInject.runtimeKmp)
implementation(libs.navigation.compose)
implementation(libs.jetbrains.lifecycle.viewmodel)
implementation(libs.jetbrains.lifecycle.runtime)
Expand Down Expand Up @@ -162,17 +157,6 @@ compose.desktop {

dependencies {

// This is the same as repeating:
// add(target, libs.kotlinInject.compilerKsp)
// where `target` is "kspDesktop", "kspAndroid", "kspIosX64" "kspIosArm64" or "kspIosSimulatorArm64"
val kotlinTargets: Sequence<KotlinTarget> = kotlin.targets.asSequence()
kotlinTargets.filter {
// Don't add KSP for common target, only final platforms
it.platformType != KotlinPlatformType.common
}.forEach {
add("ksp${it.targetName.capitalizeFirstCharacter()}", libs.kotlinInject.compilerKsp)
}

androidRuntimeClasspath(libs.jetbrains.compose.ui.tooling)
}

Expand Down Expand Up @@ -246,6 +230,6 @@ rootProject.file("iosApp/Configuration/Version.xcconfig").writeText(
""".trimIndent() + "\n"
)

tasks.withType(KspAATask::class.java).configureEach {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
dependsOn(versionInfoProvider)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Abysner - Dive planner
* Copyright (C) 2024 Neotech
* Copyright (C) 2024-2026 Neotech
*
* Abysner is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
Expand All @@ -12,9 +12,9 @@

package org.neotech.app.abysner.di

import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import me.tatarka.inject.annotations.Scope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import org.neotech.app.abysner.data.PersistenceRepositoryImpl
import org.neotech.app.abysner.data.diveplanning.PlanningRepositoryImpl
import org.neotech.app.abysner.data.PlatformFileDataSource
Expand All @@ -24,30 +24,28 @@ import org.neotech.app.abysner.domain.persistence.PersistenceRepository
import org.neotech.app.abysner.domain.settings.SettingsRepository
import org.neotech.app.abysner.presentation.MainNavController

@Scope
annotation class AppScope
abstract class AppScope

@AppScope
@Component
abstract class AppComponent(@Component val platformComponent: PlatformComponent) {
@SingleIn(AppScope::class)
@DependencyGraph
abstract class AppComponent {

abstract val mainNavController: MainNavController

@AppScope
@SingleIn(AppScope::class)
@Provides
fun providesPlanningRepository(planningRepository: PlanningRepositoryImpl): PlanningRepository = planningRepository

@AppScope
@SingleIn(AppScope::class)
@Provides
fun providesSettingsRepository(settingsRepository: SettingsRepositoryImpl): SettingsRepository = settingsRepository

@AppScope
@SingleIn(AppScope::class)
@Provides
fun providesPersistenceRepository(persistenceRepository: PersistenceRepositoryImpl): PersistenceRepository = persistenceRepository

}

abstract class PlatformComponent {

abstract val providesPlatformFileDataSource: PlatformFileDataSource
@DependencyGraph.Factory
fun interface Factory {
fun create(@Provides platformFileDataSource: PlatformFileDataSource): AppComponent
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,26 @@

package org.neotech.app.abysner.presentation

import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import me.tatarka.inject.annotations.Inject
import androidx.compose.ui.tooling.preview.Preview
import org.neotech.app.abysner.di.AppScope
import org.neotech.app.abysner.presentation.screens.about.AboutScreen
import org.neotech.app.abysner.presentation.screens.planner.PlannerScreen
import dev.zacsweers.metro.Inject
import org.neotech.app.abysner.presentation.component.BitmapRenderRoot
import org.neotech.app.abysner.presentation.screens.DiveConfigurationScreen
import org.neotech.app.abysner.presentation.screens.SettingsScreen
import org.neotech.app.abysner.presentation.screens.about.AboutScreen
import org.neotech.app.abysner.presentation.screens.planner.PlannerScreen
import org.neotech.app.abysner.presentation.screens.terms_and_conditions.TermsAndConditionsScreen
import org.neotech.app.abysner.presentation.theme.LocalThemeMode
import org.neotech.app.abysner.presentation.utilities.DestinationDefinition
import org.neotech.app.abysner.presentation.utilities.NavHost
import org.neotech.app.abysner.presentation.utilities.fadeComposable
import org.neotech.app.abysner.presentation.utilities.rootComposable
import org.neotech.app.abysner.presentation.utilities.slideComposable
import androidx.compose.foundation.layout.Box
import org.neotech.app.abysner.presentation.component.BitmapRenderRoot

enum class Destinations(override val destinationName: String) : DestinationDefinition {
PLANNER("planner"),
Expand All @@ -44,25 +42,41 @@ enum class Destinations(override val destinationName: String) : DestinationDefin
TERMS_AND_CONDITIONS_INITIAL("terms-and-conditions-initial")
}

typealias MainNavController = @Composable () -> Unit
// Metro supports @Inject on top-level functions, but the generated types are not resolved by the
// IDE, causing "Unresolved reference" errors. This wrapper class avoids those IDE errors.
// See: https://zacsweers.github.io/metro/latest/installation/#ide-support
@Inject
class MainNavController(
private val viewModelCreator: () -> MainNavControllerViewModel,
private val plannerScreen: PlannerScreen,
private val diveConfigurationScreen: DiveConfigurationScreen,
private val settingsScreen: SettingsScreen,
private val termsAndConditionsScreen: TermsAndConditionsScreen,
private val aboutScreen: AboutScreen,
) {
@Composable
operator fun invoke() {
MainNavController(
viewModel = viewModel { viewModelCreator() },
plannerScreen = plannerScreen,
diveConfigurationScreen = diveConfigurationScreen,
settingsScreen = settingsScreen,
termsAndConditionsScreen = termsAndConditionsScreen,
aboutScreen = aboutScreen,
)
}
}

@Composable
@Preview
@AppScope
@Inject
fun MainNavController(
viewModelCreator: () -> MainNavControllerViewModel,
viewModel: MainNavControllerViewModel,
plannerScreen: PlannerScreen,
diveConfigurationScreen: DiveConfigurationScreen,
settingsScreen: SettingsScreen,
termsAndConditionsScreen: TermsAndConditionsScreen,
aboutScreen: AboutScreen
aboutScreen: AboutScreen,
) {

val viewModel = viewModel {
viewModelCreator()
}

val startDestination = when (viewModel.settings.value.termsAndConditionsAccepted) {
false -> Destinations.TERMS_AND_CONDITIONS_INITIAL
true -> Destinations.PLANNER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import me.tatarka.inject.annotations.Inject
import dev.zacsweers.metro.Inject
import org.neotech.app.abysner.domain.settings.SettingsRepository
import org.neotech.app.abysner.domain.settings.model.SettingsModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import dev.zacsweers.metro.Inject
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject
import androidx.compose.ui.tooling.preview.Preview
import org.neotech.app.abysner.domain.core.model.Configuration
import org.neotech.app.abysner.domain.core.model.Salinity
import org.neotech.app.abysner.domain.core.model.UnitSystem
Expand All @@ -67,16 +66,32 @@ import org.neotech.app.abysner.presentation.utilities.volumePerMinuteUnitLabel
import kotlin.math.abs
import kotlin.math.roundToInt

typealias DiveConfigurationScreen = @Composable (navController: NavHostController) -> Unit

@OptIn(ExperimentalMaterial3Api::class)
// Metro supports @Inject on top-level functions, but the generated types are not resolved by the
// IDE, causing "Unresolved reference" errors. This wrapper class avoids those IDE errors.
// See: https://zacsweers.github.io/metro/latest/installation/#ide-support
@Inject
class DiveConfigurationScreen(
private val planningRepository: PlanningRepository,
private val settingsRepository: SettingsRepository,
) {
@Composable
operator fun invoke(navController: NavHostController) {
DiveConfigurationScreen(
navController = navController,
planningRepository = planningRepository,
settingsRepository = settingsRepository,
)
}
}

@Composable
fun DiveConfigurationScreen(
navController: NavHostController,
planningRepository: PlanningRepository,
settingsRepository: SettingsRepository,
@Assisted navController: NavHostController = rememberNavController()
) {
// TODO should be adding a ViewModel to this screen
val configuration by planningRepository.configuration.collectAsState()
val settings by settingsRepository.settings.collectAsState()
DiveConfigurationScreen(
Expand Down
Loading
Loading