Flutter 入門指北(Part 12)之數據持久化

該文已授權公眾號 「碼個蛋」,轉載請指明出處

上節講了狀態管理,但是當 App 重啟后,數據就都丟失了,這樣就比較尷尬了,什么都要重來,所以這節我們來講下數據持久化。數據持久化主要有如下方式

  • 文件讀寫
  • shared_preferences 存儲
  • 數據庫存儲

持久化的實現都需要通過三方插件來實現,接著會慢慢介紹三種實現方式

文件讀寫/ IO 操作

文件讀寫需要 path_provider 插件,寫這篇文章的時候,最新版本是 0.5.0+1,小伙伴們可以根據官網最新的版本進行替換,導入后我們就可以來看下如何實現文件的讀寫了。path_provider 的源碼比較簡單,這邊就不單獨拎出來說了,可以自行查看。path_provider 用于獲取手機的存儲文件位置,一共有三個方法

  • getTemporaryDirectory 臨時目錄,在 Android 中對應的方法為 getCacheDir,而在 iOS 中對應為 NSCachesDirectory,可以通過系統檢測并清除
  • getApplicationDocumentsDirectory 緩存目錄,在 Android 中對應為 AppData 文件夾,在 iOS 中對應為 NSDocumentsDirectory,只有當 App 被刪除才能被刪除
  • getExternalStorageDirectory 外部存儲目錄,只有在 Android 中有效,在 iOS 調用會拋出 UnsupportedError 異常,不過 Android 在寫入前記得先申請權限喲,否則也是不行滴。

讀寫文件操作需要通過 DartIO 操作完成,這邊小伙伴們可以自己看文檔 File class,接著我們就直接通過例子來看文件實現數據持久化。先看下效果吧,最終重啟 App 后,數據也能正常讀取顯示,說明數據被保存下來了

file_io.gif

看下實現的代碼,因為會涉及到多種方式,所以這邊我把視圖抽取出來實現

Widget _fileIoPart() {
    return Card(
      margin: const EdgeInsets.all(8.0),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
      child: Column(children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(12.0),
          child: Text('File IO', style: TextStyle(fontSize: 20.0, color: Theme.of(context).primaryColor)),
        ),
        // RadioList 是單選按鈕部件,通過選擇不同的情況,創建不同目錄的文件
        RadioListTile(
            value: _radioText[0],
            title: Text(_radioText[0]),
            subtitle: Text(_radioDescriptions[0]),
            groupValue: _currentValue,
            onChanged: ((value) {
              setState(() => _currentValue = value);
            })),
        RadioListTile(
            value: _radioText[1],
            title: Text(_radioText[1]),
            subtitle: Text(_radioDescriptions[1]),
            groupValue: _currentValue,
            onChanged: ((value) {
              setState(() => _currentValue = value);
            })),
        RadioListTile(
            value: _radioText[2],
            title: Text(_radioText[2]),
            subtitle: Text(_radioDescriptions[2]),
            groupValue: _currentValue,
            onChanged: ((value) {
              setState(() => _currentValue = value);
            })),
        Padding(
          padding: const EdgeInsets.all(12.0),
          // 用于寫入文本信息
          child: TextField(
            controller: _editController,
            decoration: InputDecoration(labelText: '輸入存儲的文本內容', icon: Icon(Icons.text_fields)),
          ),
        ),
        Container(
          margin: const EdgeInsets.symmetric(horizontal: 12.0),
          width: MediaQuery.of(context).size.width,
          child: RaisedButton(
            onPressed: _writeTextIntoFile,
            child: Text('寫入文件信息'),
          ),
        ),
        Padding(
          padding: const EdgeInsets.all(12.0),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[Text('文件內容:'), Expanded(child: Text(_fileContent, softWrap: true))],
          ),
        ),
        Container(
          margin: const EdgeInsets.symmetric(horizontal: 12.0),
          width: MediaQuery.of(context).size.width,
          child: RaisedButton(
            onPressed: _readTextFromFile,
            child: Text('讀取文件信息'),
          ),
        ),
      ]),
    );
  }

