跳到正文

skydoves

nowinandroid-kmp

📱 Now in Android KMP is a full Kotlin Multiplatform port of Google's Now in Android sample.

README 已保存到本站,可直接阅读

Documentation snapshot

README 快照

这篇是英文原文

下面正文是项目自己的英文 README。想读全文就用浏览器自带的整页翻译: Chrome / Edge 点地址栏右侧的翻译图标,或用右键菜单里的「翻译成中文」; 手机浏览器一般在菜单里。

本页保存的是公开项目资料快照,阅读过程不需要连接 GitHub。

Now in Android KMP

📱 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 Android, iOS, desktop, and the browser from one shared codebase.

🌐 Open the web demo to run the app right now, no install. It is the wasm build of this repository, published from main on every push.

[!IMPORTANT] This project is a port of Now in Android, 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.

[!NOTE] Every migration decision can be diffed against the source it came from. The upstream app is not vendored here, it is its own repository, so clone it wherever you like: git clone https://github.com/android/nowinandroid.git. The port was made against 12f80da, which is the commit to check out for a like-for-like diff.

Platforms

The same design system, feature modules, and data layer produce four applications:

PlatformModuleRun
Androidapp/androidApp./gradlew :app:androidApp:installDebug
iOSapp/iosApp./scripts/generate-xcodeproj.sh then open app/iosApp/NowInAndroid.xcodeproj
Desktopapp/desktopApp./gradlew :app:desktopApp:run
Webapp/webApp./gradlew :app:webApp:wasmJsBrowserDevelopmentRun, or open the live demo

The iOS project is generated from app/iosApp/project.yml with XcodeGen, and its “Compile Kotlin” build phase runs :app:shared:embedAndSignAppleFrameworkForXcode.

📘 Manifest Android Interview

Manifest Android Interview 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.

📗 Jetpack Compose Mechanisms Book

Jetpack Compose Mechanisms 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 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.

🕊️ Dove Letter

If you’re eager to dive deeper into Kotlin and Android, explore Dove Letter, 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 article.

Tech stack & Open-source libraries

  • Kotlin Multiplatform with Compose Multiplatform, targeting Android, iOS (arm64 + simulator arm64), desktop (JVM), and the browser (wasmJs).
  • Coroutines + Flow for asynchronous work, exactly as upstream.
  • Metro: compile-time dependency injection, replacing Hilt, which has no Kotlin/Native support. One @DependencyGraph per platform, every binding resolved at compile time, including the assisted TopicViewModel and InterestsViewModel.
  • Ktor: the HTTP client, with the engine each platform ships (OkHttp on Android and desktop, Darwin on iOS, fetch in the browser). Replaces Retrofit + OkHttp.
  • Sandwich: 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.
  • Room 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.
  • DataStore: 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.
  • Landscapist: 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.
  • Navigation 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/NavigationState port over essentially unchanged, list-detail scene strategy and all.
  • Compose Navigation Graph: extracts the navigation graph at compile time from @NavDestination/@NavEdge, so a destination or transition cannot change unnoticed.
  • Compose HotSwan: hot reload for the running Android app, wired as an opt-in Gradle flag.
  • Turbine and kotlinx-coroutines-test for testing Flow.
  • kotlinx.serialization for JSON, preferences, and the saved back stack, plus KSP for Room and the navigation graph.

Architecture

The layering is unchanged from Google’s official architecture guidance 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.

Every 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.

What was platform-specific, and what replaced it

ConcernAndroid originalHere
DIHilt / DaggerMetro @DependencyGraph, one per platform
NetworkingRetrofit + OkHttpKtor + Sandwich, engine per platform
Demo dataassets/ + AssetManagerCompose Resources Res.readBytes("files/…")
DatabaseRoom 2 (Android)Room 3; sqlite-bundled natively, sqlite-web in a Web Worker in the browser
PreferencesProto DataStoreDataStore + OkioStorage; localStorage in the browser
ImagesCoil 2Landscapist + landscapist-svg
ResourcesR.string / R.drawableCompose Resources, one Res per module
ConnectivityConnectivityManagerConnectivityManager / NWPathMonitor / interface poll / navigator.onLine
Time zoneACTION_TIMEZONE_CHANGEDbroadcast on Android, poll elsewhere
NotificationsNotificationCompatNotificationCompat / UNUserNotificationCenter / AWT tray
Background syncWorkManagerWorkManager on Android, a mutex-guarded app-scope coroutine elsewhere
Deep linksActivity SavedStateHandleapp-scoped DeepLinkStore written by each platform’s entry point
Jank trackingJankStatsJankMetricsState, a no-op off Android
Open a linkChrome Custom TabsCustom Tabs / UIApplication.openURL / java.awt.Desktop / window.open

