Trang chủ Kiến Thức Công Nghệ Sử dụng SQLite trong Flutter
Công Nghệ

Sử dụng SQLite trong Flutter

Chia sẻ
Sử dụng SQLite trong Flutter
Chia sẻ

Persisting data rất quan trọng đối với người dùng vì họ sẽ bất tiện khi nhập thông tin của họ mỗi lần hoặc đợi mạng tải lại cùng một dữ liệu. Trong những tình huống như thế này, tốt hơn là lưu dữ liệu của họ local.

Trong bài viết này, mình sẽ chứng minh điều này bằng cách sử dụngSQLitetrong Flutter.

Tại sao sử dụng SQLite?

SQLite là một trong những cách phổ biến nhất để lưu trữ dữ liệu cục bộ (local). Đối với bài viết này, chúng ta sẽ sử dụng package sqflite để kết nối với SQLite. Sqflite là một trongsqflitepackage được sử dụng nhiều nhất và cập nhật để kết nối với cơ sở dữ liệu SQLite databases trong Flutter.

1. Thêm dependency vào dự án của bạn

Trong dự án của bạn, hãy truy cậppubspec.yamlvà tìm kiếm cácdependencies. Trongdependencies, hãy thêm phiên bản mới nhất củasqflitepath_provider(sử dụng đúng số từ Pub).

Yaml

dependencies:
  flutter:
    sdk: flutter
  sqflite: any
  path_provider: any

GHI CHÚ:

Chúng ta sử dụngpath_provider package để lấy vị trí thường được sử dụng nhưTemporaryDirectoryApplicationDocumentsDirectory.

2. Khởi tạo DB Client

Bây giờ trong dự án của bạn, hãy tạo một tệp Database.dartmới. Trong tệp mới tạo, chúng ta cần tạo một singleton.

Tại sao chúng ta cần singleton: Chúng ta sử dụng singleton pattern để đảm bảo rằng chúng ta chỉ có một class instance và cung cấp quyền truy cập điểm toàn cục vào nó.

1. Tạo một private constructor chỉ có thể được sử dụng bên trong class:

Dart

class DBProvider {
  DBProvider._();
  static final DBProvider db = DBProvider._();
}

2. Thiết lập database

Tiếp theo, chúng ta sẽ tạo database object và cung cấp cho nó một getter nơi chúng ta sẽ khởi tạo database nếu nó chưa được khởi tạo (lazy initialization).

Dart

static Database _database;

  Future<Database> get database async {
    if (_database != null)
    return _database;

    // if _database is null we instantiate it
    _database = await initDB();
    return _database;
  }

Nếu không có object nào được gán cho database, chúng ta sử dụng hàminitDBđể tạo database. Trong hàm này, chúng ta sẽ nhận được đường dẫn để lưu trữ database và tạo các table mong muốn:

Dart

initDB() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, "TestDB.db");
    return await openDatabase(path, version: 1, onOpen: (db) {
    }, onCreate: (Database db, int version) async {
      await db.execute("CREATE TABLE Client ("
          "id INTEGER PRIMARY KEY,"
          "first_name TEXT,"
          "last_name TEXT,"
          "blocked BIT"
          ")");
    });
  }

LƯU Ý: Tên database là TestDB và table duy nhất chúng ta có được gọi là Client.

3. Khởi tạo các Model

Dữ liệu bên trong database của bạn sẽ được chuyển đổi thành Dart Maps nên trước tiên, chúng ta cần tạo các model với toMapmethod vàfromMapmethod. Mình sẽ không trình bày cách thực hiện việc này theo cách thủ công.

Model của chúng ta:

Dart

/// ClientModel.dart
import 'dart:convert';

Client clientFromJson(String str) {
  final jsonData = json.decode(str);
  return Client.fromMap(jsonData);
}

String clientToJson(Client data) {
  final dyn = data.toMap();
  return json.encode(dyn);
}

class Client {
  int id;
  String firstName;
  String lastName;
  bool blocked;

