Flutter新人实战—从0开始开发一个DIY活动记录应用(六)数据持久化之sqflite

版权声明:本文为本人原创文章,未经本人允许不得转载。

Hello,大家好这两天利用空余时间忙着写统计分析页面,所以隔了两天来继续更新。
第五篇为止我们基本把要展现的页面和UI都写完了,但是数据都是初始化好的,如何通过用户自己添加一条数据并进行数据永久保存呢,有请今天的主角:sqflite,Flutter的嵌入式数据库插件。
要使用他我们首先要在pubspec.yaml中引入这个包,同时由于数据库操作要用到本地的路径访问,所以我们首先一并添加这些包:

dependencies:
  flutter:
    sdk: flutter
  #数据库
  sqflite: ^0.11.0
  #文件路径访问
  path_provider: ^0.4.0

然后我们执行get packages,获取插件内容。下面我们正式开始今天的内容。

一、新建数据库及其增删改查方法

1、修改diy_project.dart,添加如下代码:

  //根据项目的属性内容生成一个键值对map,这个mao的键就是后面数据库的字段,他们是一一对应的。
  Map<String, dynamic> toMap() {
    Map map = <String, dynamic>{
      'name': _name,
      'date': _date,
      'place': _place,
      'contact': _contact,
      'imagePath': _imagePath,
      'singlePrice': _singlePrice,
      'nums': _nums,
      'totalAmount': _totalAmount,
      'itemCost': _itemCost,
      'laborCost': _laborCost,
      'profit': _profit,
      'isCheckOut': _isCheckOut == true ? 1 : 0
    };
    if (_id != null) {
      map['id'] = _id;
    }
    return map;
  }

  //根据map获得一个diyProject的实例
  DiyProject.fromMap(Map<String, dynamic> map) {
    _id = map['id'];
    _name = map['name'];
    _date = map['date'];
    _place = map['place'];
    _contact = map['contact'];
    _imagePath = map['imagePath'];
    _singlePrice = map['singlePrice'];
    _nums = map['nums'];
    _totalAmount = map['totalAmount'];
    _itemCost = map['itemCost'];
    _laborCost = map['laborCost'];
    _profit = map['profit'];
    _isCheckOut = map['isCheckOut'] == 1;
  }

以上代码分别是根据项目实例转换成map和通过map获得一个项目实例。注意,其中map的键是和后面数据库字段相对应的。

2、新建数据库

在model目录下新建data_base.dart 文件,我们将在里面编写数据库相关的代码。
首先导入插件的引用:

import 'package:activity_record/model/diy_project.dart'; //项目类,供数据库操作使用
import 'package:sqflite/sqflite.dart'; //数据库
import 'dart:io'; //本地文件使用
import 'dart:async';//异步操作
import 'package:path/path.dart';//本地路径访问
import 'package:path_provider/path_provider.dart';//本地路径访问

继续新建数据库类

class DataBase {
  Database _myDateBase;
  //定义表名
  final String tableName = "diyTable1";
  //定义各个字段名
  final String columnId = "id";
  final String columnName = "name";
  final String columnDate = "date";
  final String columnPlace = "place";
  final String columnContact = "contact";
  final String columnImagePath = "imagePath";
  final String columnSinglePrice = "singlePrice";
  final String columnNums = "nums";
  final String columnTotalAmount = "totalAmount";
  final String columnItemCost = "itemCost";
  final String columnLaborCost = "laborCost";
  final String columnProfit = "profit";
  final String columnIsCheckOut = "isCheckOut";

  //获取数据库
  Future get db async {
    if (_myDateBase != null) {
      print('数据库已存在');
      return _myDateBase;
    } else
      _myDateBase = await initDb();
      return _myDateBase;
  }

  //初始化数据库,根据路径版本号新建数据库
  initDb() async {
    Directory directory = await getApplicationDocumentsDirectory();
    String path = join(directory.path, "diyDB.db");
    var dataBase = await openDatabase(path, version: 1, onCreate: _onCreate);
    print('数据库创建成功,version: 1');
    return dataBase;
  }

  //新建数据库表
  FutureOr _onCreate(Database db, int version) async {
    await db.execute('''create table $tableName(
    $columnId integer primary key autoincrement,
    $columnName text not null,
    $columnDate text,
    $columnPlace text,
    $columnContact text,
    $columnImagePath integer ,
    $columnSinglePrice integer not null,
    $columnNums integer not null,
    $columnTotalAmount integer not null,
    $columnItemCost integer ,
    $columnLaborCost integer ,
    $columnProfit integer not null,
    $columnIsCheckOut integer not null)''');
    print('Table is created');
  }
}

