Flutter 百题斩#17 | SDK 组件数据入库 - sqlite


最近在着手开发我的 《匠心星问》 ,它定位是一款 题库 应用,将集题目浏览、发布、解答、做题为一体。打算第一步先以 Flutter 为核心,准备题库资源。于是诞生《每日一题》 系列,准备精心设计一些 Flutter 的问题与解答,作为题库的养料。

接下来的几题,将从一个需求逐步演进,让答题者给出方案设计。上一题已经解析完了 Flutter SDK 中的 所有 Widget 派生类数据信息,并导出了 json 文件。数据资源已经放在 github:StarQA_Issues 中了。 本题将更进一步,焦点是探讨:

将收集的所有 Widget 组件基本信息,录入 sqlite 数据库

通过关系型数据库,入库之后,便于我们更好地进行统计分析。比如本题解答完后,就可以基于数据库查询,得到下面的统计学习,结合 Flutter 的视图表现,可以更好的展示和传播。


1. 题目描述

对于已有的 widget.json, 其中内容是下面单体构成的列表。请你设计数据库表,将 json 数据录入数据库。并支持完成如下几个功能,给出对应的 sql 语句:

  • 查询指定父类名称下,所有的子组件信息。
  • 统计 Flutter SDK 中所有父类型组件有哪些。
  • 支持过滤抽象类、私有类。
  • 支持根据组件名模糊搜索。
json 复制代码
  {
    "name": "ProgressIndicator",
    "path": "/src/material/progress_indicator.dart",
    "abstract": true,
    "parents": [
      "StatefulWidget",
      "Widget"
    ],
    "desc": " A base class for Material Design progress indicators."
  },

2. 数据库设计

将 json 数据中的信息拆分成两张表:

  • widgets 表: 记录组件的基本信息。
