Flutter 数据库篇 —— 本地数据存储和状态管理的优雅结合

5,295 阅读5分钟

效果展示

  说明:通过本地数据库存储数据,将你的汽车 🚗 存入本地,并且通过状态管理刷新页面,可以控制车辆的启动状态(读写数据库)

前言 📌

  最近由于业务需求,我开始研究Flutter中的本地数据存储,要做好本地的数据存储最重要的就是对数据库的要有基本的了解,建库、建表、增删改查,都是数据库基本操作,各位在开始前一定要对数据库有一定的了解,不然会看的云里雾里😅 。

小说明 🤠

  其实单论数据库本地的存储,粗略一想会觉得~很简单啊😑 ,没什么可以学习的点。实际上手代码写一个demo也很简单,你可以轻松做到建库等一系列操作,但是本文要做的是将这些看似简单的操作和实际业务相结合,具体去扣其中的细节,相信看完还是会有不少收获的🎉。

技术选型

  我们在Flutter中操作数据库时,我们的首要目的一定是我们要对一些数据(具有一定数据量,非轻量级)的数据进行一些操作,那我们操作的目的是什么?一定是满足用户的需求,进行各类的数据展示💡 !既然如此,如何将我们数据更优雅的展示呢?或者说,如何在操作数据库时尽可能保证Flutter的渲染性能?这离不开Flutter里老生常谈的内容——状态管理。状态管理的方案有很多,我个人在学习这部分内容的时候看到很多采用bloc的方式,但我的业务项目在前期已经采用了provider,那咋办馁 👀 ,那就只能自己研究了嘛~。明确了我的状态管理方案之后,就是挑选plugin了。provider作为官方插件,优先级自然不用说。那数据库呢?综合考虑后我们的数据库采用SQLite,有人会问了,综合考虑是考虑了什么?🤯 ,那还用问吗?   高热度➕ 高LikesFlutter Favorite。口水不争气的从嘴里流了出来,太香了呀 👀

  • database(数据库):SQLite —— sqflite
  • 状态管理:provider —— provider

开始构建、步步为营

第一步:准备充分 💪

  我们首先要搭建一个新工程,在新工程了我们加入我们项目中所要用的各个三方库:

  sqflite: ^1.3.1
  provider: ^4.3.2
  path_provider: ^1.6.11
  • path_provider:用于获取本地的储存路径

第二步:由简入繁 📈

  首先第一步我们看到这样一个工程,而且我们要存数据库,那首先需要的是什么?是一个数据结构,用于我们的两个操作,这个数据结构就是我们 🚗 的数据结构。我们要有的各个属性:车的品牌,车的型号,是否启动。还有一个id,方便我们的读取,其实熟悉数据库的同学应该也能想到,这个id,就是我们之后的主键 🐳 。

class Car {
  int id;
  String brand;
  String type;
  bool start;

  Car({@required this.brand, this.type, this.start = false});

  Map<String, dynamic> toMap() {
    var map = Map<String, dynamic>();
    if (id != null) {
      map['id'] = id;
    }
    map['brand'] = brand;
    map['type'] = type;
    map['start'] = start == true ? 1 : 0;
    return map;
  }

  Car.fromMapObject(Map<String, dynamic> map) {
    this.id = map['id'];
    this.brand = map['brand'];
    this.type = map['type'];
    this.start = map['start'] == 1 ? true : false;
  }
}

第三步:关键的数据库操作 🏄🏻‍♂️

  接下来的一步非常关键,迎合我们今天的主题。我们对数据库的操作包括建库、建表、增删改查,这些基础操作,我们都去UI层写会让我们的结构看起来非常乱 🤯 ,而且复用性非常差,所以我们这里抽一个单独的类出来,进行封装,我叫他DatabaseHelper。得益于我们强大的插件,我们只需要处理上层API即可 🤠 ,底层的多端操作已经被封装。

void _createDb(Database db, int newVersion) async {
    await db.execute(
        'CREATE TABLE $carsTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colBrand TEXT, $colType TEXT, $colStart INTEGER)');
  }

  //  读取数据
  Future<List<Map<String, dynamic>>> getCarMapList() async {
    Database db = await this.database;
    var result = await db.query(carsTable);
    return result;
  }

  //  增加数据
  Future<int> insertCar(Car car) async {
    Database db = await this.database;
    var result = await db.insert(carsTable, car.toMap());
    return result;
  }

  //  刷新数据
  Future<int> updateCar(Car car) async {
    Database db = await this.database;
    var result = await db.update(carsTable, car.toMap(),
        where: '$colId = ?', whereArgs: [car.id]);
    return result;
  }

  //  删除数据
  Future<int> deleteCar(int id) async {
    Database db = await this.database;
    int result =
    await db.rawDelete('DELETE FROM $carsTable WHERE $colId = $id');
    return result;
  }

