GithubSearchKMM
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)
Modern Development
- Kotlin Multiplatform
- Jetpack Compose
- Kotlin Coroutines & Flows
- Dagger Hilt
- SwiftUI
- Koin Dependency Injection
- FlowRedux State Management
- Shared KMP ViewModel
- Clean Architecture
Tech Stacks
- Functional & Reactive programming with Kotlin Coroutines with Flow
- Clean Architecture with MVI (Uni-directional data flow)
- Multiplatform ViewModel
- 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.
- MOKO KSwift is a gradle plugin for generation Swift-friendly API for Kotlin/Native framework.
- 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.
Screenshots
Android (Light theme)
Android (Dark theme)
iOS (Light theme)
iOS (Dark theme)
Overall Architecture
What is shared?
- domain: Domain models, UseCases, Repositories.
- presentation: ViewModels, ViewState, ViewSingleEvent, ViewAction.
- data: Repository Implementations, Remote Data Source, Local Data Source.
- utils: Utilities, Logging Library
Unidirectional data flow - FlowRedux
- My implementation. Credits: freeletics/FlowRedux
- See more docs and concepts at freeletics/RxRedux
public sealed interface FlowReduxStore<Action, State> {
public val coroutineScope: CoroutineScope
public val stateFlow: StateFlow<State>
/** Get streams of actions.
*
* This [Flow] includes dispatched [Action]s (via [dispatch] function)
* and [Action]s returned from [SideEffect]s.
*/
public val actionSharedFlow: SharedFlow<Action>
/**
* @return false if cannot dispatch action ([coroutineScope] was cancelled).
*/
public fun dispatch(action: Action): Boolean
}
Multiplatform ViewModel
open class GithubSearchViewModel(
searchRepoItemsUseCase: SearchRepoItemsUseCase,
) : ViewModel() {
private val store = viewModelScope.createFlowReduxStore(
initialState = GithubSearchState.initial(),
sideEffects = GithubSearchSideEffects(
searchRepoItemsUseCase = searchRepoItemsUseCase,
).sideEffects,
reducer = { state, action -> action.reduce(state) }
)
private val eventChannel = store.actionSharedFlow
.mapNotNull { it.toGithubSearchSingleEventOrNull() }
.buffer(Channel.UNLIMITED)
.produceIn(viewModelScope)
fun dispatch(action: GithubSearchAction) = store.dispatch(action)
val stateFlow: StateFlow<GithubSearchState> by store::stateFlow
val eventFlow: Flow<GithubSearchSingleEvent> get() = eventChannel.receiveAsFlow()
}
Platform ViewModel
Android
Extends GithubSearchViewModel
to use Dagger Constructor Injection
.
@HiltViewModel
class DaggerGithubSearchViewModel @Inject constructor(searchRepoItemsUseCase: SearchRepoItemsUseCase) :
GithubSearchViewModel(searchRepoItemsUseCase)
iOS
Conform to ObservableObject
and use @Published
property wrapper.
import Foundation
import Combine
import shared
import sharedSwift
@MainActor
class IOSGithubSearchViewModel: ObservableObject {
private let vm: GithubSearchViewModel
@Published private(set) var state: GithubSearchState
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.typedValue()
vm.stateFlow.subscribeNonNullFlow(
scope: vm.viewModelScope,
onValue: { [weak self] in self?.state = $0 }
)
}
@discardableResult
func dispatch(action: GithubSearchAction) -> Bool {
self.vm.dispatch(action: action)
}
deinit {
Napier.d("\(self)::deinit")
vm.clear()
}
}
Download APK
Building & Develop
-
Android Studio Chipmunk | 2021.2.1
(note: Java 11 is now the minimum version required). -
XCode 13.2
or 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 Studio
and 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.When you see any error like this:
./GithubSearchKMM/iosApp/iosApp/ContentView.swift:4:8: No such module 'sharedSwift'
You can run the following commands (must select
Read from disk
inside Xcode):# go to iosApp directory cd iosApp # install pods pod install
Then, you can build and run inside Xcode as usual.
LOC
--------------------------------------------------------------------------------
Language Files Lines Blank Comment Code
--------------------------------------------------------------------------------
Kotlin 96 7111 863 398 5850
JSON 7 3938 0 0 3938
Swift 16 857 110 98 649
Markdown 1 255 47 0 208
Bourne Shell 2 245 28 110 107
Batch 1 91 21 0 70
XML 7 71 6 0 65
--------------------------------------------------------------------------------
Total 130 12568 1075 606 10887
--------------------------------------------------------------------------------