今天我們來介紹幾個 Persistence 的方法,即是用來儲存數據,將數據存在我們的手機等硬體裡,以便我們在重開App 或重新開機後也能夠使用之前保存的數據的方法
相比於其他儲存資料於本地的方法,SQLite 資料庫能夠提供更為迅速的插入、更新、查詢功能
之後範例會用到一些基本的SQL語句,如果你對於SQLite和SQL的各種語句還不熟悉,請查看SQLite官方的教程 SQLite教程
首先添加依賴:
...
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});
}
在你準備讀寫資料庫的數據之前,你要先打開這個資料庫,此時需要以下兩個步驟才可以打開資料庫:
sqflite
package裡的getDatabasesPath
方法並配合path
package裡的 join
方法定義資料庫的路徑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
數據就包含了一個id
,name
和age
,因此,在dogs
數據庫表中將有三列,分別是id
,name
和age
id
是int
類型,在資料表中是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
的資料,需要分為以下兩步:
把Dog
轉換成一個Map
資料類型
使用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
資料存在資料庫裡,你可以通過查詢資料庫,檢索到一隻狗的資料或者所有狗的資料,分為以下兩步:
dogs
表對像的query
方法,這將返回一個List <Map>
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'],
);
});
}
使用sqflite
package中的update()
方法,可以對已經插入到資料庫中的數據進行修改(更新)
修改數據操作包含以下兩步:
Map
資料類型;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}"
!
除了插入和修改狗狗們的數據,你還可以從資料庫中刪除狗的數據。刪除數據用到了sqflite
package中的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}]
[]
*/
如果要儲存的資料較少,我們則可以用shared_preferences
插件,來將資料存在我們的手機等硬體裡
shared_preferences
插件可以把key-value的資料保存到手機等硬體中,並通過封裝iOS上的NSUserDefaults
和Android上的SharedPreferences
為簡單的數據提供持久化儲存使用key-value雖然方便,但它僅限用於基本資料類型:
int
、double
、bool
、string
和stringList
,還有它並不適用於大量資料的存取
首先添加依賴:shared_preferences
...
dependencies:
shared_preferences: ^0.5.12
...
使用SharedPreferences
類的setter方法,Setter方法可用於各種基本資料類型,例如setInt
、setBool
和setString
// obtain shared preferences
final prefs = await SharedPreferences.getInstance();
// set value
prefs.setInt('counter', counter);
使用SharedPreferences
類相應的getter方法。對於每一個setter方法都有對應的getter方法。例如,你可以使用getInt
、getBool
和getString
方法
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.
);
}
}
有時候我們會需要在local 端的文件做讀寫的操作,常見於App啟動期間產生的持久化數據,或者從網絡下載的數據要供離線使用
為了將文件保存到手機等硬體上,你需要結合使用 dart:io
庫中的path_provider
這個package
首先添加依賴:path_provider
...
dependencies:
path_provider: ^1.6.18
...
範例:
我們將會顯示一個計數器,當計數器發生變化時,你將在硬體中寫入資料,以便在App加載時重新讀取這些數據
path_provider
package 提供一種與平台無關的方式,以一致的方式訪問設備的文件位置系統。該plugin 當前支持訪問兩種文件位置系統:
Temporary directory (臨時文件夾):
這是一個系統可以隨時清空的臨時(緩存)文件夾。在iOS上對應
NSCachesDirectory
的返回值;在Android上對應getCacheDir()
的返回值Documents directory (Documents目錄):
僅供app 使用,用於儲存只能由該app使用的文件。只有在刪除app時,系統才會清除這個目錄。在iOS上,這個目錄對應於
NSDocumentDirectory
。在Android上,則是AppData
目錄
找到正確的本地路徑
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
創建一個指向文件位置的 reference
Future<File> get _localFile async { //使用dart:io庫的File類來實現
final path = await _localPath;
return File('$path/counter.txt');
}
將資料寫入文件
現在你已經有了可以使用的File
,接下來就可以使用這個文件來讀寫數據,首先,將一些數據寫入該文件。由於使用了計數器,因此只需將整數存為字串格式,然後使用'$counter'
即可調用
Future<File> writeCounter(int counter) async {
final file = await _localFile;
// Write the file.
return file.writeAsString('$counter');
}
從文件讀取資料
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),
),
);
}
}