Flutter中的存儲

Flutter 是 Google 開源的 UI 工具包,一套代碼多端應(yīng)用極大的提升了開發(fā)效率,此外直接調(diào)用skia(c/c++)代碼的能力然想,也使得它具備媲美原生的渲染性能哟沫。但是涉及到非UI層的任務(wù)時,F(xiàn)lutter仍然需要依托原生框架巍耗,比如相機秋麸、存儲、藍(lán)牙等功能炬太,于是各種對原生能力封裝的Plugin就產(chǎn)生了灸蟆,Android、iOS各自平臺提供原生能力亲族,flutter側(cè)進行對接炒考,提供dart語言編寫的api 給flutter側(cè)調(diào)用,使得flutter開發(fā)人員依然可以一套代碼霎迫,多端應(yīng)用斋枢。

存儲數(shù)據(jù)到磁盤是開發(fā)中常見的操作,比如用戶信息知给、一些不經(jīng)常變動的數(shù)據(jù)瓤帚、通訊錄等,以便下次打開APP用戶可以不經(jīng)過網(wǎng)絡(luò)請求涩赢,快速預(yù)覽APP中的內(nèi)容戈次。根據(jù)需要存儲的數(shù)據(jù)量的大小,用戶可以選擇適合自己的方案筒扒。對于少量的數(shù)據(jù)怯邪,在原生側(cè)iOS一般直接使用UserDefaults,Android使用SharedPreferences霎肯,這兩種存儲方式一般用來存儲用戶或者APP信息等少量的數(shù)據(jù)擎颖。當(dāng)數(shù)據(jù)量大的時候就不適合使用了榛斯,一般會考慮基于SQLite的數(shù)據(jù)庫存儲,或者是基于文件的存儲搂捧。

以上也是本文將要要講述核心:詳細(xì)介紹幾個存儲的優(yōu)質(zhì)框架的原理及使用驮俗,以便對大家需要使用存儲能力的時候有所幫助。

少量數(shù)據(jù)存儲

少量數(shù)據(jù)建議直接使用shared_preferences

preferences.png

這是官方維護的倉庫允跑,它是對iOS中UserDefaults和Android中SharedPreferences的plugin封裝王凑,iOS UserDefaults存儲在plist中,Android preferences存儲在xml中聋丝,原本各自操作都很簡單索烹,所以flutter側(cè)的封裝也很簡單,整個代碼包含注釋不到200行弱睦。

因此在flutter側(cè)基于它來進行少量數(shù)據(jù)的存儲也是十分方便的百姓,在flutter側(cè)的類名也叫SharedPreferences,是個單例况木,實例化的時候會從磁盤中讀取到內(nèi)存垒拢,并且在內(nèi)存中保存一份,之后如果有新的數(shù)據(jù)存入的話火惊,會同時進行內(nèi)存和磁盤的更新求类,當(dāng)然寫磁盤操作有極小的概率可能失敗,因此內(nèi)存中數(shù)據(jù)和磁盤中數(shù)據(jù)有極小概率不一致屹耐。

Future<bool> _setValue(String valueType, String key, Object value) {
    final String prefixedKey = '$_prefix$key';
    if (value == null) {
      _preferenceCache.remove(key);
      return _store.remove(prefixedKey);
    } else {
      if (value is List<String>) {
        // Make a copy of the list so that later mutations won't propagate
        _preferenceCache[key] = value.toList();
      } else {
        _preferenceCache[key] = value;
      }
      return _store.setValue(valueType, prefixedKey, value);
    }
  }

還需要注意點的一點是尸疆,如果native側(cè)進行了SharedPreferences或者NSUserDefaults的存儲、修改操作惶岭,flutter側(cè)的SharedPreferences單例寿弱,并不會自行更新到內(nèi)存中,需要調(diào)用reload方法進行內(nèi)存的更新俗他。

Future<void> reload() async {
    final Map<String, Object> preferences =
        await SharedPreferences._getSharedPreferencesMap();
    _preferenceCache.clear();
    _preferenceCache.addAll(preferences);
  }
// 讀取操作api
Set<String> getKeys()
dynamic get(String key)
bool getBool(String key)
int getInt(String key)
double getDouble(String key)
String getString(String key)
bool containsKey(String key)
List<String> getStringList(String key)

