Coder Social home page Coder Social logo

uwaisalqadri / mangaku Goto Github PK

View Code? Open in Web Editor NEW
224.0 7.0 20.0 9.37 MB

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

Kotlin 74.36% Swift 25.64%
jetpack-compose kmm ktor swiftui kotlin swift android ios kotlin-multiplatform realm

mangaku's Introduction

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

mangaku's People

Contributors

uwaisalqadri avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

mangaku's Issues

[Bug]: API calls triggering twice

Contact Details

[email protected]

What happened?

I checked detailUseCase.getDetailManga(mangaId) from DetailViewModel in mangaku-android , the API call is triggered twice and also the same issue happening in other API's too. Please assist on it.

Version

1.0

What android version are you seeing the problem on?

Android 11

Relevant screenshots and details

No response

How to reproduce bug

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

[Bug]: Realm.close wasn't called

Contact Details

[email protected]

What happened?

When are we supposed to call realm.close ? I was trying to take some inspiration for that in this project but i can't see it called anywhere here too .

Isn't it essential to call realm.close ?

https://www.mongodb.com/docs/realm/sdk/java/realm-files/open-and-close-a-realm/#std-label-java-close-a-realm

Version

Latest Version

What android version are you seeing the problem on?

No response

Relevant screenshots and details

No response

How to reproduce bug

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.