Github Repos Search - Kotlin Multiplatform Mobile using Jetpack Compose, SwiftUI, FlowRedux, Coroutines Flow, Dagger Hilt, Koin Dependency Injection, shared KMP ViewModel, Clean Architecture
Minimal Kotlin Multiplatform project with SwiftUI, Jetpack Compose.
- Android (Jetpack compose)
- iOS (SwiftUI)
Liked some of my work? Buy me a coffee (or more likely a beer)
- Kotlin Multiplatform
- Jetpack Compose
- Kotlin Coroutines & Flows
- Dagger Hilt
- SwiftUI
- Koin Dependency Injection
- FlowRedux State Management
- Shared KMP ViewModel
- Clean Architecture
- Functional & Reactive programming with Kotlin Coroutines with Flow
- Clean Architecture with MVI (Uni-directional data flow)
- Multiplatform ViewModel and SavedStateHandle (save and restore states across process death), by @hoc081098
- Multiplatform FlowRedux State Management
- Ξrrow - Functional companion to Kotlin's Standard Library
- Dependency injection
- iOS: Koin
- Android: Dagger Hilt
 
- Declarative UI
- iOS: SwiftUI
- Android: Jetpack Compose
 
- Ktor client library for networking
- Kotlinx Serialization for JSON serialization/deserialization.
- Napier for Multiplatform Logging.
- FlowExt provides many kotlinx.coroutines.Flow operators, by @hoc081098
- Touchlab SKIE a Swift-friendly API Generator for Kotlin Multiplatform.
- kotlinx.collections.immutable: immutable collection interfaces and implementation prototypes for Kotlin..
- Testing
- Kotlin Test for running tests with Kotlin Multiplatform.
- Turbine for KotlinX Coroutines Flows testing.
- Mockative: mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API.
- Kotlinx-Kover for Kotlin Multiplatform code coverage.
 
|  |  |  |  | 
|  |  |  |  | 
|  |  |  |  | 
|  |  |  |  | 
- domain: Domain models, UseCases, Repositories.
- presentation: ViewModels, ViewState, ViewSingleEvent, ViewAction.
- data: Repository Implementations, Remote Data Source, Local Data Source.
- utils: Utilities, Logging Library
- My implementation. Credits: freeletics/FlowRedux
- See more docs and concepts at freeletics/RxRedux
public sealed interface FlowReduxStore<Action, State> {
  /**
   * The state of this store.
   */
  public val stateFlow: StateFlow<State>
  /**
   * @return false if cannot dispatch action (this store was closed).
   */
  public fun dispatch(action: Action): Boolean
  /**
   * Call this method to close this store.
   * A closed store will not accept any action anymore, thus state will not change anymore.
   * All [SideEffect]s will be cancelled.
   */
  public fun close()
  /**
   * After calling [close] method, this function will return true.
   *
   * @return true if this store was closed.
   */
  public fun isClosed(): Boolean
}open class GithubSearchViewModel(
  searchRepoItemsUseCase: SearchRepoItemsUseCase,
  private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
  private val effectsContainer = GithubSearchSideEffectsContainer(searchRepoItemsUseCase)
  private val store = viewModelScope.createFlowReduxStore(
    initialState = GithubSearchState.initial(),
    sideEffects = effectsContainer.sideEffects,
    reducer = Reducer(flip(GithubSearchAction::reduce))
      .withLogger(githubSearchFlowReduxLogger())
  )
  val termStateFlow: NonNullStateFlowWrapper<String> = savedStateHandle.getStateFlow(TERM_KEY, "").wrap()
  val stateFlow: NonNullStateFlowWrapper<GithubSearchState> = store.stateFlow.wrap()
  val eventFlow: NonNullFlowWrapper<GithubSearchSingleEvent> = effectsContainer.eventFlow.wrap()
  init {
    store.dispatch(InitialSearchAction(termStateFlow.value))
  }
  @MainThread
  fun dispatch(action: GithubSearchAction): Boolean {
    if (action is GithubSearchAction.Search) {
      savedStateHandle[TERM_KEY] = action.term
    }
    return store.dispatch(action)
  }
  companion object {
    private const val TERM_KEY = "com.hoc081098.github_search_kmm.presentation.GithubSearchViewModel.term"
    /**
     * Used by non-Android platforms.
     */
    fun create(searchRepoItemsUseCase: SearchRepoItemsUseCase): GithubSearchViewModel =
      GithubSearchViewModel(searchRepoItemsUseCase, SavedStateHandle())
  }
}Extends GithubSearchViewModel to use Dagger Constructor Injection.
@HiltViewModel
class DaggerGithubSearchViewModel @Inject constructor(
  searchRepoItemsUseCase: SearchRepoItemsUseCase,
  savedStateHandle: SavedStateHandle,
) : GithubSearchViewModel(searchRepoItemsUseCase, savedStateHandle)Conform to ObservableObject and use @Published property wrapper.
import Foundation
import Combine
import shared
@MainActor
class IOSGithubSearchViewModel: ObservableObject {
  private let vm: GithubSearchViewModel
  @Published private(set) var state: GithubSearchState
  @Published private(set) var term: String = ""
  let eventPublisher: AnyPublisher<GithubSearchSingleEventKs, Never>
  init(vm: GithubSearchViewModel) {
    self.vm = vm
    self.eventPublisher = vm.eventFlow.asNonNullPublisher()
      .assertNoFailure()
      .map(GithubSearchSingleEventKs.init)
      .eraseToAnyPublisher()
    self.state = vm.stateFlow.value
    vm.stateFlow.subscribe(
      scope: vm.viewModelScope,
      onValue: { [weak self] in self?.state = $0 }
    )
    self.vm
      .termStateFlow
      .asNonNullPublisher(NSString.self)
      .assertNoFailure()
      .map { $0 as String }
      .assign(to: &$term)
  }
  @discardableResult
  func dispatch(action: GithubSearchAction) -> Bool {
    self.vm.dispatch(action: action)
  }
  deinit {
    Napier.d("\(self)::deinit")
    vm.clear()
  }
}- 
Android Studio Hedgehog | 2023.1.1(note: Java 17 is now the minimum version required).
- 
Xcode 13.2.1or later (due to use of new Swift 5.5 concurrency APIs).
- 
Clone project: git clone https://github.com/hoc081098/GithubSearchKMM.git
- 
Android: open project by Android Studioand run as usual.
- 
iOS # Cd to root project directory cd GithubSearchKMM # Setup sh scripts/run_ios.sh There's a Build Phase script that will do the magic. π§ 
 Cmd + B to build
 Cmd + R to run.You can also build and run iOS app from Xcode as usual. 
--------------------------------------------------------------------------------
 Language             Files        Lines        Blank      Comment         Code
--------------------------------------------------------------------------------
 Kotlin                 116         7942          996          453         6493
 JSON                     7         3938            0            0         3938
 Swift                   16          960          124          102          734
 Markdown                 1          281           53            0          228
 Bourne Shell             2          249           28          116          105
 Batch                    1           92           21            0           71
 XML                      6           69            6            0           63
--------------------------------------------------------------------------------
 Total                  149        13531         1228          671        11632
--------------------------------------------------------------------------------