// 寫入操作api
Future<bool> setBool(String key, bool value)
Future<bool> setInt(String key, int value)
Future<bool> setDouble(String key, double value)
Future<bool> setString(String key, String value)
Future<bool> setStringList(String key, List<String> value)
Future<bool> remove(String key)

在項目開發(fā)的時候脖捻,我們先獲取preferences單例,然后按照上述api進行操作即可兆衅,由于比較簡單地沮,這里不再做實際示例介紹。

大量數(shù)據(jù)存儲

數(shù)據(jù)量大的話一般會基予SQLite進行操作羡亩,目前flutter側(cè)最好的基于sqlite的插件是sqflite

sqflite.png

它在原生iOS側(cè)基于FMDB封裝摩疑,Android側(cè)基于系統(tǒng)的sqlite封裝,使用起來比preferences稍微復(fù)雜點畏铆,需要打開關(guān)閉數(shù)據(jù)庫雷袋,自己建表進行增、刪、改楷怒、查蛋勺。其原理通過channel通信,將sql指令發(fā)送到原生側(cè)鸠删,原生操作完數(shù)據(jù)庫抱完,再將數(shù)據(jù)返回給flutter側(cè)。plugin flutter側(cè)將用戶的操作最終都是轉(zhuǎn)化為sql指令刃泡,當(dāng)然flutter側(cè)不只是轉(zhuǎn)發(fā)用戶的sql操作巧娱,接下來會詳細(xì)進行講述。

sql.PNG

使用sqlite存儲 demo地址

  • 首先是獲取默認(rèn)數(shù)據(jù)庫存放路徑
var databasesPath = await getDatabasesPath();

通過getDatabasesPath 第一次調(diào)用的時候烘贴,通過channel向native側(cè)發(fā)送一條消息禁添,native將路徑地址返回給flutter側(cè),flutter緩存此地址桨踪,之后再調(diào)用此方法老翘,直接返回緩存的地址。

  • 然后定義自己的數(shù)據(jù)庫db路徑
String path = join(databasesPath, 'demo.db');
以上兩步操作的結(jié)果:
flutter: databasesPath /var/mobile/Containers/Data/Application/9EAD6644-1A9A-4741-BC5E-51D9D678CA30/Documents
flutter: db path /var/mobile/Containers/Data/Application/9EAD6644-1A9A-4741-BC5E-51D9D678CA30/Documents/demo.db

這時只是獲取了地址db實例并不會創(chuàng)建馒闷。

創(chuàng)建數(shù)據(jù)庫

Database database = await openDatabase(_dbPath);

創(chuàng)建數(shù)據(jù)庫最少只需要給定一個路徑就行酪捡,當(dāng)然這個api中還有其他可選參數(shù)

Future<Database> openDatabase(String path,
    {int version,
    OnDatabaseConfigureFn onConfigure,
    OnDatabaseCreateFn onCreate,
    OnDatabaseVersionChangeFn onUpgrade,
    OnDatabaseVersionChangeFn onDowngrade,
    OnDatabaseOpenFn onOpen,
    bool readOnly = false,
    bool singleInstance = true})
  • version db版本,用來決定是否進行創(chuàng)建纳账、升降級。只有設(shè)置了version捺疼,onCreate疏虫、onUpgrade、onDowngrade這三個可選回調(diào)才可能被調(diào)用啤呼,這三個回調(diào)最多只會調(diào)用一個卧秘,當(dāng)version不變時,三個回調(diào)都不會被調(diào)用官扣。
  • onConfigure 打開db時首先執(zhí)行這個回調(diào)翅敌,在這個回調(diào)中可以執(zhí)行db的初始化操作,比如外鍵的設(shè)置或者提前寫日志惕蹄。
  • onCreate 只有當(dāng)db不存在時蚯涮,第一次調(diào)用openDatabase才會執(zhí)行此回調(diào),可以利用這個時機卖陵,創(chuàng)建一些所需的table遭顶。
  • onUpgrade 有兩個場景會執(zhí)行此回調(diào),1.初始創(chuàng)建db時泪蔫,onCreate回調(diào)未設(shè)置棒旗。2.db已經(jīng)存在,并且version比db中上次記錄的的version大撩荣∠橙啵可以在此方法中執(zhí)行必要的遷移操作饶深。
  • onDowngrade 只有version比db中記錄的的version小時才會執(zhí)行。這種情況很少見逛拱,只有當(dāng)新版本的代碼創(chuàng)建了一個數(shù)據(jù)庫敌厘,然后與舊版本的代碼交互時才會出現(xiàn)這種情況,應(yīng)該盡量避免這種情況橘券。
  • onOpen 這個回調(diào)最后執(zhí)行额湘,在version被重置之后,openDatabase返回結(jié)果之前旁舰。
  • readOnly 默認(rèn)false锋华,如果設(shè)置為true,則不允許任何修改操作
  • singleInstance 默認(rèn)為true箭窜,這樣針對同樣的dbPath毯焕,將返回同一個db實例。當(dāng)多次調(diào)用openDatabase的時候磺樱,只有首次調(diào)用的回調(diào)會生效纳猫,再次調(diào)用同一path時候,只會返回db實例竹捉,忽略新設(shè)置的參數(shù)芜辕。

