文本文件读写:一个小记事本,把读写文本文件最常见的操作都做成按钮——保存、追加一行、读取、统计行数、取最后一行或第 N 行、搜索行、删除、分享。
这一篇回答的搜索:App Inventor 读取文件最后一行、获取文件行数、读取文本数据、无法保存文件、无法创建文档、创建文本文档、文件管理器读取文件、文件管理插件、读取列表中的数据。
界面

图为由最终 .aia 生成的布局预览(列表内容为示例),不是真机截图。
文件存在哪里
文件管理器的「默认作用域」设为 App 时,写 notes/日记.txt 这样的相对路径,文件就落在 App 专属目录 /storage/emulated/0/Android/data/包名/files/notes/日记.txt:
- 不需要任何存储权限,Android 10 以后也能正常读写——「无法保存文件 / 无法创建文档」多半是写到了
/sdcard/xxx.txt这种共享目录被系统拦了。 - 卸载 App 时会一起删除;要长期保留,用「分享文件」发到微信、网盘或文件管理器。
- 文件增强组件的
ApplicationSpecificDirectory返回的就是这个目录,用它来列文件、看大小和修改时间。
怎么用
- 第一次打开会自动写一个「示例.txt」(三行),在列表里点它。
中途验证:「按行查看」显示「共 3 行、… 最后一行:第三行:这是最后一行」,下面列出 1~3 行。 - 「读第 [2] 行」→ 显示第 2 行;行号超出范围会提示。
- 点「+ 新建」,填文件名(不写扩展名自动加 .txt)和内容,点「保存」。
- 在内容框写一句话点「追加一行」:这句话加到文件末尾,并自动重读、刷新行数。
- 列表上方的搜索框可以筛出包含关键字的行;「删除」要确认;「分享文件」把整个文件发出去。
核心代码
保存、追加和读取
保存和追加都是异步的,写完触发 AfterFileSaved;读取也是异步的,内容在 GotText 里到达——不能在调用 ReadFrom 的下一句就去用内容。
when 按钮_保存.Click() {
文件管理器1.SaveFile(join(文本输入框_内容.Text, "\n"), join("notes/", 文本输入框_文件名.Text))
}
when 按钮_追加.Click() {
文件管理器1.AppendToFile(join(文本输入框_内容.Text, "\n"), join("notes/", 文本输入框_文件名.Text))
}
when 文件管理器1.AfterFileSaved(fileName) {
文件管理器1.ReadFrom(fileName)
}
when 文件管理器1.GotText(text) {
文本输入框_内容.Text = text
}
每次写入都以换行 \n 结尾,之后「追加一行」才不会和最后一行粘在一起。成品还处理了别处来的、结尾没有换行的旧文件:追加前先补一个换行。
行数、最后一行、第 N 行
按 \n 切成列表就是一行一项。文件以换行结尾时,切出来的最后一项是空文本,要去掉,否则行数会多 1、「最后一行」读到空。Windows 记事本保存的 \r\n 换行也能这样处理,残留的 \r 不影响显示。
global 行列表 = []
when 文件管理器1.GotText(text) {
行列表 = split(text, "\n")
if length(行列表) > 0 and isEmpty(行列表[length(行列表)]) {
listremove(行列表, length(行列表))
}
if length(行列表) == 0 {
标签_统计.Text = "空文件"
} else {
标签_统计.Text = join("共 ", length(行列表), " 行\n最后一行:", 行列表[length(行列表)])
}
}
when 按钮_读行.Click() {
var 行号 = 文本输入框_行号.Text
if isnumber(行号) and 行号 >= 1 and 行号 <= length(行列表) and 行号 == floor(行号) {
标签_第N行内容.Text = join("第 ", 行号, " 行:", 行列表[行号])
} else {
标签_第N行内容.Text = join("行号要在 1 到 ", length(行列表), " 之间")
}
}
列出目录里的文件
FilesList 返回完整路径。文件管理器不能直接用这个完整路径(App 作用域下会被再拼一次目录),所以只取最后的文件名,读写时还是用 notes/文件名。
global 文件名表 = []
procedure 刷新文件列表() {
var 目录 = join(文件增强1.ApplicationSpecificDirectory(), "/notes")
文件名表 = []
if 文件增强1.Exists(目录) {
foreach 路径 in 文件增强1.FilesList(目录, "txt,csv,log", false, false) {
listadd(文件名表, select(split(路径, "/"), length(split(路径, "/"))))
}
}
列表显示框_文件.Elements = 文件名表
}
常见问题
| 现象 | 原因与处理 |
|---|---|
| 保存时报「无法创建文件」 | 路径写成了 /sdcard/…、/Download/… 等共享目录,Android 10 以后不让随便写。改用相对路径(App 作用域),或把作用域设为 Shared 并按 文件操作教程 申请权限。 |
ReadFrom 之后内容还是空的 |
读取是异步的,要在 GotText 事件里用 text。 |
| 行数比实际多 1 | 文件以换行结尾,切出来最后一项是空文本,去掉它(见上面代码)。 |
| 「最后一行」读出来是空的 | 同上;另外检查文件末尾是不是有多个空行。 |
| 读别的 App 生成的文件 | 共享目录的文件要用 Shared 作用域并有读取权限;从文件选择器拿到的 content:// 地址可以直接交给 ReadFrom。 |
| 中文文件名能用吗 | 能。文件名不能包含 / 和 \,成品会拦下。 |
| 追加后最后两行粘在一起 | 原文件结尾没有换行。成品会自动补;自己写时用 segment(内容, textLength(内容), 1) 判断最后一个字符。 |
用到的组件
相关教程:文件操作教程 · 文件查找 · Android 存储基础:App 特定目录 · CSV 保存并分享
验证范围
最终 .aia 已通过:正式编辑器载入与保存重开 0 错误、后台导入、Android APK 编译,并在临时目录真实读写文件执行了积木的 18 项业务检查(首次示例文件、行数与最后一行、第 N 行与越界、自动加扩展名与结尾换行、追加后重读、\r\n 文件、无结尾换行的旧文件追加、非法文件名、取消/确认删除、分享、读不存在文件、重启不重复写示例)。手机存储权限策略以实测为准。详见验证记录。
扫码添加客服咨询