MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter SQLite数据库的封装:简化数据库操作代码

2021-09-247.1k 阅读

1. Flutter 与 SQLite 数据库简介

1.1 Flutter 概述

Flutter 是 Google 推出的一款用于构建跨平台移动应用的 UI 框架。它采用 Dart 语言开发,能够让开发者使用一套代码库同时为 iOS 和 Android 平台创建高性能、美观的原生应用。Flutter 的热重载功能极大地提高了开发效率,开发者可以即时看到代码更改后的效果,无需漫长的编译过程。

1.2 SQLite 数据库特点

SQLite 是一款轻型的、嵌入式的关系型数据库。它以其简单、高效、零配置的特点,非常适合移动应用开发。SQLite 将整个数据库存储在一个单一的文件中,易于管理和部署。在 Flutter 应用中使用 SQLite 可以实现本地数据的持久化存储,例如用户配置信息、离线缓存数据等。

2. 为何要封装 SQLite 数据库操作

2.1 提高代码复用性

在一个 Flutter 应用中,可能会在多个页面或功能模块中需要进行数据库操作。如果每次都重复编写相同的数据库打开、插入、查询等代码,不仅会使代码量大幅增加,而且后期维护成本也会很高。通过封装,可以将这些通用的数据库操作抽象成可复用的方法,在不同地方调用,减少重复代码。

2.2 简化业务逻辑

对于业务开发者来说,他们更关注的是如何利用数据库实现业务功能,而不是数据库操作的具体细节。封装后的数据库操作接口可以以一种更简洁、直观的方式暴露给业务层,使得业务代码更加清晰,开发人员能够更专注于业务逻辑的实现。

2.3 增强代码的可维护性

当数据库的结构或操作方式发生变化时,如果没有封装,可能需要在应用的多个地方进行修改,这很容易遗漏或引入新的错误。而封装后,只需要在封装层进行修改,对业务层的影响较小,提高了代码的可维护性。

3. 封装前的准备工作

3.1 安装依赖

pubspec.yaml 文件中添加 sqflite 依赖,这是 Flutter 中用于操作 SQLite 数据库的官方推荐插件。

dependencies:
  sqflite: ^2.2.0

然后运行 flutter pub get 命令下载并安装该依赖。

3.2 了解 SQLite 基本操作

在封装之前,需要对 SQLite 的基本操作有清晰的了解,包括数据库的打开、创建表、插入数据、查询数据、更新数据和删除数据等操作。以下是使用 sqflite 进行简单操作的示例:

3.2.1 打开数据库

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

Future<Database> openDatabase() async {
  final String databasesPath = await getDatabasesPath();
  final String path = join(databasesPath, 'my_database.db');
  return await openDatabase(
    path,
    version: 1,
    onCreate: (Database db, int version) async {
      await db.execute('''
        CREATE TABLE users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT,
          age INTEGER
        )
      ''');
    },
  );
}

3.2.2 插入数据

