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 08ae0954..ed0f2dff 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/MainActivity.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/MainActivity.kt @@ -3,10 +3,10 @@ package co.touchlab.kampkit.android import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import co.touchlab.kampkit.android.ui.MainScreen +import co.touchlab.kampkit.android.ui.BreedsScreen import co.touchlab.kampkit.android.ui.theme.KaMPKitTheme -import co.touchlab.kampkit.injectLogger -import co.touchlab.kampkit.models.BreedViewModel +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 @@ -14,13 +14,13 @@ import org.koin.core.component.KoinComponent class MainActivity : ComponentActivity(), KoinComponent { private val log: Logger by injectLogger("MainActivity") - private val viewModel: BreedViewModel by viewModel() + private val viewModel: BreedsViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { KaMPKitTheme { - MainScreen(viewModel, log) + BreedsScreen(viewModel, log) } } } 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 d40cc2c9..b6c202ca 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt @@ -4,9 +4,9 @@ import android.app.Application import android.content.Context import android.content.SharedPreferences import android.util.Log -import co.touchlab.kampkit.AppInfo -import co.touchlab.kampkit.initKoin -import co.touchlab.kampkit.models.BreedViewModel +import co.touchlab.kampkit.core.AppInfo +import co.touchlab.kampkit.core.initKoin +import co.touchlab.kampkit.ui.breeds.BreedsViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.parameter.parametersOf import org.koin.dsl.module @@ -18,9 +18,9 @@ class MainApp : Application() { initKoin( module { single { this@MainApp } - viewModel { BreedViewModel(get(), get { parametersOf("BreedViewModel") }) } + viewModel { BreedsViewModel(get(), get { parametersOf("BreedsViewModel") }) } single { - get().getSharedPreferences("KAMPSTARTER_SETTINGS", Context.MODE_PRIVATE) + get().getSharedPreferences("KAMPSTARTER_SETTINGS", MODE_PRIVATE) } single { AndroidAppInfo } single { diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/Composables.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt similarity index 82% rename from app/src/main/kotlin/co/touchlab/kampkit/android/ui/Composables.kt rename to app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt index b9337244..1f5d73ea 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/Composables.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt @@ -29,32 +29,32 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kampkit.android.R -import co.touchlab.kampkit.db.Breed -import co.touchlab.kampkit.models.BreedViewModel -import co.touchlab.kampkit.models.BreedViewState +import co.touchlab.kampkit.domain.breed.Breed +import co.touchlab.kampkit.ui.breeds.BreedsViewModel +import co.touchlab.kampkit.ui.breeds.BreedsViewState import co.touchlab.kermit.Logger import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState @Composable -fun MainScreen( - viewModel: BreedViewModel, +fun BreedsScreen( + viewModel: BreedsViewModel, log: Logger ) { - val dogsState by viewModel.breedState.collectAsStateWithLifecycle() + val breedsState by viewModel.breedsState.collectAsStateWithLifecycle() - MainScreenContent( - dogsState = dogsState, + 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) } + onFavorite = { viewModel.updateBreedFavorite(it.id) } ) } @Composable -fun MainScreenContent( - dogsState: BreedViewState, +fun BreedsScreenContent( + dogsState: BreedsViewState, onRefresh: () -> Unit = {}, onSuccess: (List) -> Unit = {}, onError: (String) -> Unit = {}, @@ -68,18 +68,19 @@ fun MainScreenContent( state = rememberSwipeRefreshState(isRefreshing = dogsState.isLoading), onRefresh = onRefresh ) { - if (dogsState.isEmpty) { - Empty() - } - val breeds = dogsState.breeds - if (breeds != null) { - LaunchedEffect(breeds) { - onSuccess(breeds) + if (dogsState.error == null) { + if (dogsState.isEmpty) { + Empty() + } else { + val breeds = dogsState.breeds + LaunchedEffect(breeds) { + onSuccess(breeds) + } + Success(successData = breeds, favoriteBreed = onFavorite) } - Success(successData = breeds, favoriteBreed = onFavorite) } - val error = dogsState.error - if (error != null) { + + dogsState.error?.let { error -> LaunchedEffect(error) { onError(error) } @@ -172,9 +173,9 @@ fun FavoriteIcon(breed: Breed) { @Preview @Composable -fun MainScreenContentPreview_Success() { - MainScreenContent( - dogsState = BreedViewState( +fun BreedsScreenContentPreview_Success() { + BreedsScreenContent( + dogsState = BreedsViewState( breeds = listOf( Breed(0, "appenzeller", false), Breed(1, "australian", true) diff --git a/ios/KaMPKitiOS/Breeds/BreedsScreen.swift b/ios/KaMPKitiOS/Breeds/BreedsScreen.swift index 1b0c8e33..c82259b8 100644 --- a/ios/KaMPKitiOS/Breeds/BreedsScreen.swift +++ b/ios/KaMPKitiOS/Breeds/BreedsScreen.swift @@ -29,8 +29,8 @@ struct BreedsScreen: View { } struct BreedsContent: View { - var state: BreedViewState - var onBreedFavorite: (Breed) -> Void + var state: BreedsViewState + var onBreedFavorite: (Int64) -> Void var refresh: () -> Void var body: some View { @@ -38,7 +38,7 @@ struct BreedsContent: View { VStack { List(state.breeds, id: \.id) { breed in BreedRowView(breed: breed) { - onBreedFavorite(breed) + onBreedFavorite(breed.id) } } if let error = state.error { @@ -74,7 +74,7 @@ struct BreedRowView: View { struct BreedsScreen_Previews: PreviewProvider { static var previews: some View { BreedsContent( - state: BreedViewState( + state: BreedsViewState( breeds: [ Breed(id: 0, name: "appenzeller", favorite: false), Breed(id: 1, name: "australian", favorite: true) diff --git a/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift index c4ea2b2e..0d19c64a 100644 --- a/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift +++ b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift @@ -12,19 +12,19 @@ import shared import KMPNativeCoroutinesCombine class BreedsViewModel: ObservableObject { - - @Published var state: BreedViewState = BreedViewState.companion.default() - - private var viewModelDelegate: BreedViewModelDelegate = KotlinDependencies.shared.getBreedViewModel() + + @Published var state: BreedsViewState = BreedsViewState.companion.default() + + private var viewModelDelegate: BreedsViewModelDelegate = KotlinDependencies.shared.getBreedsViewModel() private var cancellables = [AnyCancellable]() - + deinit { viewModelDelegate.clear() } func subscribeState() { - createPublisher(for: viewModelDelegate.breedStateFlow) - .sink { _ in } receiveValue: { [weak self] (breedState: BreedViewState) in + createPublisher(for: viewModelDelegate.breedsStateFlow) + .sink { _ in } receiveValue: { [weak self] (breedState: BreedsViewState) in self?.state = breedState } .store(in: &cancellables) @@ -35,8 +35,8 @@ class BreedsViewModel: ObservableObject { cancellables.removeAll() } - func onBreedFavorite(_ breed: Breed) { - viewModelDelegate.updateBreedFavorite(breed: breed) + func onBreedFavorite(_ breedId: Int64) { + viewModelDelegate.updateBreedFavorite(breedId: breedId) } func refresh() { diff --git a/shared/src/androidMain/kotlin/co/touchlab/kampkit/KoinAndroid.kt b/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/KoinAndroid.kt similarity index 95% rename from shared/src/androidMain/kotlin/co/touchlab/kampkit/KoinAndroid.kt rename to shared/src/androidMain/kotlin/co/touchlab/kampkit/core/KoinAndroid.kt index ed6bc238..595c3eec 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/kampkit/KoinAndroid.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/KoinAndroid.kt @@ -1,4 +1,4 @@ -package co.touchlab.kampkit +package co.touchlab.kampkit.core import co.touchlab.kampkit.db.KaMPKitDb import com.russhwolf.settings.Settings diff --git a/shared/src/androidMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt b/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt similarity index 91% rename from shared/src/androidMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt rename to shared/src/androidMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt index 9eb0f27e..a5cb3267 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt @@ -1,4 +1,4 @@ -package co.touchlab.kampkit.models +package co.touchlab.kampkit.core import kotlinx.coroutines.CoroutineScope import androidx.lifecycle.ViewModel as AndroidXViewModel diff --git a/shared/src/androidUnitTest/kotlin/co/touchlab/kampkit/KoinTest.kt b/shared/src/androidUnitTest/kotlin/co/touchlab/kampkit/KoinTest.kt index 4be2ca84..0171b63e 100644 --- a/shared/src/androidUnitTest/kotlin/co/touchlab/kampkit/KoinTest.kt +++ b/shared/src/androidUnitTest/kotlin/co/touchlab/kampkit/KoinTest.kt @@ -4,6 +4,8 @@ import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 +import co.touchlab.kampkit.core.AppInfo +import co.touchlab.kampkit.core.initKoin import co.touchlab.kermit.Logger import org.junit.experimental.categories.Category import org.junit.runner.RunWith diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/AppInfo.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/AppInfo.kt similarity index 57% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/AppInfo.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/core/AppInfo.kt index f2f339df..a1ff4c11 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/AppInfo.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/AppInfo.kt @@ -1,4 +1,4 @@ -package co.touchlab.kampkit +package co.touchlab.kampkit.core interface AppInfo { val appId: String diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/sqldelight/CoroutinesExtensions.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/CoroutinesExtensions.kt similarity index 92% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/sqldelight/CoroutinesExtensions.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/core/CoroutinesExtensions.kt index d8ac0aa5..fcb0faab 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/sqldelight/CoroutinesExtensions.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/CoroutinesExtensions.kt @@ -1,4 +1,4 @@ -package co.touchlab.kampkit.sqldelight +package co.touchlab.kampkit.core import com.squareup.sqldelight.Transacter import com.squareup.sqldelight.TransactionWithoutReturn diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/Koin.kt similarity index 86% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/Koin.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/core/Koin.kt index db7c2df1..5bd590b6 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/Koin.kt @@ -1,8 +1,10 @@ -package co.touchlab.kampkit +package co.touchlab.kampkit.core -import co.touchlab.kampkit.ktor.DogApi -import co.touchlab.kampkit.ktor.DogApiImpl -import co.touchlab.kampkit.models.BreedRepository +import co.touchlab.kampkit.data.dog.DogApi +import co.touchlab.kampkit.data.dog.DogApiImpl +import co.touchlab.kampkit.data.dog.DogDatabaseHelper +import co.touchlab.kampkit.data.dog.NetworkBreedRepository +import co.touchlab.kampkit.domain.breed.BreedRepository import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig import co.touchlab.kermit.platformLogWriter @@ -42,7 +44,7 @@ fun initKoin(appModule: Module): KoinApplication { private val coreModule = module { single { - DatabaseHelper( + DogDatabaseHelper( get(), getWith("DatabaseHelper"), Dispatchers.Default @@ -65,8 +67,8 @@ private val coreModule = module { val baseLogger = Logger(config = StaticConfig(logWriterList = listOf(platformLogWriter())), "KampKit") factory { (tag: String?) -> if (tag != null) baseLogger.withTag(tag) else baseLogger } - single { - BreedRepository( + single { + NetworkBreedRepository( get(), get(), get(), diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt similarity index 81% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt index d3c42256..4e9aab7c 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt @@ -1,4 +1,4 @@ -package co.touchlab.kampkit.models +package co.touchlab.kampkit.core import kotlinx.coroutines.CoroutineScope diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogApi.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogApi.kt new file mode 100644 index 00000000..3019c101 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogApi.kt @@ -0,0 +1,5 @@ +package co.touchlab.kampkit.data.dog + +interface DogApi { + suspend fun getJsonFromApi(): DogResult +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ktor/DogApiImpl.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogApiImpl.kt similarity index 92% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/ktor/DogApiImpl.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogApiImpl.kt index 1e55468b..dfb16667 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ktor/DogApiImpl.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogApiImpl.kt @@ -1,6 +1,5 @@ -package co.touchlab.kampkit.ktor +package co.touchlab.kampkit.data.dog -import co.touchlab.kampkit.response.BreedResult import co.touchlab.stately.ensureNeverFrozen import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -45,7 +44,7 @@ class DogApiImpl(private val log: KermitLogger, engine: HttpClientEngine) : DogA ensureNeverFrozen() } - override suspend fun getJsonFromApi(): BreedResult { + override suspend fun getJsonFromApi(): DogResult { log.d { "Fetching Breeds from network" } return client.get { dogs("api/breeds/list/all") diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/DatabaseHelper.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogDatabaseHelper.kt similarity index 86% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/DatabaseHelper.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogDatabaseHelper.kt index f5e82022..730af8ac 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/DatabaseHelper.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogDatabaseHelper.kt @@ -1,8 +1,8 @@ -package co.touchlab.kampkit +package co.touchlab.kampkit.data.dog -import co.touchlab.kampkit.db.Breed +import co.touchlab.kampkit.core.transactionWithContext +import co.touchlab.kampkit.db.DbBreed import co.touchlab.kampkit.db.KaMPKitDb -import co.touchlab.kampkit.sqldelight.transactionWithContext import co.touchlab.kermit.Logger import com.squareup.sqldelight.db.SqlDriver import com.squareup.sqldelight.runtime.coroutines.asFlow @@ -12,14 +12,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -class DatabaseHelper( +class DogDatabaseHelper( sqlDriver: SqlDriver, private val log: Logger, private val backgroundDispatcher: CoroutineDispatcher ) { private val dbRef: KaMPKitDb = KaMPKitDb(sqlDriver) - fun selectAllItems(): Flow> = + fun selectAllItems(): Flow> = dbRef.tableQueries .selectAll() .asFlow() @@ -35,7 +35,7 @@ class DatabaseHelper( } } - fun selectById(id: Long): Flow> = + fun selectById(id: Long): Flow> = dbRef.tableQueries .selectById(id) .asFlow() diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/response/BreedResult.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogResult.kt similarity index 67% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/response/BreedResult.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogResult.kt index 49784591..08d36915 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/response/BreedResult.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/DogResult.kt @@ -1,9 +1,9 @@ -package co.touchlab.kampkit.response +package co.touchlab.kampkit.data.dog import kotlinx.serialization.Serializable @Serializable -data class BreedResult( +data class DogResult( val message: Map>, var status: String ) diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/NetworkBreedRepository.kt similarity index 57% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedRepository.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/NetworkBreedRepository.kt index 4f8c2b22..df74d05f 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/data/dog/NetworkBreedRepository.kt @@ -1,23 +1,24 @@ -package co.touchlab.kampkit.models +package co.touchlab.kampkit.data.dog -import co.touchlab.kampkit.DatabaseHelper -import co.touchlab.kampkit.db.Breed -import co.touchlab.kampkit.ktor.DogApi +import co.touchlab.kampkit.domain.breed.Breed +import co.touchlab.kampkit.domain.breed.BreedRepository import co.touchlab.kermit.Logger import co.touchlab.stately.ensureNeverFrozen import com.russhwolf.settings.Settings import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.datetime.Clock -class BreedRepository( - private val dbHelper: DatabaseHelper, +class NetworkBreedRepository( + private val dbHelper: DogDatabaseHelper, private val settings: Settings, private val dogApi: DogApi, log: Logger, private val clock: Clock -) { +) : BreedRepository { - private val log = log.withTag("BreedModel") + private val log = log.withTag("DogRepository") companion object { internal const val DB_TIMESTAMP_KEY = "DbTimestampKey" @@ -27,15 +28,19 @@ class BreedRepository( ensureNeverFrozen() } - fun getBreeds(): Flow> = dbHelper.selectAllItems() + override fun getBreeds(): Flow> { + return dbHelper.selectAllItems().map { list -> + list.map { dbBreed -> dbBreed.toDomain() } + } + } - suspend fun refreshBreedsIfStale() { + override suspend fun refreshBreedsIfStale() { if (isBreedListStale()) { refreshBreeds() } } - suspend fun refreshBreeds() { + override suspend fun refreshBreeds() { val breedResult = dogApi.getJsonFromApi() log.v { "Breed network result: ${breedResult.status}" } val breedList = breedResult.message.keys.sorted().toList() @@ -47,8 +52,11 @@ class BreedRepository( } } - suspend fun updateBreedFavorite(breed: Breed) { - dbHelper.updateFavorite(breed.id, !breed.favorite) + override suspend fun updateBreedFavorite(breedId: Long) { + val foundBreedsWithId = dbHelper.selectById(breedId).first() + foundBreedsWithId.firstOrNull()?.let { breed -> + dbHelper.updateFavorite(breed.id, !breed.favorite) + } } private fun isBreedListStale(): Boolean { @@ -60,4 +68,6 @@ class BreedRepository( } return stale } + + private fun co.touchlab.kampkit.db.DbBreed.toDomain() = Breed(id, name, favorite) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/Breed.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/Breed.kt new file mode 100644 index 00000000..27c7787e --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/Breed.kt @@ -0,0 +1,7 @@ +package co.touchlab.kampkit.domain.breed + +data class Breed( + val id: Long, + val name: String, + val favorite: 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 new file mode 100644 index 00000000..b5cbb2e2 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/domain/breed/BreedRepository.kt @@ -0,0 +1,10 @@ +package co.touchlab.kampkit.domain.breed + +import kotlinx.coroutines.flow.Flow + +interface BreedRepository { + fun getBreeds(): Flow> + suspend fun refreshBreedsIfStale() + suspend fun refreshBreeds() + suspend fun updateBreedFavorite(breedId: Long) +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ktor/DogApi.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ktor/DogApi.kt deleted file mode 100644 index 0e296ee0..00000000 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ktor/DogApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package co.touchlab.kampkit.ktor - -import co.touchlab.kampkit.response.BreedResult - -interface DogApi { - suspend fun getJsonFromApi(): BreedResult -} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewModel.kt similarity index 69% rename from shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt rename to shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewModel.kt index 27567f07..33897c9b 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewModel.kt @@ -1,6 +1,7 @@ -package co.touchlab.kampkit.models +package co.touchlab.kampkit.ui.breeds -import co.touchlab.kampkit.db.Breed +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.Job @@ -12,27 +13,23 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlin.native.ObjCName -@ObjCName("BreedViewModelDelegate") -class BreedViewModel( +@ObjCName("BreedsViewModelDelegate") +class BreedsViewModel( private val breedRepository: BreedRepository, log: Logger ) : ViewModel() { - private val log = log.withTag("BreedViewModel") + private val log = log.withTag("BreedsViewModel") - private val mutableBreedState: MutableStateFlow = - MutableStateFlow(BreedViewState(isLoading = true)) + private val mutableBreedState: MutableStateFlow = + MutableStateFlow(BreedsViewState(isLoading = true)) @NativeCoroutinesState - val breedState: StateFlow = mutableBreedState + val breedsState: StateFlow = mutableBreedState init { observeBreeds() } - override fun onCleared() { - log.v("Clearing BreedViewModel") - } - private fun observeBreeds() { // Refresh breeds, and emit any exception that was thrown so we can handle it downstream val refreshFlow = flow { @@ -53,7 +50,7 @@ class BreedViewModel( } else { previousState.error } - BreedViewState( + BreedsViewState( isLoading = false, breeds = breeds, error = errorMessage.takeIf { breeds.isEmpty() }, @@ -77,9 +74,9 @@ class BreedViewModel( } } - fun updateBreedFavorite(breed: Breed): Job { + fun updateBreedFavorite(breedId: Long): Job { return viewModelScope.launch { - breedRepository.updateBreedFavorite(breed) + breedRepository.updateBreedFavorite(breedId) } } @@ -87,7 +84,7 @@ class BreedViewModel( log.e(throwable) { "Error downloading breed list" } mutableBreedState.update { if (it.breeds.isEmpty()) { - BreedViewState(error = "Unable to refresh breed list") + BreedsViewState(error = "Unable to refresh breed list") } else { // Just let it fail silently if we have a cache it.copy(isLoading = false) @@ -95,16 +92,3 @@ class BreedViewModel( } } } - -data class BreedViewState( - val breeds: List = emptyList(), - val error: String? = null, - val isLoading: Boolean = false, - val isEmpty: Boolean = false -) { - companion object { - // This method lets you use the default constructor values in Swift. When accessing the - // constructor directly, they will not work there and would need to be provided explicitly. - fun default() = BreedViewState() - } -} 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 new file mode 100644 index 00000000..0a908bad --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/breeds/BreedsViewState.kt @@ -0,0 +1,16 @@ +package co.touchlab.kampkit.ui.breeds + +import co.touchlab.kampkit.domain.breed.Breed + +data class BreedsViewState( + val breeds: List = emptyList(), + val error: String? = null, + val isLoading: Boolean = false, + val isEmpty: Boolean = false +) { + companion object { + // This method lets you use the default constructor values in Swift. When accessing the + // constructor directly, they will not work there and would need to be provided explicitly. + fun default() = BreedsViewState() + } +} diff --git a/shared/src/commonMain/sqldelight/co/touchlab/kampkit/db/Table.sq b/shared/src/commonMain/sqldelight/co/touchlab/kampkit/db/Table.sq index a8b51359..7c4af76a 100644 --- a/shared/src/commonMain/sqldelight/co/touchlab/kampkit/db/Table.sq +++ b/shared/src/commonMain/sqldelight/co/touchlab/kampkit/db/Table.sq @@ -1,26 +1,26 @@ import kotlin.Boolean; -CREATE TABLE Breed ( +CREATE TABLE DbBreed ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, favorite INTEGER AS Boolean NOT NULL DEFAULT 0 ); selectAll: -SELECT * FROM Breed; +SELECT * FROM DbBreed; selectById: -SELECT * FROM Breed WHERE id = ?; +SELECT * FROM DbBreed WHERE id = ?; selectByName: -SELECT * FROM Breed WHERE name = ?; +SELECT * FROM DbBreed WHERE name = ?; insertBreed: -INSERT OR IGNORE INTO Breed(name) +INSERT OR IGNORE INTO DbBreed(name) VALUES (?); deleteAll: -DELETE FROM Breed; +DELETE FROM DbBreed; updateFavorite: -UPDATE Breed SET favorite = ? WHERE id = ?; +UPDATE DbBreed SET favorite = ? WHERE id = ?; diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedViewModelTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedsViewModelTest.kt similarity index 54% rename from shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedViewModelTest.kt rename to shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedsViewModelTest.kt index 38654cad..36f83861 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedViewModelTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedsViewModelTest.kt @@ -2,13 +2,15 @@ package co.touchlab.kampkit import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test -import co.touchlab.kampkit.db.Breed +import co.touchlab.kampkit.data.dog.DogDatabaseHelper +import co.touchlab.kampkit.data.dog.DogResult +import co.touchlab.kampkit.data.dog.NetworkBreedRepository +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.models.BreedRepository -import co.touchlab.kampkit.models.BreedViewModel -import co.touchlab.kampkit.models.BreedViewState -import co.touchlab.kampkit.response.BreedResult +import co.touchlab.kampkit.ui.breeds.BreedsViewModel +import co.touchlab.kampkit.ui.breeds.BreedsViewState import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig import com.russhwolf.settings.MapSettings @@ -23,10 +25,10 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.hours -class BreedViewModelTest { +class BreedsViewModelTest { private var kermit = Logger(StaticConfig()) private var testDbConnection = testDbConnection() - private var dbHelper = DatabaseHelper( + private var dbHelper = DogDatabaseHelper( testDbConnection, kermit, Dispatchers.Default @@ -37,20 +39,20 @@ class BreedViewModelTest { // Need to start at non-zero time because the default value for db timestamp is 0 private val clock = ClockMock(Clock.System.now()) - private val repository: BreedRepository = BreedRepository(dbHelper, settings, ktorApi, kermit, clock) - private val viewModel by lazy { BreedViewModel(repository, kermit) } + private val repository: BreedRepository = NetworkBreedRepository(dbHelper, settings, ktorApi, kermit, clock) + private val viewModel by lazy { BreedsViewModel(repository, kermit) } companion object { private val appenzeller = Breed(1, "appenzeller", false) private val australianNoLike = Breed(2, "australian", false) private val australianLike = Breed(2, "australian", true) - private val breedViewStateSuccessNoFavorite = BreedViewState( + private val breedsViewStateSuccessNoFavorite = BreedsViewState( breeds = listOf(appenzeller, australianNoLike) ) - private val breedViewStateSuccessFavorite = BreedViewState( + private val breedsViewStateSuccessFavorite = BreedsViewState( breeds = listOf(appenzeller, australianLike) ) - private val breedNames = breedViewStateSuccessNoFavorite.breeds.map { it.name } + private val breedNames = breedsViewStateSuccessNoFavorite.breeds.map { it.name } } @BeforeTest @@ -68,29 +70,29 @@ class BreedViewModelTest { fun `Get breeds without cache`() = runTest { ktorApi.prepareResult(ktorApi.successResult()) - viewModel.breedState.test { + viewModel.breedsState.test { assertEquals( - breedViewStateSuccessNoFavorite, - awaitItemPrecededBy(BreedViewState(isLoading = true), BreedViewState(isEmpty = true)) + breedsViewStateSuccessNoFavorite, + awaitItemPrecededBy(BreedsViewState(isLoading = true), BreedsViewState(isEmpty = true)) ) } } @Test fun `Get breeds empty`() = runTest { - ktorApi.prepareResult(BreedResult(emptyMap(), "success")) + ktorApi.prepareResult(DogResult(emptyMap(), "success")) - viewModel.breedState.test { + viewModel.breedsState.test { assertEquals( - BreedViewState(isEmpty = true), - awaitItemPrecededBy(BreedViewState(isLoading = true)) + BreedsViewState(isEmpty = true), + awaitItemPrecededBy(BreedsViewState(isLoading = true)) ) } } @Test fun `Get updated breeds with cache and preserve favorites`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) + settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) val successResult = ktorApi.successResult() val resultWithExtraBreed = successResult.copy(message = successResult.message + ("extra" to emptyList())) @@ -99,22 +101,22 @@ class BreedViewModelTest { dbHelper.insertBreeds(breedNames) dbHelper.updateFavorite(australianLike.id, true) - viewModel.breedState.test { - assertEquals(breedViewStateSuccessFavorite, awaitItemPrecededBy(BreedViewState(isLoading = true))) + viewModel.breedsState.test { + assertEquals(breedsViewStateSuccessFavorite, awaitItemPrecededBy(BreedsViewState(isLoading = true))) expectNoEvents() viewModel.refreshBreeds().join() // id is 5 here because it incremented twice when trying to insert duplicate breeds assertEquals( - BreedViewState(breedViewStateSuccessFavorite.breeds.plus(Breed(5, "extra", false))), - awaitItemPrecededBy(breedViewStateSuccessFavorite.copy(isLoading = true)) + BreedsViewState(breedsViewStateSuccessFavorite.breeds.plus(Breed(5, "extra", false))), + awaitItemPrecededBy(breedsViewStateSuccessFavorite.copy(isLoading = true)) ) } } @Test fun `Get updated breeds when stale and preserve favorites`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, (clock.currentInstant - 2.hours).toEpochMilliseconds()) + 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())) @@ -123,49 +125,49 @@ class BreedViewModelTest { dbHelper.insertBreeds(breedNames) dbHelper.updateFavorite(australianLike.id, true) - viewModel.breedState.test { + viewModel.breedsState.test { // id is 5 here because it incremented twice when trying to insert duplicate breeds assertEquals( - BreedViewState(breedViewStateSuccessFavorite.breeds.plus(Breed(5, "extra", false))), - awaitItemPrecededBy(BreedViewState(isLoading = true), breedViewStateSuccessFavorite) + BreedsViewState(breedsViewStateSuccessFavorite.breeds.plus(Breed(5, "extra", false))), + awaitItemPrecededBy(BreedsViewState(isLoading = true), breedsViewStateSuccessFavorite) ) } } @Test fun `Toggle favorite cached breed`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) + settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) dbHelper.insertBreeds(breedNames) dbHelper.updateFavorite(australianLike.id, true) - viewModel.breedState.test { - assertEquals(breedViewStateSuccessFavorite, awaitItemPrecededBy(BreedViewState(isLoading = true))) + viewModel.breedsState.test { + assertEquals(breedsViewStateSuccessFavorite, awaitItemPrecededBy(BreedsViewState(isLoading = true))) expectNoEvents() - viewModel.updateBreedFavorite(australianLike).join() + viewModel.updateBreedFavorite(australianLike.id).join() assertEquals( - breedViewStateSuccessNoFavorite, - awaitItemPrecededBy(breedViewStateSuccessFavorite.copy(isLoading = true)) + breedsViewStateSuccessNoFavorite, + awaitItemPrecededBy(breedsViewStateSuccessFavorite.copy(isLoading = true)) ) } } @Test fun `No web call if data is not stale`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) + settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) ktorApi.prepareResult(ktorApi.successResult()) dbHelper.insertBreeds(breedNames) - viewModel.breedState.test { - assertEquals(breedViewStateSuccessNoFavorite, awaitItemPrecededBy(BreedViewState(isLoading = true))) + viewModel.breedsState.test { + assertEquals(breedsViewStateSuccessNoFavorite, awaitItemPrecededBy(BreedsViewState(isLoading = true))) assertEquals(0, ktorApi.calledCount) expectNoEvents() viewModel.refreshBreeds().join() assertEquals( - breedViewStateSuccessNoFavorite, - awaitItemPrecededBy(breedViewStateSuccessNoFavorite.copy(isLoading = true)) + breedsViewStateSuccessNoFavorite, + awaitItemPrecededBy(breedsViewStateSuccessNoFavorite.copy(isLoading = true)) ) assertEquals(1, ktorApi.calledCount) } @@ -175,10 +177,10 @@ class BreedViewModelTest { fun `Display API error on first run`() = runTest { ktorApi.throwOnCall(RuntimeException("Test error")) - viewModel.breedState.test { + viewModel.breedsState.test { assertEquals( - BreedViewState(error = "Unable to download breed list"), - awaitItemPrecededBy(BreedViewState(isLoading = true), BreedViewState(isEmpty = true)) + BreedsViewState(error = "Unable to download breed list"), + awaitItemPrecededBy(BreedsViewState(isLoading = true), BreedsViewState(isEmpty = true)) ) } } @@ -186,13 +188,13 @@ class BreedViewModelTest { @Test fun `Ignore API error with cache`() = runTest { dbHelper.insertBreeds(breedNames) - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, (clock.currentInstant - 2.hours).toEpochMilliseconds()) + settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, (clock.currentInstant - 2.hours).toEpochMilliseconds()) ktorApi.throwOnCall(RuntimeException("Test error")) - viewModel.breedState.test { + viewModel.breedsState.test { assertEquals( - breedViewStateSuccessNoFavorite, - awaitItemPrecededBy(BreedViewState(isLoading = true)) + breedsViewStateSuccessNoFavorite, + awaitItemPrecededBy(BreedsViewState(isLoading = true)) ) expectNoEvents() @@ -200,8 +202,8 @@ class BreedViewModelTest { viewModel.refreshBreeds().join() assertEquals( - breedViewStateSuccessNoFavorite, - awaitItemPrecededBy(breedViewStateSuccessNoFavorite.copy(isLoading = true)) + breedsViewStateSuccessNoFavorite, + awaitItemPrecededBy(breedsViewStateSuccessNoFavorite.copy(isLoading = true)) ) } } @@ -210,10 +212,10 @@ class BreedViewModelTest { fun `Ignore API error on refresh with cache`() = runTest { ktorApi.prepareResult(ktorApi.successResult()) - viewModel.breedState.test { + viewModel.breedsState.test { assertEquals( - breedViewStateSuccessNoFavorite, - awaitItemPrecededBy(BreedViewState(isLoading = true), BreedViewState(isEmpty = true)) + breedsViewStateSuccessNoFavorite, + awaitItemPrecededBy(BreedsViewState(isLoading = true), BreedsViewState(isEmpty = true)) ) expectNoEvents() @@ -221,25 +223,25 @@ class BreedViewModelTest { viewModel.refreshBreeds().join() assertEquals( - breedViewStateSuccessNoFavorite, - awaitItemPrecededBy(breedViewStateSuccessNoFavorite.copy(isLoading = true)) + breedsViewStateSuccessNoFavorite, + awaitItemPrecededBy(breedsViewStateSuccessNoFavorite.copy(isLoading = true)) ) } } @Test fun `Show API error on refresh without cache`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) + settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) ktorApi.throwOnCall(RuntimeException("Test error")) - viewModel.breedState.test { - assertEquals(BreedViewState(isEmpty = true), awaitItemPrecededBy(BreedViewState(isLoading = true))) + viewModel.breedsState.test { + assertEquals(BreedsViewState(isEmpty = true), awaitItemPrecededBy(BreedsViewState(isLoading = true))) expectNoEvents() viewModel.refreshBreeds().join() assertEquals( - BreedViewState(error = "Unable to refresh breed list"), - awaitItemPrecededBy(BreedViewState(isEmpty = true, isLoading = true)) + BreedsViewState(error = "Unable to refresh breed list"), + awaitItemPrecededBy(BreedsViewState(isEmpty = true, isLoading = true)) ) } } @@ -247,7 +249,7 @@ class BreedViewModelTest { // There's a race condition where intermediate states can get missed if the next state comes too fast. // This function addresses that by awaiting an item that may or may not be preceded by the specified other items -private suspend fun ReceiveTurbine.awaitItemPrecededBy(vararg items: BreedViewState): BreedViewState { +private suspend fun ReceiveTurbine.awaitItemPrecededBy(vararg items: BreedsViewState): BreedsViewState { var nextItem = awaitItem() for (item in items) { if (item == nextItem) { diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/DogApiTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/DogApiTest.kt index 77998e35..b84921ea 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/DogApiTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/DogApiTest.kt @@ -1,7 +1,7 @@ package co.touchlab.kampkit -import co.touchlab.kampkit.ktor.DogApiImpl -import co.touchlab.kampkit.response.BreedResult +import co.touchlab.kampkit.data.dog.DogApiImpl +import co.touchlab.kampkit.data.dog.DogResult import co.touchlab.kermit.LogWriter import co.touchlab.kermit.Logger import co.touchlab.kermit.LoggerConfig @@ -40,7 +40,7 @@ class DogApiTest { val result = dogApi.getJsonFromApi() assertEquals( - BreedResult( + DogResult( mapOf( "affenpinscher" to emptyList(), "african" to listOf("shepherd") diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkDogRepositoryTest.kt similarity index 85% rename from shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedRepositoryTest.kt rename to shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkDogRepositoryTest.kt index 9656b8d3..e7850239 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedRepositoryTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/NetworkDogRepositoryTest.kt @@ -1,10 +1,11 @@ package co.touchlab.kampkit import app.cash.turbine.test -import co.touchlab.kampkit.db.Breed +import co.touchlab.kampkit.data.dog.DogDatabaseHelper +import co.touchlab.kampkit.data.dog.NetworkBreedRepository +import co.touchlab.kampkit.domain.breed.Breed import co.touchlab.kampkit.mock.ClockMock import co.touchlab.kampkit.mock.DogApiMock -import co.touchlab.kampkit.models.BreedRepository import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig import com.russhwolf.settings.MapSettings @@ -17,11 +18,11 @@ import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.time.Duration.Companion.hours -class BreedRepositoryTest { +class NetworkDogRepositoryTest { private var kermit = Logger(StaticConfig()) private var testDbConnection = testDbConnection() - private var dbHelper = DatabaseHelper( + private var dbHelper = DogDatabaseHelper( testDbConnection, kermit, Dispatchers.Default @@ -32,7 +33,7 @@ class BreedRepositoryTest { // Need to start at non-zero time because the default value for db timestamp is 0 private val clock = ClockMock(Clock.System.now()) - private val repository: BreedRepository = BreedRepository(dbHelper, settings, ktorApi, kermit, clock) + private val repository = NetworkBreedRepository(dbHelper, settings, ktorApi, kermit, clock) companion object { private val appenzeller = Breed(1, "appenzeller", false) @@ -78,7 +79,7 @@ class BreedRepositoryTest { @Test fun `Get updated breeds when stale and preserve favorites`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, (clock.currentInstant - 2.hours).toEpochMilliseconds()) + 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())) @@ -103,14 +104,14 @@ class BreedRepositoryTest { assertEquals(breedsFavorite, awaitItem()) expectNoEvents() - repository.updateBreedFavorite(australianLike) + repository.updateBreedFavorite(australianLike.id) assertEquals(breedsNoFavorite, awaitItem()) } } @Test fun `No web call if data is not stale`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) + settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, clock.currentInstant.toEpochMilliseconds()) ktorApi.prepareResult(ktorApi.successResult()) repository.refreshBreedsIfStale() @@ -132,7 +133,7 @@ class BreedRepositoryTest { @Test fun `Rethrow on API error when stale`() = runTest { - settings.putLong(BreedRepository.DB_TIMESTAMP_KEY, (clock.currentInstant - 2.hours).toEpochMilliseconds()) + settings.putLong(NetworkBreedRepository.DB_TIMESTAMP_KEY, (clock.currentInstant - 2.hours).toEpochMilliseconds()) ktorApi.throwOnCall(RuntimeException("Test error")) val throwable = assertFails { diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt index 1bb86861..0b72423c 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/SqlDelightTest.kt @@ -1,5 +1,6 @@ package co.touchlab.kampkit +import co.touchlab.kampkit.data.dog.DogDatabaseHelper import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig import kotlinx.coroutines.Dispatchers @@ -12,15 +13,15 @@ import kotlin.test.assertTrue class SqlDelightTest { - private lateinit var dbHelper: DatabaseHelper + private lateinit var dbHelper: DogDatabaseHelper - private suspend fun DatabaseHelper.insertBreed(name: String) { + private suspend fun DogDatabaseHelper.insertBreed(name: String) { insertBreeds(listOf(name)) } @BeforeTest fun setup() = runTest { - dbHelper = DatabaseHelper( + dbHelper = DogDatabaseHelper( testDbConnection(), Logger(StaticConfig()), Dispatchers.Default diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/TestAppInfo.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/TestAppInfo.kt index 3e243553..6cfd8476 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/TestAppInfo.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/TestAppInfo.kt @@ -1,5 +1,7 @@ package co.touchlab.kampkit +import co.touchlab.kampkit.core.AppInfo + object TestAppInfo : AppInfo { override val appId: String = "Test" } diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/mock/DogApiMock.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/mock/DogApiMock.kt index 5460f847..f1ae5d83 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/mock/DogApiMock.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/mock/DogApiMock.kt @@ -1,30 +1,30 @@ package co.touchlab.kampkit.mock -import co.touchlab.kampkit.ktor.DogApi -import co.touchlab.kampkit.response.BreedResult +import co.touchlab.kampkit.data.dog.DogApi +import co.touchlab.kampkit.data.dog.DogResult // TODO convert this to use Ktor's MockEngine class DogApiMock : DogApi { - private var nextResult: () -> BreedResult = { error("Uninitialized!") } + private var nextResult: () -> DogResult = { error("Uninitialized!") } var calledCount = 0 private set - override suspend fun getJsonFromApi(): BreedResult { + override suspend fun getJsonFromApi(): DogResult { val result = nextResult() calledCount++ return result } - fun successResult(): BreedResult { + fun successResult(): DogResult { val map = HashMap>().apply { put("appenzeller", emptyList()) put("australian", listOf("shepherd")) } - return BreedResult(map, "success") + return DogResult(map, "success") } - fun prepareResult(breedResult: BreedResult) { - nextResult = { breedResult } + fun prepareResult(dogResult: DogResult) { + nextResult = { dogResult } } fun throwOnCall(throwable: Throwable) { diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/KoinIOS.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt similarity index 84% rename from shared/src/iosMain/kotlin/co/touchlab/kampkit/KoinIOS.kt rename to shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt index 82ae4d7c..74e8c0c0 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/KoinIOS.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt @@ -1,7 +1,7 @@ -package co.touchlab.kampkit +package co.touchlab.kampkit.core import co.touchlab.kampkit.db.KaMPKitDb -import co.touchlab.kampkit.models.BreedViewModel +import co.touchlab.kampkit.ui.breeds.BreedsViewModel import co.touchlab.kermit.Logger import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings @@ -32,7 +32,7 @@ actual val platformModule = module { single { Darwin.create() } - single { BreedViewModel(get(), getWith("BreedViewModel")) } + single { BreedsViewModel(get(), getWith("BreedsViewModel")) } } // Access from Swift to create a logger @@ -42,5 +42,5 @@ fun Koin.loggerWithTag(tag: String) = @Suppress("unused") // Called from Swift object KotlinDependencies : KoinComponent { - fun getBreedViewModel() = getKoin().get() + fun getBreedsViewModel() = getKoin().get() } diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt similarity index 96% rename from shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt rename to shared/src/iosMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt index 296b6316..ef4ba26e 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/ViewModel.kt @@ -1,4 +1,4 @@ -package co.touchlab.kampkit.models +package co.touchlab.kampkit.core import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope import kotlinx.coroutines.MainScope diff --git a/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt b/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt index 99cf751d..52631c11 100644 --- a/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt +++ b/shared/src/iosTest/kotlin/co/touchlab/kampkit/KoinTest.kt @@ -1,5 +1,6 @@ package co.touchlab.kampkit +import co.touchlab.kampkit.core.initKoinIos import co.touchlab.kermit.Logger import org.koin.core.context.stopKoin import org.koin.core.parameter.parametersOf