语言: 简体中文 | English | 日本語 | 한국어 | Français | Deutsch | Español
一个基于 API 文生图教程(7.12 版本)的 Android 原生 Kotlin 客户端,后端完全用 Android 原生方式实现(OkHttp + Coroutines + Room + DataStore),不依赖 Python / 任何外部服务器。
最新版本:Releases 页面
每次 push 到 main 分支会自动触发 GitHub Actions 工作流:
- 自增 PATCH 版本号(v2.0.0 → v2.0.1 → ...)并 commit 回仓库
- 编译 Release APK(用 keystore 签名)
- 自动创建 GitHub Release,附带 APK + SHA256 校验文件
v2.0.0 重大更新:
- ✅ 29 个社区风格全部使用真实 NovelAI Diffusion 4.5 画师串,零占位符
- ✅ 移除 Markdown 链接复制功能(按用户反馈精简 UI)
- ✅ 新增手机品牌识别 + 保活引导页(覆盖 9 大厂商)
- ✅ Android 15 / 16 前台服务通知 + 实时进度条
- ✅ 性能优化(Coil 全局缓存 / Bitmap 降采样 / @Immutable / Room 索引)
- ✅ 生成任务从前台 Activity 迁移到 Service,App 切后台不中断
| 教程章节 | 教程实现 | 本 App 实现 |
|---|---|---|
| §5.1 API 概述 | https://API 服务器 Job 异步模式 |
NaiApiClient + NaiRepository.generate() |
§5.5.8 _get_artist_string 风格路由 |
Python _get_artist_string 方法 |
StyleRegistry.resolveArtistString() |
| §5.5.10 Job 异步流程 | requests.post + time.sleep 轮询 |
client.createJob + delay() + client.pollJob |
§5.5.11 文生图 generate_text_to_image |
Python 同步方法 | NaiRepository.generate() suspend 函数 |
| §5.5.13 29 个社区风格 | nai_styles.json 文件 |
CommunityStyles.ALL Kotlin 常量 |
| §5.7.3 7 个内置画风串 | ARTIST_PRESETS dict |
ArtistPresets.ALL Kotlin 常量(含 7.12 完整画师串) |
§5.7.4 --variants N 并发生成 |
ThreadPoolExecutor |
generateVariants() 用 async { ... }.awaitAll() |
| §5.5.4 自动风格检测 | AUTO_STYLE_KEYWORDS 表 |
AutoStyleKeywords.detect() |
| §5.5.12 余额查询 | GET /api/me |
NaiRepository.checkBalance() |
§5.12 文件命名 YYYYMMDD_NNN.png |
next_dated_output_path |
ImageSaver.savePrivate() |
| §5.6 内置 7 预设 | 2.5d/fresh/doujin/galgame/comicDoujin/animeOld/realistic_loli | 同 |
| §5.8 9 个尺寸选项 | 竖图/横图/方图/2K/4K × 3 | SizeOptions.ALL |
| §5.10 全部 CLI 参数 | --steps/--scale/--cfg/--sampler/--negative/--artist |
生成页"高级参数"折叠面板 |
| 层 | 选型 | 理由 |
|---|---|---|
| UI | Jetpack Compose + Material 3 | Google 现代声明式 UI,2024+ 官方推荐 |
| 视觉风格 | iOS 极简风(纯白/纯灰 + 细边框 + iOS 系统蓝) | 用户在 AskUserQuestion 中选定 |
| 异步 | Kotlin Coroutines + Flow | 替代 Python 的 time.sleep + ThreadPoolExecutor |
| 网络 | OkHttp 4.12 + kotlinx.serialization | 替代 Python requests,端点少不需要 Retrofit |
| 数据库 | Room 2.6 (HistoryDao + FavoritesDao) | 历史记录 + Prompt 收藏夹 |
| 偏好 | Preferences DataStore | 替代 Python .env(存 token、上次参数) |
| 图片加载 | Coil 2.7 | 加载 ByteArray / 保存到相册 |
| 后台 | 前台服务 (foregroundServiceType=dataSync) | App 后台时继续轮询 API Job |
| 桌面 | AppWidgetProvider + RemoteViews | "快速生图"小组件 |
| 最低 SDK | Android 8.0 (API 26) | 用户选定,覆盖 95%+ 设备 |
| 目标 SDK | Android 15 (API 35) | 适配 Android 15/16 前台服务新规范 |
naigen-app/
├── settings.gradle.kts # 单 module 工程配置
├── build.gradle.kts # 顶层插件声明
├── gradle.properties # JVM 内存 + AndroidX
├── gradle/wrapper/gradle-wrapper.properties
└── app/
├── build.gradle.kts # 依赖 + 编译配置
├── proguard-rules.pro # 保留 serializer / Room / OkHttp
└── src/main/
├── AndroidManifest.xml # 权限 + Activity + Service + Widget + FileProvider
├── java/com/naigen/app/
│ ├── NaiApplication.kt # 入口 + 手动 DI(不引入 Hilt)
│ ├── MainActivity.kt # 单 Activity + Compose Navigation
│ ├── data/
│ │ ├── api/ # NaiApiClient + DTO
│ │ ├── model/ # GenRequest / GenResult / StylePreset / SizeOption
│ │ ├── repository/ # NaiRepository (Job 异步) + History/Favorites
│ │ ├── db/ # AppDatabase + Dao + Entity
│ │ ├── prefs/ # SettingsStore (DataStore)
│ │ └── styles/ # ArtistPresets + CommunityStyles + AutoStyleKeywords + SizeOptions
│ ├── service/ # GenerationService + GenerationBus(前台服务 + 进度总线)
│ ├── ui/
│ │ ├── theme/ # Color/Type/Shape/Theme (iOS 极简风)
│ │ ├── components/ # GroupedList + ListRow + TagChip + PrimaryButton
│ │ ├── navigation/ # AppNavGraph + 6 个 Tab
│ │ └── screen/
│ │ ├── generate/ # 生成页(核心交互)
│ │ ├── history/ # 历史记录
│ │ ├── favorites/ # Prompt 收藏夹
│ │ ├── stylepicker/ # 风格选择
│ │ ├── keepalive/ # 后台保活引导
│ │ └── settings/ # Token / 余额 / NSFW
│ ├── widget/ # QuickGenWidgetReceiver
│ └── util/
│ ├── keepalive/ # ManufacturerHelper(厂商识别 + Intent 跳转)
│ ├── ImageSaver.kt # 降采样解码 + MediaStore 相册保存
│ ├── DateUtils.kt
│ └── ShareUtils.kt
└── res/
├── values/ # colors.xml + strings.xml + themes.xml
├── values-night/ # 暗色主题
├── drawable/ # launcher 图标 (vector)
├── mipmap-anydpi-v26/ # 自适应图标
├── layout/ # widget_quick_gen.xml
└── xml/ # FileProvider paths + Widget info + Backup rules
- 把整个
naigen-app目录复制到本地(注意路径不要含中文/空格) - 打开 Android Studio →
File→Open→ 选择naigen-app根目录 - 等待 Gradle sync 完成(首次会下载 ~500MB 依赖,国内可挂梯子或在
settings.gradle.kts里把mavenCentral()改成阿里云镜像)
两种方式:
- App 内填入(推荐):编译安装后打开 App → 底部 Tab "设置" → "Token" 输入框填入
STA1N-xxxxx… - 代码内硬编码:编辑
SettingsStore.kt第 23 行token的默认值(不推荐,会进入 APK)
Token 仅保存在 App 私有目录的 DataStore 中,不会上传任何服务器。
cd naigen-app
./gradlew assembleRelease
# 产物:app/build/outputs/apk/release/app-release.apk(用 keystore 签名)或 Android Studio → Build → Generate Signed Bundle / APK → APK → 选 keystore。
如需本地调试,可用 ./gradlew assembleDebug(debug 签名,仅本地)。
adb install -r app/build/outputs/apk/release/app-release.apk或直接把 APK 传到手机点击安装(需开启"未知来源")。
- 打开 App,在生成页正向提示词框输入英文 prompt(逗号分隔)
- 选风格:默认 2.5d,可在"风格" Tab 详细挑选 36 个预设
- 选尺寸:默认竖图,循环切换 9 个尺寸
- 点底部"生成"按钮,等待 30~40 秒
- 完成后展示图片,可:
- 保存到相册(
Pictures/NaiGen/) - 系统分享到其他 App
- 保存到相册(
- 在"风格/尺寸"组里点 +/− 调整变体张数(1-6)
- 点底部"并发出 N 张"
- 同时提交 N 个独立 Job,耗时与单张接近,便于挑最佳
- 在"智能"组里打开"自动风格检测"
- 输入 prompt 后,根据关键词自动切换风格(例如 prompt 含"水彩" → community:4 水彩水墨风)
- 点"自定义画师串"行展开
- 可覆盖:Steps / Scale / CFG / Sampler / 自定义画师串
- 底部"⚡"按钮,或设置页 → "查询余额"
- 底部 Tab "历史"
- 缩略图 + prompt + 风格 + 耗时 + 时间
- 单条可:保存到相册 / 分享 / 删除
- 底部 Tab "收藏" → 右上角 + 新建
- 保存常用 prompt 模板,可标记"负面词"
- 单条可一键复制
教程 §5.5.17 提到「不管传什么 --style 出来都是 2.5d」的常见 bug,本 App 已通过以下方式防护:
-
StyleRegistry.resolveArtistString()(对应教程_get_artist_string)实现完整三段路由:- ① 社区风格
community:ID→ 取communityStyles.byId(id).artistString - ② 内置预设 → 取
artistPresets.get(key).artistString - ③ 都不在 → 原样返回(视为自定义画师串)
- ① 社区风格
-
NaiRepository.generate()强制调用resolveArtistString(styleKey)翻译成画师串,再赋值给 API 的artist字段 -
customArtist字段(对应--artist)非空时覆盖styleKey,进入路由第 ③ 兜底分支
v2.0.0 已全部替换为真实画师串。 29 个社区风格(id=130)现在全部使用基于 NovelAI Diffusion 4.5 语法编写的真实画师串,每个风格搭配 24 位画师加权 + 风格关键词明示,可直接出图。
如需替换为你私有的 nai_styles.json 数据:编辑 app/src/main/java/com/naigen/app/data/styles/CommunityStyles.kt,把对应 StylePreset(...) 的 artistString 字段替换即可。
| 权限 | 用途 | 何时请求 |
|---|---|---|
| INTERNET | 调用 API | 安装即生效 |
| FOREGROUND_SERVICE_DATA_SYNC | 后台轮询生成任务 | 启动 GenerationService 时 |
| POST_NOTIFICATIONS | 显示生成进度通知 | Android 13+ 首次启动时弹窗 |
| REQUEST_IGNORE_BATTERY_OPTIMIZATIONS | 申请加入电池白名单 | 保活页跳转电池优化时 |
| READ_MEDIA_IMAGES | 保存到相册时扫描已存在文件 | 保存到相册时(仅 Android 13+) |
| WRITE_EXTERNAL_STORAGE | 保存到相册(Android 9 及以下) | 保存到相册时(仅 Android 9-) |
所有用户数据(token / 历史 / 收藏 / 生成图片)均存储在 App 私有目录,不上传任何服务器。
App 启动后通过 Build.MANUFACTURER + Build.BRAND + 系统属性反射识别当前设备厂商,覆盖:
| 厂商 | 识别关键字 | 保活跳转入口 |
|---|---|---|
| 小米 / Redmi | xiaomi / redmi / ro.miui.ui.version.name | com.miui.securitycenter 自启动 + com.miui.powerkeeper 省电策略 |
| 华为 | huawei / ro.build.version.emui | com.huawei.systemmanager 应用启动管理 + 电池优化 |
| 荣耀 | honor | com.hihonor.systemmanager(fallback 到华为包名) |
| OPPO | oppo / ro.build.version.opporom | com.coloros.safecenter 自启动 + com.coloros.oppoguardelf 电池 |
| 一加 | oneplus | 与 OPPO 同源(已合并 ColorOS) |
| vivo / iQOO | vivo / ro.vivo.os.build.display.id | com.vivo.permissionmanager 后台弹出 + 自启动 |
| 三星 | samsung | com.samsung.android.lool 后台使用限制 |
| 魅族 | meizu | com.meizu.safe 自启动 + 后台运行 |
| Realme | realme | 与 OPPO 同源 |
| 原生 Android | 其他 | 仅电池优化(无需厂商保活) |
保活页(底部 Tab "保活")会:
- 显示识别到的厂商名称 + 专属提示文案
- 按顺序列出该厂商需要开启的保活项(自启动 / 电池 / 后台弹窗 / 通知)
- 每项右侧有跳转按钮,点击直达该厂商的设置子页(多个候选 Intent 自动 fallback)
- 显示当前状态(电池白名单是否已加入、通知权限是否已开),从设置返回时自动刷新
- 兜底入口:应用详情页(所有 ROM 都支持)
| 系统 | 强制要求 | 本 App 实现 |
|---|---|---|
| Android 14+ (API 34) | 前台服务必须声明 foregroundServiceType | Manifest 已声明 dataSync |
| Android 14+ | startForeground 必须用带 type 的重载 | startForegroundCompat() 按 SDK 分流 |
| Android 15 (API 35) | targetSdk 必须 ≥ 34 | 已设为 35 |
| Android 16 (API 36) | 前台服务启动 6 秒内必须显示进度通知 | setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE) + 启动时立即 startForeground |
| Android 16 | 通知渠道重要性必须正确 | 进度渠道 LOW,结果渠道 DEFAULT,已分开声明 |
- 轮询每 2 秒一次,每次同步更新通知 text + progress
- 通知模板:
正在生成图像+生成中… 12s · job abc12345 - 进度条:max=60,current=已耗时(不足 0 显示 indeterminate)
- 完成时切换到结果渠道发普通通知(带点击跳转 MainActivity)
- 取消时直接 stopService 并取消通知
| 维度 | 优化项 | 位置 |
|---|---|---|
| 网络层 | OkHttp 共享单例,连接池复用 + HTTP/2 | NaiApiClient.client |
| 网络层 | 重试 + 超时分层(connect 15s / read 60s / call 不限) | 同上 |
| 图片加载 | 全局 Coil ImageLoader,内存缓存 25% maxMemory,磁盘 50MB | NaiApplication.newImageLoader() |
| 图片加载 | 缩略图用 inSampleSize 降采样解码,避免大图 OOM | ImageSaver.makeThumbnail() |
| 图片加载 | 私有目录图片读取也带降采样 | ImageSaver.readPrivateBitmap() |
| Compose | StylePreset / GenRequest / GenResult 等加 @Immutable,跳过重组 | data/model/Models.kt |
| Compose | LazyColumn 全部用 key + contentType | HistoryScreen / StylePickerScreen |
| 数据库 | HistoryEntity 加 createdAt / styleKey 索引 | HistoryEntity.kt |
| 数据库 | FavoriteEntity 加 tag / isNegative 索引 | FavoriteEntity.kt |
| 协程 | 轮询用 ensureActive() 检查取消,避免无效等待 |
NaiRepository.generate() |
| 协程 | 并发变体用 async { }.awaitAll(),flowOn(Dispatchers.IO) |
NaiRepository.generateVariants() |
| 状态管理 | ViewModel 用 StateFlow + combine,UI 用 collectAsStateWithLifecycle | GenerateViewModel |
| Service | 单 coroutine scope,单 job,cancel 时立即停 | GenerationService |
| 通知 | setOnlyAlertOnce(true) + setSilent(true),避免每次更新都震动 |
buildProgressNotification() |
国内网络问题,在 settings.gradle.kts 把 mavenCentral() 改成阿里云镜像:
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }打开 App 设置页填入 STA1N-... 格式的 token,token 不会显示在截图中(默认密文化)。
token 错误或已过期,到购买入口 https://www.qianxun1688.com/links/D07F549B 获取新 token。
API 服务器排队过长,可在 NaiRepository.kt 中搜索 MAX_POLL_TIME_MS,把 180_000L 改成 300_000L。
清理项目:./gradlew clean → ./gradlew assembleRelease。
- v2.0.x (2026-07-15) — 当前版本,对齐教程 7.12 版本。完整变更见 Releases
- v1.0.0 (2026-07-15) — 初版
本工程代码可自由使用。API 服务和购买入口属于第三方,使用前请遵守其服务条款。