Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

viewmodel expect/actual #238

Merged
merged 5 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ android {
dependencies {
implementation(project(":shared"))
implementation(libs.bundles.app.ui)
implementation(libs.multiplatformSettings.common)
implementation(libs.kotlinx.dateTime)
coreLibraryDesugaring(libs.android.desugaring)
implementation(libs.koin.android)
testImplementation(libs.junit)
Expand Down
72 changes: 0 additions & 72 deletions app/src/main/java/co/touchlab/kampkit/android/BreedViewModel.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.activity.compose.setContent
import co.touchlab.kampkit.android.ui.MainScreen
import co.touchlab.kampkit.android.ui.theme.KaMPKitTheme
import co.touchlab.kampkit.injectLogger
import co.touchlab.kampkit.models.BreedViewModel
import co.touchlab.kermit.Logger
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinComponent
Expand All @@ -22,8 +23,5 @@ class MainActivity : ComponentActivity(), KoinComponent {
MainScreen(viewModel, log)
}
}
if (viewModel.breedStateFlow.value.data == null) {
viewModel.refreshBreeds()
}
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/co/touchlab/kampkit/android/MainApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ 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 org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module

class MainApp : Application() {
Expand All @@ -16,7 +18,7 @@ class MainApp : Application() {
initKoin(
module {
single<Context> { this@MainApp }
viewModel { BreedViewModel() }
viewModel { BreedViewModel(get(), get { parametersOf("BreedViewModel") }) }
single<SharedPreferences> {
get<Context>().getSharedPreferences("KAMPSTARTER_SETTINGS", Context.MODE_PRIVATE)
}
Expand Down
10 changes: 5 additions & 5 deletions app/src/main/java/co/touchlab/kampkit/android/ui/Composables.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.flowWithLifecycle
import co.touchlab.kampkit.android.BreedViewModel
import co.touchlab.kampkit.android.R
import co.touchlab.kampkit.db.Breed
import co.touchlab.kampkit.models.BreedViewModel
import co.touchlab.kampkit.models.DataState
import co.touchlab.kampkit.models.ItemDataSummary
import co.touchlab.kermit.Logger
Expand All @@ -47,16 +47,16 @@ fun MainScreen(
log: Logger
) {
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleAwareDogsFlow = remember(viewModel.breedStateFlow, lifecycleOwner) {
viewModel.breedStateFlow.flowWithLifecycle(lifecycleOwner.lifecycle)
val lifecycleAwareDogsFlow = remember(viewModel.breeds, lifecycleOwner) {
viewModel.breeds.flowWithLifecycle(lifecycleOwner.lifecycle)
}

@SuppressLint("StateFlowValueCalledInComposition") // False positive lint check when used inside collectAsState()
val dogsState by lifecycleAwareDogsFlow.collectAsState(viewModel.breedStateFlow.value)
val dogsState by lifecycleAwareDogsFlow.collectAsState(viewModel.breeds.value)

MainScreenContent(
dogsState = dogsState,
onRefresh = { viewModel.refreshBreeds(true) },
onRefresh = { viewModel.refreshBreeds() },
onSuccess = { data -> log.v { "View updating with ${data.allItems.size} breeds" } },
onError = { exception -> log.e { "Displaying error: $exception" } },
onFavorite = { viewModel.updateBreedFavorite(it) }
Expand Down
4 changes: 4 additions & 0 deletions ios/KaMPKitiOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
3DFF917C64A18A83DA010EE1 /* Pods_KaMPKitiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B859F3FB23133D22AB9DD835 /* Pods_KaMPKitiOS.framework */; };
461C74AA2788F5F3004B1FFC /* CombineAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 461C74A92788F5F3004B1FFC /* CombineAdapters.swift */; };
46A5B5EF26AF54F7002EFEAA /* BreedListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */; };
46A5B60826B04921002EFEAA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46A5B60626B04920002EFEAA /* Main.storyboard */; };
46B5284D249C5CF400A7725D /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B5284C249C5CF400A7725D /* Koin.swift */; };
Expand Down Expand Up @@ -38,6 +39,7 @@
/* 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 = "<group>"; };
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 = "<group>"; };
461C74A92788F5F3004B1FFC /* CombineAdapters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineAdapters.swift; sourceTree = "<group>"; };
46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedListScreen.swift; sourceTree = "<group>"; };
46A5B60726B04920002EFEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
46B5284C249C5CF400A7725D /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -134,6 +136,7 @@
F1465F0B23AA94BF0055F7C3 /* LaunchScreen.storyboard */,
F1465F0E23AA94BF0055F7C3 /* Info.plist */,
46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */,
461C74A92788F5F3004B1FFC /* CombineAdapters.swift */,
);
path = KaMPKitiOS;
sourceTree = "<group>";
Expand Down Expand Up @@ -351,6 +354,7 @@
buildActionMask = 2147483647;
files = (
46B5284D249C5CF400A7725D /* Koin.swift in Sources */,
461C74AA2788F5F3004B1FFC /* CombineAdapters.swift in Sources */,
46A5B5EF26AF54F7002EFEAA /* BreedListScreen.swift in Sources */,
F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */,
);
Expand Down
1 change: 0 additions & 1 deletion ios/KaMPKitiOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

// Lazy so it doesn't try to initialize before startKoin() is called
// swiftlint:disable force_cast
lazy var log = koin.loggerWithTag(tag: "AppDelegate")

func application(_ application: UIApplication, didFinishLaunchingWithOptions
Expand Down
21 changes: 15 additions & 6 deletions ios/KaMPKitiOS/BreedListScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
// Copyright © 2021 Touchlab. All rights reserved.
//

import Combine
import SwiftUI
import shared

// swiftlint:disable force_cast
private let log = koin.loggerWithTag(tag: "ViewController")

class ObservableBreedModel: ObservableObject {
private var viewModel: NativeViewModel?
private var viewModel: BreedCallbackViewModel?

@Published
var loading = false
Expand All @@ -24,8 +24,12 @@ class ObservableBreedModel: ObservableObject {
@Published
var error: String?

private var cancellables = [AnyCancellable]()

func activate() {
viewModel = NativeViewModel { [weak self] dataState in
let viewModel = KotlinDependencies.shared.getBreedViewModel()

doPublish(viewModel.breeds) { [weak self] dataState in
self?.loading = dataState.loading
self?.breeds = dataState.data?.allItems
self?.error = dataState.exception
Expand All @@ -36,11 +40,16 @@ class ObservableBreedModel: ObservableObject {
if let errorMessage = dataState.exception {
log.e(message: {"Displaying error: \(errorMessage)"})
}
}
}.store(in: &cancellables)

self.viewModel = viewModel
}

func deactivate() {
viewModel?.onDestroy()
cancellables.forEach { $0.cancel() }
cancellables.removeAll()

viewModel?.clear()
viewModel = nil
}

Expand All @@ -49,7 +58,7 @@ class ObservableBreedModel: ObservableObject {
}

func refresh() {
viewModel?.refreshBreeds(forced: true)
viewModel?.refreshBreeds()
}
}

Expand Down
40 changes: 40 additions & 0 deletions ios/KaMPKitiOS/CombineAdapters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Combine
import shared

/// Create a Combine publisher from the supplied `FlowAdapter`. Use this in contexts where more transformation will be
/// done on the Swift side before the value is bound to UI
func createPublisher<T>(_ flowAdapter: FlowAdapter<T>) -> AnyPublisher<T, KotlinError> {
return Deferred<Publishers.HandleEvents<PassthroughSubject<T, KotlinError>>> {
let subject = PassthroughSubject<T, KotlinError>()
let canceller = flowAdapter.subscribe(
onEach: { item in subject.send(item) },
onComplete: { subject.send(completion: .finished) },
onThrow: { error in subject.send(completion: .failure(KotlinError(error))) }
)
return subject.handleEvents(receiveCancel: { canceller.cancel() })
}.eraseToAnyPublisher()
}

/// Prepare the supplied `FlowAdapter` to be bound to UI. The `onEach` callback will be called from `DispatchQueue.main`
/// on every new emission.
///
/// Note that this calls `assertNoFailure()` internally so you should handle errors upstream to avoid crashes.
func doPublish<T>(_ flowAdapter: FlowAdapter<T>, onEach: @escaping (T) -> Void) -> Cancellable {
return createPublisher(flowAdapter)
.assertNoFailure()
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { onEach($0) }
}

/// Wraps a `KotlinThrowable` in a `LocalizedError` which can be used as a Combine error type
class KotlinError: LocalizedError {
let throwable: KotlinThrowable

init(_ throwable: KotlinThrowable) {
self.throwable = throwable
}
var errorDescription: String? {
throwable.message
}
}
1 change: 1 addition & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ kotlin {
}
val androidMain by getting {
dependencies {
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.sqlDelight.android)
implementation(libs.ktor.client.okHttp)
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package co.touchlab.kampkit.models

import kotlinx.coroutines.CoroutineScope
import androidx.lifecycle.ViewModel as AndroidXViewModel
import androidx.lifecycle.viewModelScope as androidXViewModelScope

actual abstract class ViewModel actual constructor() : AndroidXViewModel() {
actual val viewModelScope: CoroutineScope = androidXViewModelScope

actual override fun onCleared() {
super.onCleared()
}
}
11 changes: 11 additions & 0 deletions shared/src/commonMain/kotlin/co/touchlab/kampkit/Koin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package co.touchlab.kampkit

import co.touchlab.kampkit.ktor.DogApi
import co.touchlab.kampkit.ktor.DogApiImpl
import co.touchlab.kampkit.models.BreedRepository
import co.touchlab.kermit.Logger
import co.touchlab.kermit.StaticConfig
import co.touchlab.kermit.platformLogWriter
Expand Down Expand Up @@ -63,6 +64,16 @@ private val coreModule = module {
// See https://github.com/touchlab/Kermit
val baseLogger = Logger(config = StaticConfig(logWriterList = listOf(platformLogWriter())), "KampKit")
factory { (tag: String?) -> if (tag != null) baseLogger.withTag(tag) else baseLogger }

single {
BreedRepository(
get(),
get(),
get(),
getWith("BreedRepository"),
get()
)
}
}

internal inline fun <reified T> Scope.getWith(vararg params: Any?): T {
Expand Down
3 changes: 0 additions & 3 deletions shared/src/commonMain/kotlin/co/touchlab/kampkit/Platform.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package co.touchlab.kampkit.models

import co.touchlab.kampkit.DatabaseHelper
import co.touchlab.kampkit.db.Breed
import co.touchlab.kampkit.injectLogger
import co.touchlab.kampkit.ktor.DogApi
import co.touchlab.kermit.Logger
import co.touchlab.stately.ensureNeverFrozen
Expand All @@ -11,15 +10,16 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.datetime.Clock
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class BreedModel : KoinComponent {
private val dbHelper: DatabaseHelper by inject()
private val settings: Settings by inject()
private val dogApi: DogApi by inject()
private val log: Logger by injectLogger("BreedModel")
private val clock: Clock by inject()
class BreedRepository(
private val dbHelper: DatabaseHelper,
private val settings: Settings,
private val dogApi: DogApi,
log: Logger,
private val clock: Clock
) {

private val log = log.withTag("BreedModel")

companion object {
internal const val DB_TIMESTAMP_KEY = "DbTimestampKey"
Expand Down
Loading