這幾個可選回調(diào)順序是

1. [onConfigure]
2. [onCreate] or [onUpgrade] or [onDowngrade]
5. [onOpen]

我們可以創(chuàng)建多個db實例,每個db實例中可以創(chuàng)建多張table块差。這點和原生操作數(shù)據(jù)庫是一致的侵续。

除了在每次數(shù)據(jù)庫初始創(chuàng)建onCreate或者升級時的onUpgrade回調(diào)中操作表,還可以在其他時機進行操作憨闰,我們可以新建表状蜗、修改表字段,修改表字段對應(yīng)值得類型等鹉动,對表的操作都是通過sql語句進行操作轧坎,下面展示幾個示例:

  • 新增一張表
_database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY, name TEXT, value INTEGER)');

如果db中已經(jīng)存在相同的表,再次創(chuàng)建不會生效泽示,對原有表不會有影響缸血。

  • 新增表字段
_database.execute('alter table Test2 ADD num2 REAL NOT NULL Default 0');
  • 更改表字段
_database.execute('alter table Test2 rename column num to num3');

合適的場景是,當(dāng)表里的字段不滿足時边琉,可以在數(shù)據(jù)庫升級的回調(diào)onUpgrade中進行表的更改属百,當(dāng)然這有業(yè)務(wù)決定。

其他操作变姨,還有這些sql語句和原生側(cè)操作一樣族扰。

修改字段默認(rèn)值
alter table 表名 drop constraint 約束名字 // 刪除表的字段的原有約束
alter table 表名 add constraint 約束名字 DEFAULT 默認(rèn)值 for 字段名稱  // 添加一個表的字段的約束并指定默認(rèn)值

修改字段類型:
alter table 表名 alter column name nvarchar(10) not null

當(dāng)數(shù)據(jù)庫和表都建立好之后,接下來就是數(shù)據(jù)操作了,這里flutter側(cè)有兩種方式操作渔呵,可以直接編寫sql語句怒竿,也可以使用flutter側(cè) helpers操作,兩種操作各有優(yōu)劣扩氢。Raw Sql方式更加直觀耕驰,但是sql語句編寫容易出錯

-- Raw Sql SQL helpers
優(yōu)勢 直觀,sql直接發(fā)送到native側(cè)處理 書寫簡單录豺,不易出錯
劣勢 直接編寫sql 語句容易出錯 需要一層轉(zhuǎn)換朦肘,底層仍是調(diào)用Raw Sql方式
  • Raw Sql方式
// 增
int id1 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');

// 刪
await _database.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);

// 改
await _database.rawUpdate('UPDATE Test SET name = ?, value = ? WHERE name = ?', ['updated name', '9876', 'some name']);

// 查
List<Map> list = await _database.rawQuery('SELECT * FROM Test');
  • SQL helpers
    SQL helpers是flutter側(cè)對直接操作sql的封裝,以insert為例双饥,借助SqlBuilder這個類提供的能力媒抠,將SQL helpers Api轉(zhuǎn)化成sql字符串,最終還是轉(zhuǎn)換為RAW Sql方式執(zhí)行咏花。