關鍵的部分在于 _writeTextIntoFile_readTextFromFile 兩個方法的實現。看下實現的代碼

 // 如果寫入外部內存需要讀寫權限,這邊使用了第三方插件 `permission_handler`
  void _writeTextIntoFile() async {
    if (_currentValue == _radioText[2]) {
      PermissionStatus status = await PermissionHandler().checkPermissionStatus(PermissionGroup.storage);
      if (status == PermissionStatus.granted) // 如果是寫入外部存儲,則檢測權限狀態,同意則寫入
        _writeContent();
      else if (status == PermissionStatus.disabled) // 拒絕了提示手動打開
        Fluttertoast.showToast(msg: '未打開相關權限');
      else // 未同意則主動申請權限
        PermissionHandler().requestPermissions([PermissionGroup.storage]);
    } else // 不是寫入外部存儲直接寫入文件
      _writeContent();
  }

 // 文本寫入文件 
  void _writeContent() async {
    // 寫入文本操作
    var text = _editController.value.text; // 獲取文本框的內容
    File file = File(await _getFilePath()); // 獲取相應的文件

    if (text == null || text.isEmpty) {
      Fluttertoast.showToast(msg: '請輸入內容'); // 內容為空,則不寫入并提醒
    } else {
      // 內容不空,則判斷是否已經存在,存在先刪除,重新創建后寫入信息
      if (await file.exists()) file.deleteSync();
      file.createSync(); // createSync 是一個同步的創建過程
      file.writeAsStringSync(text); // writeAsStringSync 是同步寫入的過程
      _editController.clear(); // 寫入文件后清空輸入框信息
    }
  }

  // 讀取文本操作
  void _readTextFromFile() async {
    File file = File(await _getFilePath());
    if (await file.exists()) {
      setState(() => _fileContent = file.readAsStringSync()); // 文件存在則直接顯示文本信息
    } else {
      setState(() => _fileContent = ''); // 文件不存在則清空顯示文本信息,并提示
      Fluttertoast.showToast(msg: '文件還未創建,請先通過寫入信息來創建文件');
    }
  }

因為外部存儲的文件需要涉及到權限問題,而且 iOS 也不支持,所以如果需要使用文件來持久化數據的話,盡量使用另外兩種。因為在例子中,我們保存的數據相對比較簡單,所以這邊就不得不說另外一種更方便的持久化方式了 shared_preferences

SharedPreferences

寫 Android 的小伙伴對這個應該不陌生了,但是 Flutter 并沒有自帶的 shared_preferences 功能,需要第三方插件來實現,引入 shared_preferences 插件,寫文章的時候最新版本是 ^0.5.1+2,還是先看下最后的效果

shared.gif

代碼的實現相對比較簡單

Widget _sharedPart() {
    return Card(
        margin: const EdgeInsets.all(8.0),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
        child: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(12.0),
              child:
                  Text('Shared Preferences', style: TextStyle(fontSize: 20.0, color: Theme.of(context).primaryColor)),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 12.0),
              // 用于設置 key 信息
              child: TextField(
                controller: _shareKeyController,
                decoration: InputDecoration(labelText: '輸入 share 存儲的 key', icon: Icon(Icons.lock_outline)),
              ),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 12.0),
              // 用于寫入文本信息
              child: TextField(
                controller: _shareValueController,
                decoration: InputDecoration(labelText: '輸入 share 存儲的 value', icon: Icon(Icons.text_fields)),
              ),
            ),
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 12.0),
              width: MediaQuery.of(context).size.width,
              child: RaisedButton(
                onPressed: _writeIntoShare,
                child: Text('寫入 share'),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[Text('share 存儲內容:'), Expanded(child: Text(_shareContent, softWrap: true))],
              ),
            ),
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 12.0),
              width: MediaQuery.of(context).size.width,
              child: RaisedButton(
                onPressed: _readFromShare,
                child: Text('讀取 share'),
              ),
            ),
          ],
        ));
  }

實現的關鍵部分就是方法 _writeIntoShare_readFromShare

