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()
  }
}