Future<int> insert(String table, Map<String, dynamic> values,
      {String nullColumnHack, ConflictAlgorithm conflictAlgorithm}) {
    final builder = SqlBuilder.insert(table, values,
        nullColumnHack: nullColumnHack, conflictAlgorithm: conflictAlgorithm);
    return rawInsert(builder.sql, builder.arguments);
  }
// 增
// table表名趴生;values Map數(shù)據(jù),可以是model2json轉(zhuǎn)成的數(shù)據(jù)昏翰;
// nullColumnHack字段為空時處理語句苍匆,conflictAlgorithm沖突處理枚舉
uture<int> insert(String table, Map<String, dynamic> values,
      {String nullColumnHack, ConflictAlgorithm conflictAlgorithm});

// 刪
// where篩選條件,如果where為null棚菊,則刪除整個表中的數(shù)據(jù)浸踩,whereArgs即其參數(shù)
Future<int> delete(String table, {String where, List<dynamic> whereArgs});

// 改
// values將要更新到表中的值,如果后面的篩選條件不設(shè)置统求,將更新整個表
Future<int> update(String table, Map<String, dynamic> values,
      {String where,
      List<dynamic> whereArgs,
      ConflictAlgorithm conflictAlgorithm});

// 查
// distinct是否排重民轴,true的話返回的每行數(shù)據(jù)都是唯一的
// 返回表中哪幾列的數(shù)據(jù),傳null將返回所有列球订,最好對數(shù)據(jù)進行過濾,以免讀取太多不相干的數(shù)據(jù)
Future<List<Map<String, dynamic>>> query(String table,
      {bool distinct,
      List<String> columns,
      String where,
      List<dynamic> whereArgs,
      String groupBy,
      String having,
      String orderBy,
      int limit,
      int offset});

SQL helpers可以和數(shù)據(jù)模型結(jié)合使用瑰钮,根據(jù)業(yè)務(wù)需求冒滩,編寫對應(yīng)的增刪改查api,外部使用就會非常精簡浪谴,下面是個具體的小例子:

final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';

class Todo {
  int id;
  String title;
  bool done;

  Map<String, dynamic> toMap() {
    var map = <String, dynamic>{
      columnTitle: title,
      columnDone: done == true ? 1 : 0
    };
    if (id != null) {
      map[columnId] = id;
    }
    return map;
  }

  Todo();

  Todo.fromMap(Map<String, dynamic> map) {
    id = map[columnId];
    title = map[columnTitle];
    done = map[columnDone] == 1;
  }
}

class TodoProvider {
  Database db;

  Future open(String path) async {
    db = await openDatabase(path, version: 1,
        onCreate: (Database db, int version) async {
      await db.execute('''
create table $tableTodo ( 
  $columnId integer primary key autoincrement, 
  $columnTitle text not null,
  $columnDone integer not null)
''');
    });
  }

  Future<Todo> insert(Todo todo) async {
    todo.id = await db.insert(tableTodo, todo.toMap());
    return todo;
  }

  Future<Todo> getTodo(int id) async {
    List<Map> maps = await db.query(tableTodo,
        columns: [columnId, columnDone, columnTitle],
        where: '$columnId = ?',
        whereArgs: [id]);
    if (maps.length > 0) {
      return Todo.fromMap(maps.first);
    }
    return null;
  }

  Future<int> delete(int id) async {
    return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
  }

  Future<int> update(Todo todo) async {
    return await db.update(tableTodo, todo.toMap(),
        where: '$columnId = ?', whereArgs: [todo.id]);
  }

  Future close() async => db.close();
}

以上代碼可以直在sqlite存儲 demo地址中查看开睡。

  • 事務(wù)
    如果有一組不可分割的操作,可以使用transaction進行處理苟耻,也即事務(wù)
Future<T> transaction<T>(Future<T> Function(Transaction txn) action,
      {bool exclusive});