Running in a browser

The web demo lives at skydoves.github.io/nowinandroid-kmp. deploy-web.yml builds wasmJsBrowserDistribution on every push to main and publishes it to GitHub Pages, so the demo is always the current state of the branch.

The wasm target reuses commonMain unchanged; wasmJsMain joins the same nonAndroidMain source set as iOS and desktop, so most platform seams are already satisfied. Three things are genuinely browser-only:

  • The database runs in a Web Worker. androidx.sqlite:sqlite-web is the only wasm driver, and it ships the Kotlin half of a worker protocol without the worker, so app/webApp supplies one over the official SQLite WASM build. That build is compiled with FTS5 and no FTS4, while Room only offers @Fts3/@Fts4, so the worker rewrites USING FTS4(...) to USING fts5(...). The app only ever asks the FTS tables for MATCH, count(*) and inserts, which behave the same either way.
  • The database is in memory. Persisting to OPFS requires the page to be cross-origin isolated, so the browser build re-syncs on load instead. Preferences still persist, in localStorage.
  • Topic icons are served from this origin. The demo data points them at a Firebase Storage bucket that answers browsers without an Access-Control-Allow-Origin header, so the fetch is blocked before any decoder sees it. The nineteen icons are copied into the web app and requests rewritten to point there. Some news header images come from hosts with the same restriction and fall back to the placeholder; fixing those needs a proxy rather than a code change.

Offline first

Reads 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 / git pull / move HEAD” shape as upstream, now with the fetch modelled as an ApiResponse.

Search 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.

Building

# Android
./gradlew :app:androidApp:installDebug

# Desktop
./gradlew :app:desktopApp:run

# Desktop installers (dmg / msi / deb)
./gradlew :app:desktopApp:packageDistributionForCurrentOS

# Web (wasmJs), served at localhost:8080
./gradlew :app:webApp:wasmJsBrowserDevelopmentRun

# iOS: generate the Xcode project, then build or open it
./scripts/generate-xcodeproj.sh
open app/iosApp/NowInAndroid.xcodeproj

Requires JDK 21 (the Gradle daemon toolchain is pinned in gradle/gradle-daemon-jvm.properties), the Android SDK, and Xcode plus XcodeGen for iOS.

[!NOTE] 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.

Testing

157 unit tests, almost all of them in commonTest, so the same suite runs on three runtimes:

./gradlew desktopTest              # 157 tests on the desktop JVM
./gradlew testAndroidHostTest      # 117 of them on the Android host JVM
./gradlew iosSimulatorArm64Test    # 138 of them on the iOS simulator
./gradlew spotlessCheck            # formatting and license headers
./gradlew navCheck                 # navigation graph baselines (all seven modules)

They cover the data layer (changeListSync incremental sync, deletion, first-run read marking, notification fan-out), the real Room SQL including the CASE WHEN filters and the many-to-many join, the DataStore serializer, the Ktor + Sandwich success/HTTP-error/transport-failure branches, all six feature ViewModels, the multi-back-stack Navigator, and NiaAppState under a real composition via runComposeUiTest.

Three groups cannot run everywhere, and each is in a source set that says so:

WhereWhy
nonAndroidTest, Room DAO teststhe bundled SQLite driver loads a JNI library an Android host test cannot
nonAndroidTest, runComposeUiTest casesan Android host test has no window to compose into
desktopTest, sync tests over the bundled demo JSONCompose Resources needs a Context on Android, and a Kotlin/Native test binary is not handed a dependency’s resource bundle

The browser target is deliberately not in that matrix. wasmJsMain joins nonAndroidMain so it inherits the shared platform code, but wasmJsTest does not join nonAndroidTest, whose reason for existing is Room’s bundled SQLite JNI library. The browser has no JNI and no bundled driver.

Hot reload

Compose HotSwan reloads Compose changes into the app already running on the device. Edit a composable, save, and the screen updates in place, keeping the state you had: the topics you followed, how far you had scrolled, which screen you were on. No rebuild, no reinstall, no navigating back to where you were.

Find this library useful? :heart:

Support it by joining stargazers for this repository. :star: And follow me for my next creations! 🤩

License

Designed and developed by 2026 skydoves (Jaewoong Eum)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Official distribution

获取与安装

暂未发现可确认的官方软件包地址

当前 README 快照没有出现 npm、PyPI、Crates.io、pub.dev 等官方包页链接。本站不会根据仓库名称猜测下载地址。

本站不托管项目文件;需要安装时,请以项目维护者发布的官方文档为准。

使用前核验

本站保存公开资料用于阅读,不代表安全审计或功能背书。安装前请核对许可证、依赖来源和发布签名,不要直接运行来源不明的二进制文件或高权限脚本。