Open3
KMPの学びまとめ
- Firestoreの実装をそれぞれに寄せながらもKMPで共通化する方法
- Koinを使ってKotlin側はなるべくいい感じにしておく
1 commonMainのinterface定義
DataSourceのinterfaceを定義。
interface FirestoreDataSourceContract {
fun subscribeToScoreCollection(
onUpdate: (List<Score>) -> Unit,
onError: (Throwable) -> Unit
): Subscription
}
interface Subscription {
fun unsubscribe()
}
2 Koinの初期設定をcommonMainにそれぞれ書く
fun initAndroidKoin(appDeclaration:KoinAppDeclaration) = startKoin {
appDeclaration()
modules(
viewModelModule,
useCaseModule,
repositoryModule,
platformModule
)
}
fun initIosKoin(onKoinStart: () -> Module) {
startKoin {
modules(
viewModelModule,
useCaseModule,
repositoryModule,
onKoinStart(),
)
}
}
Swift側
1 KMP側
iosMainに連携用のボイラープレートを作っておく
fun createSwiftLibDependencyModule(factory: SwiftLibDependencyFactoryContract): Module = module {
single { factory.provideFirestoreDataSource() } bind FirestoreDataSourceContract::class
}
interface SwiftLibDependencyFactoryContract {
fun provideFirestoreDataSource(): FirestoreDataSourceContract
}
2 Swiftでの設定
まずエントリポイントで連携が初期化されるようにする
@main
struct iOSApp: App {
init() {
SharedKt.doInitIosKoin(
onKoinStart: {
Shared_iosKt.createSwiftLibDependencyModule(
factory: SwiftLibDependencyFactory.shared
)
}
)
}
}
Factoryはこのように
class SwiftLibDependencyFactory: SwiftLibDependencyFactoryContract {
static var shared = SwiftLibDependencyFactory()
func provideFirestoreDataSource() -> any FirestoreDataSourceContract {
return FirestoreDataSource()
}
}
DataSourceは次のようにした
class FirestoreDataSource: Shared.FirestoreDataSourceContract {
func subscribeToScoreCollection(
onUpdate: @escaping ([Score]) -> Void,
onError: @escaping (KotlinThrowable) -> Void
) -> any Subscription {
let db = Firestore.firestore()
let listener = db.collection("scores").addSnapshotListener { querySnapshot, error in
if let error = error {
onError(KotlinThrowable(message: error.localizedDescription))
return
}
guard let documents = querySnapshot?.documents else {
onUpdate([])
return
}
var scores = documents.map { $0.data() }.map { data in
Score(
userRef: (data["user"] as! DocumentReference).path,
score: data["score"] as! Int32,
updatedAt: timestampToMillis(data["updatedAt"] as! Timestamp)
)
}
scores.sort { $0.score > $1.score }
onUpdate(scores)
}
return ScoreCollectionSubscription(listener: listener)
}
}
class ScoreCollectionSubscription: Subscription {
let listener: ListenerRegistration?
init(listener: ListenerRegistration?) {
self.listener = listener
}
func unsubscribe() {
listener?.remove()
}
}
ViewModel
ViewModelはまず基本KMP側で書く
class RankingViewModel: ViewModel(), KoinComponent {
private val firestoreDataSource: FirestoreDataSourceContract by inject()
private val _scores = MutableStateFlow<List<Score>>(emptyList())
val scores: StateFlow<List<Score>> = _scores
private var subscription: Subscription? = null
fun startObservingScores() {
subscription = firestoreDataSource.subscribeToScoreCollection(
onUpdate = { scores ->
_scores.value = scores
},
onError = { error ->
// Handle the error
}
)
}
fun stopObservingScores() {
subscription?.unsubscribe()
subscription = null
}
}
StateFlowはそのままだと変更検知が出来なさそうなのでiosMainに購読処理的なものを生やしておいた
fun RankingViewModel.observeScores(onNewScores: (List<Score>) -> Unit): Job {
return viewModelScope.launch {
scores.collect { scoreList ->
onNewScores(scoreList)
}
}
}
そしてObservableObjectを作る
class RankingViewModelObserver: ObservableObject {
private let viewModel: RankingViewModel
private var job: Kotlinx_coroutines_coreJob?
@Published var scores: [Score] = []
init() {
self.viewModel = RankingViewModel()
self.job = viewModel.observeScores(onNewScores: { [weak self] scores in
self?.scores = scores
})
}
deinit {
viewModel.stopObservingScores()
job?.cancel(cause: nil)
}
func start() {
viewModel.startObservingScores()
}
}