sql 复制代码
CREATE TABLE widgets (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  path TEXT NOT NULL,
  is_abstract INTEGER NOT NULL DEFAULT 0,
  is_private INTEGER NOT NULL DEFAULT 0,
  description TEXT,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
  • widget_inheritance 表: 记录组件的继承关系。
sql 复制代码
CREATE TABLE widget_inheritance (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  widget_id INTEGER NOT NULL,
  parent_name TEXT NOT NULL,
  inheritance_order INTEGER NOT NULL,
)

3. Flutter 中解析 json

现在新建一个项目,把上一题中生成的 json 数据放在 assets 中。在 Flutter 代码中通过 rootBundle 可以读取资源数据,然后解析为 WidgetPo 列表即可:

dart 复制代码
/// 从 assets 加载 Widget 数据
Future<List<WidgetPo>> loadWidgets() async {
 try {
   final jsonString = await rootBundle.loadString('assets/data/Widget.json');
   final List<dynamic> jsonList = json.decode(jsonString);
   
   return jsonList.asMap().entries.map((entry) {
     final index = entry.key + 1; // 从 1 开始
     final json = entry.value;
     final widget = WidgetPo.fromJson(json);
     // 设置顺序 ID
     return WidgetPo(
       id: index,
       name: widget.name,
       path: widget.path,
       parents: widget.parents,
       isAbstract: widget.isAbstract,
       isPrivate: widget.isPrivate,
       desc: widget.desc,
     );
   }).toList();
 } catch (e) {
   throw Exception('加载 Widget.json 失败: $e');
 }

4. 数据入库

这里使用 sqlflite 插件访问数据库。

下面的 DatabaseHelper 类通过单例模式,来封装常用数据库操作。只需要调用它,就可以方便地打开、初始化或关闭数据库。运行项目之后,调用 initDatabase方法,就可以创建出数据库:

dart 复制代码
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

/// 数据库帮助类
class DatabaseHelper {
  static const String _databaseName = 'widget_parser.db';
  static const int _databaseVersion = 1;
  
  static Database? _database;
  
  /// 获取数据库实例
  static Future<Database> get database async {
    _database ??= await initDatabase();
    return _database!;
  }
  
  /// 初始化数据库
  static Future<Database> initDatabase() async {
    final String dbPath = await getDatabasesPath();
    final String path = join(dbPath, _databaseName);
    print("Database path:$path");
    return await openDatabase(
      path,
      version: _databaseVersion,
      onCreate: _onCreate,
    );
  }
  
  /// 创建表结构
  static Future<void> _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE widgets (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        path TEXT NOT NULL,
        is_abstract INTEGER NOT NULL DEFAULT 0,
        is_private INTEGER NOT NULL DEFAULT 0,
        description TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT DEFAULT CURRENT_TIMESTAMP
      )
    ''');
    
    await db.execute('''
      CREATE TABLE widget_inheritance (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        widget_id INTEGER NOT NULL,
        parent_name TEXT NOT NULL,
        inheritance_order INTEGER NOT NULL
      )
    ''');
  }
  
  /// 关闭数据库
  static Future<void> close() async {
    if (_database != null) {
      await _database!.close();
      _database = null;
    }
  }
}

目前已经有了 List<WidgetPo> 列表,小面接力棒交给数据的入库操作。sqlflite 中可以通过 Batch 来批量操作,只需要一次提交操作。相比于每次都通过 Database 插入一条效率要高一些。操作完后,我们就有了下锅的粮食,接下来就可以通过 sql 来分析了。

dart 复制代码
/// 批量插入
static Future<void> insertBatch(List<WidgetPo> widgets) async {
  final db = await DatabaseHelper.database;
  await db.transaction((Transaction txn) async {
    final Batch batch = txn.batch();
    for (WidgetPo widget in widgets) {
      final int widgetId = widget.id ?? 0;
      batch.insert(
        'widgets',
        widget.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace,
      );
      for (int i = 0; i < widget.parents.length; i++) {
        batch.insert('widget_inheritance', {
          'widget_id': widgetId,
          'parent_name': widget.parents[i],
          'inheritance_order': i,
        });
      }
    }
    await batch.commit(noResult: true);
  });
}

5. sql 练习

  1. 查询指定父类名称下,所有的子组件信息

替换 ? 为目标父类名,如 'StatelessWidget'

sql 复制代码
SELECT w.*
FROM widgets w
JOIN widget_inheritance wi ON w.id = wi.widget_id
WHERE wi.parent_name = ?

  1. 统计 Flutter SDK 中所有父类型组件有哪些(去重)
sql 复制代码
SELECT DISTINCT parent_name
FROM widget_inheritance
ORDER BY parent_name COLLATE NOCASE

  1. 支持过滤抽象类、私有类(带 where 条件)

这样就可以分析出 Flutter SDK 中公开可用的 StatelessWidget 派生类一共有 131 个:

sql 复制代码
SELECT w.*
FROM widgets w
JOIN widget_inheritance wi ON w.id = wi.widget_id
WHERE wi.parent_name = ?
  AND w.is_abstract = 0
  AND w.is_private = 0

  1. 支持根据组件名模糊搜索

比如问号改为 Button ,就可以查看出 Flutter SDK 中可用的名字里有 Button 的组件一共有哪些:

sql 复制代码
SELECT *
FROM widgets
WHERE 
  is_private=0 
AND
  name LIKE '%?%'

6.小结

这一题的核心是:把我们前面收集到的所有 Widget 组件信息,从 JSON 文件转存到本地数据库里。通过建两张表来分别保存组件的基本信息和继承关系,再用 Flutter 的 sqflite 插件批量写入数据库。这样做的好处是,后续查询和分析就非常方便,比如可以查某个父类有哪些子类、有哪些是抽象类、哪些是私有的,甚至还可以按组件名模糊搜索。

数据库一建好,我们就算是把 Flutter 的组件体系"数字化"存档了。后续还可以继续解析和入库更复杂的结构,比如每个组件类中有哪些字段,字段类型的详细信息等等。甚至你可以分析出 Flutter 组件中有哪些组件有 padding 属性。


后续的《每日一题》也将持续围绕实战问题展开,欢迎持续关注和参与,一起构建一个 开放、精致、有深度 的 Flutter 题库生态!

你是否也有想出的题目,欢迎投稿加入《匠心星问》共同打造!

如果你有其他的看法,或者有什么想要的题目、或者想提供题目和答案,都欢迎在评论区留言。更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。

相关推荐
2501_9160074713 分钟前
iOS 抓包工具有哪些?2025实用指南与场景推荐
android·ios·小程序·https·uni-app·iphone·webview
Hilaku29 分钟前
别再手写i18n了!深入浏览器原生Intl对象(数字、日期、复数处理)
前端·javascript·代码规范
每天吃饭的羊33 分钟前
强制缓存与协商缓存
前端
缘来小哥38 分钟前
Nodejs的多版本管理,不仅仅只是nvm的使用
前端·node.js
陈随易39 分钟前
Vite和pnpm都在用的tinyglobby文件匹配库
前端·后端·程序员
LeeAt39 分钟前
还在为移动端项目组件发愁?快来试试React Vant吧!
前端·web components
BD_Marathon1 小时前
面向对象高级:static
android·java·开发语言
_一条咸鱼_1 小时前
Android Runtime死代码消除原理深度剖析(93)
android·面试·android jetpack
鹏程十八少1 小时前
4. Android 用户狂赞的UI特效!揭秘折叠卡片+流光动画的终极实现方案
前端
JMchen1 小时前
下载集成工具类基于okhttp3
android