// 一個具體小例子
database.transaction((txn) async {
                int id1 = await txn.rawInsert(
                    'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
                print('inserted1: $id1');
                int id2 = await txn.rawInsert(
                    'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
                    ['another name', 12345678, 3.1416]);
                print('inserted2: $id2');
              });

需要注意的是篇恒,事務(wù)中不要直接再調(diào)用database進行操作,否則可能造成死鎖凶杖,而是使用變量txn操作胁艰。

  • 批處理
    經(jīng)過上面的介紹我們可以知道,所有操作都是通過channel發(fā)送到原生側(cè)處理的,操作多的話腾么,就會產(chǎn)生很多的來回通信數(shù)據(jù)奈梳,為了優(yōu)化這個過程可以將一組操作放到Batch中,它會將一組指令打包成一條解虱,注意最后有個batch.commit();操作
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
results = await batch.commit();

存儲數(shù)據(jù)到文件

除了以上兩種方式攘须,還可以將數(shù)據(jù)存儲到文件中,這里也推薦官方維護的path_provider

path_provider.png

  • 首先 仍是需要制定/獲取文件路徑
Future<String> get _localPath async {
    Directory _path = await getApplicationDocumentsDirectory();
    Directory _directory = await Directory("${_path.path}/test").create(recursive: true);
    return _directory.path;
  }
  • 然后獲取文件
    文件格式類型按自己需求指定殴泰。
Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/homePageId001.json');
  }

  • 讀寫操作不只有字符串于宙,還可以是其他類型。
Future<String> readStr() async {
   try {
     final file = await _localFile;
     var content = await file.readAsString();
     return content;
   } catch (e) {
     return 'error';
   }
  }
Future<File> writeStr(str) async {
    final file = await _localFile;
    return file.writeAsString('$str');
  }

開發(fā)時檢查

無論是以上哪種方式悍汛,都是可以在開發(fā)時查看存儲到磁盤中的數(shù)據(jù)是否正常捞魁,這也和原生開發(fā)時類似。以iOS為例员凝,每個APP運行數(shù)據(jù)都是隔離的署驻,有自己的文件夾,我們可以將所開發(fā)APP的文件夾下載下來健霹,選中Xcode-> Window -> Devices and simulators 然后按照下圖操作:


下載.png

選中運行的程序旺上,下載所在APP運行的數(shù)據(jù),雙擊顯示包內(nèi)容:


path.png

我們的db文件操作文件存儲在此糖埋,preferences文件存儲在AppData -> Library -> Preferences目錄下宣吱,操作文件preferences可以直接打開查看,這里說下db文件:

dbdemo.png

每次對數(shù)據(jù)有修改瞳别,我們可以在開發(fā)時征候,查看對應(yīng)db中的表或者數(shù)據(jù)的改動。

以上就是我對Flutter中存儲的總結(jié)祟敛,大家根據(jù)業(yè)務(wù)情況選擇適合自己的方案疤坝,當(dāng)然還有其他優(yōu)秀框架,歡迎一起交流馆铁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末跑揉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子埠巨,更是在濱河造成了極大的恐慌历谍,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辣垒,死亡現(xiàn)場離奇詭異望侈,居然都是意外死亡,警方通過查閱死者的電腦和手機勋桶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門脱衙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侥猬,“玉大人,你說我怎么就攤上這事岂丘×昃浚” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵奥帘,是天一觀的道長铜邮。 經(jīng)常有香客問我,道長寨蹋,這世上最難降的妖魔是什么松蒜? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮已旧,結(jié)果婚禮上秸苗,老公的妹妹穿的比我還像新娘。我一直安慰自己运褪,他們只是感情好惊楼,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秸讹,像睡著了一般檀咙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上璃诀,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天弧可,我揣著相機與錄音,去河邊找鬼劣欢。 笑死棕诵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凿将。 我是一名探鬼主播校套,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牧抵!你這毒婦竟也來了搔确?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤灭忠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后座硕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弛作,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年华匾,在試婚紗的時候發(fā)現(xiàn)自己被綠了映琳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片机隙。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖萨西,靈堂內(nèi)的尸體忽然破棺而出有鹿,到底是詐尸還是另有隱情,我是刑警寧澤谎脯,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布葱跋,位于F島的核電站,受9級特大地震影響源梭,放射性物質(zhì)發(fā)生泄漏娱俺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一废麻、第九天 我趴在偏房一處隱蔽的房頂上張望荠卷。 院中可真熱鬧,春花似錦烛愧、人聲如沸油宜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慎冤。三九已至,卻和暖如春社牲,著一層夾襖步出監(jiān)牢的瞬間粪薛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工搏恤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留违寿,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓熟空,卻偏偏與公主長得像藤巢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子息罗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容