void _writeIntoShare() async {
    var shareKey = _shareKeyController.value.text;
    var shareContent = _shareValueController.value.text;

    if (shareKey == null || shareKey.isEmpty) {
      Fluttertoast.showToast(msg: '請輸入 key');
    } else if (shareContent == null || shareContent.isEmpty) {
      Fluttertoast.showToast(msg: '請輸入保存的內容');
    } else {
      // 通過 `getInstance` 獲取 `shared_preferences` 單例
      var sp = await SharedPreferences.getInstance();
      // sp 能保存的數據類型包括 `int`, `String`, `bool`, `double`, `StringList`
      sp.setString(shareKey, shareContent);
    }
  }

  void _readFromShare() async {
    var shareKey = _shareKeyController.value.text;

    if (shareKey == null || shareKey.isEmpty) {
      Fluttertoast.showToast(msg: '請輸入 key');
    } else {
      var sp = await SharedPreferences.getInstance();
      // 數據讀取的類型同寫入類型,如果傳入的 key 不存在則返回 null
      var value = sp.getString(shareKey);

      if (value == null) {
        Fluttertoast.showToast(msg: '未找到該 key');
        setState(() => _shareContent = '');
      } else {
        setState(() => _shareContent = value);
      }
    }
  }

這兩種數據持久化的方式主要用于存儲相對簡單,關系不復雜的數據,如果涉及到大量的,且字段之間有關系的情況就需要通過數據庫來實現了,Android 和 iOS 都自帶 sqlite 數據庫。

以上代碼查看 data_persistence_main.dart 文件

Sqflite

Flutter 實現數據庫存儲需要通過插件 sqflite 來實現,寫文章的時候最新的版本是 sqflite 1.1.3,但是該版本需要 flutter 1.2 以上才行,所以我選擇的是 sqflite 1.1.0,小伙伴可以根據自己的 flutter 版本選擇相應的 sqflite 版本

sqflite 的基本操作語句,在文檔中已經寫得非常明白了,所以就不搬運了,這邊直接講下對于數據庫的一些封裝處理吧,因為打開數據庫是一個很消耗資源的一個過程,所以呢,推薦實現單例會比較好。例如我們要實現一個 student 存儲表

class DatabaseUtils {
  final String _tableStudent = 'student';

  static Database _database; // 創建單例,防止重復打開消耗內存

  static DatabaseUtils _instance;

  static DatabaseUtils get instance => _instance;

  DatabaseUtils._internal() {
    getDatabasesPath().then((path) async {
      _database = await openDatabase(join(path, 'demo.db'), version: 2, onCreate: (db, version) {
        // 創建數據庫的時候在這邊調用
        db.execute('create table $_tableStudent '
            'id integer primary key autoincrement,'
            'name text not null,'
            'age integer not null default 0,'
            'gender integer not null default 0');

        // 更新升級增加的字段
        db.execute('alter table $_tableStudent add column birthday text');
      }, onUpgrade: (db, oldVersion, newVersion) {
        // 更新升級數據庫的時候在這操作
        if (oldVersion == 1) db.execute('alter table $_tableStudent add column birthday text');
      }, onOpen: (db) {
        // 打開數據庫時候的回調
        print('${db.path}');
      });
    });
  }

  factory DatabaseUtils() {
    // 如果當前的單例已經存在,則不再創建,否則重新創建,factory 關鍵詞看第一章
    if (_instance == null) _instance = DatabaseUtils._internal();
    return _instance;
  }
}

那么對數據庫的操作就完全考驗你的 SQL 的掌握程度了,但是千萬記住,sqlite 中的類型只有,整型 integer ,字符類型 text,浮點類型 real,二進制 blob。數據庫的具體例子會等到最后的實際項目中展示,原諒我不懂如何展示一個界面給你操作,實現數據庫的各種功能。

該部分代碼查看 db_util.dart 文件,里面有一些基本的操作寫法,小伙伴可自行查看。

最后代碼的地址還是要的:

  1. 文章中涉及的代碼:demos

  2. 基于郭神 cool weather 接口的一個項目,實現 BLoC 模式,實現狀態管理:flutter_weather

  3. 一個課程(當時買了想看下代碼規范的,代碼更新會比較慢,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改,很多地方看著不舒服,然后就改成自己的實現方式了):flutter_shop

如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支持我繼續寫下去的動力~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372