需求是"改完 Android 端怎么送到手机上"。不发应用商店,也不想每次手动传包,
于是做了最小闭环:后端存一份安装包清单,App 启动时查、下载、交给系统安装器。
后端
- 新增 /app/release/latest 与 /app/release/download/{file},都免登录:
还没装上新包的客户端未必带着有效令牌,检查更新不该被登录挡住
- download_url 用相对路径,客户端拿自己配置的 API 地址拼——同一份清单要在
内网、公网、模拟器之间共用,写死域名只会让其中一个坏掉
- has_update 只比对 version_code,不比版本名字符串(0.10.0 与 0.9.0 的字典序是坑)
- 下载只认清单里登记的文件名,含 ..%2F 一律 9001
- scripts/publish_apk.py 负责复制 APK 并写 latest.json(sha256/size/published_at),
backend/static/apk/ 不进版本库
安卓
- AppUpdater:查清单 → 下到应用自己的 updates 目录(先写 .part 再改名)→
校验 sha256 → 经 FileProvider 交给系统安装器
- UpdateDialog:版本号、更新说明、下载进度、失败重试;「允许安装未知应用」
是系统开关,只跳设置页,不代点
- 启动时自动查一次,首页底部另有「检查更新」入口
- 图标换成和 Web 的 <Logo/> 同一套图形:墨绿圆角底、三条米白圆头线、陶土点;
同时出 mipmap 各密度 PNG、自适应图标与 512px 预览
- versionCode=2 / versionName=0.2.0;发布签名从 android/keystore.properties 读,
没有该文件时回落调试签名,保证别人 clone 下来也能 assembleRelease
文档
- API.md 第 7 节:两个接口、清单字段、签名与 sha256 的约定、9001
- DEPLOY.md 第 5 节:出包命令(JDK 17 + Gradle 8.9 + 干净的 GRADLE_USER_HOME)、
发布清单、传到服务器、验证
- ROADMAP 第 6 节记录本次范围与本机踩过的构建环境坑
验证
- 后端 pytest 103 passed;ruff check / format --check 全绿
- assembleRelease 成功,app-release.apk 7.4 MB,apksigner 验签显示 CN=Dadao Task
- 本地与生产 GET /app/release/latest?version_code=1 → has_update=true,
version_code=2 → false;下载返回 200 / application/vnd.android.package-archive
安卓端
Kotlin + Jetpack Compose。功能与 Web 端同构:登录 → 工作区 → 任务 / 工单 / 知识。
它已经能编译出 APK 了(本机实测)
BUILD SUCCESSFUL in 38s
app/build/outputs/apk/debug/app-debug.apk 9.94 MB
而且真的在模拟器上跑通了:登录 → 建工作区 → 任务列表(中文标题、状态、截止日、勾选完成)→ 工单列表 → 知识列表,全部打到真实后端。
| 登录 | 任务列表 |
|---|---|
![]() |
![]() |
一键起模拟器 + 装包(环境变量都替你设好了):
pwsh -File android/scripts/emulator.ps1 -Install
本机从零把编译链搭起来踩过的坑都固化在仓库里了,别再走一遍:
| 现象 | 原因 | 仓库里的解法 |
|---|---|---|
Failed to load native library 'native-platform.dll' |
旧 ~/.gradle 缓存坏了 |
用独立 GRADLE_USER_HOME(我用的 .local/gradle-home) |
validateSigningDebug: AccessDeniedException debug.keystore.lock |
沙箱/受限用户对 ~/.android 无写权限 |
工程内 keystore:android/keystore/debug.keystore,app/build.gradle.kts 显式指向 |
compileDebugKotlin: AccessDeniedException %TEMP%\kotlin-compiler-*.alive |
%TEMP% 不可写 |
构建前把 TEMP/TMP 指到工程内可写目录 |
Could not connect to Kotlin compile daemon |
受限环境里 Kotlin 编译守护进程连不回来 | gradle.properties 里 kotlin.compiler.execution.strategy=in-process |
| 依赖下载极慢(5 分钟 2MB) | Google Maven 直连慢 | settings.gradle.kts 先走阿里云镜像,再回落 google/mavenCentral |
需要:JDK 17(JAVA_HOME)、Android SDK platform 35 + build-tools。
cd android
gradle :app:assembleDebug # 或 ./gradlew(wrapper 需 Android Studio 补齐)
连不上后端?先看这三件事
- 后端要监听所有网卡:
uvicorn app.main:app --host 0.0.0.0 --port 8100。 默认的127.0.0.1只有本机可见,手机连不上。 - 地址要填对:包里的默认值是模拟器地址
http://10.0.2.2:8100。 真机在登录页点「改地址」填电脑的局域网 IP(如http://192.168.8.151:8100), 点「测试连接」看到「连得通」再登录。改一次会记住,不用重新打包。 - 防火墙:Windows 没弹过「允许访问」的话,手机包会被丢。管理员 PowerShell 执行
New-NetFirewallRule -DisplayName "DadaoTask backend 8100" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8100 -Profile Private。
还有一个容易查半天的点:主清单配了 network_security_config(白名单只有
10.0.2.2/127.0.0.1/localhost),这种情况下 android:usesCleartextTraffic="true" 不生效,
真机连 http://192.168.8.x 会被系统拦成 "CLEARTEXT communication not permitted",
表现就是"点了登录没反应/登录不上去",而模拟器一切正常。
所以 android/app/src/debug/res/xml/network_security_config.xml 单独给 debug 放开了明文,
release 依旧只走 HTTPS 白名单。
连哪个后端
android/local.properties(不入库):
dadao.apiBase=http://10.0.2.2:8100 # 模拟器访问宿主机
# dadao.apiBase=http://192.168.8.160:8100 # 真机走局域网
真机调试时后端必须监听 0.0.0.0 而不是 127.0.0.1:
uvicorn app.main:app --host 0.0.0.0 --port 8100
app/src/debug/AndroidManifest.xml 对 debug 变体放开了明文 HTTP——
release 仍然只允许 network_security_config.xml 里列出的域,别把这条挪到 main。
安装到手机
adb install -r app/build/outputs/apk/debug/app-debug.apk
首次打开 → 手机号登录(开发环境验证码在接口响应里回显,形如 开发环境验证码:123456)。
结构
app/src/main/java/com/dadao/task/
├── MainActivity.kt 入口 + Splash/Login/Home/Tasks/Knowledge 路由
├── data/
│ ├── Api.kt OkHttp + kotlinx.serialization;信封解包、业务码抛 ApiException
│ └── TokenStore.kt 令牌本地存储(TODO:换 EncryptedSharedPreferences)
└── ui/
├── Theme.kt 与 Web 一致的墨青 + 陶土色板
├── LoginScreen.kt 手机号 + 验证码
├── HomeScreen.kt 工作区列表 / 新建 / 生成邀请码
├── TaskListScreen.kt 任务视图切换、快速新建、勾选完成、详情与评论
└── KnowledgeScreen.kt 知识四态 tab、手记、按状态给动作(发布需 owner/admin)
模拟器(本机搭建记录,别重复踩)
sdkmanager 在这台机器上拉不到远端仓库索引(Failed to download any source lists!,但直连
dl.google.com 的 zip 是通的),所以系统镜像是手动装的:
- 下
https://dl.google.com/android/repository/sys-img/google_apis/x86_64-35_r09.zip(1.66 GB), 解压到%LOCALAPPDATA%\Android\Sdk\system-images\android-35\google_apis\x86_64—— 注意压缩包里还有一层x86_64/,要把内容提上一层,否则 AGP 找不到system.img。 - 建 AVD 时把用户目录指到工程内(
~/.android在受限用户下不可写):$env:ANDROID_USER_HOME="$repo\.local\android-user" $env:ANDROID_AVD_HOME="$repo\.local\android-avd" avdmanager create avd -n dadao -k "system-images;android-35;google_apis;x86_64" -d pixel_6 --force - 启动必须带
-no-window -gpu swiftshader_indirect(无头环境下 GL 走软件渲染);emulator -accel-check应显示WHPX ... installed and usable。
驱动模拟器点按不要凭截图猜坐标——adb shell input tap 用的是物理像素,而截图常被降采样。
用 adb shell uiautomator dump 拿 bounds 最稳。另外 adb shell input text 打不了中文(会 NPE),
中文数据走接口造。
已知待办
- 工单页还没做(Web 已有):需要"我指派的 / 我发起的"两栏 + 接收/开工/提交/验收按钮;
接口在
Api.kt里已经齐了(tickets/ticketAction/verifyTicket/promote)。 - 明文令牌存储(
TokenStore里已标 TODO)。 - 无离线缓存,弱网直接报错——与产品 D7「在线优先」一致。
- 邀请链接
https://dadaotask.com/join/<wsId>/<code>已在 Manifest 注册,落地处理尚未实现。 - 未在本机跑过模拟器(无 system-image、无连接设备),端到端只验证到"能编译、能装"。

