MangaKu Save

MangaKu App Powered by Jetpack Compose, SwiftUI, MVI Pattern and Kotlin Multiplatform

Project README

MangaKu


Mangaku

🤖 Introduction

MangaKu App Powered by Kotlin Multiplatform, Jetpack Compose, SwiftUI and MVI Pattern!

Module

  • shared: data and domain layer
  • mangaku-ios: ios presentation layer
  • mangaku-android: android presentation layer

Table of Contents

🦾 Features

A few things you can do with MangaKu:

  • View Popular Manga
  • Easily search for any Manga
  • See Manga Detail
  • Save your favorite manga

⚠️ This project have no concern about backward compatibility, and only support the very latest or experimental api's for both android and ios ⚠️

🚗 Installation

  • Follow the KMP Guide by Jetbrains for getting started building a project with KMP.
  • Install Kotlin Multiplatform plugin in Android Studio
  • Clone or download the repo
  • Rebuild Project
  • To run in iOS, Open Xcode from the mangaku-ios folder

📸 Screenshot

💡 Libraries

shared:

mangaku-ios:

mangaku-android:

💨 Presentation State-Event

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)

image

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

🚀 Expect and Actual

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

☕️ Buy Me a Coffee

If you like this project please support me by Buy Me A Coffee ;-)

🏛 Project Structure

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
Open Source Agenda is not affiliated with "MangaKu" Project. README Source: uwaisalqadri/MangaKu
Stars
225
Open Issues
0
Last Commit
1 month ago

Open Source Agenda Badge

Open Source Agenda Rating