Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

摘星(Notes)

一个基于 Kotlin Multiplatform (KMP) + Compose Multiplatform + 模块化架构 的跨平台应用示例,覆盖 Android 与 iOS,聚焦于以下能力:

  • 模块化拆分(androidApp / iosApp / composeApp / core / feature / android/output
  • MVI 状态管理
  • Navigation3 导航与登录拦截
  • 邮件列表分页展示(使用 Paging3)
  • DataStore(JSON)本地数据存储(Android 端支持 Android Keystore 加密)
  • Ktor 网络层统一封装(Android/ iOS 引擎)
  • Koin 依赖注入
  • 自适应布局(Material3 Adaptive)

目录


项目结构

Notes
├── androidApp # Android 入口应用
├── iosApp # iOS 入口应用(Xcode 工程)
├── composeApp # 共享应用入口与跨平台 UI/导航
├── core
│ ├── framework # MVI 基类、导航协议、全局 Effect
│ ├── data # 数据层(多平台实现)
│ ├── network # Ktor 网络层(多平台实现)
│ └── theme # Compose 主题与通用组件
├── feature
│ ├── login # 登录功能(MVI)
│ ├── main # 首页/收藏/邮件详情
│ └── settings # 设置页(偏好项映射 + 退出登录)
└── android
├── output
│ └── login # Fused Library 实验打包模块(Android)
└── baselineprofile # Android 基线配置文件

settings.gradle.kts 中启用模块:

  • :androidApp
  • :iosApp
  • :composeApp
  • :core:data
  • :core:theme
  • :core:network
  • :core:framework
  • :feature:main
  • :feature:login
  • :feature:settings
  • :android:output:login
  • :android:baselineprofile

技术栈

KMP / 工程

  • AGP: 9.1.0
  • Kotlin: 2.3.20
  • JDK: 21
  • Android compileSdk: 36
  • Android minSdk: 28

UI

  • Compose Multiplatform(org.jetbrains.compose
  • Material3 + Material3 Adaptive
  • Navigation3(androidx.navigation3

架构与异步

  • MVI(自定义 MviViewModel
  • Kotlin Coroutines + Flow
  • Koin DI

数据层

  • Room3 + Paging3
  • DataStore + Kotlinx Serialization
  • Android Keystore + AES/GCM(Android)

网络层

  • Ktor Client(OkHttp / Darwin 引擎)
  • kotlinx serialization

架构设计

1) 分层与职责

  • androidApp:Android 入口与平台集成。
  • iosApp:iOS 入口(Xcode 工程)。
  • composeApp:共享应用入口、跨平台 UI 与导航。
  • feature:*:按业务功能拆分 UI + ViewModel + Intent/Action/State。
  • core:data:本地数据源、模型与仓储,多平台实现。
  • core:network:Ktor 客户端、拦截器、错误统一处理。
  • core:framework:MVI 与导航协议复用。
  • core:theme:主题、Toast、Dialog、TopBar 等通用 UI。

2) MVI 模式

core/framework/mvi/MviViewModel 提供统一能力:

  • intent 输入(MutableSharedFlow
  • state 持有(MutableStateFlow
  • effect 单次事件流(Toast、Dialog、导航等)
  • 基于 SavedStateHandle 的状态恢复

每个 feature 使用:

  • State:当前 UI 状态
  • Intent:用户输入
  • Action + Reducer:纯状态变换
  • ViewModel:处理 Intent 与副作用

3) 导航与登录拦截

应用使用 Navigation3 + 自定义 Destination 协议。

  • 需要登录访问的页面实现 RequireLogin
  • AppNavHost 在导航前执行 navCheck
    • 未登录 + 目标需登录 => 记录 pendingDestination,跳转登录页。
    • 登录成功后自动恢复到 pendingDestination

这套机制同样适配 Deep Link(例如直接打开邮件详情)。

4) 数据流(简化)

Composable -> Intent -> ViewModel -> Repository
<- State <- Reducer <- Action
Repository -> Room/DataStore/Network -> Flow<PagingData/Model>

核心功能

登录(feature:login

  • 账户/密码输入与本地校验(LoginValidator
  • 模拟异步登录,成功后写入 UserRepository
  • 通过全局 Toast 通知登录成功

首页与收藏(feature:main

  • 邮件分页展示(Paging3)
  • 搜索(防抖 300ms)
  • 批量收藏、取消收藏、删除
  • 自适应 List-Detail 布局,支持不同窗口尺寸

设置(feature:settings

  • 动态主题色开关
  • 深色模式(跟随系统/浅色/深色)
  • 退出登录(二次确认对话框)

Deep Link

  • 支持域名:https://notes.zhangls.me
  • 支持路径:/email(代码内解析 id 参数)

快速开始

环境要求

  • Android Studio(建议最新稳定版)
  • Xcode(建议最新稳定版)
  • JDK 21
  • Android SDK / Build Tools 36
  • 可用 Android 模拟器或真机(Android 9+)

本地配置(Android)

在项目根目录创建或更新 local.properties

sdk.dir=/path/to/Android/sdk
# 打包签名(debug/release 都会读取 release signingConfig)signing.path=/path/to/your.jks
signing.storePassword=***
signing.keyAlias=***
signing.keyPassword=***

说明:

  • 当前 androidApp/build.gradle.ktsdebugrelease 都绑定 release 签名配置。
  • 如果本地不需要签名打包,可自行调整 buildTypes.debug.signingConfig

构建与运行

构建项目

./gradlew build

运行 Android

./gradlew :androidApp:installDebug

运行 iOS

使用 Xcode 打开 iosApp/iosApp.xcodeproj 运行。


Deep Link 调试(Android)

通过 ADB 触发邮件详情页(若未登录会先走登录拦截,登录成功后回跳):

adb shell am start \
-a android.intent.action.VIEW \
-d "https://notes.zhangls.me/email?id=1&token=debug" \
me.zhangls.notes

Fused Library 打包(Android 实验)

项目包含 :android:output:login 模块(com.android.fused-library),用于融合导出登录相关能力。

./gradlew :android:output:login:assemble

注意:该能力目前在 Android 官方仍属实验性质,仓库内也已标注“仅供测试”。


数据与安全

本地数据

  • Android 端使用 Room:AccountEntityEmailEntity,并启用 schema 导出。
  • DataStore:
    • settings.json -> SettingsModel
    • user.json -> UserModel?

加密策略(Android)

  • DataStore 序列化读写前后使用 AESUtils 处理。
  • 密钥由 Android Keystore 管理(AES/GCM/NoPadding)。

网络结果统一

  • safeApiCall 将异常和业务 code 映射到 NetworkResult
    • Success<T>
    • Failure(NetworkError.*)

当前实现边界

以下内容是当前代码中的真实状态,便于二次开发时快速判断:

  • TokenProviderImplgetAccessToken/refreshToken 仍为占位实现(返回 null)。
  • JokeViewModelappId/appSecret 为空,网络示例默认不可用。
  • 测试代码目前多为模板样例(ExampleUnitTest / ExampleInstrumentedTest)。

常见问题

1. 为什么启动后总是有初始邮件?

NotesApp.onCreate()(Android)会调用 initData(),将 LocalEmailsDataProvider 数据写入仓储。

2. 为什么我明明配置了账号仍可能回到登录页?

RequireLogin 页面会统一走导航拦截;当 UserRepository.userFlow 为空时,首屏会是登录页。

3. 为什么打包时提示签名配置问题?

因为当前 debug/release 都依赖 local.properties 中的签名字段,需保证路径与密码有效。

About

摘星(Notes) 是一个基于 KMP 的 Android 多模块示例项目,采用 MVI 架构,集成 Navigation3/Room3/Paging3、DataStore、Koin 与 Ktor,完整演示了登录拦截、邮件列表与详情、自适应布局和设置管理等现代 KMP 工程实践。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages