iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0
自我挑戰組

從零開始的Flutter世界系列 第 29

Day29 Flutter Persistence

  • 分享至 

  • xImage
  •  

今天我們來介紹幾個 Persistence 的方法,即是用來儲存數據,將數據存在我們的手機等硬體裡,以便我們在重開App 或重新開機後也能夠使用之前保存的數據的方法

SQLite

相比於其他儲存資料於本地的方法,SQLite 資料庫能夠提供更為迅速的插入、更新、查詢功能

之後範例會用到一些基本的SQL語句,如果你對於SQLite和SQL的各種語句還不熟悉,請查看SQLite官方的教程 SQLite教程

首先添加依賴:

  • sqflite :使用SQLite 資料庫
  • path:以便能正確的定義資料庫在硬體上的儲存位置
...
dependencies:
	sqflite: ^1.3.1
  path: ^1.7.0
...

範例:

定義要儲存的資料結構

新建一個要儲存的Model 類,之後存在資料庫裡的資料就會有這些欄位

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({this.id, this.name, this.age});
}

打開資料庫

在你準備讀寫資料庫的數據之前,你要先打開這個資料庫,此時需要以下兩個步驟才可以打開資料庫:

  1. 使用sqflitepackage裡的getDatabasesPath方法並配合pathpackage裡的 join方法定義資料庫的路徑
  2. 使用sqflite中的openDatabase()功能打開資料庫

為了使用關鍵字await,必須將代碼放在async函數內。您應該將以下所有表函數放在內void main() async {}

// Avoid errors caused by flutter upgrade.
// Importing 'package:flutter/widgets.dart' is required.
WidgetsFlutterBinding.ensureInitialized();
// Open the database and store the reference.
final Future<Database> database = openDatabase(
  // Set the path to the database. Note: Using the `join` function from the
  // `path` package is best practice to ensure the path is correctly
  // constructed for each platform.
  join(await getDatabasesPath(), 'doggie_database.db'),
);

創建資料表

接下來,你需要創建一個表用以儲存各種狗的信息,在此範例中,要創建一個名為dogs資料庫表,它定義了可以被儲存的數據。這樣,每筆Dog數據就包含了一個idnameage,因此,在dogs數據庫表中將有三列,分別是idnameage

  • idint類型,在資料表中是SQLite的INTEGER數據類型,推薦將id作為資料庫表的主鍵,用以改善查詢和修改的時間,另外name是Dart的String類型,在資料表中是SQLite的TEXT數據類型,age也是Dart的int類型,在資料表中是SQLite的INTEGER數據類型

    關於SQLite資料庫能夠儲存的更多資料類型請查閱官方的 SQLite Datatypes文檔

final Future<Database> database = openDatabase(
  // Set the path to the database. Note: Using the `join` function from the
  // `path` package is best practice to ensure the path is correctly
  // constructed for each platform.
  join(await getDatabasesPath(), 'doggie_database.db'),
  // When the database is first created, create a table to store dogs.
  onCreate: (db, version) {
    // Run the CREATE TABLE statement on the database.
    return db.execute(
      "CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
    );
  },
  // Set the version. This executes the onCreate function and provides a
  // path to perform database upgrades and downgrades.
  version: 1,
);

插入一筆資料

