{"id":20206368,"url":"https://github.com/uwaisalqadri/mangaku","last_synced_at":"2025-04-09T21:23:10.916Z","repository":{"id":38690886,"uuid":"388662667","full_name":"uwaisalqadri/MangaKu","owner":"uwaisalqadri","description":"MangaKu App Powered by Jetpack Compose, SwiftUI, MVI Pattern and Kotlin Multiplatform","archived":false,"fork":false,"pushed_at":"2024-04-04T14:44:46.000Z","size":9591,"stargazers_count":244,"open_issues_count":0,"forks_count":21,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-02T19:07:03.561Z","etag":null,"topics":["android","ios","jetpack-compose","kmm","kotlin","kotlin-multiplatform","ktor","realm","swift","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/uwaisalqadri.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-07-23T03:11:36.000Z","updated_at":"2025-04-01T07:21:48.000Z","dependencies_parsed_at":"2024-11-20T10:16:21.399Z","dependency_job_id":null,"html_url":"https://github.com/uwaisalqadri/MangaKu","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uwaisalqadri%2FMangaKu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uwaisalqadri%2FMangaKu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uwaisalqadri%2FMangaKu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uwaisalqadri%2FMangaKu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/uwaisalqadri","download_url":"https://codeload.github.com/uwaisalqadri/MangaKu/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248113041,"owners_count":21049771,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","ios","jetpack-compose","kmm","kotlin","kotlin-multiplatform","ktor","realm","swift","swiftui"],"created_at":"2024-11-14T05:23:31.321Z","updated_at":"2025-04-09T21:23:10.895Z","avatar_url":"https://github.com/uwaisalqadri.png","language":"Kotlin","funding_links":["https://www.buymeacoffee.com/uwaisalqadri"],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e MangaKu\u003c/h1\u003e \u003cbr\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://gitpoint.co/\"\u003e\n    \u003cimg alt=\"Mangaku\" title=\"Mangaku\" src=\"https://cdn.dribbble.com/users/5027078/screenshots/12022789/media/3e928c7fa9ac0a4e0c320c81302917ea.png\" width=\"500\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n## \u003ca name=\"introduction\"\u003e\u003c/a\u003e 🤖 Introduction\n\nMangaKu App Powered by Kotlin Multiplatform, Jetpack Compose, SwiftUI and MVI Pattern!\n\n**Module**\n\n* **`shared`**: data and domain layer\n* **`mangaku-ios`**: ios presentation layer\n* **`mangaku-android`**: android presentation layer\n\n## Table of Contents\n\n- [Introduction](#introduction)\n- [Features](#features)\n- [Installation](#installation)\n- [Screenshot](#screenshot)\n- [Libraries](#libraries)\n- [Presentation State-Event](#presentation-state-event)\n- [Expect and Actual](#expect-actual)\n- [Project Structure](#project-structure)\n\n## \u003ca name=\"features\"\u003e\u003c/a\u003e 🦾 Features\n\nA few things you can do with MangaKu:\n\n* View Popular Manga\n* Easily search for any Manga\n* See Manga Detail\n* Save your favorite manga\n\n⚠️ **`This project have no concern about backward compatibility, and only support the very latest or experimental api's for both android and ios `** ⚠️\n\n## \u003ca name=\"installation\"\u003e\u003c/a\u003e 🚗 Installation\n\n- Follow the [KMP Guide by Jetbrains](https://kotlinlang.org/docs/kmm-overview.html) for getting started building a project with KMP.\n- Install Kotlin Multiplatform plugin in Android Studio\n- Clone or download the repo\n- Rebuild Project\n- To run in iOS, Open Xcode from the `mangaku-ios` folder\n\n## \u003ca name=\"screenshot\"\u003e\u003c/a\u003e 📸 Screenshot\n\n\u003c!-- \u003cp align=\"center\"\u003e\n  \u003cimg src = \"https://i.ibb.co/K0fPv1s/Screen-Shot-2021-10-04-at-13-56-33.png\" width=400\u003e\n\u003c/p\u003e --\u003e\n\n## \u003ca name=\"libraries\"\u003e\u003c/a\u003e 💡 Libraries\n\n`shared`:\n* [Ktor](https://github.com/ktorio/ktor)\n* [Realm-Kotlin](https://github.com/realm/realm-kotlin)\n* [KMPNativeCoroutines](https://github.com/rickclephas/KMP-NativeCoroutines)\n* [KMMViewModel](https://github.com/rickclephas/KMM-ViewModel)\n* [Koin](https://github.com/InsertKoinIO/koin)\n* [Kermit](https://github.com/touchlab/Kermit)\n\n`mangaku-ios`:\n* [Swift's New Concurrency](https://developer.apple.com/news/?id=2o3euotz)\n* [SDWebImage](https://github.com/SDWebImage/SDWebImage)\n* [SwiftUI](https://developer.apple.com/documentation/swiftui)\n\n`mangaku-android`:\n* [Jetpack Compose](https://developer.android.com/jetpack/compose)\n* [Accompanist](https://github.com/google/accompanist)\n* [Koin](https://github.com/InsertKoinIO/koin)\n* [Compose Destinations](https://github.com/raamcosta/compose-destinations)\n* Some Kotlinx \u0026 Jetpack Components\n\n## \u003ca name=\"presentation-state-event\"\u003e\u003c/a\u003e 💨 Presentation State-Event\nI'm using [KMMViewModel](https://github.com/rickclephas/KMM-ViewModel) library to share ViewModel that will be consumed by both Android and iOS with State and Event on each ViewModel (following the MVI Pattern)\n\n![image](https://github.com/uwaisalqadri/MangaKu/assets/55146646/7f7cf567-3d26-41b0-a910-1511376da379)\n\nState and Event\n```kotlin\ndata class MyMangaState(\n    val mangas: List\u003cManga\u003e = listOf(),\n    val isFavorite: Boolean = false,\n    val isLoading: Boolean = false,\n    val isEmpty: Boolean = false,\n    val errorMessage: String = \"\"\n)\n\nsealed class MyMangaEvent {\n    data object GetMyMangas: MyMangaEvent()\n    data class CheckFavorite(val mangaId: String): MyMangaEvent()\n    data class AddFavorite(val manga: Manga): MyMangaEvent()\n    data class DeleteFavorite(val mangaId: String): MyMangaEvent()\n    data object Empty: MyMangaEvent()\n}\n```\nReducing State and Event\n\n**MyMangaViewModel.kt**\n```kotlin\nfun onTriggerEvent(event: MyMangaEvent) {\n   when (event) {\n      is MyMangaEvent.GetMyMangas -\u003e {\n         getMyManga()\n      }\n     is MyMangaEvent.Empty -\u003e {\n         _state.value = MyMangaState(isEmpty = true)\n      }\n     is MyMangaEvent.CheckFavorite -\u003e {\n         checkFavorite(event.mangaId)\n      }\n     is MyMangaEvent.AddFavorite -\u003e {\n         addMyManga(event.manga)\n     }\n     is MyMangaEvent.DeleteFavorite -\u003e {\n         deleteMyManga(event.mangaId)\n     }\n  }        \n}\n\n private fun checkFavorite(mangaId: String) = viewModelScope.coroutineScope.launch {\n    myMangaUseCase.getMyMangaById(mangaId).collect { result -\u003e\n       _state.value = _state.value.copy(isFavorite = result.map { it.id }.contains(mangaId))\n    }\n}\n\nprivate fun getMyManga() = viewModelScope.coroutineScope.launch {\n    _state.value = _state.value.copy(isLoading = true)\n\n     myMangaUseCase.getMyManga().catch { cause: Throwable -\u003e\n        _state.value = _state.value.copy(errorMessage = cause.message.orEmpty())\n     }.collect {\n        if (it.isEmpty()) _state.value = MyMangaState(isEmpty = true)\n        else _state.value = MyMangaState(mangas = it)\n     }\n}\n```\n\nCompose UI based on State that triggered from Event\n\n**DetailScreen.kt**\n```kotlin\nButton(\n   elevation = ButtonDefaults.elevation(0.dp, 0.dp),\n   onClick = {\n       setShowDialog(true)\n       if (!viewState.isLoading) {\n           viewState.manga?.let {\n              if (favState.isFavorite) mangaViewModel.onTriggerEvent(MyMangaEvent.DeleteFavorite(it.id))\n                else mangaViewModel.onTriggerEvent(MyMangaEvent.AddFavorite(it))\n            }\n         }\n    }\n) {\n   Icon(\n     imageVector = if (favState.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,\n     contentDescription = null,\n     tint = Color.Red,\n     modifier = Modifier.size(25.dp),\n  )\n}\n\n```\n\n**DetailPageView.swift**\n```swift\n.navigationBarItems(trailing: Button(action: {\n   if let data = viewState.manga {\n     favState.isFavorite ? mangaViewModel.onTriggerEvent(event: MyMangaEvent.DeleteFavorite(mangaId: data.id))\n     : mangaViewModel.onTriggerEvent(event: MyMangaEvent.AddFavorite(manga: data))\n    isShowDialog.toggle()\n   }\n}) {\n  Image(systemName: favState.isFavorite ? \"heart.fill\" : \"heart\")\n    .resizable()\n    .foregroundColor(.red)\n    .frame(width: 22, height: 20)\n})\n```\n\n## \u003ca name=\"expect-actual\"\u003e\u003c/a\u003e 🚀 Expect and Actual\nin 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\n\nso the solution is ✨`expect` and `actual`✨, we can write `expect` inside `commonMain` and write \"actual\" implementation with `actual` inside `androidMain` and `iosMain`\nand then each module will use `expect`\n\nexample:\n\n[**`commonMain/utils/DateFormatter.kt`**](https://github.com/uwaisalqadri/MangaKu/blob/master/core/src/commonMain/kotlin/com/uwaisalqadri/mangaku/utils/DateFormatter.kt)\n```kotlin\nexpect fun formatDate(dateString: String, format: String): String\n```\n\n[**`androidMain/utils/DateFormatter.kt`**](https://github.com/uwaisalqadri/MangaKu/blob/master/core/src/androidMain/kotlin/com/uwaisalqadri/mangaku/utils/DateFormatter.kt)\n\nSimpleDateFormat\n\n```kotlin\nactual fun formatDate(dateString: String, format: String): String {\n    val date = SimpleDateFormat(Constants.formatFromApi).parse(dateString)\n    val dateFormatter = SimpleDateFormat(format, Locale.getDefault())\n    return dateFormatter.format(date ?: Date())\n}\n\n```\n\n[**`iosMain/utils/DateFormatter.kt`**](https://github.com/uwaisalqadri/MangaKu/blob/master/core/src/iosMain/kotlin/com/uwaisalqadri/mangaku/utils/DateFormatter.kt)\n\nNSDateFormatter\n\n```kotlin\nactual fun formatDate(dateString: String, format: String): String {\n    val dateFormatter = NSDateFormatter().apply {\n\tdateFormat = Constants.formatFromApi\n     }\n\n    val formatter = NSDateFormatter().apply {\n\tdateFormat = format\n\tlocale = NSLocale(localeIdentifier = \"id_ID\")\n     }\n\n    return formatter.stringFromDate(dateFormatter.dateFromString(dateString) ?: NSDate())\n}\n\n```\nwe definitely can use `Foundation` the same way we use it in Xcode\n\n## \u003ca name=\"buy-me-coffee\"\u003e\u003c/a\u003e ☕️ Buy Me a Coffee\nIf you like this project please support me by \u003ca href=\"https://www.buymeacoffee.com/uwaisalqadri\" target=\"_blank\"\u003e\u003cimg src=\"https://cdn.buymeacoffee.com/buttons/v2/default-blue.png\" alt=\"Buy Me A Coffee\" height=32\u003e\u003c/a\u003e ;-)\n\n## \u003ca name=\"project-structure\"\u003e\u003c/a\u003e 🏛 Project Structure\n**`shared`**:\n\n* `data`\n  - `mapper`\n  - `repository`\n  - `source`\n    - `local`\n    \t- `entity`\n    - `remote`\n      - `response`\n* `di`\n* `domain`\n  - `model`\n  - `repository`\n  - `usecase`\n    - `browse`\n    - `detail`\n    - `mymanga`\n    - `search` \n* `utils`\n\n**`mangaku-android`**:\n - `ui`\n    - `composables`\n    - `home`\n      - `composables`\n    - `favorite`\n    - `search`\n    - `detail` \n- `di`\n- `utils`\n\n**`mangaku-ios`**: \n - `Dependency`\n - `App`\n - `Main`\n - `Resources`\n - `ReusableView`\n - `Extensions`\n - `Utils`\n - `Features`\n    - `Browse`\n        - `Navigator`\n        - `Views`\n    - `Search`\n    - `Detail`\n    - `MyManga`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fuwaisalqadri%2Fmangaku","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fuwaisalqadri%2Fmangaku","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fuwaisalqadri%2Fmangaku/lists"}