MangaKu App Powered by Jetpack Compose, SwiftUI, MVI Pattern and Kotlin Multiplatform
MangaKu App Powered by Kotlin Multiplatform, Jetpack Compose, SwiftUI and MVI Pattern!
Module
shared
: data and domain layermangaku-ios
: ios presentation layermangaku-android
: android presentation layerA few things you can do with MangaKu:
⚠️ This project have no concern about backward compatibility, and only support the very latest or experimental api's for both android and ios
⚠️
mangaku-ios
foldershared
:
mangaku-ios
:
mangaku-android
:
I'm using KMMViewModel library to share ViewModel that will be consumed by both Android and iOS with State and Event on each ViewModel (following the MVI Pattern)
State and Event
data class MyMangaState(
val mangas: List<Manga> = listOf(),
val isFavorite: Boolean = false,
val isLoading: Boolean = false,
val isEmpty: Boolean = false,
val errorMessage: String = ""
)
sealed class MyMangaEvent {
data object GetMyMangas: MyMangaEvent()
data class CheckFavorite(val mangaId: String): MyMangaEvent()
data class AddFavorite(val manga: Manga): MyMangaEvent()
data class DeleteFavorite(val mangaId: String): MyMangaEvent()
data object Empty: MyMangaEvent()
}
Reducing State and Event
MyMangaViewModel.kt
fun onTriggerEvent(event: MyMangaEvent) {
when (event) {
is MyMangaEvent.GetMyMangas -> {
getMyManga()
}
is MyMangaEvent.Empty -> {
_state.value = MyMangaState(isEmpty = true)
}
is MyMangaEvent.CheckFavorite -> {
checkFavorite(event.mangaId)
}
is MyMangaEvent.AddFavorite -> {
addMyManga(event.manga)
}
is MyMangaEvent.DeleteFavorite -> {
deleteMyManga(event.mangaId)
}
}
}
private fun checkFavorite(mangaId: String) = viewModelScope.coroutineScope.launch {
myMangaUseCase.getMyMangaById(mangaId).collect { result ->
_state.value = _state.value.copy(isFavorite = result.map { it.id }.contains(mangaId))
}
}
private fun getMyManga() = viewModelScope.coroutineScope.launch {
_state.value = _state.value.copy(isLoading = true)
myMangaUseCase.getMyManga().catch { cause: Throwable ->
_state.value = _state.value.copy(errorMessage = cause.message.orEmpty())
}.collect {
if (it.isEmpty()) _state.value = MyMangaState(isEmpty = true)
else _state.value = MyMangaState(mangas = it)
}
}
Compose UI based on State that triggered from Event
DetailScreen.kt
Button(
elevation = ButtonDefaults.elevation(0.dp, 0.dp),
onClick = {
setShowDialog(true)
if (!viewState.isLoading) {
viewState.manga?.let {
if (favState.isFavorite) mangaViewModel.onTriggerEvent(MyMangaEvent.DeleteFavorite(it.id))
else mangaViewModel.onTriggerEvent(MyMangaEvent.AddFavorite(it))
}
}
}
) {
Icon(
imageVector = if (favState.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = null,
tint = Color.Red,
modifier = Modifier.size(25.dp),
)
}
DetailPageView.swift
.navigationBarItems(trailing: Button(action: {
if let data = viewState.manga {
favState.isFavorite ? mangaViewModel.onTriggerEvent(event: MyMangaEvent.DeleteFavorite(mangaId: data.id))
: mangaViewModel.onTriggerEvent(event: MyMangaEvent.AddFavorite(manga: data))
isShowDialog.toggle()
}
}) {
Image(systemName: favState.isFavorite ? "heart.fill" : "heart")
.resizable()
.foregroundColor(.red)
.frame(width: 22, height: 20)
})
in KMP, 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
✨, we can write 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())
}
we definitely can use Foundation
the same way we use it in Xcode
If you like this project please support me by ;-)
shared
:
data
mapper
repository
source
local
entity
remote
response
di
domain
model
repository
usecase
browse
detail
mymanga
search
utils
mangaku-android
:
ui
composables
home
composables
favorite
search
detail
di
utils
mangaku-ios
:
Dependency
App
Main
Resources
ReusableView
Extensions
Utils
Features
Browse
Navigator
Views
Search
Detail
MyManga