如何使用SQLite在Flutter中持久保存数据

3,101 阅读7分钟

决定一个应用程序是否具有性能的最大因素之一是它如何在本地获取和存储数据。本地存储的持久性数据在移动应用开发的早期就已经出现了。

我们在本教程中使用的SQLite数据库是一个持久性数据存储,即使在应用程序关闭后也能保留数据。这意味着,即使用户重新启动应用程序或设备本身,我们存储在数据库中的所有内容都会存在。

在本指南中,我们将向你展示如何创建一个简单的应用程序,使用一个表单接受用户的一些细节,并将其存储在数据库中。因此,请启动您最喜欢的文本编辑器,让我们开始行动吧

创建一个新的Flutter项目

首先,打开您的终端并创建一个新的Flutter项目。

flutter create sqlite_demo
cd sqlite_demo

接下来,添加所需的依赖项。在这篇文章中,你需要sqflite包来使用SQLite数据库。

dependencies:
  flutter:
    sdk: flutter
  sqflite: any

打开一个模拟器设备或将一个真实的设备连接到你的系统,并使用以下命令启动该应用程序。

flutter run

对于这个应用程序,你需要一个有状态的部件来管理本地表单的状态

import 'package:flutter/material.dart';

void main() {
  runApp(SqliteDemoApp());
}

class SqliteDemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SQLite demo',
      theme: ThemeData(
        primarySwatch: Colors.purple,
      ),
      home: MainApp(title: 'SQLite demo'),
    );
  }
}

class MainApp extends StatefulWidget {
  MainApp({Key? key, this.title}) : super(key: key);
  final String? title;

  @override
  _MainAppState createState() => _MainAppState();
}

DatabaseHelper

为了访问和使用数据库,创建一个单子DatabaseHelper 类。一个单子类将确保我们只有一个数据库连接的实例和对数据库的全局访问,可以从项目的任何地方调用。

在Dart中,factory 关键字被用来创建一个只返回一个实例的构造函数。

import 'dart:async';

import 'package:path/path.dart';

class DatabaseHelper {
  static final DatabaseHelper _databaseHelper = DatabaseHelper._();

  DatabaseHelper._();

  late Database db;

  factory DatabaseHelper() {
    return _databaseHelper;
  }
}

Flutter and SQLite Example Table

在这个项目中,我们将在数据库中创建一个用户表,它将包含一个ID、姓名、电子邮件和年龄列。创建该表的SQL查询如下。

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT, 
  name TEXT NOT NULL,
  age INTEGER NOT NULL, 
  email TEXT NOT NULL
)

DatabaseHelper 类中,添加一个initDB() 异步函数,它将连接到SQLite数据库。getDatabasesPath() 方法将给出数据库文件的默认位置,你需要在该文件上附加数据库的名称--users_demo.db ,在本例中。

openDatabase() 函数用于打开数据库,。它接受数据库文件的路径、版本号和一个可选的onCreate 回调,该回调在第一次创建数据库时被执行。

onCreate 回调中,你需要创建用户表,以便它可以用来存储用户信息和进行CRUD操作。

class DatabaseHelper {
  // ...
  Future<void> initDB() async {
    String path = await getDatabasesPath();
    db = await openDatabase(
      join(path, 'users_demo.db'),
      onCreate: (database, version) async {
        await database.execute(
          """
            CREATE TABLE users (
              id INTEGER PRIMARY KEY AUTOINCREMENT, 
              name TEXT NOT NULL,
              age INTEGER NOT NULL, 
              email TEXT NOT NULL
            )
          """,
        );
      },
      version: 1,
    );
  }
// ...
}

模型类

接下来,让我们创建一个模型类来表示数据库中用户的数据。一个简单的数据模型类将提供处理SQLite友好格式所需的方法,将其转换为Dart对象,以后可以在应用程序中使用。

class User {
  int? id;
  String name;
  int age;
  String email;

  User({this.id, required this.name, required this.age, required this.email});

  User.fromMap(Map<String, dynamic> res)
      : id = res["id"],
        name = res["name"],
        age = res["age"],
        email = res["email"];

  Map<String, Object?> toMap() {
    return {'id': id, 'name': name, 'age': age, 'email': email};
  }
}

Flutter and SQLite Example Model Class

创建用户模型类是为了定义数据库中用户表所期望的属性。用户模型类由name,age, 和email 属性组成,并有一个构造函数用于创建一个新的用户实例。