Future<void> insertUser(Database db, String name, int age) async {
  await db.insert(
    'users',
    {'name': name, 'age': age},
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

3.2.3 查询数据

Future<List<Map<String, dynamic>>> queryUsers(Database db) async {
  return await db.query('users');
}

3.2.4 更新数据

Future<void> updateUser(Database db, int id, String name, int age) async {
  await db.update(
    'users',
    {'name': name, 'age': age},
    where: 'id =?',
    whereArgs: [id],
  );
}

3.2.5 删除数据

Future<void> deleteUser(Database db, int id) async {
  await db.delete(
    'users',
    where: 'id =?',
    whereArgs: [id],
  );
}

4. 数据库封装实现

4.1 创建数据库操作封装类

首先,创建一个名为 DBHelper 的类来封装所有的数据库操作。这个类将提供单例模式,确保在整个应用中只有一个数据库实例。

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DBHelper {
  static final DBHelper _instance = DBHelper._internal();
  factory DBHelper() => _instance;
  late Database _db;

  DBHelper._internal();

  Future<void> initDB() async {
    final String databasesPath = await getDatabasesPath();
    final String path = join(databasesPath,'my_database.db');
    _db = await openDatabase(
      path,
      version: 1,
      onCreate: (Database db, int version) async {
        await db.execute('''
          CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT,
            age INTEGER
          )
        ''');
      },
    );
  }

  Future<int> insert(String table, Map<String, dynamic> values) async {
    return await _db.insert(table, values, conflictAlgorithm: ConflictAlgorithm.replace);
  }

  Future<List<Map<String, dynamic>>> query(String table, {String? where, List<dynamic>? whereArgs}) async {
    return await _db.query(table, where: where, whereArgs: whereArgs);
  }

  Future<int> update(String table, Map<String, dynamic> values, {String? where, List<dynamic>? whereArgs}) async {
    return await _db.update(table, values, where: where, whereArgs: whereArgs);
  }

  Future<int> delete(String table, {String? where, List<dynamic>? whereArgs}) async {
    return await _db.delete(table, where: where, whereArgs: whereArgs);
  }

  Future<void> close() async {
    await _db.close();
  }
}

4.2 单例模式分析

在上述代码中,DBHelper 类使用了单例模式。通过 static final DBHelper _instance = DBHelper._internal(); 定义了一个私有的静态实例,通过 factory DBHelper() => _instance; 提供了一个工厂构造函数,使得外部只能通过 DBHelper() 来获取这个单例实例。这种方式确保了在整个应用中,无论在多少个地方调用 DBHelper(),都只会创建一个数据库实例,避免了多次打开数据库带来的性能开销。

4.3 封装的数据库操作方法

4.3.1 initDB 方法

initDB 方法用于初始化数据库。它首先获取应用的数据库存储路径,然后打开或创建数据库文件。如果数据库版本为 1 且数据库不存在,则会执行 onCreate 回调中的 SQL 语句来创建 users 表。这个方法只需要在应用启动时调用一次,后续的数据库操作都基于这个已经打开的数据库实例。

4.3.2 insert 方法

insert 方法封装了数据插入操作。它接收表名 table 和要插入的数据 values 作为参数。values 是一个 Map<String, dynamic> 类型,键对应表中的列名,值对应要插入的数据。conflictAlgorithm 设置为 ConflictAlgorithm.replace,表示如果插入的数据与已有数据冲突(例如主键冲突),则替换已有数据。

4.3.3 query 方法

query 方法用于查询数据。它接收表名 table,以及可选的 where 子句和 whereArgs 参数。where 子句用于指定查询条件,whereArgs 用于替换 where 子句中的占位符。这个方法返回一个 Future<List<Map<String, dynamic>>>,其中每个 Map 代表查询结果中的一行数据,键为列名,值为对应的数据。

4.3.4 update 方法

update 方法用于更新数据。它接收表名 table、要更新的数据 values,以及可选的 where 子句和 whereArgs 参数。values 中包含要更新的列和对应的值,where 子句指定更新的条件,通过这种方式可以精确地更新满足条件的数据行。

4.3.5 delete 方法

delete 方法用于删除数据。它接收表名 table,以及可选的 where 子句和 whereArgs 参数。where 子句指定删除的条件,通过这种方式可以删除满足特定条件的数据行。

4.3.6 close 方法

close 方法用于关闭数据库连接。在应用退出或不再需要使用数据库时,应该调用这个方法来释放资源,避免内存泄漏。

5. 在 Flutter 应用中使用封装后的数据库操作

5.1 初始化数据库

main.dart 文件中,在 runApp 之前调用 DBHelperinitDB 方法来初始化数据库。

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await DBHelper().initDB();
  runApp(const MyApp());
}

5.2 插入数据示例

在某个页面的 StatefulWidget 中,可以这样插入数据:

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
    _insertData();
  }

  Future<void> _insertData() async {
    await DBHelper().insert('users', {'name': 'John', 'age': 25});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter SQLite Example'),
      ),
      body: const Center(
        child: Text('Data inserted successfully'),
      ),
    );
  }
}

5.3 查询数据示例

查询数据并在页面上显示:

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<Map<String, dynamic>> _users = [];

  @override
  void initState() {
    super.initState();
    _fetchData();
  }

  Future<void> _fetchData() async {
    final List<Map<String, dynamic>> users = await DBHelper().query('users');
    setState(() {
      _users = users;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter SQLite Example'),
      ),
      body: ListView.builder(
        itemCount: _users.length,
        itemBuilder: (context, index) {
          final user = _users[index];
          return ListTile(
            title: Text(user['name']),
            subtitle: Text('Age: ${user['age']}'),
          );
        },
      ),
    );
  }
}

5.4 更新数据示例

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
    _updateData();
  }

  Future<void> _updateData() async {
    await DBHelper().update('users', {'age': 26}, where: 'name =?', whereArgs: ['John']);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter SQLite Example'),
      ),
      body: const Center(
        child: Text('Data updated successfully'),
      ),
    );
  }
}

5.5 删除数据示例

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
    _deleteData();
  }

  Future<void> _deleteData() async {
    await DBHelper().delete('users', where: 'name =?', whereArgs: ['John']);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter SQLite Example'),
      ),
      body: const Center(
        child: Text('Data deleted successfully'),
      ),
    );
  }
}

6. 异常处理与优化

6.1 异常处理

在数据库操作过程中,可能会出现各种异常,例如数据库打开失败、SQL 语句执行错误等。为了使应用更加健壮,需要对这些异常进行处理。在 DBHelper 类的各个方法中,可以添加 try - catch 块来捕获异常并进行相应处理。

6.1.1 initDB 方法的异常处理

