写了个 Android App 叫 NLecho,一开始只是想在手机上给自己博客发篇文章。写到现在,它顺手把我那台服务器也装了进去。
仓库在这里:https://github.com/qine233/typecho-app,Releases 里有 CI 打好的包,Android 8.0 以上能直接装。
它到底是什么
一个 Typecho 博客的原生客户端:Kotlin + Jetpack Compose + Material 3,数据全走 Typecho 自带的 XML-RPC 接口,服务端不需要装任何插件就能看文章、写文章、管评论。
技术上没什么花活,但有一件事我做得比较轴:XML-RPC 的编解码是自己手写的(XmlRpcClient),没引第三方库。因为 Typecho 的 XML-RPC 和 WordPress 有几处不一致,参数顺序、状态命名、还有 PHP 8 的 warning 会混进响应头(这些另开一篇写),拿现成库反而要先跟它搏斗。
界面没用 Material 默认那一套(药丸高亮、tone 容器、TopAppBar 都不要了),是照着小红书的信息流语言重做的:双列瀑布流、单列大图日记、笔记式详情页,底部自绘导航 + 居中那颗「+ 发布」。顶栏底栏最后走的是扁平——通栏实底加一道发丝线,不悬浮、不模糊、不投影。中间有一版仿 iOS 26 的液态玻璃做完自己看着都不对,整个撤了。
一个博主 + 一台手机
这个 App 是我自己日常在用的,所以「一个人写博客、用手机处理所有事」这条路上的细节做得很满:
- 首页双列瀑布流,顶部搜索药丸 + 分类横向筛选条,翻页走站点真实分页参数,配骨架屏和进场动画
- 日记动态:把归在日记分类下的文章按日期倒序排成时间线,单列大图
- 详情页多图轮播 + 全屏查看器,评论按
parentId还原成嵌套回复树;进详情是「封面飞过去」而不是换页——卡片封面和详情首屏大图是同一个共享元素,360ms 一起长大挪位 - 长按卡片就地弹一张预览,标题 + 摘要 + 分类 + 评论数,点它等于打开原文
- 编辑器:Markdown 正文 + 实时预览(只写 / 上下分屏 / 只看渲染三种排布),撤销重做、全屏沉浸写作、分类标签、存草稿、本地自动存稿(进程被杀重进会问要不要恢复)
- 插图从相册选 → 本地压缩 → 上传 → 正文内联;正文里已经有图时会排出一行缩略图,长按拖到想要的位置就当场改写那一段 Markdown,这一步还算进撤销历史
- 发布时间能挑,新建默认此刻、编辑默认沿用原文时间,保存时原样送回站点,改老文章不会被顺手顶到最新
- 管理台:文章列表(发布/草稿/私密、批量删)、评论审核(通过/垃圾/删除/回复)、页面、分类管理
- 重复文章清理:一页页把整站翻完(最多 600 篇),标题+正文归一后完全相同算一组,每组留最早那篇。这个入口存在是因为 XML-RPC 当年吞过 cid,点两次发布就多出副本,手动翻列表是清不完的
桌面小组件
买菜记账和健康体重各有一组桌面卡片,2×2 / 3×2 / 3×3 三种规格,长按桌面就能加。
卡片只读本机缓存,断网也画得出来;标题行那颗「刷新」只拉数据,不会替我把本机队列里的记录悄悄提交上去。点「记一笔」走深链直接进录入面板,品名和分类已经替我填好了。
卡片是磨砂玻璃加大圆角——Glance 1.1.1 既没有 blur 也没有 alpha 修饰符,RemoteViews 在 android.jar 里一个 blur 方法都没有,所以那张玻璃是我自己把壁纸糊出来的一张位图(这个也单独写一篇)。
预算进度条上有一道竖线标的是「这一期过了多少」(周 2/7、月 20/31):钱落在线左边就是比时间慢着花。光说「用掉 60%」说明不了任何事。
后端那台服务器也在里面
这是我本来没打算做、做起来最上头的部分。底栏最右那格「宝塔」(管理员账号才有)连上服务器之后:
- 看板读 CPU / 内存 / 负载 / 运行时长 / 磁盘 / 网速,翻数据库、计划任务、面板日志
- 网站、数据库、FTP、MySQL、计划任务、文件管理器、反向代理,每一个都是专门的一页,进去像面板那样按对象操作:域名、伪静态、SSL、重定向、日志、限速、备份,建库改密备份删库,建任务改周期立即执行,目录里建文件改权限解压……
- 危险动作全部要点确认,删站、删库这种要亲手把站点主域名(或者「删除」两个字)打一遍才按得下去
- 面板 API 文档里那 329 条接口随包打进来,做成数据驱动的通用调用台当兜底——但没有专门页面的接口才去那里翻
另外还有一张 SSH 终端卡:直连服务器的 sshd,和面板的 HTTP 接口是两条路。面板被改坏、nginx 起不来、API 开关被人关掉,这里照样进得去。vim、top、htop 照常跑,屏幕底下摆着手机键盘上没有的 Esc、^C、Tab、方向键;密码和私钥只进系统密钥库存密文,第一次连一台机器要亲手核对指纹;切到后台会话也不掉(这条是一边打脸一边补真的,见下)。
顺手把杂事也接了
- 健康记录:体重 / 体脂按天记,概览给当前值、BMI、目标进度、7/30/90 天变化和趋势折线
- 买菜记账:本月大额 + 涨跌、分类占比、近 7 个月柱状、月度报告、每周/每月/每年三档预算和超支提醒
这两个要站点装一个配套的 NolineApi 插件,不装其余功能照常
- Cloudreve 网盘:浏览目录、传文件、下载、改名、移动、生成永久直链和带提取码的分享;编辑器配了它,插图就只认它
- 常驻状态通知:还有多少条记录没交到站点、上次补交是什么时候、这个月哪几类超了预算;后台补交走 WorkManager 周期任务,进程被杀排期还在
- 数据与存储:按安装包/图片缓存/更新包/文章缓存/待同步/草稿/账号分类列出实际占用并画占比条,草稿和待同步队列只报数,永远不进「清理缓存」
- 检测更新:问 GitHub 要最新版,可选镜像站加速,下完对 sha256 再交给安装器;二十来 MB 的包挂在前台服务上下载,切后台不断
技术栈一览
| 语言 / UI | Kotlin 2.1、Jetpack Compose、Material 3;顶栏底栏是自绘的实底 + 发丝线 |
| 架构 | 单 Activity + Navigation-Compose,AppContainer 手写依赖注入,ViewModel + StateFlow |
| 网络 | OkHttp 4.12;XML-RPC 手写编解码;NoLine / Cloudreve / 宝塔走 REST;搜索订阅用 XmlPullParser 手解 |
| 存储 | DataStore(配置、互动状态、搜索历史)、Keystore AES-256-GCM(接口密钥、图床密码、面板密钥)、JSON 文件(缓存与离线队列) |
| 后台 | WorkManager 2.10 周期补交 + 常驻通知;前台服务扛下载和 SSH 会话;桌面卡片用 Glance 1.1.1 |
| 构建 | AGP 8.7.3、Gradle 8.9、JDK 17,minSdk 26 / target 35 |
| 测试 | 252 个单元测试,GitHub Actions 打 tag 出 Release |
说几句实话
- 全站搜索只有 10 条:Typecho 没给搜索留任何 XML-RPC 口子(
wp.getPosts根本没实现),只能订阅站点自己的搜索结果 RSS,硬上限 10 条且不翻页。App 用「本机已加载的笔记」补了一段,两边都没命中的老文章还是搜不出来 - 勾了「记住登录状态」,密码是明文存在 DataStore 里的——XML-RPC 每次请求都要带原文密码,这是这条路的原罪
- 站点必须是 HTTPS,我在
network_security_config里全局关了明文流量 - 多作者场景没做过:分类判定、日记时间线都是按「站主一个人写」设计的,换个多人博客大概要重做
- 点赞收藏只在本机,Typecho 没对应接口
- SSH 那条「会话挂在前台服务」是补出来的。一开始帮助卡片上写着「退到后台都不会断」,实测二十秒就报
Software caused connection abort——那句当时是假的。现在才是真的 - 没写 LICENSE,默认保留所有权利;要用先跟我说
想改的话照着 README 就能跑起来:./gradlew assembleDebug,产物在 app/build/outputs/apk/debug/app-debug.apk。包名 noline.zhouzhou,debug 构建带 .debug 后缀,可以和 release 并存。
这个 App 看着挺实用的,能把博客和服务器都整合进手机里确实方便不少,感谢分享开源代码,回头试试看。