要在dogs資料表中插入一筆Dog的資料,需要分為以下兩步:

  1. Dog轉換成一個Map資料類型

  2. 使用insert()方法把Map保存到`dogs資料表中

// Update the Dog class to include a `toMap` method.
class Dog {
  final int id;
  final String name;
  final int age;

  Dog({this.id, this.name, this.age});

  // Convert a Dog into a Map. The keys must correspond to the names of the
  // columns in the database.
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }
}
// Define a function that inserts dogs into the database
Future<void> insertDog(Dog dog) async {
  // Get a reference to the database.
  final Database db = await database;

  // Insert the Dog into the correct table. You might also specify the
  // `conflictAlgorithm` to use in case the same dog is inserted twice.
  //
  // In this case, replace any previous data.
  await db.insert(
    'dogs',
    dog.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}
// Create a Dog and add it to the dogs table.
final fido = Dog(
  id: 0,
  name: 'Fido',
  age: 35,
);

await insertDog(fido);

查詢資料表

現在已經有了一筆Dog資料存在資料庫裡,你可以通過查詢資料庫,檢索到一隻狗的資料或者所有狗的資料,分為以下兩步:

  1. 調用dogs表對像的query方法,這將返回一個List <Map>
  2. List<Map>轉換成List<Dog>資料類型
// A method that retrieves all the dogs from the dogs table.
Future<List<Dog>> dogs() async {
  // Get a reference to the database.
  final Database db = await database;

  // Query the table for all The Dogs.
  final List<Map<String, dynamic>> maps = await db.query('dogs');

  // Convert the List<Map<String, dynamic> into a List<Dog>.
  return List.generate(maps.length, (i) {
    return Dog(
      id: maps[i]['id'],
      name: maps[i]['name'],
      age: maps[i]['age'],
    );
  });
}

修改一筆資料

使用sqflitepackage中的update()方法,可以對已經插入到資料庫中的數據進行修改(更新)

修改數據操作包含以下兩步:

  1. 將一筆狗的數據轉換成Map資料類型;
  2. 使用 where語句定位到具體將要被修改的資料
Future<void> updateDog(Dog dog) async {
  // Get a reference to the database.
  final db = await database;

  // Update the given Dog.
  await db.update(
    'dogs',
    dog.toMap(),
    // Ensure that the Dog has a matching id.
    where: "id = ?",
    // Pass the Dog's id as a whereArg to prevent SQL injection.
    whereArgs: [dog.id],
  );
}
// Update Fido's age.
await updateDog(Dog(
  id: 0,
  name: 'Fido',
  age: 42,
));

// Print the updated results.
print(await dogs()); // Prints Fido with age 42.

使用whereArgs將參數傳遞給where語句,有助於防止SQL注入攻擊

這裡請勿使用字串插補 (String interpolation),比如:where: "id = ${dog.id}"

刪除一筆資料

除了插入和修改狗狗們的數據,你還可以從資料庫中刪除狗的數據。刪除數據用到了sqflitepackage中的delete()方法。

在此範例,新建一個方法用來接收一個id並且刪除資料庫中與這個id匹配的那一筆資料。為了達到這個目的,你必須使用where語句限定哪一筆才是要被刪除的資料

Future<void> deleteDog(int id) async {
  // Get a reference to the database.
  final db = await database;

  // Remove the Dog from the Database.
  await db.delete(
    'dogs',
    // Use a `where` clause to delete a specific dog.
    where: "id = ?",
    // Pass the Dog's id as a whereArg to prevent SQL injection.
    whereArgs: [id],
  );
}

完整的Sqlite 範例:

import 'dart:async';

import 'package:flutter/widgets.dart';

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

void main() async {
  // Avoid errors caused by flutter upgrade.
  // Importing 'package:flutter/widgets.dart' is required.
  WidgetsFlutterBinding.ensureInitialized();
  // Open the database and store the reference.
  final Future<Database> database = openDatabase(
    // Set the path to the database. Note: Using the `join` function from the
    // `path` package is best practice to ensure the path is correctly
    // constructed for each platform.
    join(await getDatabasesPath(), 'doggie_database.db'),
    // When the database is first created, create a table to store dogs.
    onCreate: (db, version) {
      return db.execute(
        "CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
      );
    },
    // Set the version. This executes the onCreate function and provides a
    // path to perform database upgrades and downgrades.
    version: 1,
  );

  Future<void> insertDog(Dog dog) async {
    // Get a reference to the database.
    final Database db = await database;

    // Insert the Dog into the correct table. Also specify the
    // `conflictAlgorithm`. In this case, if the same dog is inserted
    // multiple times, it replaces the previous data.
    await db.insert(
      'dogs',
      dog.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  Future<List<Dog>> dogs() async {
    // Get a reference to the database.
    final Database db = await database;

    // Query the table for all The Dogs.
    final List<Map<String, dynamic>> maps = await db.query('dogs');

    // Convert the List<Map<String, dynamic> into a List<Dog>.
    return List.generate(maps.length, (i) {
      return Dog(
        id: maps[i]['id'],
        name: maps[i]['name'],
        age: maps[i]['age'],
      );
    });
  }

  Future<void> updateDog(Dog dog) async {
    // Get a reference to the database.
    final db = await database;

    // Update the given Dog.
    await db.update(
      'dogs',
      dog.toMap(),
      // Ensure that the Dog has a matching id.
      where: "id = ?",
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [dog.id],
    );
  }

  Future<void> deleteDog(int id) async {
    // Get a reference to the database.
    final db = await database;

    // Remove the Dog from the database.
    await db.delete(
      'dogs',
      // Use a `where` clause to delete a specific dog.
      where: "id = ?",
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [id],
    );
  }

  var fido = Dog(
    id: 0,
    name: 'Fido',
    age: 35,
  );

  // Insert a dog into the database.
  await insertDog(fido);

  // Print the list of dogs (only Fido for now).
  print(await dogs());

  // Update Fido's age and save it to the database.
  fido = Dog(
    id: fido.id,
    name: fido.name,
    age: fido.age + 7,
  );
  await updateDog(fido);

  // Print Fido's updated information.
  print(await dogs());

  // Delete Fido from the database.
  await deleteDog(fido.id);

  // Print the list of dogs (empty).
  print(await dogs());
}

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({this.id, this.name, this.age});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }

  // Implement toString to make it easier to see information about
  // each dog when using the print statement.
  @override
  String toString() {
    return 'Dog{id: $id, name: $name, age: $age}';
  }
}

/*印出 
[Dog{id: 0, name: Fido, age: 35}]
[Dog{id: 0, name: Fido, age: 42}]
[]
*/

key-value

如果要儲存的資料較少,我們則可以用shared_preferences插件,來將資料存在我們的手機等硬體裡

shared_preferences插件可以把key-value的資料保存到手機等硬體中,並通過封裝iOS上的NSUserDefaults和Android上的SharedPreferences為簡單的數據提供持久化儲存

使用key-value雖然方便,但它僅限用於基本資料類型:intdoubleboolstringstringList,還有它並不適用於大量資料的存取

首先添加依賴:shared_preferences

...
dependencies:
  shared_preferences: ^0.5.12
...

儲存資料

使用SharedPreferences類的setter方法,Setter方法可用於各種基本資料類型,例如setIntsetBoolsetString

// obtain shared preferences
final prefs = await SharedPreferences.getInstance();

// set value
prefs.setInt('counter', counter);

讀取資料

使用SharedPreferences類相應的getter方法。對於每一個setter方法都有對應的getter方法。例如,你可以使用getIntgetBoolgetString方法

final prefs = await SharedPreferences.getInstance();

// Try reading data from the counter key. If it doesn't exist, return 0.
final counter = prefs.getInt('counter') ?? 0;

刪除資料

使用remove()方法刪除數據

final prefs = await SharedPreferences.getInstance();

prefs.remove('counter');

完整的key-value 範例:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of the application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shared preferences demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Shared preferences demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _loadCounter();
  }

  //Loading counter value on start
  _loadCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      _counter = (prefs.getInt('counter') ?? 0);
    });
  }

  //Incrementing counter after click
  _incrementCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      _counter = (prefs.getInt('counter') ?? 0) + 1;
      prefs.setInt('counter', _counter);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Read and write files

有時候我們會需要在local 端的文件做讀寫的操作,常見於App啟動期間產生的持久化數據,或者從網絡下載的數據要供離線使用

為了將文件保存到手機等硬體上,你需要結合使用 dart:io庫中的path_provider這個package

首先添加依賴:path_provider

...
dependencies:
  path_provider: ^1.6.18
...

範例:

我們將會顯示一個計數器,當計數器發生變化時,你將在硬體中寫入資料,以便在App加載時重新讀取這些數據

path_providerpackage 提供一種與平台無關的方式,以一致的方式訪問設備的文件位置系統。該plugin 當前支持訪問兩種文件位置系統:

  1. Temporary directory (臨時文件夾):

    這是一個系統可以隨時清空的臨時(緩存)文件夾。在iOS上對應NSCachesDirectory的返回值;在Android上對應getCacheDir()的返回值

  2. Documents directory (Documents目錄):

    僅供app 使用,用於儲存只能由該app使用的文件。只有在刪除app時,系統才會清除這個目錄。在iOS上,這個目錄對應於NSDocumentDirectory。在Android上,則是AppData目錄

  1. 找到正確的本地路徑

    Future<String> get _localPath async {
      final directory = await getApplicationDocumentsDirectory();
    
      return directory.path;
    }
    
  2. 創建一個指向文件位置的 reference

    Future<File> get _localFile async { //使用dart:io庫的File類來實現
      final path = await _localPath;
      return File('$path/counter.txt');
    }
    
  3. 將資料寫入文件

    現在你已經有了可以使用的File,接下來就可以使用這個文件來讀寫數據,首先,將一些數據寫入該文件。由於使用了計數器,因此只需將整數存為字串格式,然後使用'$counter'即可調用

    Future<File> writeCounter(int counter) async {
      final file = await _localFile;
    
      // Write the file.
      return file.writeAsString('$counter');
    }
    
  4. 從文件讀取資料

    Future<int> readCounter() async {
      try {
        final file = await _localFile;
    
        // Read the file.
        String contents = await file.readAsString();
    
        return int.parse(contents);
      } catch (e) {
        // If encountering an error, return 0.
        return 0;
      }
    }
    

完整的計數器讀取、寫入文件範例:

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Reading and Writing Files',
      home: FlutterDemo(storage: CounterStorage()),
    ),
  );
}

class CounterStorage {
  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();

    return directory.path;
  }

  Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/counter.txt');
  }

  Future<int> readCounter() async {
    try {
      final file = await _localFile;

      // Read the file
      String contents = await file.readAsString();

      return int.parse(contents);
    } catch (e) {
      // If encountering an error, return 0
      return 0;
    }
  }

  Future<File> writeCounter(int counter) async {
    final file = await _localFile;

    // Write the file
    return file.writeAsString('$counter');
  }
}

class FlutterDemo extends StatefulWidget {
  final CounterStorage storage;

  FlutterDemo({Key key, @required this.storage}) : super(key: key);

  @override
  _FlutterDemoState createState() => _FlutterDemoState();
}

class _FlutterDemoState extends State<FlutterDemo> {
  int _counter;

  @override
  void initState() {
    super.initState();
    widget.storage.readCounter().then((int value) {
      setState(() {
        _counter = value;
      });
    });
  }

  Future<File> _incrementCounter() {
    setState(() {
      _counter++;
    });

    // Write the variable as a string to the file.
    return widget.storage.writeCounter(_counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Reading and Writing Files')),
      body: Center(
        child: Text(
          'Button tapped $_counter time${_counter == 1 ? '' : 's'}.',
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

上一篇
Day28 Networking & http
下一篇
Day30 Flutter Camera、播放影片
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言