为了在用户类的实例和SQLite DB使用的Map对象之间进行转换,定义了toMap()fromMap() 方法。

CRUD操作

现在让我们回到DatabaseHelper 类,编写DB方法来进行CRUD操作。

database 类的一个实例被分配到DatabaseHelper 类的initDB() 方法中的db 属性。database 实例可以访问insert(),update(),query(), 和delete() 等方法,以执行对数据库的CRUD操作。

class DatabaseHelper {
  // ...
  Future<int> insertUser(User user) async {
    int result = await db.insert('users', user.toMap());
    return result;
  }

  Future<int> updateUser(User user) async {
    int result = await db.update(
      'users',
      user.toMap(),
      where: "id = ?",
      whereArgs: [user.id],
    );
    return result;
  }

  Future<List<User>> retrieveUsers() async {
    final List<Map<String, Object?>> queryResult = await db.query('users');
    return queryResult.map((e) => User.fromMap(e)).toList();
  }

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

添加用户

现在是时候开始构建应用程序的用户界面了。

在这一节中,我们将创建一个如下所示的表单,它将包括一个姓名、年龄和一个电子邮件字段。当点击提交按钮时,这些字段的数据将被存储在用户表的相应列中。

Flutter and SQLite Example Form

在小组件的主状态类中,初始化用于访问DatabaseHelper 类、User 类、姓名、年龄和电子邮件的属性。重写initState() 生命周期方法,并初始化数据库,以便在创建widget时它是可用的。

小组件build() 方法的其余部分由基本的Flutter小组组成,如Form,TextFormField, 和ElevatedButton ,用于创建表单的UI。

import 'package:flutter/services.dart';
import 'package:sqlite_demo/model/user.dart';
import 'package:sqlite_demo/utils/database_helper.dart';

// ...
class _MainAppState extends State<MainApp> {
  late DatabaseHelper dbHelper;
  final nameController = TextEditingController();
  final ageController = TextEditingController();
  final emailController = TextEditingController();
  bool isEditing = false;
  late User _user;

  @override
  void initState() {
    super.initState();
    this.dbHelper = DatabaseHelper();
    this.dbHelper.initDB().whenComplete(() async {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        resizeToAvoidBottomInset: false,
        appBar: AppBar(
          title: Text(widget.title!),
        ),
        body: Column(
          children: <Widget>[
            Expanded(
                child: new Column(
              children: [
                Padding(
                    padding: EdgeInsets.all(8.0),
                    child: Form(
                        child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                          TextFormField(
                            controller: nameController,
                            decoration: const InputDecoration(
                                hintText: 'Enter your name', labelText: 'Name'),
                          ),
                          TextFormField(
                            controller: ageController,
                            keyboardType: TextInputType.number,
                            inputFormatters: [
                              FilteringTextInputFormatter.allow(
                                  RegExp(r'[0-9]')),
                            ],
                            decoration: const InputDecoration(
                                hintText: 'Enter your age', labelText: 'Age'),
                          ),
                          TextFormField(
                            controller: emailController,
                            decoration: const InputDecoration(
                                hintText: 'Enter your email',
                                labelText: 'Email'),
                          ),
                          Row(
                              mainAxisAlignment: MainAxisAlignment.start,
                              children: [
                                new Container(
                                    margin:
                                        new EdgeInsets.symmetric(vertical: 10),
                                    child: new ElevatedButton(
                                      child: const Text('Submit'),
                                      onPressed: addOrEditUser,
                                    )),
                              ])
                        ]))),
                Expanded(
                  flex: 1,
                  child: SafeArea(child: userWidget()),
                )
              ],
            )),
          ],
        ));
  }

  // ...
}

让我们在_MainAppState 类中添加CRUD逻辑。

addOrEditUser() 方法中,从电子邮件、年龄和姓名字段中获取数值。根据数据当前是否被编辑,使用dbHelper 实例添加或更新表中的用户记录。

class _MainAppState extends State<MainApp> { 
  // ...
  Future<void> addOrEditUser() async {
    String email = emailController.text;
    String name = nameController.text;
    String age = ageController.text;

    if (!isEditing) {
      User user = new User(name: name, age: int.parse(age), email: email);
      await addUser(user);
    } else {
      _user.email = email;
      _user.age = int.parse(age);
      _user.name = name;
      await updateUser(_user);
    }
    resetData();
    setState(() {});
  }