  Client({
    this.id,
    this.firstName,
    this.lastName,
    this.blocked,
  });

  factory Client.fromMap(Map<String, dynamic> json) => new Client(
        id: json["id"],
        firstName: json["first_name"],
        lastName: json["last_name"],
        blocked: json["blocked"] == 1,
      );

  Map<String, dynamic> toMap() => {
        "id": id,
        "first_name": firstName,
        "last_name": lastName,
        "blocked": blocked,
      };
}

4. CRUD operations

Create (Khởi tạo)

SQFlitepackage cung cấp hai cách để xử lý các operation này bằng cách sử dụng RawSQL queries hoặc bằng cách sử dụng tên table và map chứa dữ liệu:

Sử dụngrawInsert:

Dart

newClient(Client newClient) async {
    final db = await database;
    var res = await db.rawInsert(
      "INSERT Into Client (id,first_name)"
      " VALUES (${newClient.id},${newClient.firstName})");
    return res;
  }

Sử dụnginsert:

Dart

newClient(Client newClient) async {
    final db = await database;
    var res = await db.insert("Client", newClient.toMap());
    return res;
  }

Một ví dụ khác sử dụng ID lớn nhất làm ID mới:

Dart

newClient(Client newClient) async {
    final db = await database;
    //get the biggest id in the table
    var table = await db.rawQuery("SELECT MAX(id)+1 as id FROM Client");
    int id = table.first["id"];
    //insert to the table using the new id 
    var raw = await db.rawInsert(
        "INSERT Into Client (id,first_name,last_name,blocked)"
        " VALUES (?,?,?,?)",
        [id, newClient.firstName, newClient.lastName, newClient.blocked]);
    return raw;
  }

Read

Get Client by id

Dart

getClient(int id) async {
    final db = await database;
    var res =await  db.query("Client", where: "id = ?", whereArgs: [id]);
    return res.isNotEmpty ? Client.fromMap(res.first) : Null ;
  }

Trong đoạn code trên, chúng ta cung cấp query với mộtidlàm tham số bằngwhereArgs. Sau đó, chúng ta trả về kết quả đầu tiên nếu danh sách không rỗng, nếu không chúng ta trả về null.

Get all Clients với điều kiện

Trong ví dụ này, mình đã sử dụngrawQueryvà mình đã map danh sách kết quả thành danh sách cácClientobject:

Dart

getAllClients() async {
    final db = await database;
    var res = await db.query("Client");
    List<Client> list =
        res.isNotEmpty ? res.map((c) => Client.fromMap(c)).toList() : [];
    return list;
  }

Ví dụ: Chỉ nhận được những Blocked Client

Dart

getBlockedClients() async {
    final db = await database;
    var res = await db.rawQuery("SELECT * FROM Client WHERE blocked=1");
    List<Client> list =
        res.isNotEmpty ? res.toList().map((c) => Client.fromMap(c)) : null;
    return list;
  }

Update

Cập nhật dữ liệu

Dart

updateClient(Client newClient) async {
    final db = await database;
    var res = await db.update("Client", newClient.toMap(),
        where: "id = ?", whereArgs: [newClient.id]);
    return res;
  }

Ví dụ: Block hoặc unblock một Client:

Dart

blockOrUnblock(Client client) async {
    final db = await database;
    Client blocked = Client(
        id: client.id,
        firstName: client.firstName,
        lastName: client.lastName,
        blocked: !client.blocked);
    var res = await db.update("Client", blocked.toMap(),
        where: "id = ?", whereArgs: [client.id]);
    return res;
  }

Delete

Xóa dữ liệu

Dart

deleteClient(int id) async {
    final db = await database;
    db.delete("Client", where: "id = ?", whereArgs: [id]);
  }

Xóa tất cả Client

Dart

deleteAll() async {
    final db = await database;
    db.rawDelete("Delete * from Client");
  }

Demo

