[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-96155":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":16,"stars7d":16,"stars30d":17,"stars90d":16,"forks30d":16,"starsTrendScore":16,"compositeScore":18,"rankGlobal":10,"rankLanguage":10,"license":19,"archived":20,"fork":20,"defaultBranch":21,"hasWiki":20,"hasPages":22,"topics":23,"createdAt":10,"pushedAt":10,"updatedAt":31,"readmeContent":32,"aiSummary":33,"trendingCount":16,"starSnapshotCount":16,"syncStatus":15,"lastSyncTime":34,"discoverSource":35},96155,"nowinandroid-kmp","skydoves\u002Fnowinandroid-kmp","skydoves","📱 Now in Android KMP is a full Kotlin Multiplatform port of Google's Now in Android sample.","https:\u002F\u002Fskydoves.github.io\u002Fnowinandroid-kmp\u002F",null,"Kotlin",113,5,1,2,0,6,42.93,"Apache License 2.0",false,"main",true,[24,25,26,27,28,29,30,7],"android","compose","compose-multiplatform","jetpack-compose","kmp","kotlin-multiplatform","nowinandroid","2026-09-20 04:01:32","\u003Ch1 align=\"center\">Now in Android KMP\u003C\u002Fh1>\n\n\u003Cp align=\"center\">\n  \u003Ca href=\"https:\u002F\u002Fopensource.org\u002Flicenses\u002FApache-2.0\">\u003Cimg alt=\"License\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FLicense-Apache%202.0-blue.svg\"\u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fandroid-arsenal.com\u002Fapi?level=24\">\u003Cimg alt=\"API\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FAPI-24%2B-brightgreen.svg?style=flat\"\u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fmultiplatform.html\">\u003Cimg alt=\"Kotlin Multiplatform\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FKotlin-Multiplatform-7F52FF.svg\"\u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fskydoves\">\u003Cimg alt=\"Profile\" src=\"https:\u002F\u002Fskydoves.github.io\u002Fbadges\u002Fskydoves.svg\"\u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fskydoves.github.io\u002Fnowinandroid-kmp\u002F\">\u003Cimg alt=\"Web demo\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FWeb-Live%20Demo-4285F4.svg\"\u002F>\u003C\u002Fa>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n📱 Now in Android KMP is a full Kotlin Multiplatform port of Google's Now in Android sample, running the same screens, ViewModels, navigation, and data layer on \u003Cb>Android, iOS, desktop, and the browser\u003C\u002Fb> from one shared codebase.\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n🌐 \u003Cb>\u003Ca href=\"https:\u002F\u002Fskydoves.github.io\u002Fnowinandroid-kmp\u002F\">Open the web demo\u003C\u002Fa>\u003C\u002Fb> to run the app right now, no install. It is the wasm build of this repository, published from \u003Ccode>main\u003C\u002Fcode> on every push.\n\u003C\u002Fp>\n\n> [!IMPORTANT]\n> This project is a port of [**Now in Android**](https:\u002F\u002Fgithub.com\u002Fandroid\u002Fnowinandroid), designed and built by the Android team at Google and licensed under Apache 2.0. All of the product design, the app's content, its architecture guidance, and its demo data come from that project. This repository only moves it onto Kotlin Multiplatform; the original is the reference implementation and the place to look for the canonical Android version.\n\n> [!NOTE]\n> Every migration decision can be diffed against the source it came from. The upstream app is not\n> vendored here, it is its own repository, so clone it wherever you like:\n> `git clone https:\u002F\u002Fgithub.com\u002Fandroid\u002Fnowinandroid.git`. The port was made against\n> [`12f80da`](https:\u002F\u002Fgithub.com\u002Fandroid\u002Fnowinandroid\u002Fcommit\u002F12f80da6518e161ed16a06a68e71fb8a873576d6),\n> which is the commit to check out for a like-for-like diff.\n\n\u003Cp align=\"center\">\n\u003Cimg src=\"previews\u002Fscreenshot.png\"\u002F>\n\u003C\u002Fp>\n\n## Platforms\n\nThe same design system, feature modules, and data layer produce four applications:\n\n| Platform | Module | Run |\n|---|---|---|\n| Android | `app\u002FandroidApp` | `.\u002Fgradlew :app:androidApp:installDebug` |\n| iOS | `app\u002FiosApp` | `.\u002Fscripts\u002Fgenerate-xcodeproj.sh` then open `app\u002FiosApp\u002FNowInAndroid.xcodeproj` |\n| Desktop | `app\u002FdesktopApp` | `.\u002Fgradlew :app:desktopApp:run` |\n| Web | `app\u002FwebApp` | `.\u002Fgradlew :app:webApp:wasmJsBrowserDevelopmentRun`, or open the [live demo](https:\u002F\u002Fskydoves.github.io\u002Fnowinandroid-kmp\u002F) |\n\nThe iOS project is generated from `app\u002FiosApp\u002Fproject.yml` with [XcodeGen](https:\u002F\u002Fgithub.com\u002Fyonaskolb\u002FXcodeGen), and its \"Compile Kotlin\" build phase runs `:app:shared:embedAndSignAppleFrameworkForXcode`.\n\n\u003Ca href=\"https:\u002F\u002Fwww.android.skydoves.me\u002F\">\n\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002Fe014ce01-3461-40af-bb2a-eb44f3f55f36\" width=\"13%\" align=\"right\"\u002F>\n\u003C\u002Fa>\n\n## 📘 Manifest Android Interview\n\n[Manifest Android Interview](https:\u002F\u002Fandroid.skydoves.me\u002F) is a comprehensive guide designed to enhance your Android development expertise through 108 interview questions with detailed answers, 162 additional practical questions, and 50+ \"Pro Tips for Mastery\" sections. The interview questions primarily focus on Android development—including the Framework, UI, Jetpack Libraries, and Business Logic—as well as Jetpack Compose, covering Fundamentals, Runtime, and UI.\n\n\u003Ca href=\"https:\u002F\u002Fhowcomposeworks.com\u002F\">\n\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002F0f0f72fc-49ce-48b5-b3dd-f2c04e907f80\" width=\"13%\" align=\"right\"\u002F>\n\u003C\u002Fa>\n\n## 📗 Jetpack Compose Mechanisms Book\n\n[Jetpack Compose Mechanisms](https:\u002F\u002Fhowcomposeworks.com\u002F) takes you from \"how to use Compose\" into \"how Compose actually works,\" tracing the AOSP source line by line through the compiler, runtime, and UI layers beneath every Composable, with practical, production-ready examples from the author's own Compose tooling and libraries. It then ties all three layers together into deep, real-world performance tuning, from stability inference to the skip decision. Fully updated for Kotlin 2.4.0 and Compose Compiler 2.4.0. [The Course: Jetpack Compose Mechanisms](https:\u002F\u002Fdoveletter.dev\u002Fcourse\u002Fcompose) with 120+ practical questions with full answers and 240+ interactive assessments, +880 PDF page equivalents will enhance your Compose internals skills, and you can claim the certificate at the end.\n\n\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fdoveletter\">\n\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002F3ecd2a7b-9713-40cd-8817-fa568271cefa\" width=\"13%\" align=\"right\"\u002F>\n\u003C\u002Fa>\n\n## 🕊️ Dove Letter\n\nIf you're eager to dive deeper into Kotlin and Android, explore [Dove Letter](https:\u002F\u002Fgithub.com\u002Fdoveletter), a private subscription repository where you can learn, discuss, and share knowledge. To get more details about this unique opportunity, check out the [Learn Kotlin and Android With Dove Letter](https:\u002F\u002Fmedium.com\u002F@skydoves\u002Flearn-kotlin-and-android-with-dove-letter-26265da11903) article.\n\n## Tech stack & Open-source libraries\n\n- [Kotlin Multiplatform](https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fmultiplatform.html) with [Compose Multiplatform](https:\u002F\u002Fwww.jetbrains.com\u002Fcompose-multiplatform\u002F), targeting Android, iOS (arm64 + simulator arm64), desktop (JVM), and the browser (wasmJs).\n- [Coroutines](https:\u002F\u002Fgithub.com\u002FKotlin\u002Fkotlinx.coroutines) + [Flow](https:\u002F\u002Fkotlin.github.io\u002Fkotlinx.coroutines\u002Fkotlinx-coroutines-core\u002Fkotlinx.coroutines.flow\u002F) for asynchronous work, exactly as upstream.\n- [Metro](https:\u002F\u002Fgithub.com\u002FZacSweers\u002Fmetro): compile-time dependency injection, replacing Hilt, which has no Kotlin\u002FNative support. One `@DependencyGraph` per platform, every binding resolved at compile time, including the assisted `TopicViewModel` and `InterestsViewModel`.\n- [Ktor](https:\u002F\u002Fktor.io\u002F): the HTTP client, with the engine each platform ships (OkHttp on Android and desktop, Darwin on iOS, `fetch` in the browser). Replaces Retrofit + OkHttp.\n- [Sandwich](https:\u002F\u002Fgithub.com\u002Fskydoves\u002Fsandwich): every network call returns `ApiResponse`, so an HTTP error and a transport failure are two inspectable results rather than one thrown exception. `changeListSync` logs the typed failure before it unwraps.\n- [Room](https:\u002F\u002Fdeveloper.android.com\u002Fkotlin\u002Fmultiplatform\u002Froom) 3: the offline-first cache on every platform, FTS search included. The bundled native SQLite driver on Android, iOS and desktop; `androidx.sqlite:sqlite-web` over a Web Worker in the browser.\n- [DataStore](https:\u002F\u002Fdeveloper.android.com\u002Ftopic\u002Flibraries\u002Farchitecture\u002Fdatastore): user preferences, with a kotlinx.serialization `OkioSerializer` in place of the JVM-only protobuf one. The browser has no filesystem, so it stores the same serialized bytes in `localStorage`.\n- [Landscapist](https:\u002F\u002Fgithub.com\u002Fskydoves\u002Flandscapist): image loading on every target through `LandscapistImage` and its own engine. The engine decodes no SVG and every topic icon is one, so `landscapist-svg`'s `SvgImageDecoder` wraps the raster decoder and rasterises them (AndroidSVG on Android, Skia's `SVGDOM` elsewhere). Loading states use `ShimmerPlugin` from `landscapist-placeholder`, which takes the shape of the image it replaces. Upstream's fixed 80.dp spinner covered half of a news header and was clamped to a ring on a 32.dp topic icon.\n- [Navigation 3](https:\u002F\u002Fdeveloper.android.com\u002Fguide\u002Fnavigation\u002Fnavigation-3): the back stack is a list of route objects the app owns. Upstream had already migrated to Navigation 3, so its multi-back-stack `Navigator`\u002F`NavigationState` port over essentially unchanged, list-detail scene strategy and all.\n- [Compose Navigation Graph](https:\u002F\u002Fgithub.com\u002Fskydoves\u002Fcompose-nav-graph): extracts the navigation graph at compile time from `@NavDestination`\u002F`@NavEdge`, so a destination or transition cannot change unnoticed.\n- [Compose HotSwan](https:\u002F\u002Fhotswan.dev\u002F): hot reload for the running Android app, wired as an opt-in Gradle flag.\n- [Turbine](https:\u002F\u002Fgithub.com\u002Fcashapp\u002Fturbine) and [kotlinx-coroutines-test](https:\u002F\u002Fgithub.com\u002FKotlin\u002Fkotlinx.coroutines\u002Ftree\u002Fmaster\u002Fkotlinx-coroutines-test) for testing `Flow`.\n- [kotlinx.serialization](https:\u002F\u002Fgithub.com\u002FKotlin\u002Fkotlinx.serialization) for JSON, preferences, and the saved back stack, plus [KSP](https:\u002F\u002Fgithub.com\u002Fgoogle\u002Fksp) for Room and the navigation graph.\n\n## Architecture\n\nThe layering is unchanged from [Google's official architecture guidance](https:\u002F\u002Fdeveloper.android.com\u002Ftopic\u002Farchitecture) and from the upstream app: a UI layer over a data layer, with a small domain layer of use cases, and dependencies pointing only downward.\n\n\u003Cp align=\"center\">\n\u003Cimg src=\"figure\u002Farchitecture.png\" width=\"880\"\u002F>\n\u003C\u002Fp>\n\nEvery module above is a Kotlin Multiplatform module whose `commonMain` holds the real implementation. Platform source sets exist only where a platform genuinely differs, and each one is a named seam rather than a scattering of `if (isAndroid)` checks.\n\n### What was platform-specific, and what replaced it\n\n| Concern | Android original | Here |\n|---|---|---|\n| DI | Hilt \u002F Dagger | Metro `@DependencyGraph`, one per platform |\n| Networking | Retrofit + OkHttp | Ktor + Sandwich, engine per platform |\n| Demo data | `assets\u002F` + `AssetManager` | Compose Resources `Res.readBytes(\"files\u002F…\")` |\n| Database | Room 2 (Android) | Room 3; `sqlite-bundled` natively, `sqlite-web` in a Web Worker in the browser |\n| Preferences | Proto DataStore | DataStore + `OkioStorage`; `localStorage` in the browser |\n| Images | Coil 2 | Landscapist + `landscapist-svg` |\n| Resources | `R.string` \u002F `R.drawable` | Compose Resources, one `Res` per module |\n| Connectivity | `ConnectivityManager` | `ConnectivityManager` \u002F `NWPathMonitor` \u002F interface poll \u002F `navigator.onLine` |\n| Time zone | `ACTION_TIMEZONE_CHANGED` | broadcast on Android, poll elsewhere |\n| Notifications | `NotificationCompat` | `NotificationCompat` \u002F `UNUserNotificationCenter` \u002F AWT tray |\n| Background sync | WorkManager | WorkManager on Android, a mutex-guarded app-scope coroutine elsewhere |\n| Deep links | Activity `SavedStateHandle` | app-scoped `DeepLinkStore` written by each platform's entry point |\n| Jank tracking | JankStats | `JankMetricsState`, a no-op off Android |\n| Open a link | Chrome Custom Tabs | Custom Tabs \u002F `UIApplication.openURL` \u002F `java.awt.Desktop` \u002F `window.open` |\n\n### Running in a browser\n\nThe web demo lives at **[skydoves.github.io\u002Fnowinandroid-kmp](https:\u002F\u002Fskydoves.github.io\u002Fnowinandroid-kmp\u002F)**.\n[`deploy-web.yml`](.github\u002Fworkflows\u002Fdeploy-web.yml) builds `wasmJsBrowserDistribution` on every push\nto `main` and publishes it to GitHub Pages, so the demo is always the current state of the branch.\n\nThe wasm target reuses `commonMain` unchanged; `wasmJsMain` joins the same `nonAndroidMain` source\nset as iOS and desktop, so most platform seams are already satisfied. Three things are genuinely\nbrowser-only:\n\n- **The database runs in a Web Worker.** `androidx.sqlite:sqlite-web` is the only wasm driver, and it\n  ships the Kotlin half of a worker protocol without the worker, so `app\u002FwebApp` supplies one over\n  the official SQLite WASM build. That build is compiled with FTS5 and no FTS4, while Room only\n  offers `@Fts3`\u002F`@Fts4`, so the worker rewrites `USING FTS4(...)` to `USING fts5(...)`. The app only\n  ever asks the FTS tables for `MATCH`, `count(*)` and inserts, which behave the same either way.\n- **The database is in memory.** Persisting to OPFS requires the page to be cross-origin isolated,\n  so the browser build re-syncs on load instead. Preferences still persist, in `localStorage`.\n- **Topic icons are served from this origin.** The demo data points them at a Firebase Storage bucket\n  that answers browsers without an `Access-Control-Allow-Origin` header, so the fetch is blocked\n  before any decoder sees it. The nineteen icons are copied into the web app and requests rewritten\n  to point there. Some news header images come from hosts with the same restriction and fall back to\n  the placeholder; fixing those needs a proxy rather than a code change.\n\n### Offline first\n\nReads come exclusively from the local database, so every screen renders without a network round trip. `changeListSync` fetches the change list since the last sync, deletes what the server deleted, pulls the changed models in batches of 40, and only then advances the stored version. That is the same \"git fetch \u002F git pull \u002F move HEAD\" shape as upstream, now with the fetch modelled as an `ApiResponse`.\n\nSearch is Room's FTS4 index over titles, content, and topic descriptions. FTS4 honours a trailing `*` only, so `\"*query*\"` is really a prefix match; that is equally true on Android, and the upstream spelling is kept so behaviour matches.\n\n## Building\n\n```bash\n# Android\n.\u002Fgradlew :app:androidApp:installDebug\n\n# Desktop\n.\u002Fgradlew :app:desktopApp:run\n\n# Desktop installers (dmg \u002F msi \u002F deb)\n.\u002Fgradlew :app:desktopApp:packageDistributionForCurrentOS\n\n# Web (wasmJs), served at localhost:8080\n.\u002Fgradlew :app:webApp:wasmJsBrowserDevelopmentRun\n\n# iOS: generate the Xcode project, then build or open it\n.\u002Fscripts\u002Fgenerate-xcodeproj.sh\nopen app\u002FiosApp\u002FNowInAndroid.xcodeproj\n```\n\nRequires JDK 21 (the Gradle daemon toolchain is pinned in `gradle\u002Fgradle-daemon-jvm.properties`), the Android SDK, and Xcode plus [XcodeGen](https:\u002F\u002Fgithub.com\u002Fyonaskolb\u002FXcodeGen) for iOS.\n\n> [!NOTE]\n> There is no `iosX64` target: Room 3 and `androidx.sqlite` 2.7 stopped publishing it, so the simulator slice is Apple Silicon only and `EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64` is set in the Xcode project.\n\n## Testing\n\n**157 unit tests**, almost all of them in `commonTest`, so the same suite runs on three runtimes:\n\n```bash\n.\u002Fgradlew desktopTest              # 157 tests on the desktop JVM\n.\u002Fgradlew testAndroidHostTest      # 117 of them on the Android host JVM\n.\u002Fgradlew iosSimulatorArm64Test    # 138 of them on the iOS simulator\n.\u002Fgradlew spotlessCheck            # formatting and license headers\n.\u002Fgradlew navCheck                 # navigation graph baselines (all seven modules)\n```\n\nThey cover the data layer (`changeListSync` incremental sync, deletion, first-run read marking,\nnotification fan-out), the real Room SQL including the `CASE WHEN` filters and the many-to-many\njoin, the DataStore serializer, the Ktor + Sandwich success\u002FHTTP-error\u002Ftransport-failure branches,\nall six feature ViewModels, the multi-back-stack `Navigator`, and `NiaAppState` under a real\ncomposition via `runComposeUiTest`.\n\nThree groups cannot run everywhere, and each is in a source set that says so:\n\n| Where | Why |\n|---|---|\n| `nonAndroidTest`, Room DAO tests | the bundled SQLite driver loads a JNI library an Android *host* test cannot |\n| `nonAndroidTest`, `runComposeUiTest` cases | an Android host test has no window to compose into |\n| `desktopTest`, sync tests over the bundled demo JSON | Compose Resources needs a `Context` on Android, and a Kotlin\u002FNative test binary is not handed a dependency's resource bundle |\n\nThe browser target is deliberately not in that matrix. `wasmJsMain` joins `nonAndroidMain` so it\ninherits the shared platform code, but `wasmJsTest` does not join `nonAndroidTest`, whose reason for\nexisting is Room's bundled SQLite JNI library. The browser has no JNI and no bundled driver.\n\n## Hot reload\n\n[Compose HotSwan](https:\u002F\u002Fhotswan.dev\u002F) reloads Compose changes into the app already running on the\ndevice. Edit a composable, save, and the screen updates in place, keeping the state you had: the\ntopics you followed, how far you had scrolled, which screen you were on. No rebuild, no reinstall,\nno navigating back to where you were.\n\n\u003Cp align=\"center\">\n\u003Cimg src=\"previews\u002Fhot-reload.gif\" width=\"820\"\u002F>\n\u003C\u002Fp>\n\n## Find this library useful? :heart:\nSupport it by joining __[stargazers](https:\u002F\u002Fgithub.com\u002Fskydoves\u002Fnowinandroid-kmp\u002Fstargazers)__ for this repository. :star: \u003Cbr>\nAnd __[follow](https:\u002F\u002Fgithub.com\u002Fskydoves)__ me for my next creations! 🤩\n\n## License\n\n```\nDesigned and developed by 2026 skydoves (Jaewoong Eum)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http:\u002F\u002Fwww.apache.org\u002Flicenses\u002FLICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","Now in Android KMP 是一个基于 Kotlin Multiplatform（KMP）技术对 Google 官方 Now in Android 示例应用的全平台迁移项目，支持 Android、iOS、桌面（JVM）和 Web（WASM）四端共用同一套业务逻辑与 UI 代码。其核心功能包括跨平台 ViewModel、Jetpack Compose Multiplatform UI、统一导航与数据层（含 Room\u002FKMM 数据同步），并采用模块化架构与现代 Android 架构指南。适用于希望验证 KMP 实际落地能力、学习跨平台架构设计或快速构建多端一致体验的移动\u002F桌面\u002FWeb 应用开发场景。","2026-09-11 02:30:06","CREATED_QUERY"]