MangaKu
🤖
Introduction
MangaKu App Powered by Kotlin Multiplatform Mobile, Jetpack Compose, and SwiftUI
Module
core
: data and domain layeriosApp
: ios presentation layerandroidApp
: android presentation layerbuildSrc
:androidApp
andcore
dependencies
Table of Contents
- Introduction
- Features
- Installation
- Screenshot
- Libraries
- Domain to Presentation
- Expect and Actual
- Project Structure
🦾
Features
A few things you can do with MangaKu:
- View Popular Manga
- Easily search for any Manga
- See Manga Detail
- Save your favorite manga
🚗
Installation
- Follow the KMM Guide by Jetbrains for getting started building a project with KMM.
- Install Kotlin Multiplatform Mobile plugin in Android Studio
- Clone or download the repo
- Rebuild Project
- To run in iOS, Open Xcode and
pod install
insideiosApp
folder to install shared module and ios dependencies
📸
Screenshot
💡
Libraries
core
:
iosApp
:
androidApp
:
- Jetpack Compose
- Accompanist
- Koin
- Some Kotlinx & Jetpack Components
💨
Domain to Presentation
In Android, Because both core
and androidApp
write in Kotlin, we can simply collect flow :
private fun fetchManga() = viewModelScope.launch {
_uiState.value = UiState(loading = true)
trendingUseCase().collect { result ->
if (result.isNotEmpty()) _uiState.value = UiState(listManga = result)
}
}
But in iOS, we need to deal with swift, here i'm using createPublisher()
from KMPNativeCoroutines
to collect flow as Publisher in Combine
:
private func fetchTrendingManga() {
loading = true
createPublisher(for: trendingUseCase.invokeNative())
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .finished:
self.loading = false
case .failure(let error):
self.errorMessage = error.localizedDescription
}
} receiveValue: { value in
self.trendingManga = value
}.store(in: &cancellables)
}
🚀
Expect and Actual
in KMM, there is a negative case when there's no support to share code for some feature in both ios and android, and it's expensive to write separately in each module
so the solution is expect
and actual
expect
inside commonMain
and write "actual" implementation with actual
inside androidMain
and iosMain
and then each module will use expect
example:
commonMain/utils/DateFormatter.kt
expect fun formatDate(dateString: String, format: String): String
androidMain/utils/DateFormatter.kt
SimpleDateFormat
actual fun formatDate(dateString: String, format: String): String {
val date = SimpleDateFormat(Constants.formatFromApi).parse(dateString)
val dateFormatter = SimpleDateFormat(format, Locale.getDefault())
return dateFormatter.format(date ?: Date())
}
iosMain/utils/DateFormatter.kt
NSDateFormatter
actual fun formatDate(dateString: String, format: String): String {
val dateFormatter = NSDateFormatter().apply {
dateFormat = Constants.formatFromApi
}
val formatter = NSDateFormatter().apply {
dateFormat = format
locale = NSLocale(localeIdentifier = "id_ID")
}
return formatter.stringFromDate(dateFormatter.dateFromString(dateString) ?: NSDate())
}
yes, we can use Foundation
same as what we use in Xcode
🏛
Project Structure
core
:
data
mapper
entity
response
repository
source
local
entity
remote
response
di
domain
model
repository
usecase
browse
detail
mymanga
search
utils
androidApp
:
ui
composables
home
composables
favorite
search
detail
di
utils
iosApp
:
Dependency
App
Main
Resources
ReusableView
Extensions
Utils
Features
Browse
Views
Search
Detail
MyManga