以上分别说明数据库是如何建立起来的:

1、定义表明和表的字段名,因为我这个项目属性不多就用了一张表,注意这个字段名要和之前的diyProject里的map方法里的键要一致,因为后面的数据操作都要依赖这个。
2、(1)初始化数据库,首先通过getApplicationDocumentsDirectory();获得一个本地目录用于存放数据库文件,
(2)然后通过join(directory.path, "diyDB.db")在某个目录里新建一个数据库文件,其中括号内属性是目录路径和数据库名称,同时返回这个数据库文件的路径。
(3)最后通过这个路径我们打开数据库并初始化数据库,包括数据库版本和新建数据表
3、获得这个数据库实例,首先判断数据库是否存在,如果不存在则新建,存在则返回。

回到home_page.dart
添加:

//实例化数据库对象
  DataBase _database = new DataBase();

运行一下程序看看,如果是首次运行应该会在控制台看到这样的话:


image.png

当你下次再运行应用的时候就会再控制台看到提示 ‘数据库已存在’,除非卸载应用重新安装,否则数据库文件将一直存在。

3、增加数据库增删改查方法

数据库已经有了,显然我们是要对数据库进行操作的,虽然数据库感觉很复杂的样子,但是归纳起来无非就是增删改查,数据库操作会涉及数据库sql语句,如果这块没有基础的话我建议去了解下sql语句基础,对普通的增删改查有个了解,知道怎么写即可。
继续在数据库类里添加以下代码:
1、新增数据

//插入diyProject
  Future<int> insertDiyProject(DiyProject diy) async {
    //获取数据库实例
    Database database = await db;
    //diy.toMap()是将diy实例转换成字段名和值对应的map
    var result = database.insert(tableName, diy.toMap());
    print('数据已插入');
    return result;
  }

2、查询数据

//获取所有diyProject
  Future<List> getDiyProjects() async {
    //获取数据库实例
    Database database = await db;
    //返回一个 map型的数组,其中map是由字段名和值构成
    var result = await database
        .rawQuery("select * from $tableName order by $columnId desc");
    print('获取所有diyProject,当前diyProject有: $result');
    return result;
  }

  //获取所有未结账的diyProject
  Future<List> getUnCheckedDiyProjects() async {
    Database database = await db;
    var result = await database.rawQuery(
        "select * from $tableName where $columnIsCheckOut = 0 order by $columnId desc");
    return result;
  }

  //获取diyProject总数
  Future<int> getDiyCount() async {
    Database database = await db;
    var result = await database.rawQuery("select count(*) from $tableName");
    /*查询结果返回的是一个map类型的数组,虽然这里查询结果只有一条,但是很多查询是会返回多条数据的,所以是一个数组类型。
     这里我们取数组的第一个值,然后再通过键来取对应的数据
    */
    return result[0]['count(*)'];
  }

  

  //获取单个diyProject
  Future<DiyProject> getDiyProject(int id) async {
    Database database = await db;
    //根据id查询对应的diy项目,并返回一个map类型的数组
    var result = await database
        .rawQuery("select * from $tableName where $columnId = $id");
    if (result.length == 0) {
      return null;
    } else
      return DiyProject.fromMap(result[0]);
  }

我这里一共写了4个查询方法,注释里已经分别写了查询的内容,都是后面会用到的。

注意点:数据库查询操作返回的都是map类型的数组,map中的键就是查询对象,值就是查询的结果。哪怕查询结果只有一条返回的也是一个数组。

3、更新数据

//更新diyProject
  Future<int> updateDiyProject(DiyProject diyProject) async {
    Database database = await db;
    var result = database.update(tableName, diyProject.toMap(),
        where: "$columnId = ?", whereArgs: [diyProject.id]);
    print('我是更新数据的方法 本次更新的res: $result');
    return result;
  }

4、删除数据

//删除diyProject
  Future<int> deleteDiyProject(int id) async {
    Database database = await db;
    var result = await database
        .rawDelete("delete from $tableName where $columnId = $id");
    return result;
  }

4、添加关闭数据库方法

别忘了当不再使用的时候关闭数据库

//关闭数据库
  Future close() async {
    Database database = await db;
    database.close();
  }

到这里呢,数据库类我们就完全写好了,这样我们就可以通过不同的方法来对数据库进行读写数据啦。看不到效果可不行,当然要去业务逻辑里实现以下。

二、实现用户和数据库的交互

在进行业务逻辑实现之前,我们先要合并一些文件。在之前的文章中,为了给大家演示清楚页面框架结构和里面控件的关系,我将框架和UI分开了,但是因为在这个应用中页面UI的内容都会根据用户输入而改变,所以需要将框架和页面UI放在一个dart文件里,这样操作更方便。
我们需要分别合并以下文件:

