Flutter 匠心千刃 | 批量文件生成器
最近需要测试文件发送,但是手上的文件不太多,自己的文件也不太适合测试。所以想为 匠心千刃 打造一个 批量随机文件生成器,来辅助生成批量的指定大小范围的测试文件。通过 Flutter 构建交互界面,进行表单输入配置项,点击按钮触发 生成逻辑
:
1. 如何生成指定大小的文件
由于测试文件对内容没有什么硬性要求,而文件本身就是字节数字组。所以只需要创建字节列表,通过 File 对象写入即可。
如下 FileWriter
类负责文件的写入,这里取随机 0~9 数字作为文件内容;通过 List.filled
构造指定长度的字节列表,然后通过 File#writeAsBytes
方法写入磁盘:
class FileWriter {
Random _random = Random();
Future<void> write(String path, int length) async {
int value = _random.nextInt(10);
List<int> result = List.filled(length, value);
await File(path).writeAsBytes(result);
}
}
在 mian 函数中测试一下,在项目根目录下,生成一个名为 test.gen
大小为 1024 的文件:
import 'dart:io';
import 'dart:math';
import 'package:path/path.dart' as path;
void main() async {
FileWriter fileWriter = FileWriter();
String filepath = path.join(Directory.current.path,'test.gen');
await fileWriter.writeFile(filepath, 1024);
print(filepath);
}
这就是工具的核心代码,是不是非常简单。下面就来看一下,如何基于它打造命令行和 UI 交互应用。
2. 业务逻辑: 批量生成文件
既然能生成一个文件,那批量生成自然也不在话下。如下所示,我希望在一个文件夹下,可以批量生成大小在一定范围内的文件,后缀名依次递增:
交互中可以选择文件大小单位,这里通过 ByteUnit
维护,包含 B
、KB
、MB
、GB
四个单位;另外通过 bytes
方法可以获取当前单位对应的字节数:
enum ByteUnit {
b('B'),
kb('KB'),
mb('MB'),
gb('GB'),
;
final String label;
int get bytes => switch (this) {
ByteUnit.b => 1,
ByteUnit.kb => 1024,
ByteUnit.mb => 1024 * 1024,
ByteUnit.gb => 1024 * 1024 * 1024,
};
const ByteUnit(this.label);
}
然后定义文件生成的配置数据,包括生成的文件数量、文件夹、单位、最大值、最小值六个可配置项:
class FileGenConfig {
final int fileCount;
final String filename;
final String dir;
final ByteUnit unit;
final double min;
final double max;
FileGenConfig({
this.fileCount = 10,
required this.filename,
required this.dir,
this.unit = ByteUnit.mb,
this.min = 10,
this.max = 11,
});
(int, int) get bytesRange => (
(min * unit.bytes).toInt(),
(max * unit.bytes).toInt(),
);
}
最后在 FileWriter
中定义 gen 方法,遍历 count 次,取最大最小字节数间的随机长度,通过 writeFile
函数写入即可:
Future<void> gen(FileGenConfig config) async {
Directory directory = Directory(config.dir);
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
var (min, max) = config.bytesRange;
for (int i = 0; i < config.fileCount; i++) {
int length = min + _random.nextInt(max - min);
String name = path.basenameWithoutExtension(config.filename);
String extension = path.extension(config.filename);
String filepath = path.join(directory.path, '${name}_$i$extension');
await writeFile(filepath, length);
}
}
3. 视图层: 构建交互界面
业务逻辑已经在上面完成了,现在视图层的主要任务是,在交互过程中维护配置数据,并在运行按钮中执行核心业务逻辑 gen 方法。匠心千刃中的视图交互在文章开始已经展示了,其中组件基于TolyUI 进行构建。包括:
- 文件输入选择器,选择文件夹的位置。
- 数字输入增加器,交互时修改数字相关配置项。
- 群组单值选择器,交互时修改可枚举的配置数据。
当前界面的结构分为三个部分:
- 上方 FileGenTitleBar 是 工具标题 和 操作按钮。
- 中间 FileGenForm 是交互 表单面板, 作用在于通过交互设置生成器的配置参数。
- 下方 FileGenInfo 根据表单中的配置内容,实时生成介绍信息。
FileGenPage 负责渲染整个批量文件生成器的视图构建,三个部分的组合逻辑如下所示。其中 DisplayPanel 是一个白色的装饰盒:
class FileGenPage extends StatefulWidget {
const FileGenPage({super.key});
@override
State<FileGenPage> createState() => _FileGenPageState();
}
class _FileGenPageState extends State<FileGenPage> {
final FileGenConfigModel model = FileGenConfigModel();
@override
void dispose() {
model.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FileGenTitleBar(onTap: _onTap),
DisplayPanel(
children: [
FileGenForm(model: model),
const SizedBox(height: 24),
FileGenInfo(tips: model.tips),
],
)
],
),
);
}
}
4. 业务逻辑与状态变化
在输入面板的交互过程中,配置信息会不断变化。变化之后,需要通知底部文案界面发生变化;以及顶部按钮也需要感知最新的配置数据,进行正确的生成。
在 Flutter 中建立通知更新的关系是非常简单的,对于输入框来说通过 TextEditingController
可以绑定输入框内容的展示;像文件单位的监听通知,可以通过 ValueNotifier
对象进行处理:
这里通过 FileGenConfigModel 统一维护这些可具体对象,比如处理初始化、销毁等逻辑:
class FileGenConfigModel {
TextEditingController dirPathCtrl = TextEditingController();
TextEditingController nameCtrl = TextEditingController(text: 'test.txt');
TextEditingController countCtrl = TextEditingController(text: '10');
TextEditingController minCtrl = TextEditingController(text: '10');
TextEditingController maxCtrl = TextEditingController(text: '11');
ValueNotifier<ByteUnit> unitCtrl = ValueNotifier(ByteUnit.mb);
FileGenConfigModel() {
reset();
}
void reset() {
nameCtrl.text = 'test.txt';
countCtrl.text = '10';
maxCtrl.text = '11';
minCtrl.text = '10';
unitCtrl.value = ByteUnit.mb;
_initDir();
}
void _initDir() async {
Directory directory = await getTemporaryDirectory();
dirPathCtrl.text = p.join(directory.path, 'toly_gen');
}
void dispose() {
dirPathCtrl.dispose();
nameCtrl.dispose();
countCtrl.dispose();
minCtrl.dispose();
maxCtrl.dispose();
unitCtrl.dispose();
}
}
然后,可以通过 ValueListenableBuilder 组件,来观察可监听对象。在对象变化通知后,触发局部区域的重新构建。下面以 CupertinoSlidingSegmentedControl 组件为例,可以监听 FileGenConfigModel 中的unitCtrl 对象,实现变化更新通知。
Widget _buildUnitSelector() => Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 240,
child: ValueListenableBuilder<ByteUnit>(
valueListenable: model.unitCtrl,
builder: (_, v, __) => CupertinoSlidingSegmentedControl<ByteUnit>(
groupValue: v,
onValueChanged: model.changeUnit,
backgroundColor: const Color(0xfff5f7fa),
padding: const EdgeInsets.all(5),
children: const {
ByteUnit.b: Text("B"),
ByteUnit.kb: Text("KB"),
ByteUnit.mb: Text("MB"),
ByteUnit.gb: Text("GB"),
},
),
)));
其他的输入类型的组件,使用的都是 TolyUI 中的 TolyInput 组件,包括输入文字选择器、数字输入选择器。感兴趣的可以详见 TolyUI 的组件部分。
对于底部的文字,在表单数据发生改变时,都要进行更新。可以在 FileGenConfigModel
中,通过 Listenable.merge
合并多个可监听对象,添加监听器,来更新文字提示的内容。视图层同样可以通过 ValueListenableBuilder
进行局部构建:
ValueNotifier<String> tips = ValueNotifier('');
void _initListener() {
Listenable.merge([dirPathCtrl, nameCtrl, countCtrl, minCtrl, maxCtrl, unitCtrl])
.addListener(_updateInfo);
}
void _updateInfo() {
String info = '将在 ${dirPathCtrl.text} 文件夹下生成 ${countCtrl.text} 个,'
'大小在 ${minCtrl.text}${unitCtrl.value.label} 到 ${maxCtrl.text}${unitCtrl.value.label} '
'之间名称为 ${nameCtrl.text} 系列的文件,点击右上角按钮运行生成。';
tips.value = info;
}
5.点击按钮与生成结果
顶部栏在封装中,提供三个 ActionType 表示三种按钮类型,通过 onTap
回调点击事件。所以在外界可以根据 ActionType
来处理不同的事件。
void _onTap(ActionType value) async {
switch (value) {
case ActionType.refresh:
model.reset();
$message.success(message: '已重置配置');
break;
case ActionType.openDir:
String dir = model.dirPathCtrl.text;
bool exist = Directory(dir).existsSync();
if (exist) {
fxOpen(dir);
} else {
$message.warning(message: '当前文件夹不存在!');
}
break;
case ActionType.start:
await FileWriter().gen(model.config);
$message.success(message: '生成成功!');
}
}
最重要的生成按钮,只要触发之前的 FileWriter#gen
方法,根据 FileGenConfigModel 创建 FileConfig
配置即可:
到这里,一个简单的批量随机文件生成器就完成啦,后面还可以继续优化。比如超大文件如何优雅地生成,执行过程中的状态、进度、生成内容的选择等等。那本就到这里,感谢观看,下次再见~
转载自:https://juejin.cn/post/7399325667256811571