, May 25, 2022

0 kết quả được tìm thấy

Sử dụng SQLite trong Flutter

  • Đăng bởi  Chau Le
  •  Jan 23, 2022

  •   9 min reads
Sử dụng SQLite trong Flutter

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).

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:

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).

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:

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:

/// 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:

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:

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:

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

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:

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

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

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:

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

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

Xóa tất cả Client

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:

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.

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:

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:

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:

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.

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.

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:

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 liên quan

Fix lỗi Flutter 3 không thể build app trên iOS

Cách fix lỗi Flutter 3 không thể build và run được app trên iOS với video hướng dẫn chia tiết...

Fix lỗi Flutter 3 không thể build app trên iOS
Flutter Coding UI Speed Code

Nhận bản UI siêu đẹp nhưng làm sao để phân tích rồi code ra một cách chính xác nhất? Series này 200Lab sẽ cho bạn một góc nhìn thực tế về quá trình code UI cho app Movie Ticket....

Flutter Coding UI Speed Code
Flutter 3 - Những cập nhật mới có gì hot

Bài viết này cung cấp cho bạn những thông tin cập nhật mới nhất của Flutter 3, giúp bạn có cái nhìn tổng quan về các thay đổi và tính năng bổ ích...

Flutter 3 - Những cập nhật mới có gì hot
Mọi thứ bạn cần biết về Route Transition của Flutter

Chúng ta cũng có thể kết hợp nhiều transition để tạo ra thứ gì đó tuyệt vời như scale và rotate cùng một lúc....

Mọi thứ bạn cần biết về Route Transition của Flutter
Hướng dẫn custom RenderObject của riêng bạn

Khi "khám phá" source code Flutter, bạn sẽ phát hiện ra rằng phần lớn các widget không sử dụng composition hay CustomPaint. Thay vào đó, chúng sử dụng RenderObject...

Hướng dẫn custom RenderObject của riêng bạn
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.