1、ui目录下的diy_add_show.dart 和pages目录下的diy_add_dialog.dart合并,所谓合并是指将ui文件里的控件内容复制到pages页面文件里的Scaffold所属的body属性里。
2、ui目录下的diy_info_show.dart和pages目录下的diy_item_info.dart合并
3、ui目录下的diy_list_show.dart和pages目录下的home_page.dart合并

要实现用户新增数据到数据库并展现在首页列表中需要分以下几个步骤:

1、通过用户输入的内容获得一个diyProject实例对象
2、当用户点击保存按钮的时候将项目实例对象内容插到数据库
3、告诉首页数据库里新增了一条数据需要重绘UI以显示新增的这条数据

通过以上分析我们可以基本得知在新增项目页面,用户主要分为两类操作,在各个输入框中填入项目信息和点击保存。
那么我们首先在diy_add_dialog.dart中新增点击保存后的方法:
diy_add_dialog.dart

//点击保存按钮进行数据保存入库
  _saveDiyItem(
      String name,
      String date,
      String place,
      String contact,
      String imagePath,
      int singlePrice,
      int nums,
      int totalAmount,
      int itemCost,
      int laborCost,
      int profit,
      bool isCheckOut) async {
    DiyProject newDiyProject = new DiyProject(
        name,
        date,
        place,
        contact,
        imagePath,
        singlePrice,
        nums,
        totalAmount,
        itemCost,
        laborCost,
        profit,
        isCheckOut);
    int savedID = await widget.db.insertDiyProject(newDiyProject);
    print('插入的savedID:$savedID');

以上方法执行了两步操作:
1、根据参数实例化一个diyProject实例对象
2、通过调用数据库的insertDiyProject方法将对象插入数据库

要在这个页面对数据库进行操作,所以需要将数据库作为这个类的构造函数的参数,这样当从首页进入到这个页面的时候将数据库实例作为参数传带进来。
所以在类的开头作如下修改:
diy_add_dialog.dart

class DiyAddDialog extends StatefulWidget {
  DiyAddDialog({Key key, this.db,}) : super(key: key);
  var db;

  @override
  DiyAddDialogState createState() => new DiyAddDialogState();
}

保存的方法有了,剩下的就是在保存按钮里添加这个方法,同时点击后关闭添加页面,回到首页进行展示。
继续修改diy_add_dialog.dart,首先在appbar里添加保存按钮:
diy_add_dialog.dart

class DiyAddDialogState extends State<DiyAddDialog> {
...
@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(_title),
        actions: <Widget>[
          new FlatButton(
            child: new Text(
              '保存',
              style: Theme.of(context)
                  .textTheme
                  .subhead
                  .copyWith(color: Colors.white),
            ),
            onPressed: () {},
          )
        ],
      ),
      body:
...

然后在onPressed: () {}的回调函数里添加保存数据的方法:

          onPressed: () {
              _saveDiyItem(
                  _nameTextEditingController.text,
                  DateFormat.yMd("en_US").format(_selectedDate),
                  _placeTextEditingController.text,
                  _contactTextEditingController.text,
                  _imagePath,
                  int.parse(_singlePriceTextEditingController.text),
                  int.parse(_numsTextEditingController.text),
                  _totalAmountCalculate(),
                  int.parse(_itemCostTextEditingController.text == ''
                      ? '0'
                      : _itemCostTextEditingController.text),
                  int.parse(_laborCostTextEditingController.text == ''
                      ? '0'
                      : _laborCostTextEditingController.text),
                  _profitCalculate(),
                  _isCheckedOut);
              Navigator.of(context).pop();
            },

以上代码注意:
1、_savedDiyItem()方法里的参数有通过用户输入获取的,也有通过方法计算得知的,比如_nameTextEditingController.text就是用户输入的名字,.text是将监听获得内容转换成文本。
2、而int.parse(_singlePriceTextEditingController.text),则是将文本输入的金额转换成int型。
3、像总金额、利润等值不是通过用户输入的,而是通过用户输入的内容计算而来,所以我们要添加两个计算的方法如下:

//根据输入计算总金额
  _totalAmountCalculate() {
    int _totalAmount = int.parse(_singlePriceTextEditingController.text) *
        int.parse(_numsTextEditingController.text);
    return _totalAmount;
  }

  //根据输入计算利润
  _profitCalculate() {
    int totalAmount = _totalAmountCalculate();
    int profit = totalAmount -
        int.parse(_itemCostTextEditingController.text == ''
            ? '0'
            : _itemCostTextEditingController.text) -
        int.parse(_laborCostTextEditingController.text == ''
            ? '0'
            : _laborCostTextEditingController.text);
    return profit;
  }

以上两段代码中可能你都发现了三元运算符int.parse(_laborCostTextEditingController.text == '' ? '0' : _laborCostTextEditingController.text,作用是当用户没有输入的时候给他一个默认值0,否则插入数据库会报错。
4、在onPressed回调函数最后添加Navigator.of(context).pop();表示点击按钮后就会将该页面出栈,回到首页。
说了这么多,是时候自己操作一把了,打开新增页面,自己尝试输入内容,然后点击保存:

image.png

控制台会看到如下信息:
image.png

而且也回到首页了,但是是不是一脸蒙蔽,因为首页并没有展示这条diy项目信息,到底是怎么回事呢,因为我们并没有告诉首页展示的数据内容发生了变化。下面我们回到home_page.dart,让刚才添加的内容展现出来。
修改home_page.dart如下
添加展示内容列表

  //实例化diyProjects数组,并初始化空,作为首页要展示的内容列表
  List<DiyProject> _diyProjects = <DiyProject>[];

每次运行程序的时候或者需要刷新的时候,我们需要先从数据库遍历数据,并加到要展示的内容列表,从而进行数据展示,用到的就是之前数据库类里的查询方法:

  //遍历数据库初始化_diyProjects数据
  _getDiyProjects() async {
    print('=========开始读取数据库全部数据=========');
    List result = await _database.getDiyProjects();
    _diyProjects.clear();
    if (result.length != 0) {
      setState(() {
        result.forEach((diy) => _diyProjects.add(DiyProject.fromMap(diy)));
      });
    }
  }

以上代码首先通过数据库查询获得一个map类型的数组,然后通过diyProject的fromMap方法将这些查询的结果转化成一个个diyProject实例,然后添加到_diyProjects数组中。并通过setState告诉应用要重绘页面。

查询数据库并告诉应用进行页面重绘的方法是有了,那么何时执行这个方法呢??

其实你想一想应该就知道:
1、每次打开应用的时候,我们需要能看到数据库已有的数据。
在初始化里添加这个遍历数据的方法,这样每次打开应用就会看到所有数据了。

 //数据初始化
  @override
  void initState() {
    super.initState();
    _getDiyProjects();
  }

2、每次点击新增然后添加完点击保存的时候我们想看到新的数据。
注意这个时间点的描述是这样的:点击新增页面的保存按钮然后退回到首页的时候。我们知道页面路由的pop是可以返回数据的,我们这里虽然不用返回数据,但是我们就是要在知道返回回来的时候遍历数据库进行数据刷新。
所以我们需要在点击新增按钮的地方使用async异步延迟的方法来进到新页面从而知晓何时从页面返回,从而进行数据刷新,修改floatingActionButton的onPressed回调函数,通过异步方式进入新页面并等待返回时刷新数据

floatingActionButton: new FloatingActionButton(
          onPressed: () {
            _addDiyProject();
          },
          child: new Icon(Icons.add),
        ),

//进入新增页面
  Future _addDiyProject() async {
    await Navigator.of(context).push(new MaterialPageRoute(
        fullscreenDialog: true,
        builder: (context) {
          return new DiyAddDialog(
            db: _database,
            diyProjects: _diyProjects,
          );
        }));
    //返回后,通过_getDiyProjects()方法重新遍历数据库并刷新页面
    _getDiyProjects();
  }

这样当我们点击新增按钮,通过输入信息后点击保存返回首页就可以看到自己新增的card数据了,而且由于数据是保存在数据库当中,即使关闭应用重新打开之前添加的数据也都会在,主要不卸载数据都不会丢失。

可能你也有这样的疑虑:能不能通过Navigator.of(context).pop直接返回这个新增的diyProject,然后将接收到的返回的diyProject插入数组然后刷新页面呢?当然是可以的,你尝试着实现下看看。

留一个小作业给大家吧,还记得在上一篇我在首页添加了一个tabbar,分别进行所有项目和未结项目的展示,我这里主要是针对所有项目进行了说明,如何实现未结项目的展示尝试着做一下把。

今日总结

1、数据库sqflite的使用
2、通过获取用户输入实例化对象并插入数据库中
3、通过带async的Navigator.of(context).push,得知何时返回,并执行相应操作
4、通过setState() 告诉系统状态发生变化,需要重新绘制页面UI从而展示新的内容

最后附上项目源码地址:https://gitee.com/xusujun33/activity_record_jia.git
有不明白的或者有出入的可以对照查看,当然程序一直在更新,所以可能会有内容上的出入,但是方法都是相通的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容