Đối với bản demo của chúng ta, chúng ta sẽ tạo một ứng dụng Flutter đơn giản để tương tác với database của chúng ta.

Trước tiên, chúng ta sẽ bắt đầu với bố cục của ứng dụng:

Dart

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Flutter SQLite")),
      body: FutureBuilder<List<Client>>(
        future: DBProvider.db.getAllClients(),
        builder: (BuildContext context, AsyncSnapshot<List<Client>> snapshot) {
          if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data.length,
              itemBuilder: (BuildContext context, int index) {
                Client item = snapshot.data[index];
                return ListTile(
                  title: Text(item.lastName),
                  leading: Text(item.id.toString()),
                  trailing: Checkbox(
                    onChanged: (bool value) {
                      DBProvider.db.blockClient(item);
                      setState(() {});
                    },
                    value: item.blocked,
                  ),
                );
              },
            );
          } else {
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () async {
          Client rnd = testClients[math.Random().nextInt(testClients.length)];
          await DBProvider.db.newClient(rnd);
          setState(() {});
        },
      ),
    );
  }

Ghi chú:

1. FutureBuilderđược sử dụng để lấy dữ liệu từ database.

2. FAB để thêm một random client vào database khi nó được nhấp vào.

Dart

List<Client> testClients = [
    Client(firstName: "Raouf", lastName: "Rahiche", blocked: false),
    Client(firstName: "Zaki", lastName: "oun", blocked: true),
    Client(firstName: "oussama", lastName: "ali", blocked: false),
  ];

3. CircularProgressIndicatorđược hiển thị nếu không có dữ liệu.

4. Khi người dùng nhấp vào checkbox, client sẽ bị chặn hoặc bỏ chặn tùy theo state hiện tại.

Giờ đây, rất dễ dàng để thêm các feature mới, ví dụ: nếu bạn muốn xóa một client khi mục được vuốt, chỉ cần wrapListTilebằng mộtDismissibleWidget như sau:

Dart

return Dismissible(
   key: UniqueKey(),
   background: Container(color: Colors.red),
   onDismissed: (direction) {
    DBProvider.db.deleteClient(item.id);
   },
    child: ListTile(...),
  );

Đối với hàmOnDismissedcủa chúng ta, chúng ta đang sử dụng Database provider để calldeleteClientmethod. Đối với đối số, chúng ta chuyển id của mục.

Cấu trúc lại để sử dụng BLoC Pattern

Chúng ta đã làm rất nhiều điều trong bài viết này nhưng trong ứng dụng thế giới thực, việc biến state thành một phần của giao diện người dùng không thực sự là một điều tốt. Thay vào đó, chúng ta nên luôn giữ chúng tách biệt.

Có rất nhiều pattern để quản lý state trong Flutter nhưng mình sẽ sử dụng BLoC trong bài viết này vì nó rất linh hoạt.

Khởi tạo BLoC:

Dart

class ClientsBloc {
  ClientsBloc() {
    getClients();
  }
  final _clientController =     StreamController<List<Client>>.broadcast();
  get clients => _clientController.stream;

  dispose() {
    _clientController.close();
  }

  getClients() async {
    _clientController.sink.add(await DBProvider.db.getAllClients());
  }
}

Ghi chú:

  1. getClientssẽ lấy dữ liệu từ Database (Client table) một cách không đồng bộ (asynchronously). Chúng ta sẽ call method này bất cứ khi nào chúng ta cập nhật table, do đó là lý do để đặt nó vào phần body của constructor.
  2. StreamController<T>.broadcastconstructor để chúng ta có thể nghe stream nhiều lần. Trong ví dụ của chúng ta, nó không tạo ra nhiều sự khác biệt vì chúng ta chỉ nghe stream một lần nhưng sẽ tốt hơn nếu xem xét các trường hợp bạn muốn nghe stream nhiều lần.
  3. Đừng quên đóng stream của bạn. Điều này giúp chúng ta không bị rò rỉ bộ nhớ. Trong ví dụ của chúng ta, chúng ta sẽ đóng nó bằng cách sử dụng dispose method củaStatefulWidgetcủa chúng ta.