  Future<int> addUser(User user) async {
    return await this.dbHelper.insertUser(user);
  }

  Future<int> updateUser(User user) async {
    return await this.dbHelper.updateUser(user);
  }

  void resetData() {
    nameController.clear();
    ageController.clear();
    emailController.clear();
    isEditing = false;
  }

  // ...
}

显示一个用户列表

使用FutureBuilder widget,通过查询用户表来显示用户的列表。

从数据库查询是一个异步操作,因此需要由一个未来处理。FutureBuilder widget帮助确定future的当前状态,并允许你在异步操作进行中显示不同的视图。

Flutter and SQLite Example User List

Flutter and SQLite Example User List

retrieveUsers() 方法作为一个future传递给FutureBuilder widget,并显示一个CircularProgress widget,直到数据加载。

使用ListView widget,遍历用户数据并在屏幕上显示。用Dismissible widget包裹单个用户数据,这样当用户在数据上滑动时,就会执行一个删除动作。

要删除一个用户记录,在Dismissible widget的onDismissed 选项中调用deleteUser() 方法。

Widget userWidget() {
    return FutureBuilder(
      future: this.dbHelper.retrieveUsers(),
      builder: (BuildContext context, AsyncSnapshot<List<User>> snapshot) {
        if (snapshot.hasData) {
          return ListView.builder(
              itemCount: snapshot.data?.length,
              itemBuilder: (context, position) {
                return Dismissible(
                    direction: DismissDirection.endToStart,
                    background: Container(
                      color: Colors.red,
                      alignment: Alignment.centerRight,
                      padding: EdgeInsets.symmetric(horizontal: 10.0),
                      child: Icon(Icons.delete_forever),
                    ),
                    key: UniqueKey(),
                    onDismissed: (DismissDirection direction) async {
                      await this
                          .dbHelper
                          .deleteUser(snapshot.data![position].id!);
                    },
                    child: new GestureDetector(
                      behavior: HitTestBehavior.opaque,
                      onTap: () => populateFields(snapshot.data![position]),
                      child: Column(
                        children: <Widget>[
                          Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: <Widget>[
                              Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  Padding(
                                    padding: const EdgeInsets.fromLTRB(
                                        12.0, 12.0, 12.0, 6.0),
                                    child: Text(
                                      snapshot.data![position].name,
                                      style: TextStyle(
                                          fontSize: 22.0,
                                          fontWeight: FontWeight.bold),
                                    ),
                                  ),
                                  Padding(
                                    padding: const EdgeInsets.fromLTRB(
                                        12.0, 6.0, 12.0, 12.0),
                                    child: Text(
                                      snapshot.data![position].email.toString(),
                                      style: TextStyle(fontSize: 18.0),
                                    ),
                                  ),
                                ],
                              ),
                              Padding(
                                padding: const EdgeInsets.all(8.0),
                                child: Column(
                                  mainAxisAlignment:
                                      MainAxisAlignment.spaceEvenly,
                                  children: <Widget>[
                                    Container(
                                      decoration: BoxDecoration(
                                          color: Colors.black26,
                                          borderRadius:
                                              BorderRadius.circular(100)),
                                      child: Padding(
                                        padding: const EdgeInsets.all(8.0),
                                        child: Text(
                                          snapshot.data![position].age
                                              .toString(),
                                          style: TextStyle(
                                            fontSize: 16,
                                            color: Colors.white,
                                          ),
                                        ),
                                      ),
                                    )
                                  ],
                                ),
                              ),
                            ],
                          ),
                          Divider(
                            height: 2.0,
                            color: Colors.grey,
                          )
                        ],
                      ),
                    ));
              });
        } else {
          return Center(child: CircularProgressIndicator());
        }
      },
    );
  }

每当单个用户数据被点选时,我们将用数据填充表格,并将isEditing 标志设置为true

void populateFields(User user) {
    _user = user;
    nameController.text = _user.name;
    ageController.text = _user.age.toString();
    emailController.text = _user.email;
    isEditing = true;
}

总结

在本指南中,我们走过了如何通过使用sqflite Flutter包访问和运行SQLite中的数据库查询。

客户端应用程序层面的数据持久性是至关重要的。没有它,终端用户必须依赖互联网的可用性来存储和访问数据。当用户有一个强大的网络连接时,你可以将数据从本地数据库同步到你的服务器。

干杯,继续努力。

The postHow to persist data in Flutter using SQLiteappeared first onLogRocket Blog.