Flutter 入門指北(Part 12)之?dāng)?shù)據(jù)持久化

該文已授權(quán)公眾號 「碼個蛋」卦停,轉(zhuǎn)載請指明出處

上節(jié)講了狀態(tài)管理,但是當(dāng) App 重啟后恼蓬,數(shù)據(jù)就都丟失了惊完,這樣就比較尷尬了,什么都要重來处硬,所以這節(jié)我們來講下數(shù)據(jù)持久化小槐。數(shù)據(jù)持久化主要有如下方式

  • 文件讀寫
  • shared_preferences 存儲
  • 數(shù)據(jù)庫存儲

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

文件讀寫/ IO 操作

文件讀寫需要 path_provider 插件郁油,寫這篇文章的時候本股,最新版本是 0.5.0+1攀痊,小伙伴們可以根據(jù)官網(wǎng)最新的版本進行替換桐腌,導(dǎo)入后我們就可以來看下如何實現(xiàn)文件的讀寫了拄显。path_provider 的源碼比較簡單,這邊就不單獨拎出來說了案站,可以自行查看躬审。path_provider 用于獲取手機的存儲文件位置,一共有三個方法

  • getTemporaryDirectory 臨時目錄蟆盐,在 Android 中對應(yīng)的方法為 getCacheDir承边,而在 iOS 中對應(yīng)為 NSCachesDirectory,可以通過系統(tǒng)檢測并清除
  • getApplicationDocumentsDirectory 緩存目錄石挂,在 Android 中對應(yīng)為 AppData 文件夾博助,在 iOS 中對應(yīng)為 NSDocumentsDirectory,只有當(dāng) App 被刪除才能被刪除
  • getExternalStorageDirectory 外部存儲目錄痹愚,只有在 Android 中有效富岳,在 iOS 調(diào)用會拋出 UnsupportedError 異常,不過 Android 在寫入前記得先申請權(quán)限喲拯腮,否則也是不行滴窖式。

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

file_io.gif

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

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 是單選按鈕部件启妹,通過選擇不同的情況,創(chuàng)建不同目錄的文件
        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: '輸入存儲的文本內(nèi)容', 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('文件內(nèi)容:'), 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('讀取文件信息'),
          ),
        ),
      ]),
    );
  }

關(guān)鍵的部分在于 _writeTextIntoFile_readTextFromFile 兩個方法的實現(xiàn)削祈〕崮纾看下實現(xiàn)的代碼

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

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

    if (text == null || text.isEmpty) {
      Fluttertoast.showToast(msg: '請輸入內(nèi)容'); // 內(nèi)容為空,則不寫入并提醒
    } else {
      // 內(nèi)容不空吨拍,則判斷是否已經(jīng)存在褪猛,存在先刪除,重新創(chuàng)建后寫入信息
      if (await file.exists()) file.deleteSync();
      file.createSync(); // createSync 是一個同步的創(chuàng)建過程
      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: '文件還未創(chuàng)建伊滋,請先通過寫入信息來創(chuàng)建文件');
    }
  }

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

SharedPreferences

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

shared.gif

代碼的實現(xiàn)相對比較簡單

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),
              // 用于設(shè)置 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 存儲內(nèi)容:'), 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'),
              ),
            ),
          ],
        ));
  }

