From b7300a90bfbef2ba62a5a43a3d9da725b092ce8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Mon, 29 Jul 2024 17:18:18 +0100 Subject: [PATCH] DI + Navigation + ViewModel --- README.md | 83 ++++++------------- composeApp/build.gradle.kts | 8 +- .../org/ooni/probe/AndroidApplication.kt | 24 +++++- .../kotlin/org/ooni/probe/MainActivity.kt | 28 +------ .../commonMain/kotlin/org/ooni/probe/App.kt | 21 +++-- .../kotlin/org/ooni/probe/di/Dependencies.kt | 7 +- .../DashboardScreen.kt} | 30 +++++-- .../DashboardViewModel.kt} | 18 ++-- .../ooni/probe/ui/navigation/Navigation.kt | 34 ++++++++ .../org/ooni/probe/ui/navigation/Screen.kt | 11 +++ .../org/ooni/probe/MainViewController.kt | 31 +------ .../org/ooni/probe/SetupDependencies.kt | 31 +++++++ gradle/libs.versions.toml | 20 +++-- iosApp/iosApp/ContentView.swift | 16 +++- iosApp/iosApp/iOSApp.swift | 7 +- 15 files changed, 219 insertions(+), 150 deletions(-) rename composeApp/src/commonMain/kotlin/org/ooni/probe/ui/{main/MainScreen.kt => dashboard/DashboardScreen.kt} (51%) rename composeApp/src/commonMain/kotlin/org/ooni/probe/ui/{main/MainViewModel.kt => dashboard/DashboardViewModel.kt} (78%) create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt create mode 100644 composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt diff --git a/README.md b/README.md index fde73c04..9462564d 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,42 @@ # OONI Probe Multiplatform -The goal of this repo is to outline a proposed architecture for building a cross platform app that -targets Android, iOS and Desktop (windows and macOS). +Multiplatform (Android and iOS currently) version of the Probe app. -The idea is not to use this project as-is, but rather use it as a reference and playground to -experiment with design pattern related to iteratively refactoring OONI Probe Android, iOS and Desktop -under a unified code base. +## Project structure - -### Project structure - -* `/composeApp` is for code that will be shared across your Compose Multiplatform applications. +* `composeApp` is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders: - `commonMain` is for code that’s common for all targets. - - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. - For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, - `iosMain` would be the right folder for such calls. - -* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, - you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. - -### Architecture overview - -To best understand the architecture it's best you look at the commit history: - -#### kmp boilerplate -commit: https://github.com/ooni/probe-multiplatform/commit/e8f2f6dc4f09f15679e064e5350be257e5de9335 - -nothing really to see here, this is just the output, as-is of -https://kmp.jetbrains.com/: - -### general app architecture + - `androidMain` Android-specific code + - `iosMain` iOS-specific code written in Kotlin -commit: https://github.com/ooni/probe-multiplatform/commit/917e92c4689e6ee664a36a7b9266d56422257e1b +* `iosApp` contains the iOS application configuration and the engine integration written in Swift -this is where all the setup of the architecture of the app is done to create a -structure that should be relatively scalable and modular to support the -specific cross platform constraints we have in our app +* `.github` contains the Continuous Integration configuration for Github -### golang bridging -commit: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105 +* `gradle/libs.versions.toml` specifies the versions of the plugins and dependencies used across + the different modules. -this is where the golang bridging actually happens. +## Architecture overview -The build of the library is actually done inside of gradle steps using a combination of cmake + Makefiles as part of building the app. +Our aim is to take advantage of multiplatform features as much as possible, specially [Compose +Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/). Platform specific modules +will be limited to the minimum required to setup and launch the apps with a compose wrapper, +besides platform-specific code that we can’t avoid, such as the loading our pre-compiled engine. -The relevant bits to do this are here: -* main gradle entry point: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-9ea83bf74425e7270f5dd[%E2%80%A6]feb671f9578fabdec009eb4ba4a -* cmake config references from gradle: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-ee1cbd25a6321e45f790ea552825ea601d5ac9a6233aaba6e3e71143957985cd -* Makefile doing the actual build: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-ac4f5f32e2945fdd5a37c125502d77bb5ccdebc1d9797469916295930d587f01 +### Principals -The JNI is built by hand instead of relying on gomobile. The reason for this is -that gomobile doesn't actually work that well (it doesn't support complex -types), so instead we build a very minimal bridge API surface that can then be -mapped to correct types directly inside of kotlin. +* [Dependency Inversion](https://developer.android.com/topic/modularization/patterns#dependency_inversion) +* [Unidirectional Data Flow](https://developer.android.com/develop/ui/compose/architecture#udf) +* [Model-View-ViewModel](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-viewmodel.html) -* Specifically the mobile API has only 2 functions: apiCall and apiCallWithArgs: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-a18d06043032f63c9ef45e5de6fd5a014d533786993260306511f2fe0135f070R134 -* These two functions are mapped using the JNI and linked into the native GoOONIProbeClient: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-68b2196fd77caf703a289903ce6a1b5d03a167c774d5a6150fb4c9bf734c0228 -* There is then a bridge which actually instantiates the OONIProbeClient and calls the static methods on top of it: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-0160d602dbe85af66fc75f7f96003f6cd0129f965a6a39636117702deebc1ce5 - - The reason to use a bridge instead of calling OONIProbeClient directly is that we need to be able to inject dependencies at runtime to swap out the native implementation for each platform -* What a mobile app developers ends up using, in the end, is the nicer typed - interface which uses the bridge to call the native functions and handles the serialization/deserialization of function call arguments and return values from the native calls: https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-799618a943c0407c70266082ddf3882bd252160bbc83a135d999df074d4109d9 +### Main module structure -What will eventually be calling probe-cli under the hood would live in here: -https://github.com/ooni/probe-multiplatform/commit/38f95f35223808f3d531a4690458776645c02105#diff-a18d06043032f63c9ef45e5de6fd5a014d533786993260306511f2fe0135f070 +* `engine` the Oonimkall engine abstraction in kotlin -What is inside of the BEGIN API section should all be replaced with just an -import from `github.com/ooni/probe-engine` which should export a type API -struct which lives somewhere inside of a `mobileapi` package that implements -the `Call`, `CallWithArgs` and `Init` methods. +* `probe` our Probe app code -The shim code on the other hand can and probably should live directly inside of -the probe codebase so that it's as easy to change as possible and understand -the bridging layer properly. + * `di` dependency injection + * `shared` classes and methods shared across the whole app + * `data` data layer code (database, preferences, network...) + * `ui` UI layer code, organized into features/screens diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 6f5f9559..8c355404 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -41,19 +41,18 @@ kotlin { sourceSets { androidMain.dependencies { - implementation(libs.compose.ui.tooling.preview) - implementation(libs.androidx.activity.compose) + implementation(compose.preview) implementation(libs.android.oonimkall) } commonMain.dependencies { implementation(compose.runtime) implementation(compose.foundation) - implementation(compose.material) implementation(compose.material3) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.kotlin.serialization) + implementation(libs.bundles.ui) implementation(libs.bundles.tooling) } @@ -98,13 +97,14 @@ android { } buildFeatures { buildConfig = true + compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } dependencies { - debugImplementation(libs.compose.ui.tooling) + debugImplementation(compose.uiTooling) } android { lint { diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt index 0802c08b..79a58291 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt @@ -1,5 +1,27 @@ package org.ooni.probe import android.app.Application +import android.os.Build +import org.ooni.engine.AndroidOonimkallBridge +import org.ooni.probe.di.Dependencies +import org.ooni.probe.shared.Platform +import org.ooni.probe.shared.PlatformInfo -class AndroidApplication : Application() +class AndroidApplication : Application() { + val dependencies by lazy { + Dependencies( + platformInfo = platformInfo, + oonimkallBridge = AndroidOonimkallBridge(), + baseFileDir = filesDir.absolutePath, + ) + } + + private val platformInfo by lazy { + object : PlatformInfo { + override val version = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + override val platform = Platform.Android + override val osVersion = Build.VERSION.SDK_INT.toString() + override val model = "${Build.MANUFACTURER} ${Build.MODEL}" + } + } +} diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt index 8dcc5492..8d31aaa9 100644 --- a/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt @@ -1,38 +1,16 @@ package org.ooni.probe -import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import org.ooni.engine.AndroidOonimkallBridge -import org.ooni.probe.di.Dependencies -import org.ooni.probe.shared.Platform -import org.ooni.probe.shared.PlatformInfo class MainActivity : ComponentActivity() { + private val app get() = applicationContext as AndroidApplication + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { - App(setupDependencies()) + App(app.dependencies) } } - - private fun setupDependencies(): Dependencies { - val platformInfo = - object : PlatformInfo { - override val version = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" - override val platform = Platform.Android - override val osVersion = Build.VERSION.SDK_INT.toString() - override val model = "${Build.MANUFACTURER} ${Build.MODEL}" - } - val bridge = AndroidOonimkallBridge() - val dependencies = - Dependencies( - platformInfo = platformInfo, - oonimkallBridge = bridge, - baseFileDir = filesDir.absolutePath, - ) - return dependencies - } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt index 1cf398e3..6f41befd 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt @@ -2,33 +2,40 @@ package org.ooni.probe import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController import co.touchlab.kermit.Logger import org.jetbrains.compose.ui.tooling.preview.Preview import org.ooni.probe.di.Dependencies import org.ooni.probe.ui.Theme -import org.ooni.probe.ui.main.MainScreen +import org.ooni.probe.ui.navigation.Navigation @Composable @Preview fun App(dependencies: Dependencies) { - LaunchedEffect(Unit) { - logAppStart(dependencies) - } + val navController = rememberNavController() Theme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { - MainScreen( - dependencies.mainViewModel, - ) + Scaffold { + Navigation( + navController = navController, + dependencies = dependencies, + ) + } } } + + LaunchedEffect(Unit) { + logAppStart(dependencies) + } } private fun logAppStart(dependencies: Dependencies) { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt index 9ca7635d..23a7ac45 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -4,13 +4,14 @@ import kotlinx.serialization.json.Json import org.ooni.engine.Engine import org.ooni.engine.OonimkallBridge import org.ooni.probe.shared.PlatformInfo -import org.ooni.probe.ui.main.MainViewModel +import org.ooni.probe.ui.dashboard.DashboardViewModel class Dependencies( val platformInfo: PlatformInfo, private val oonimkallBridge: OonimkallBridge, private val baseFileDir: String, ) { + // Data private val json by lazy { Json { encodeDefaults = true @@ -18,7 +19,9 @@ class Dependencies( } } + // Engine private val engine by lazy { Engine(oonimkallBridge, json, baseFileDir) } - val mainViewModel by lazy { MainViewModel(engine) } + // ViewModels + val dashboardViewModel get() = DashboardViewModel(engine) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt similarity index 51% rename from composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainScreen.kt rename to composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt index 548804d7..7334c255 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt @@ -1,23 +1,24 @@ -package org.ooni.probe.ui.main +package org.ooni.probe.ui.dashboard import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.ooni.probe.ui.Theme @Composable -fun MainScreen(viewModel: MainViewModel) { - val state by viewModel.state.collectAsState() - +fun DashboardScreen( + state: DashboardViewModel.State, + onEvent: (DashboardViewModel.Event) -> Unit, +) { Column { Button( - onClick = { viewModel.onEvent(MainViewModel.Event.StartClick) }, + onClick = { onEvent(DashboardViewModel.Event.StartClick) }, enabled = !state.isRunning, ) { Text("Run Test") @@ -32,3 +33,14 @@ fun MainScreen(viewModel: MainViewModel) { ) } } + +@Preview +@Composable +fun DashboardScreenPreview() { + Theme { + DashboardScreen( + state = DashboardViewModel.State(isRunning = false, log = ""), + onEvent = {}, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt similarity index 78% rename from composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainViewModel.kt rename to composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt index c19eeef1..eb744e6a 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardViewModel.kt @@ -1,6 +1,7 @@ -package org.ooni.probe.ui.main +package org.ooni.probe.ui.dashboard -import kotlinx.coroutines.CoroutineScope +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableSharedFlow @@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach @@ -15,9 +17,9 @@ import kotlinx.coroutines.flow.update import org.ooni.engine.Engine import org.ooni.engine.TaskSettings -class MainViewModel( +class DashboardViewModel( private val engine: Engine, -) { +) : ViewModel() { private val events = MutableSharedFlow(extraBufferCapacity = 1) private val _state = MutableStateFlow(State()) @@ -44,7 +46,13 @@ class MainViewModel( } } } - .launchIn(CoroutineScope(Dispatchers.IO)) + /* + This is only needed for this example. The best practice is for the data layer to + switch to a background dispatcher whenever is needed, and the viewModel should run + on the default (Main) dispatcher. + */ + .flowOn(Dispatchers.IO) + .launchIn(viewModelScope) } fun onEvent(event: Event) { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt new file mode 100644 index 00000000..80911a2b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt @@ -0,0 +1,34 @@ +package org.ooni.probe.ui.navigation + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import org.ooni.probe.di.Dependencies +import org.ooni.probe.ui.dashboard.DashboardScreen + +@Composable +fun Navigation( + navController: NavHostController, + dependencies: Dependencies, +) { + NavHost( + navController = navController, + startDestination = Screen.Dashboard.route, + modifier = Modifier.fillMaxSize(), + ) { + composable(route = Screen.Dashboard.route) { + val viewModel = + viewModel { + dependencies.dashboardViewModel + } + val state by viewModel.state.collectAsState() + DashboardScreen(state, viewModel::onEvent) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt new file mode 100644 index 00000000..aa1f9de5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt @@ -0,0 +1,11 @@ +package org.ooni.probe.ui.navigation + +sealed class Screen( + val route: String, +) { + data object Dashboard : Screen("dashboard") + + data object Results : Screen("results") + + data object Settings : Screen("settings") +} diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/MainViewController.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/MainViewController.kt index 8d73fc2b..6517ce51 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/MainViewController.kt @@ -1,36 +1,9 @@ package org.ooni.probe import androidx.compose.ui.window.ComposeUIViewController -import org.ooni.engine.OonimkallBridge import org.ooni.probe.di.Dependencies -import org.ooni.probe.shared.Platform -import org.ooni.probe.shared.PlatformInfo -import platform.Foundation.NSBundle -import platform.Foundation.NSTemporaryDirectory -import platform.UIKit.UIDevice -fun mainViewController(bridge: OonimkallBridge) = +fun mainViewController(dependencies: Dependencies) = ComposeUIViewController { - App(setupDependencies(bridge)) + App(dependencies) } - -fun setupDependencies(bridge: OonimkallBridge): Dependencies { - return Dependencies( - platformInfo = - object : PlatformInfo { - override val version = - (NSBundle.mainBundle.infoDictionary?.get("CFBundleVersion") as? String).orEmpty() - - override val platform = Platform.Ios - - override val osVersion = - with(UIDevice.currentDevice) { - "$systemName $systemVersion" - } - - override val model = UIDevice.currentDevice.model - }, - oonimkallBridge = bridge, - baseFileDir = NSTemporaryDirectory(), - ) -} diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt new file mode 100644 index 00000000..313294fe --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -0,0 +1,31 @@ +package org.ooni.probe + +import org.ooni.engine.OonimkallBridge +import org.ooni.probe.di.Dependencies +import org.ooni.probe.shared.Platform +import org.ooni.probe.shared.PlatformInfo +import platform.Foundation.NSBundle +import platform.Foundation.NSTemporaryDirectory +import platform.UIKit.UIDevice + +fun setupDependencies(bridge: OonimkallBridge) = + Dependencies( + platformInfo = platformInfo, + oonimkallBridge = bridge, + baseFileDir = NSTemporaryDirectory(), + ) + +private val platformInfo get() = + object : PlatformInfo { + override val version = + (NSBundle.mainBundle.infoDictionary?.get("CFBundleVersion") as? String).orEmpty() + + override val platform = Platform.Ios + + override val osVersion = + with(UIDevice.currentDevice) { + "$systemName $systemVersion" + } + + override val model = UIDevice.currentDevice.model + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ccb2eaea..5cfad43e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,8 +5,6 @@ android-compileSdk = "34" android-minSdk = "24" android-targetSdk = "34" -androidx-activityCompose = "1.9.1" -compose = "1.6.8" compose-plugin = "1.6.11" kotlin = "2.0.0" @@ -21,14 +19,24 @@ cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotli ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.1.1" } [libraries] -androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -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" } +# UI +lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version = "2.8.0" } +navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.7.0-alpha07" } + +# Engine android-oonimkall = { module = "org.ooni:oonimkall", version = "2024.05.22-092559" } + +# Serialization kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1" } + +# Logging kermit = { module = "co.touchlab:kermit", version = "2.0.4" } [bundles] +ui = [ + "lifecycle-viewmodel-compose", + "navigation", +] tooling = [ - "kermit" + "kermit", ] diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index daa9a9a7..6fb7d16a 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -3,16 +3,28 @@ import SwiftUI import composeApp struct ComposeView: UIViewControllerRepresentable { + let dependencies: Dependencies + + init(dependencies: Dependencies) { + self.dependencies = dependencies + } + func makeUIViewController(context: Context) -> UIViewController { - MainViewControllerKt.mainViewController(bridge: IosOonimkallBridge()) + MainViewControllerKt.mainViewController(dependencies: dependencies) } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } struct ContentView: View { + let dependencies: Dependencies + + init(dependencies: Dependencies) { + self.dependencies = dependencies + } + var body: some View { - ComposeView() + ComposeView(dependencies: dependencies) .ignoresSafeArea(.keyboard) // Compose has own keyboard handler } } diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 0648e860..5c959de1 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -1,10 +1,13 @@ import SwiftUI +import composeApp @main struct iOSApp: App { + let dependencies = SetupDependenciesKt.setupDependencies(bridge: IosOonimkallBridge()) + var body: some Scene { WindowGroup { - ContentView() + ContentView(dependencies: dependencies) } } -} \ No newline at end of file +}