Future<void> initDB() async {
  try {
    final String databasesPath = await getDatabasesPath();
    final String path = join(databasesPath,'my_database.db');
    _db = await openDatabase(
      path,
      version: 1,
      onCreate: (Database db, int version) async {
        await db.execute('''
          CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT,
            age INTEGER
          )
        ''');
      },
    );
  } catch (e) {
    print('Error initializing database: $e');
  }
}

6.1.2 insert 方法的异常处理

Future<int> insert(String table, Map<String, dynamic> values) async {
  try {
    return await _db.insert(table, values, conflictAlgorithm: ConflictAlgorithm.replace);
  } catch (e) {
    print('Error inserting data: $e');
    return -1;
  }
}

通过这种方式,在出现异常时,应用不会崩溃,而是可以根据具体情况进行错误提示或其他处理。

6.2 性能优化

6.2.1 批量操作

对于插入、更新或删除大量数据的情况,可以使用批量操作来提高性能。例如,sqflite 提供了 batch 方法来执行多个 SQL 操作。

Future<void> batchInsert(List<Map<String, dynamic>> users) async {
  final batch = _db.batch();
  for (final user in users) {
    batch.insert('users', user);
  }
  await batch.commit();
}

6.2.2 合理使用事务

事务可以确保一组数据库操作要么全部成功,要么全部失败。在涉及多个相关的数据库操作时,使用事务可以保证数据的一致性。

Future<void> transferMoney(int fromUserId, int toUserId, double amount) async {
  await _db.transaction((txn) async {
    await txn.update('users', {'balance': FieldKey.expr('balance -?'), 'amount'}, where: 'id =?', whereArgs: [fromUserId]);
    await txn.update('users', {'balance': FieldKey.expr('balance +?'), 'amount'}, where: 'id =?', whereArgs: [toUserId]);
  });
}

7. 拓展与进阶

7.1 数据库版本管理

随着应用的发展,数据库结构可能需要不断更新。可以通过在 openDatabase 方法中设置 version 参数,并在 onUpgrade 回调中编写数据库升级逻辑。

Future<Database> openDatabase() async {
  final String databasesPath = await getDatabasesPath();
  final String path = join(databasesPath,'my_database.db');
  return await openDatabase(
    path,
    version: 2,
    onCreate: (Database db, int version) async {
      await db.execute('''
        CREATE TABLE users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT,
          age INTEGER
        )
      ''');
    },
    onUpgrade: (Database db, int oldVersion, int newVersion) async {
      if (oldVersion == 1 && newVersion == 2) {
        await db.execute('''
          ALTER TABLE users ADD COLUMN email TEXT
        ''');
      }
    },
  );
}

7.2 数据加密

对于一些敏感数据,如用户密码等,可以在存储到数据库之前进行加密处理。可以使用 encrypt 等加密库对数据进行加密和解密。

import 'package:encrypt/encrypt.dart';

final key = Key.fromLength(32);
final iv = IV.fromLength(16);
final encrypter = Encrypter(AES(key));

Future<void> insertEncryptedUser(String name, String password) async {
  final encryptedPassword = encrypter.encrypt(password, iv: iv).base64;
  await DBHelper().insert('users', {'name': name, 'encrypted_password': encryptedPassword});
}

Future<String?> getDecryptedPassword(String name) async {
  final List<Map<String, dynamic>> results = await DBHelper().query('users', where: 'name =?', whereArgs: [name]);
  if (results.isNotEmpty) {
    final encryptedPassword = results[0]['encrypted_password'];
    final decrypted = encrypter.decrypt64(encryptedPassword, iv: iv);
    return decrypted;
  }
  return null;
}

7.3 与其他框架结合

Flutter 生态中有许多其他优秀的框架,如 ProviderBloc 等。可以将封装后的 SQLite 数据库操作与这些框架结合,实现更复杂、可维护的应用架构。例如,使用 Provider 来管理数据库状态,使得不同页面可以方便地共享数据库数据。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class DatabaseProvider with ChangeNotifier {
  List<Map<String, dynamic>> _users = [];

  List<Map<String, dynamic>> get users => _users;

  Future<void> fetchUsers() async {
    final users = await DBHelper().query('users');
    _users = users;
    notifyListeners();
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter SQLite with Provider'),
      ),
      body: FutureBuilder<void>(
        future: Provider.of<DatabaseProvider>(context, listen: false).fetchUsers(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          return ListView.builder(
            itemCount: Provider.of<DatabaseProvider>(context).users.length,
            itemBuilder: (context, index) {
              final user = Provider.of<DatabaseProvider>(context).users[index];
              return ListTile(
                title: Text(user['name']),
                subtitle: Text('Age: ${user['age']}'),
              );
            },
          );
        },
      ),
    );
  }
}

通过以上步骤,我们完成了 Flutter 中 SQLite 数据库的封装,简化了数据库操作代码,提高了应用的开发效率和可维护性。同时,通过异常处理、性能优化和拓展进阶等方面的内容,使应用在实际使用中更加健壮和高效。