實現(xiàn)的關(guān)鍵部分就是方法 _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: '請輸入保存的內(nèi)容');
    } else {
      // 通過 `getInstance` 獲取 `shared_preferences` 單例
      var sp = await SharedPreferences.getInstance();
      // sp 能保存的數(shù)據(jù)類型包括 `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();
      // 數(shù)據(jù)讀取的類型同寫入類型藤韵,如果傳入的 key 不存在則返回 null
      var value = sp.getString(shareKey);

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

這兩種數(shù)據(jù)持久化的方式主要用于存儲相對簡單虐沥,關(guān)系不復(fù)雜的數(shù)據(jù),如果涉及到大量的泽艘,且字段之間有關(guān)系的情況就需要通過數(shù)據(jù)庫來實現(xiàn)了欲险,Android 和 iOS 都自帶 sqlite 數(shù)據(jù)庫。

以上代碼查看 data_persistence_main.dart 文件

Sqflite

Flutter 實現(xiàn)數(shù)據(jù)庫存儲需要通過插件 sqflite 來實現(xiàn)悉盆,寫文章的時候最新的版本是 sqflite 1.1.3盯荤,但是該版本需要 flutter 1.2 以上才行,所以我選擇的是 sqflite 1.1.0焕盟,小伙伴可以根據(jù)自己的 flutter 版本選擇相應(yīng)的 sqflite 版本

sqflite 的基本操作語句秋秤,在文檔中已經(jīng)寫得非常明白了,所以就不搬運了脚翘,這邊直接講下對于數(shù)據(jù)庫的一些封裝處理吧灼卢,因為打開數(shù)據(jù)庫是一個很消耗資源的一個過程,所以呢来农,推薦實現(xiàn)單例會比較好鞋真。例如我們要實現(xiàn)一個 student 存儲表

class DatabaseUtils {
  final String _tableStudent = 'student';

  static Database _database; // 創(chuàng)建單例,防止重復(fù)打開消耗內(nèi)存

  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) {
        // 創(chuàng)建數(shù)據(jù)庫的時候在這邊調(diào)用
        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) {
        // 更新升級數(shù)據(jù)庫的時候在這操作
        if (oldVersion == 1) db.execute('alter table $_tableStudent add column birthday text');
      }, onOpen: (db) {
        // 打開數(shù)據(jù)庫時候的回調(diào)
        print('${db.path}');
      });
    });
  }

  factory DatabaseUtils() {
    // 如果當(dāng)前的單例已經(jīng)存在沃于,則不再創(chuàng)建涩咖,否則重新創(chuàng)建,factory 關(guān)鍵詞看第一章
    if (_instance == null) _instance = DatabaseUtils._internal();
    return _instance;
  }
}

那么對數(shù)據(jù)庫的操作就完全考驗?zāi)愕?SQL 的掌握程度了繁莹,但是千萬記住檩互,sqlite 中的類型只有,整型 integer 咨演,字符類型 text闸昨,浮點類型 real,二進制 blob。數(shù)據(jù)庫的具體例子會等到最后的實際項目中展示饵较,原諒我不懂如何展示一個界面給你操作拍嵌,實現(xiàn)數(shù)據(jù)庫的各種功能。

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

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

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

  2. 基于郭神 cool weather 接口的一個項目打洼,實現(xiàn) BLoC 模式龄糊,實現(xiàn)狀態(tài)管理:flutter_weather

  3. 一個課程(當(dāng)時買了想看下代碼規(guī)范的逆粹,代碼更新會比較慢募疮,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改僻弹,很多地方看著不舒服阿浓,然后就改成自己的實現(xiàn)方式了):flutter_shop

如果對你有幫助的話,記得給個 Star蹋绽,先謝過芭毙,你的認(rèn)可就是支持我繼續(xù)寫下去的動力~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市卸耘,隨后出現(xiàn)的幾起案子退敦,更是在濱河造成了極大的恐慌,老刑警劉巖蚣抗,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侈百,死亡現(xiàn)場離奇詭異,居然都是意外死亡翰铡,警方通過查閱死者的電腦和手機钝域,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锭魔,“玉大人例证,你說我怎么就攤上這事∶耘酰” “怎么了织咧?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長漠秋。 經(jīng)常有香客問我笙蒙,道長,這世上最難降的妖魔是什么膛堤? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任手趣,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绿渣。我一直安慰自己朝群,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布中符。 她就那樣靜靜地躺著姜胖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淀散。 梳的紋絲不亂的頭發(fā)上右莱,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音档插,去河邊找鬼慢蜓。 笑死,一個胖子當(dāng)著我的面吹牛郭膛,可吹牛的內(nèi)容都是我干的晨抡。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼则剃,長吁一口氣:“原來是場噩夢啊……” “哼耘柱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起棍现,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤调煎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后己肮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體士袄,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年朴肺,在試婚紗的時候發(fā)現(xiàn)自己被綠了窖剑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡戈稿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鞍盗,到底是詐尸還是另有隱情需了,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布肋乍,位于F島的核電站,受9級特大地震影響敷存,放射性物質(zhì)發(fā)生泄漏墓造。R本人自食惡果不足惜堪伍,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望觅闽。 院中可真熱鬧帝雇,春花似錦、人聲如沸蛉拙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孕锄。三九已至吮廉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間畸肆,已是汗流浹背宦芦。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恼除,地道東北人踪旷。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像豁辉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子舀患,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345