• Stars
    star
    238
  • Rank 169,306 (Top 4 %)
  • Language
    Kotlin
  • Created over 3 years ago
  • Updated 8 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

MangaKu


Mangaku

🤖 Introduction

MangaKu App Powered by Kotlin Multiplatform Mobile, Jetpack Compose, and SwiftUI

Module

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

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 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 inside mangaku-ios folder to install shared module and ios dependencies

📸 Screenshot

💡 Libraries

shared:

mangaku-ios:

mangaku-android:

💨 Domain to Presentation

In Android, Because both shared and mangaku-android written in Kotlin, we can simply collect flow :

private fun getTrendingManga() = viewModelScope.launch {
  _trendingManga.value = Result.loading()
  browseUseCase.getManga()
   .catch { cause: Throwable ->
     _trendingManga.value = Result.failed(cause)
   }
   .collect { result ->
     if (result.isNotEmpty())
     _trendingManga.value = Result.success(result)
   }
 }

But in iOS, we have to deal with swift, here i'm using createPublisher() from KMPNativeCoroutines to collect flow as Publisher in Combine :

func fetchTrendingManga() {
  trendingManga = .loading
  createPublisher(for: browseUseCase.getTrendingMangaNative())
   .receive(on: DispatchQueue.main)
   .sink { completion in
     switch completion {
       case .finished: ()
       case .failure(let error):
         self.trendingManga = .error(error: error)
       }
    } receiveValue: { value in
        self.trendingManga = .success(data: value)
    }.store(in: &cancellables)
}

or even better, you can use asyncFunction / asyncResult / asyncStream function to collect coroutine flow as new swift's concurrency features, checkout branch feat/experimenting-swift-new concurrency to see the example

combining two powerful concurrency feature from both native framework, how cool is that !?

func fetchTrendingManga() {
    Task {
      trendingManga = .loading
      do {
        let nativeFlow = try await asyncFunction(for: browseUseCase.getTrendingMangaNative())
        let stream = asyncStream(for: nativeFlow)
        for try await data in stream {
          trendingManga = .success(data: data)
        }
      } catch {
        trendingManga = .error(error: error)
      }
    }
  }

learn more: https://github.com/rickclephas/KMP-NativeCoroutines

🚀 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✨, 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())
}

yes, we can use Foundation same as what we use 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