Flutter: Using sqflite with riverpod

This is a rewrite of my previous post but using riverpod instead of provider. The advantages of using riverpod is that the code is more readable, much more simpler and avoid nested builder as you can use the provider without context. This is an early post while i'm still figuring out riverpod, i will update when necessary with more practical example.

If you have question, need some help or feedback, don't hesitate to reach me on twitter.

Introduction

Riverpod is another state management library by the same author of provider package. Riverpod come with the goal to make provider even simpler without affecting provider. It can be use with flutter_hooks directly.

Advantages

Database and data provider are not tightly coupled. You can easily swap it during unit test and widget test.

Improvement that can be done

  • Proper error handling if needed.
  • Proper SQL pagination for large datasets

Test Consideration

  • Unit Test are not possible except to mock it (if you aspire to achieve that 100% mark)
  • Exclude the provider during widget test and pass empty list instead.
  • For widget test i swap it with dummy data instead
  • If you still want to test it automatically, write an integration test intead.

Define Database Provider

import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart' as sql;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart' as path;

final dbProvider = ChangeNotifierProvider<DataBaseHelperProvider>((ref) {
  return DataBaseProvider();
});

class DataBaseHelperProvider with ChangeNotifier {
  static final tableName = 'my_table';
  sql.Database db;

  DataBaseProvider() {
    // this will run when provider is instantiate the first time
    init();
  }

  void init() async {
    final dbPath = await sql.getDatabasesPath();
    db = await sql.openDatabase(
      path.join(dbPath, 'places.db'),
      onCreate: (db, version) {
        final stmt = '''CREATE TABLE IF NOT EXISTS $tableName (
            id TEXT PRIMARY KEY,
            title TEXT,
            image TEXT
        )'''.trim().replaceAll(RegExp(r'[\s]{2,}'), ' ');
        return db.execute(stmt);
      },
      version: 1,
    );
    // the init funciton is async so it won't block the main thread
    // notify provider that depends on it when done
    notifyListeners();
  }

  Future<void> insert(String table, Map<String, Object> data) async {
    await db.insert(table, data, conflictAlgorithm: sql.ConflictAlgorithm.replace);
  }

  Future<List<Map<String, dynamic>>> getData(String table) async {
    return await db.query(table);
  }
}

Other provider that depends on Database provider

// data_provider.dart
import 'dart:io'; // because we are using File in here

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import './db_helper_provider.dart';
import '../models/data.dart';

final dataProvider = ChangeNotifierProvider<DataProvider>((ref) {
  final db = ref.watch(dbProvider);
  return DataProvider([], db);
});

class DataProvider with ChangeNotifier {
  final DataBaseProvider dbProvider;
  List<Data> _items = [];
  final tableName = 'my_table';

  DataProvider(this._items, this.dbProvider) {
    if (dbProvider != null)
      fetchAndSetData();
  }

  List<Data> get items => [..._items];

  void addPlace(String title, File selectedImage) {
    if (dbProvider.db != null) { // do not execute if db is not instantiate
      final newPlace = Place(
          id: DateTime.now().millisecondsSinceEpoch.toString(),
          title: title,
          location: null,
          image: selectedImage
        );
      _items.add(newPlace);
      notifyListeners();
      dbProvider.insert(tableName,
        {'id': newPlace.id, 'title': newPlace.title, 'image': newPlace.image.path});
    }
  }

  Future<void> fetchAndSetData() async {
    if (dbProvider.db != null) { // do not execute if db is not instantiate
      final dataList = await dbProvider.getData(tableName);
      _items = dataList.map((item) => Data(
        id: item['id'],
        title: item['title'],
        image: File(item['image'])
      )).toList();
      notifyListeners();
    }
  }

}

Using the Provider method

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:../provider/database_helper.dart';
import 'package:../provider/data_provider.dart';

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        ...
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          // using riverpod provider method in widget
          // we can access it via build context directly
          onTap: () => context.read(dataProvider).addPlace( ... )
        )
    );
  }
}

References