Bây giờ, hãy thêm một số method vào block của chúng ta để tương tác với database:

Dart

blockUnblock(Client client) {
  DBProvider.db.blockOrUnblock(client);
  getClients();
}

delete(int id) {
  DBProvider.db.deleteClient(id);
  getClients();
}

add(Client client) {
  DBProvider.db.newClient(client);
  getClients();
}

Và đó là tất cả cho BLoC của chúng ta!

Bước tiếp theo của chúng ta sẽ là tìm cách cung cấp bloc cho các widget của chúng ta. Chúng ta cần một cách để làm cho bloc có thể truy cập từ các phần khác nhau của tree đồng thời có thể tự giải phóng khỏi bộ nhớ khi không sử dụng.

Đối với điều này, có thể xem thư viện này bởi

Trong trường hợp của chúng ta, bloc sẽ chỉ được sử dụng bởi một widget nên chúng ta có thể khai báo và loại bỏ nó khỏi stateful widget của chúng ta.

Dart

final bloc = ClientsBloc();

@override
void dispose() {
  bloc.dispose();
  super.dispose();
}

Tiếp theo, chúng ta cần sử dụngStreamBuilderthay vìFutureBuilder. Điều này là do chúng ta hiện đang nghe stream (clients stream) thay vì tương lai.

Dart

StreamBuilder<List<Client>>(
  stream: bloc.clients,
  ...
)

Bước cuối cùng sẽ là cấu trúc lại code của chúng ta để chúng ta call các method từ bloc của chúng ta chứ không phải database trực tiếp:

Dart

onDismissed: (direction) {
  bloc.delete(item.id);
},

Đây là kết quả cuối cùng

Cuối cùng, bạn có thể tìm thấy code source cho ví dụ này trong repo này (kiểm tra nhánh sqlite_demo_bloc để xem phiên bản mới sau khi tái cấu trúc). Mình hy vọng bạn thích bài viết này.

Bài viết được lược dịch từ Raouf Rahiche.

Bài viết cùng chuyên mục
Tối ưu ứng dụng với cấu trúc dữ liệu cơ bản và bitwise
Công Nghệ

Tối ưu ứng dụng với cấu trúc dữ liệu cơ bản và bitwise

Trong bài viết này, 200Lab sẽ chia sẻ những trường hợp dễ...

Công Nghệ

So sánh Flutter vs React Native: Framework nào đáng học năm 2021

Điểm chung của Flutter, React Native đều là Cross-platform Mobile, build native...

HTTP/2 là gì? So sánh HTTP/2 và HTTP/1
Công Nghệ

HTTP/2 là gì? So sánh HTTP/2 và HTTP/1

Từ khi Internet ra đời, sự phát triển về các giao thức...

Upload File từ Frontend đến Backend mà rất nhiều bạn vẫn đang làm sai!!
Công Nghệ

Upload File từ Frontend đến Backend mà rất nhiều bạn vẫn đang làm sai!!

1. Client encode file (base64) rồi gởi về backend 200Lab đã từng...

Công Nghệ

React Native – Hướng dẫn làm việc với Polyline và Animated-Polyline trên Map

Vẽ đường đi trên bản đồ là một nghiệp vụ vô cùng...

Công Nghệ

Hybrid App và Native App: Những khác biệt to lớn

Bất cứ khi nào một công ty quyết định làm ứng dụng...

Web/System Architecture 101 – Kiến trúc web/hệ thống cơ bản cho người mới
Công Nghệ

Web/System Architecture 101 – Kiến trúc web/hệ thống cơ bản cho người mới

Đây là một kiến trúc cơ bản mà bất kì một người...

Công Nghệ

Tư duy kiến trúc thông qua các trò chơi mà rất nhiều bạn không biết

Tư duy kiến trúc là gì? Tư duy kiến trúc có thể...