第四步:同样关键的状态管理 🔮

  这一步也是我们的核心,我们如何将我们的数据库中的数据和UI部分相结合并且合理控制渲染范围 🖼,听起来很复杂的样子,我们可以思考几个核心点,一点一点组成我们的这个部分 ⚒ 。

  • 首先,选择了provider作为状态管理,我们根据传统要将我们封装的provider类混入ChangeNotifier
  • 我们需要多个状态改变的方法,每一个方法中都要通知刷新,即加入notifyListeners()方法。
  • 我们要明确我们需要哪些方法:增加数据,修改数据,减少数据,读取数据。 相信带着上面这几个点,你看下面这段代码会有思路的多 😁 :
class CarsProvider with ChangeNotifier {
  DatabaseHelper _databaseHelper = DatabaseHelper();
  List<Car> _cars;
  int _count = 0;

  UnmodifiableListView<Car> get allCars => UnmodifiableListView(_cars);

  UnmodifiableListView<Car> get unStartedCars =>
      UnmodifiableListView(_cars.where((car) => car.start == false));

  UnmodifiableListView<Car> get startedCars =>
      UnmodifiableListView(_cars.where((car) => car.start == true));

  void getCarList() {
    final Future<Database> dbFuture = _databaseHelper.initializeDatabase();
    dbFuture.then((database) {
      Future<List<Car>> carListFuture = _databaseHelper.getCarList();
      carListFuture.then((carList) {
        this._cars = carList;
        this._count = carList.length;
        notifyListeners();
      });
    });
  }

  int get count => _count;

  void addCar(Car car) async {
    int result;
    if (car.id != null) {
      result = await _databaseHelper.updateCar(car);
    } else {
      result = await _databaseHelper.insertCar(car);
    }
    if (result != 0) {
      print('Success');
    } else {
      print('Failed');
    }
    notifyListeners();
  }

  void startCar(BuildContext context, Car car) async {
    car.start = !car.start;
    int result = await _databaseHelper.updateCar(car);

    if (result != 0 && car.start == true) {
      _showSnackBar(context, '${car.brand}' +'  '+'${car.type}' + '      ' +'Start');
    }
    notifyListeners();
  }

  void deleteCar(BuildContext context, Car car) async {
    if (car.id == null) {
      _showSnackBar(context, 'No Car was deleted');
      return;
    }

    int result = await _databaseHelper.deleteCar(car.id);
    if (result != 0) {
      _showSnackBar(context, 'Car Deleted Successfully');
    } else {
      _showSnackBar(context, 'Error Occurred while deleting note');
    }
    notifyListeners();
  }

  void _showSnackBar(BuildContext context, String message) {
    final snackBar = SnackBar(
      duration: Duration(milliseconds: 500),
      content: Text(
        message,
        style: TextStyle(color: Colors.white),
      ),
      backgroundColor: Theme.of(context).primaryColor,
    );
    Scaffold.of(context).showSnackBar(snackBar);
  }
}

第五步:丰满UI内容 🎁

  其实以上两个部分是我们的核心,如果能理解上面两个核心的类,我再抛出一些UI级的操作你应该会很好理解 🚀 。 这里我就展示一个增加车辆的操作。更多内容可以查看文末的源码。

//新增一辆车
  void onAdd() {
    if (carBrandController.text.isNotEmpty && carTypeController.text.isNotEmpty ) {
      final Car car = Car(brand: carBrandController.text, type:carTypeController.text, start: started);
      Provider.of<CarsProvider>(context, listen: false).addCar(car);
      Navigator.pop(context);
    }
  }

结尾与展望

  我们在学习一些内容的时候有时候感觉上会很简单,实际上手还是有很多困难,遇到问题及时解决非常重要 ⚙️ 。每次接触Flutter中的新知识点都会有所收获 🎊 ,我也会尽量把自己的收获整理出来,对我来说,一方面这巩固了我的记忆加深了我的理解,另一方面也可以给遇到相关问题的朋友一些帮助。欢迎大家关注我的博客 🤝 。

源码

强烈建议给个🌟鼓励作者,哈哈哈哈哈哈哈:本文demo