diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3e720db9..41ad7c2a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,4 +54,6 @@ dependencies { coreLibraryDesugaring(libs.android.desugaring) implementation(libs.koin.android) testImplementation(libs.junit) + implementation(libs.compose.navigation) + implementation(libs.koin.compose) } diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/MainActivity.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/MainActivity.kt index ed0f2dff..46e13ed9 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/MainActivity.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/MainActivity.kt @@ -3,24 +3,17 @@ package co.touchlab.kampkit.android import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import co.touchlab.kampkit.android.ui.BreedsScreen +import co.touchlab.kampkit.android.ui.MainNavCoordinator import co.touchlab.kampkit.android.ui.theme.KaMPKitTheme -import co.touchlab.kampkit.core.injectLogger -import co.touchlab.kampkit.ui.breeds.BreedsViewModel -import co.touchlab.kermit.Logger -import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.component.KoinComponent class MainActivity : ComponentActivity(), KoinComponent { - private val log: Logger by injectLogger("MainActivity") - private val viewModel: BreedsViewModel by viewModel() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { KaMPKitTheme { - BreedsScreen(viewModel, log) + MainNavCoordinator() } } } diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt index b6c202ca..c259a2df 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt @@ -6,6 +6,7 @@ import android.content.SharedPreferences import android.util.Log import co.touchlab.kampkit.core.AppInfo import co.touchlab.kampkit.core.initKoin +import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel import co.touchlab.kampkit.ui.breeds.BreedsViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.parameter.parametersOf @@ -18,7 +19,14 @@ class MainApp : Application() { initKoin( module { single { this@MainApp } - viewModel { BreedsViewModel(get(), get { parametersOf("BreedsViewModel") }) } + viewModel { + BreedsViewModel(get(), get { parametersOf("BreedsViewModel") }) + } + viewModel { params -> + BreedDetailsViewModel( + params.get(), get(), get { parametersOf("BreedDetailsViewModel") } + ) + } single { get().getSharedPreferences("KAMPSTARTER_SETTINGS", MODE_PRIVATE) } diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedDetailsScreen.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedDetailsScreen.kt new file mode 100644 index 00000000..2b9a8962 --- /dev/null +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedDetailsScreen.kt @@ -0,0 +1,97 @@ +package co.touchlab.kampkit.android.ui + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.touchlab.kampkit.android.R +import co.touchlab.kampkit.domain.breed.Breed +import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel +import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewState + +@Composable +fun BreedDetailsScreen(viewModel: BreedDetailsViewModel) { + val state by viewModel.detailsState.collectAsStateWithLifecycle() + val error = state.error + Box(Modifier.fillMaxSize()) { + when { + state.isLoading -> Loading() + error != null -> Error(error) + else -> DetailsContents( + state = state, + onFavoriteClick = viewModel::onFavoriteClick + ) + } + } +} + +@Composable +private fun BoxScope.DetailsContents( + state: BreedDetailsViewState, + onFavoriteClick: () -> Unit +) { + Row(Modifier.align(Alignment.Center)) { + Text(state.breed?.name ?: "") + Spacer(Modifier.width(4.dp)) + state.breed?.let { breed -> + FavoriteIcon( + breed = breed, + onClick = onFavoriteClick + ) + } + } +} + +@Composable +private fun BoxScope.Error(error: String) { + Text( + text = error, + color = Color.Red, + modifier = Modifier.align(Alignment.Center) + ) +} + +@Composable +fun BoxScope.Loading() { + CircularProgressIndicator(Modifier.align(Alignment.Center)) +} + +@Composable +fun FavoriteIcon( + breed: Breed, + onClick: () -> Unit +) { + Crossfade( + targetState = !breed.favorite, + animationSpec = TweenSpec( + durationMillis = 500, + easing = FastOutSlowInEasing + ), + modifier = Modifier.clickable { onClick() } + ) { fav -> + if (fav) { + Image( + painter = painterResource(id = R.drawable.ic_favorite_border_24px), + contentDescription = stringResource(R.string.favorite_breed, breed.name) + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_favorite_24px), + contentDescription = stringResource(R.string.unfavorite_breed, breed.name) + ) + } + } +} diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt index 1f5d73ea..ac905db0 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt @@ -1,8 +1,5 @@ package co.touchlab.kampkit.android.ui -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.TweenSpec import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -23,6 +20,7 @@ import androidx.compose.runtime.LaunchedEffect 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -30,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kampkit.android.R import co.touchlab.kampkit.domain.breed.Breed +import co.touchlab.kampkit.ui.breeds.BreedsNavRequest import co.touchlab.kampkit.ui.breeds.BreedsViewModel import co.touchlab.kampkit.ui.breeds.BreedsViewState import co.touchlab.kermit.Logger @@ -39,16 +38,26 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState @Composable fun BreedsScreen( viewModel: BreedsViewModel, + onBreedDetailsNavRequest: (breedId: Long) -> Unit, log: Logger ) { val breedsState by viewModel.breedsState.collectAsStateWithLifecycle() + breedsState.breedsNavRequest?.let { navRequest -> + LaunchedEffect(navRequest) { + if (navRequest is BreedsNavRequest.ToDetails) { + onBreedDetailsNavRequest(navRequest.breedId) + viewModel.onBreedDetailsNavRequestCompleted() + } + } + } + BreedsScreenContent( dogsState = breedsState, onRefresh = { viewModel.refreshBreeds() }, onSuccess = { data -> log.v { "View updating with ${data.size} breeds" } }, onError = { exception -> log.e { "Displaying error: $exception" } }, - onFavorite = { viewModel.updateBreedFavorite(it.id) } + onBreedClick = { viewModel.onBreedClick(it) }, ) } @@ -58,7 +67,7 @@ fun BreedsScreenContent( onRefresh: () -> Unit = {}, onSuccess: (List) -> Unit = {}, onError: (String) -> Unit = {}, - onFavorite: (Breed) -> Unit = {} + onBreedClick: (breedId: Long) -> Unit = {}, ) { Surface( color = MaterialTheme.colors.background, @@ -76,7 +85,7 @@ fun BreedsScreenContent( LaunchedEffect(breeds) { onSuccess(breeds) } - Success(successData = breeds, favoriteBreed = onFavorite) + Success(successData = breeds, onBreedClick = onBreedClick) } } @@ -91,7 +100,7 @@ fun BreedsScreenContent( } @Composable -fun Empty() { +private fun Empty() { Column( modifier = Modifier .fillMaxSize() @@ -104,7 +113,7 @@ fun Empty() { } @Composable -fun Error(error: String) { +private fun Error(error: String) { Column( modifier = Modifier .fillMaxSize() @@ -112,24 +121,24 @@ fun Error(error: String) { verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text(text = error) + Text(text = error, color = Color.Red) } } @Composable -fun Success( +private fun Success( successData: List, - favoriteBreed: (Breed) -> Unit + onBreedClick: (breedId: Long) -> Unit ) { - DogList(breeds = successData, favoriteBreed) + DogList(breeds = successData, onBreedClick) } @Composable -fun DogList(breeds: List, onItemClick: (Breed) -> Unit) { +fun DogList(breeds: List, onItemClick: (breedId: Long) -> Unit) { LazyColumn { items(breeds) { breed -> DogRow(breed) { - onItemClick(it) + onItemClick(breed.id) } Divider() } @@ -150,24 +159,16 @@ fun DogRow(breed: Breed, onClick: (Breed) -> Unit) { @Composable fun FavoriteIcon(breed: Breed) { - Crossfade( - targetState = !breed.favorite, - animationSpec = TweenSpec( - durationMillis = 500, - easing = FastOutSlowInEasing + if (breed.favorite) { + Image( + painter = painterResource(id = R.drawable.ic_favorite_24px), + contentDescription = stringResource(R.string.unfavorite_breed, breed.name) + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_favorite_border_24px), + contentDescription = stringResource(R.string.favorite_breed, breed.name) ) - ) { fav -> - if (fav) { - Image( - painter = painterResource(id = R.drawable.ic_favorite_border_24px), - contentDescription = stringResource(R.string.favorite_breed, breed.name) - ) - } else { - Image( - painter = painterResource(id = R.drawable.ic_favorite_24px), - contentDescription = stringResource(R.string.unfavorite_breed, breed.name) - ) - } } } diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/MainNavCoordinator.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/MainNavCoordinator.kt new file mode 100644 index 00000000..426640e2 --- /dev/null +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/MainNavCoordinator.kt @@ -0,0 +1,45 @@ +package co.touchlab.kampkit.android.ui + +import androidx.compose.runtime.Composable + import androidx.navigation.NavController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import org.koin.androidx.compose.get +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +private const val BREEDS = "breeds" +private const val BREED_DETAILS = "breedDetails" +private const val BREED_ID_ARG = "breedId" + +@Composable +fun MainNavCoordinator() { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = "breeds") { + composable(BREEDS) { + BreedsScreen( + viewModel = koinViewModel(), + onBreedDetailsNavRequest = { breedId -> + navController.navigateToBreedDetails(breedId) + }, + log = get { parametersOf("BreedsScreen") } + ) + } + composable( + route = "$BREED_DETAILS/{$BREED_ID_ARG}", + arguments = listOf(navArgument(BREED_ID_ARG) { type = NavType.LongType }) + ) { + val breedId = it.arguments?.getLong(BREED_ID_ARG) + BreedDetailsScreen( + viewModel = koinViewModel { parametersOf(breedId) }, + ) + } + } +} + +private fun NavController.navigateToBreedDetails(breedId: Long) { + navigate("$BREED_DETAILS/$breedId") +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e301cfb7..e6f7d7c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ androidx-core = "1.9.0" androidx-test-junit = "1.1.3" androidx-activity-compose = "1.5.1" androidx-lifecycle = "2.6.0" +androidx-navigation-compose = "2.5.3" junit = "4.13.2" @@ -51,6 +52,7 @@ compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "co compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } compose-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } +compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation-compose" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } @@ -62,6 +64,7 @@ junit = { module = "junit:junit", version.ref = "junit" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin"} kotlinx-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } diff --git a/ios/KaMPKitiOS.xcodeproj/project.pbxproj b/ios/KaMPKitiOS.xcodeproj/project.pbxproj index 9d39915c..088a4ea6 100644 --- a/ios/KaMPKitiOS.xcodeproj/project.pbxproj +++ b/ios/KaMPKitiOS.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 3C61D1352A55909300D4DF1D /* BreedDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C61D1342A55909300D4DF1D /* BreedDetailsViewModel.swift */; }; + 3C6AEC072A30881B0003F34A /* BreedDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6AEC062A30881B0003F34A /* BreedDetailsScreen.swift */; }; + 3C6AEC092A30C17A0003F34A /* MainNavCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6AEC082A30C17A0003F34A /* MainNavCoordinator.swift */; }; 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C73D0492A335103003E6929 /* BreedsViewModel.swift */; }; 3CD290EC2A251417004C7AD1 /* KMPNativeCoroutinesCombine in Frameworks */ = {isa = PBXBuildFile; productRef = 3CD290EB2A251417004C7AD1 /* KMPNativeCoroutinesCombine */; }; 3CD290EE2A251417004C7AD1 /* KMPNativeCoroutinesCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3CD290ED2A251417004C7AD1 /* KMPNativeCoroutinesCore */; }; @@ -41,8 +44,11 @@ /* Begin PBXFileReference section */ 1DFCC00C8DAA719770A18D1A /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; }; 2A1ED6A4A2A53F5F75C58E5F /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; }; + 3C61D1342A55909300D4DF1D /* BreedDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedDetailsViewModel.swift; sourceTree = ""; }; + 3C6AEC062A30881B0003F34A /* BreedDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedDetailsScreen.swift; sourceTree = ""; }; + 3C6AEC082A30C17A0003F34A /* MainNavCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavCoordinator.swift; sourceTree = ""; }; 3C73D0492A335103003E6929 /* BreedsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsViewModel.swift; sourceTree = ""; }; - 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsScreen.swift; sourceTree = ""; }; + 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BreedsScreen.swift; path = KaMPKitiOS/BreedsScreen.swift; sourceTree = SOURCE_ROOT; }; 46A5B60726B04920002EFEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46B5284C249C5CF400A7725D /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; }; B859F3FB23133D22AB9DD835 /* Pods_KaMPKitiOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KaMPKitiOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -89,6 +95,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C61D1332A55908200D4DF1D /* BreedDetails */ = { + isa = PBXGroup; + children = ( + 3C6AEC062A30881B0003F34A /* BreedDetailsScreen.swift */, + 3C61D1342A55909300D4DF1D /* BreedDetailsViewModel.swift */, + ); + path = BreedDetails; + sourceTree = ""; + }; 3C73D0482A335061003E6929 /* Breeds */ = { isa = PBXGroup; children = ( @@ -98,6 +113,30 @@ path = Breeds; sourceTree = ""; }; + 579753BDA8BAA9A5A399C611 /* bartlomiejpedryc.xcuserdatad */ = { + isa = PBXGroup; + children = ( + 57975F3C51683ABE14191024 /* xcschemes */, + ); + path = bartlomiejpedryc.xcuserdatad; + sourceTree = ""; + }; + 5797556592C6669DF1E7702C /* xcuserdata */ = { + isa = PBXGroup; + children = ( + 579753BDA8BAA9A5A399C611 /* bartlomiejpedryc.xcuserdatad */, + ); + name = xcuserdata; + path = KaMPKitiOS.xcodeproj/xcuserdata; + sourceTree = ""; + }; + 57975F3C51683ABE14191024 /* xcschemes */ = { + isa = PBXGroup; + children = ( + ); + path = xcschemes; + sourceTree = ""; + }; 6278498AD96A4D949D39BF44 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -126,6 +165,7 @@ F1465EFE23AA94BF0055F7C3 /* Products */, DF9BBECBCD175B90105DA8D9 /* Pods */, 6278498AD96A4D949D39BF44 /* Frameworks */, + 5797556592C6669DF1E7702C /* xcuserdata */, ); sourceTree = ""; }; @@ -142,6 +182,7 @@ F1465EFF23AA94BF0055F7C3 /* KaMPKitiOS */ = { isa = PBXGroup; children = ( + 3C61D1332A55908200D4DF1D /* BreedDetails */, 3C73D0482A335061003E6929 /* Breeds */, F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */, 46A5B60626B04920002EFEAA /* Main.storyboard */, @@ -149,6 +190,7 @@ F1465F0923AA94BF0055F7C3 /* Assets.xcassets */, F1465F0B23AA94BF0055F7C3 /* LaunchScreen.storyboard */, F1465F0E23AA94BF0055F7C3 /* Info.plist */, + 3C6AEC082A30C17A0003F34A /* MainNavCoordinator.swift */, ); path = KaMPKitiOS; sourceTree = ""; @@ -374,8 +416,12 @@ files = ( 46B5284D249C5CF400A7725D /* Koin.swift in Sources */, 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */, + 3C61D1352A55909300D4DF1D /* BreedDetailsViewModel.swift in Sources */, + 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */, 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */, F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */, + 3C6AEC072A30881B0003F34A /* BreedDetailsScreen.swift in Sources */, + 3C6AEC092A30C17A0003F34A /* MainNavCoordinator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -465,7 +511,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "Apple Development: brady.aiello@gmail.com (94U525PPDD)"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6A5MWU525T; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; diff --git a/ios/KaMPKitiOS.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/KaMPKitiOS.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..b2334037 --- /dev/null +++ b/ios/KaMPKitiOS.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "kmp-nativecoroutines", + "kind" : "remoteSourceControl", + "location" : "https://github.com/rickclephas/KMP-NativeCoroutines.git", + "state" : { + "revision" : "a1269d3052fcc32d3861eaea32b715d58a46fe32", + "version" : "1.0.0-ALPHA-9" + } + }, + { + "identity" : "rxswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveX/RxSwift.git", + "state" : { + "revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4", + "version" : "6.6.0" + } + } + ], + "version" : 2 +} diff --git a/ios/KaMPKitiOS/AppDelegate.swift b/ios/KaMPKitiOS/AppDelegate.swift index 139d7889..ddcf29af 100644 --- a/ios/KaMPKitiOS/AppDelegate.swift +++ b/ios/KaMPKitiOS/AppDelegate.swift @@ -22,10 +22,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { startKoin() - let viewController = UIHostingController(rootView: BreedsScreen()) + let navController = UINavigationController() + let mainCoordinator = MainNavCoordinator(navController: navController) + mainCoordinator.start() self.window = UIWindow(frame: UIScreen.main.bounds) - self.window?.rootViewController = viewController + self.window?.rootViewController = navController self.window?.makeKeyAndVisible() log.v(message: {"App Started"}) diff --git a/ios/KaMPKitiOS/BreedDetails/BreedDetailsScreen.swift b/ios/KaMPKitiOS/BreedDetails/BreedDetailsScreen.swift new file mode 100644 index 00000000..c6de5b2f --- /dev/null +++ b/ios/KaMPKitiOS/BreedDetails/BreedDetailsScreen.swift @@ -0,0 +1,56 @@ +// +// BreedDetailsScreen.swift +// KaMPKitiOS +// +// Created by Bartłomiej Pedryc on 07/06/2023. +// Copyright © 2023 Touchlab. All rights reserved. +// + +import SwiftUI +import shared +import Foundation + +struct BreedDetailsScreen: View { + @StateObject + var viewModel: BreedDetailsViewModel + + var body: some View { + let state = viewModel.state + ZStack { + if let error = state.error { + Text(error) + } + if state.error == nil && state.isLoading { + ProgressView() + } + if let breed = state.breed, state.error == nil, !state.isLoading { + BreedDetailsContent( + breedName: breed.name, + isBreedFavorite: breed.favorite, + onFavoriteClick: { viewModel.onFavoriteClick() } + ) + } + } + .onAppear(perform: { + viewModel.subscribeState() + }) + .onDisappear(perform: { + viewModel.unsubscribeState() + }) + } +} + +struct BreedDetailsContent: View { + var breedName: String + var isBreedFavorite: Bool + var onFavoriteClick: () -> Void + var body: some View { + HStack { + Text(breedName) + Button(action: onFavoriteClick) { + Image(systemName: (!isBreedFavorite) ? "heart" : "heart.fill") + .padding(4.0) + } + } + } +} diff --git a/ios/KaMPKitiOS/BreedDetails/BreedDetailsViewModel.swift b/ios/KaMPKitiOS/BreedDetails/BreedDetailsViewModel.swift new file mode 100644 index 00000000..0d21c6aa --- /dev/null +++ b/ios/KaMPKitiOS/BreedDetails/BreedDetailsViewModel.swift @@ -0,0 +1,44 @@ +// +// BreedDetailsViewModel.swift +// KaMPKitiOS +// +// Created by Bartłomiej Pedryc on 05/07/2023. +// Copyright © 2023 Touchlab. All rights reserved. +// + +import Combine +import Foundation +import shared +import KMPNativeCoroutinesCombine + +class BreedDetailsViewModel: ObservableObject { + + @Published var state: BreedDetailsViewState = BreedDetailsViewState.companion.default() + + private var viewModelDelegate: BreedDetailsViewModelDelegate + private var cancellables = [AnyCancellable]() + init(breedId: Int64) { + self.viewModelDelegate = KotlinDependencies.shared.getBreedDetailsViewModel(breedId: breedId) + } + + deinit { + viewModelDelegate.clear() + } + + func subscribeState() { + createPublisher(for: viewModelDelegate.detailsStateFlow) + .sink { _ in } receiveValue: { [weak self] (detailsState: BreedDetailsViewState) in + self?.state = detailsState + } + .store(in: &cancellables) + } + + func unsubscribeState() { + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } + + func onFavoriteClick() { + viewModelDelegate.onFavoriteClick() + } +} diff --git a/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift index 0d19c64a..8d6d725e 100644 --- a/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift +++ b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift @@ -15,8 +15,12 @@ class BreedsViewModel: ObservableObject { @Published var state: BreedsViewState = BreedsViewState.companion.default() + private var navCoordinator: BreedsNavCoordinator private var viewModelDelegate: BreedsViewModelDelegate = KotlinDependencies.shared.getBreedsViewModel() private var cancellables = [AnyCancellable]() + init(navCoordinator: BreedsNavCoordinator) { + self.navCoordinator = navCoordinator + } deinit { viewModelDelegate.clear() @@ -26,17 +30,25 @@ class BreedsViewModel: ObservableObject { createPublisher(for: viewModelDelegate.breedsStateFlow) .sink { _ in } receiveValue: { [weak self] (breedState: BreedsViewState) in self?.state = breedState + self?.handleNavRequests(breedsState: breedState) } .store(in: &cancellables) } + private func handleNavRequests(breedsState: BreedsViewState) { + if let navRequest = breedsState.breedsNavRequest as? BreedsNavRequest.ToDetails { + self.navCoordinator.onBreedDetailsRequest(breedId: navRequest.breedId) + self.viewModelDelegate.onBreedDetailsNavRequestCompleted() + } + } + func unsubscribeState() { cancellables.forEach { $0.cancel() } cancellables.removeAll() } - func onBreedFavorite(_ breedId: Int64) { - viewModelDelegate.updateBreedFavorite(breedId: breedId) + func onBreedClick(_ breedId: Int64) { + viewModelDelegate.onBreedClick(breedId: breedId) } func refresh() { diff --git a/ios/KaMPKitiOS/BreedsScreen.swift b/ios/KaMPKitiOS/BreedsScreen.swift new file mode 100644 index 00000000..019f9124 --- /dev/null +++ b/ios/KaMPKitiOS/BreedsScreen.swift @@ -0,0 +1,98 @@ +// +// BreedListView.swift +// KaMPKitiOS +// +// Created by Russell Wolf on 7/26/21. +// Copyright © 2021 Touchlab. All rights reserved. +// + +import Combine +import SwiftUI +import shared +import KMPNativeCoroutinesCombine + +private let log = koin.loggerWithTag(tag: "ViewController") + + +struct BreedsScreen: View { + @StateObject + var viewModel: BreedsViewModel + + var body: some View { + BreedListContent( + loading: viewModel.state.isLoading, + breeds: viewModel.state.breeds, + error: viewModel.state.error, + onBreedClick: { viewModel.onBreedClick($0) }, + refresh: { viewModel.refresh() } + ) + .onAppear(perform: { + viewModel.subscribeState() + }) + .onDisappear(perform: { + viewModel.unsubscribeState() + }) + } +} + +struct BreedListContent: View { + var loading: Bool + var breeds: [Breed]? + var error: String? + var onBreedClick: (Int64) -> Void + var refresh: () -> Void + + var body: some View { + ZStack { + VStack { + if let breeds = breeds { + List(breeds, id: \.id) { breed in + BreedRowView(breed: breed) { + onBreedClick(breed.id) + } + } + } + if let error = error { + Text(error) + .foregroundColor(.red) + } + Button("Refresh") { + refresh() + } + } + if loading { Text("Loading...") } + } + } +} + +struct BreedRowView: View { + var breed: Breed + var onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack { + Text(breed.name) + .padding(4.0) + Spacer() + Image(systemName: (!breed.favorite) ? "heart" : "heart.fill") + .padding(4.0) + } + } + } +} + +struct BreedListScreen_Previews: PreviewProvider { + static var previews: some View { + BreedListContent( + loading: false, + breeds: [ + Breed(id: 0, name: "appenzeller", favorite: false), + Breed(id: 1, name: "australian", favorite: true) + ], + error: nil, + onBreedClick: { _ in }, + refresh: {} + ) + } +} diff --git a/ios/KaMPKitiOS/MainNavCoordinator.swift b/ios/KaMPKitiOS/MainNavCoordinator.swift new file mode 100644 index 00000000..3f90e810 --- /dev/null +++ b/ios/KaMPKitiOS/MainNavCoordinator.swift @@ -0,0 +1,42 @@ +// +// AppNavigationController.swift +// KaMPKitiOS +// +// Created by Bartłomiej Pedryc on 07/06/2023. +// Copyright © 2023 Touchlab. All rights reserved. +// + +import SwiftUI +import Foundation + +class MainNavCoordinator: BreedsNavCoordinator { + private let navController: UINavigationController + + init(navController: UINavigationController) { + self.navController = navController + } + + func start() { + let controller = buildBreedsController(navCoordinator: self) + navController.pushViewController(controller, animated: false) + } + + func onBreedDetailsRequest(breedId: Int64) { + let controller = buildBreedDetailsController(breedId: breedId) + navController.pushViewController(controller, animated: true) + } +} + +private func buildBreedsController(navCoordinator: BreedsNavCoordinator) -> UIHostingController { + let viewModel = BreedsViewModel(navCoordinator: navCoordinator) + return UIHostingController(rootView: BreedsScreen(viewModel: viewModel)) +} + +private func buildBreedDetailsController(breedId: Int64) -> UIHostingController { + let viewModel = BreedDetailsViewModel(breedId: breedId) + return UIHostingController(rootView: BreedDetailsScreen(viewModel: viewModel)) +} + +protocol BreedsNavCoordinator { + func onBreedDetailsRequest(breedId: Int64) +} diff --git a/ios/Pods/Pods.xcodeproj/project.pbxproj b/ios/Pods/Pods.xcodeproj/project.pbxproj index 79a804f9..193e5a24 100644 --- a/ios/Pods/Pods.xcodeproj/project.pbxproj +++ b/ios/Pods/Pods.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ 46EB2E00000080 /* Pods */, 46EB2E00000020 /* Products */, 46EB2E00000070 /* Targets Support Files */, + 57975FCC640FD6F802D419B1 /* xcschemes */, ); sourceTree = ""; }; @@ -217,6 +218,14 @@ path = "Target Support Files/Pods-KaMPKitiOS"; sourceTree = ""; }; + 57975FCC640FD6F802D419B1 /* xcschemes */ = { + isa = PBXGroup; + children = ( + ); + name = xcschemes; + path = Pods.xcodeproj/xcuserdata/bartlomiejpedryc.xcuserdatad/xcschemes; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ diff --git a/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt b/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt index a5cb3267..90122412 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt @@ -1,10 +1,10 @@ package co.touchlab.kampkit.core +import androidx.lifecycle.ViewModel import kotlinx.coroutines.CoroutineScope -import androidx.lifecycle.ViewModel as AndroidXViewModel import androidx.lifecycle.viewModelScope as androidXViewModelScope -actual abstract class ViewModel actual constructor() : AndroidXViewModel() { +actual abstract class ViewModel actual constructor() : ViewModel() { actual val viewModelScope: CoroutineScope = androidXViewModelScope actual override fun onCleared() { diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogDatabaseHelper.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogDatabaseHelper.kt index 730af8ac..85dea97d 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogDatabaseHelper.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogDatabaseHelper.kt @@ -7,6 +7,7 @@ import co.touchlab.kermit.Logger import com.squareup.sqldelight.db.SqlDriver import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList +import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -35,11 +36,11 @@ class DogDatabaseHelper( } } - fun selectById(id: Long): Flow> = + fun selectById(id: Long): Flow = dbRef.tableQueries .selectById(id) .asFlow() - .mapToList(Dispatchers.Default) + .mapToOneOrNull(Dispatchers.Default) .flowOn(backgroundDispatcher) suspend fun deleteAll() { diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/NetworkBreedRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/NetworkBreedRepository.kt index df74d05f..c875f686 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/NetworkBreedRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/NetworkBreedRepository.kt @@ -18,7 +18,7 @@ class NetworkBreedRepository( private val clock: Clock ) : BreedRepository { - private val log = log.withTag("DogRepository") + private val log = log.withTag("NetworkBreedRepository") companion object { internal const val DB_TIMESTAMP_KEY = "DbTimestampKey" @@ -28,10 +28,15 @@ class NetworkBreedRepository( ensureNeverFrozen() } + override fun getBreed(id: Long): Flow { + return dbHelper + .selectById(id) + .map { dbBreed -> dbBreed?.toDomain() } + } override fun getBreeds(): Flow> { - return dbHelper.selectAllItems().map { list -> - list.map { dbBreed -> dbBreed.toDomain() } - } + return dbHelper + .selectAllItems() + .map { list -> list.map { dbBreed -> dbBreed.toDomain() } } } override suspend fun refreshBreedsIfStale() { @@ -53,10 +58,11 @@ class NetworkBreedRepository( } override suspend fun updateBreedFavorite(breedId: Long) { - val foundBreedsWithId = dbHelper.selectById(breedId).first() - foundBreedsWithId.firstOrNull()?.let { breed -> - dbHelper.updateFavorite(breed.id, !breed.favorite) - } + dbHelper + .selectById(breedId) + .first()?.let { breed -> + dbHelper.updateFavorite(breed.id, !breed.favorite) + } } private fun isBreedListStale(): Boolean { diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/BreedRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/BreedRepository.kt index b5cbb2e2..626bd672 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/BreedRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/BreedRepository.kt @@ -3,6 +3,7 @@ package co.touchlab.kampkit.domain.breed import kotlinx.coroutines.flow.Flow interface BreedRepository { + fun getBreed(id: Long): Flow fun getBreeds(): Flow> suspend fun refreshBreedsIfStale() suspend fun refreshBreeds() diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breedDetails/BreedDetailsViewModel.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breedDetails/BreedDetailsViewModel.kt new file mode 100644 index 00000000..31507691 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breedDetails/BreedDetailsViewModel.kt @@ -0,0 +1,51 @@ +package co.touchlab.kampkit.ui.breedDetails + +import co.touchlab.kampkit.core.ViewModel +import co.touchlab.kampkit.domain.breed.BreedRepository +import co.touchlab.kermit.Logger +import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.native.ObjCName + +@ObjCName("BreedDetailsViewModelDelegate") +class BreedDetailsViewModel( + private val breedId: Long, + private val breedRepository: BreedRepository, + log: Logger +) : ViewModel() { + private val log = log.withTag("BreedDetailsViewModel") + + private val mutableDetailsState: MutableStateFlow = + MutableStateFlow(BreedDetailsViewState(isLoading = true)) + + @NativeCoroutinesState + val detailsState: StateFlow = mutableDetailsState + + init { + loadDetails() + } + + private fun loadDetails() { + viewModelScope.launch { + breedRepository.getBreed(breedId).collect { breed -> + mutableDetailsState.update { previousState -> + val error = if (breed == null) "Couldn't load the breed details" else null + previousState.copy( + isLoading = false, + breed = breed ?: previousState.breed, + error = error + ) + } + } + } + } + + fun onFavoriteClick() { + viewModelScope.launch { + breedRepository.updateBreedFavorite(breedId) + } + } +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breedDetails/BreedDetailsViewState.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breedDetails/BreedDetailsViewState.kt new file mode 100644 index 00000000..d6e144ec --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breedDetails/BreedDetailsViewState.kt @@ -0,0 +1,13 @@ +package co.touchlab.kampkit.ui.breedDetails + +import co.touchlab.kampkit.domain.breed.Breed + +data class BreedDetailsViewState( + val breed: Breed? = null, + val error: String? = null, + val isLoading: Boolean = false +) { + companion object { + fun default() = BreedDetailsViewState() + } +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsNavRequest.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsNavRequest.kt new file mode 100644 index 00000000..8132580c --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsNavRequest.kt @@ -0,0 +1,5 @@ +package co.touchlab.kampkit.ui.breeds + +sealed class BreedsNavRequest { + data class ToDetails(val breedId: Long) : BreedsNavRequest() +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewModel.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewModel.kt index 33897c9b..e309fb38 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewModel.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewModel.kt @@ -50,7 +50,7 @@ class BreedsViewModel( } else { previousState.error } - BreedsViewState( + previousState.copy( isLoading = false, breeds = breeds, error = errorMessage.takeIf { breeds.isEmpty() }, @@ -74,9 +74,17 @@ class BreedsViewModel( } } - fun updateBreedFavorite(breedId: Long): Job { + fun onBreedClick(breedId: Long): Job { return viewModelScope.launch { - breedRepository.updateBreedFavorite(breedId) + mutableBreedState.update { + it.copy(breedsNavRequest = BreedsNavRequest.ToDetails(breedId)) + } + } + } + + fun onBreedDetailsNavRequestCompleted() { + mutableBreedState.update { + it.copy(breedsNavRequest = null) } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewState.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewState.kt index 0a908bad..75633c56 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewState.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewState.kt @@ -6,7 +6,8 @@ data class BreedsViewState( val breeds: List = emptyList(), val error: String? = null, val isLoading: Boolean = false, - val isEmpty: Boolean = false + val isEmpty: Boolean = false, + val breedsNavRequest: BreedsNavRequest? = null ) { companion object { // This method lets you use the default constructor values in Swift. When accessing the diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedsViewModelTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedsViewModelTest.kt index 36f83861..8f22573f 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedsViewModelTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedsViewModelTest.kt @@ -9,6 +9,7 @@ import co.touchlab.kampkit.domain.breed.Breed import co.touchlab.kampkit.domain.breed.BreedRepository import co.touchlab.kampkit.mock.ClockMock import co.touchlab.kampkit.mock.DogApiMock +import co.touchlab.kampkit.ui.breeds.BreedsNavRequest import co.touchlab.kampkit.ui.breeds.BreedsViewModel import co.touchlab.kampkit.ui.breeds.BreedsViewState import co.touchlab.kermit.Logger @@ -95,7 +96,8 @@ class BreedsViewModelTest { settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) val successResult = ktorApi.successResult() - val resultWithExtraBreed = successResult.copy(message = successResult.message + ("extra" to emptyList())) + val resultWithExtraBreed = + successResult.copy(message = successResult.message + ("extra" to emptyList())) ktorApi.prepareResult(resultWithExtraBreed) dbHelper.insertBreeds(breedNames) @@ -119,7 +121,8 @@ class BreedsViewModelTest { settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, (clock.currentInstant - 2.hours).toEpochMilliseconds()) val successResult = ktorApi.successResult() - val resultWithExtraBreed = successResult.copy(message = successResult.message + ("extra" to emptyList())) + val resultWithExtraBreed = + successResult.copy(message = successResult.message + ("extra" to emptyList())) ktorApi.prepareResult(resultWithExtraBreed) dbHelper.insertBreeds(breedNames) @@ -135,21 +138,13 @@ class BreedsViewModelTest { } @Test - fun `Toggle favorite cached breed`() = runTest { - settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) - + fun `Navigate to breed details`() = runTest { dbHelper.insertBreeds(breedNames) - dbHelper.updateFavorite(australianLike.id, true) + viewModel.onBreedClick(1).join() viewModel.breedsState.test { - assertEquals(breedsViewStateSuccessFavorite, awaitItemPrecededBy(BreedsViewState(isLoading = true))) - expectNoEvents() - - viewModel.updateBreedFavorite(australianLike.id).join() - assertEquals( - breedsViewStateSuccessNoFavorite, - awaitItemPrecededBy(breedsViewStateSuccessFavorite.copy(isLoading = true)) - ) + val state = awaitItem() + assertEquals(BreedsNavRequest.ToDetails(1), state.breedsNavRequest) } } diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkDogRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkBreedRepositoryTest.kt similarity index 99% rename from shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkDogRepositoryTest.kt rename to shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkBreedRepositoryTest.kt index e7850239..102564fb 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkDogRepositoryTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkBreedRepositoryTest.kt @@ -18,7 +18,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.time.Duration.Companion.hours -class NetworkDogRepositoryTest { +class NetworkBreedRepositoryTest { private var kermit = Logger(StaticConfig()) private var testDbConnection = testDbConnection() diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt index 0b72423c..a859f770 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt @@ -54,7 +54,7 @@ class SqlDelightTest { val breeds = dbHelper.selectAllItems().first() val firstBreed = breeds.first() dbHelper.updateFavorite(firstBreed.id, true) - val newBreed = dbHelper.selectById(firstBreed.id).first().first() + val newBreed = dbHelper.selectById(firstBreed.id).first() assertNotNull( newBreed, "Could not retrieve Breed by Id" diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt index 74e8c0c0..faf9243f 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt @@ -1,6 +1,7 @@ package co.touchlab.kampkit.core import co.touchlab.kampkit.db.KaMPKitDb +import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel import co.touchlab.kampkit.ui.breeds.BreedsViewModel import co.touchlab.kermit.Logger import com.russhwolf.settings.NSUserDefaultsSettings @@ -32,7 +33,9 @@ actual val platformModule = module { single { Darwin.create() } - single { BreedsViewModel(get(), getWith("BreedsViewModel")) } + factory { BreedsViewModel(get(), getWith("BreedsViewModel")) } + + factory { params -> BreedDetailsViewModel(params.get(), get(), getWith("BreedDetailsViewModel")) } } // Access from Swift to create a logger @@ -43,4 +46,5 @@ fun Koin.loggerWithTag(tag: String) = @Suppress("unused") // Called from Swift object KotlinDependencies : KoinComponent { fun getBreedsViewModel() = getKoin().get() + fun getBreedDetailsViewModel(breedId: Long) = getKoin().get { parametersOf(breedId) } } diff --git a/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt b/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt index 52631c11..c2d502e5 100644 --- a/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt +++ b/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt @@ -1,6 +1,7 @@ package co.touchlab.kampkit import co.touchlab.kampkit.core.initKoinIos +import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel import co.touchlab.kermit.Logger import org.koin.core.context.stopKoin import org.koin.core.parameter.parametersOf @@ -18,6 +19,7 @@ class KoinTest { doOnStartup = { } ).checkModules { withParameters { parametersOf("TestTag") } + withParameters { parametersOf(0L) } } }