文本文件读写助手:创建、追加一行、读最后一行、统计行数(附 .aia 源码)

文本文件读写:一个小记事本,把读写文本文件最常见的操作都做成按钮——保存、追加一行、读取、统计行数、取最后一行或第 N 行、搜索行、删除、分享。

下载 TextFileNotes.aia 返回搜索需求专题

这一篇回答的搜索:App Inventor 读取文件最后一行、获取文件行数、读取文本数据、无法保存文件、无法创建文档、创建文本文档、文件管理器读取文件、文件管理插件、读取列表中的数据。

界面

文本文件读写界面布局预览

图为由最终 .aia 生成的布局预览(列表内容为示例),不是真机截图。

文件存在哪里

文件管理器的「默认作用域」设为 App 时,写 notes/日记.txt 这样的相对路径,文件就落在 App 专属目录 /storage/emulated/0/Android/data/包名/files/notes/日记.txt:

  • 不需要任何存储权限,Android 10 以后也能正常读写——「无法保存文件 / 无法创建文档」多半是写到了 /sdcard/xxx.txt 这种共享目录被系统拦了。
  • 卸载 App 时会一起删除;要长期保留,用「分享文件」发到微信、网盘或文件管理器。
  • 文件增强组件的 ApplicationSpecificDirectory 返回的就是这个目录,用它来列文件、看大小和修改时间。

怎么用

  1. 第一次打开会自动写一个「示例.txt」(三行),在列表里点它。
    中途验证:「按行查看」显示「共 3 行、… 最后一行:第三行:这是最后一行」,下面列出 1~3 行。
  2. 「读第 [2] 行」→ 显示第 2 行;行号超出范围会提示。
  3. 点「+ 新建」,填文件名(不写扩展名自动加 .txt)和内容,点「保存」。
  4. 在内容框写一句话点「追加一行」:这句话加到文件末尾,并自动重读、刷新行数。
  5. 列表上方的搜索框可以筛出包含关键字的行;「删除」要确认;「分享文件」把整个文件发出去。

核心代码

保存、追加和读取

保存和追加都是异步的,写完触发 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 文件、无结尾换行的旧文件追加、非法文件名、取消/确认删除、分享、读不存在文件、重启不重复写示例)。手机存储权限策略以实测为准。详见验证记录。

文档反馈