/MangaKu

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

Primary LanguageKotlin

MangaKu


MovieCatalogue

🤖 Introduction

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

Module

  • core: data and domain layer
  • iosApp: ios presentation layer
  • androidApp: android presentation layer
  • buildSrc: androidApp and core 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 iosApp folder to install shared module and ios dependencies

📸 Screenshot

💡 Libraries

core:

iosApp:

androidApp:

💨 Domain to Presentation

In Android, Because both core and androidApp 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

🏛 Project Structure

core:

  • data
    • mapper
    • 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
      • Navigator
      • Views
    • Search
    • Detail
    • MyManga