Flutter 构建完整应用手册-持久化

将键值数据存储在磁盘上

如果我们有一小部分我们想要保存的键值,我们可以使用shared_preferences插件。

通常我们不得不编写原生平台集成来存储这两个平台的数据。 幸运的是,shared_preferences插件可用于此目的。 共享偏好设置插件包装iOS上的NSUserDefaults和Android上的SharedPreferences,为简单数据提供持久存储。

建立

在我们开始之前,我们需要将shared_preferences插件添加到我们的pubspec.yaml文件中:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: "<newest version>"

保存数据

要持久化键值数据,我们可以使用SharedPreferences类。 为了保存数据,我们调用set方法。 请注意,数据是异步持久的。 如果我们想要在保存数据时得到通知,请使用commit()函数。

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

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

读取数据

SharedPreferences prefs = await SharedPreferences.getInstance();

int counter = (prefs.getInt('counter') ?? 0) + 1;

在上面的例子中,我们从counter键加载数据,如果它不存在,则返回0。

移除数据

SharedPreferences prefs = await SharedPreferences.getInstance();

prefs.remove('counter');

Setter和getter方法适用于所有原始类。

支持的类型

虽然使用键值存储非常简单方便,但它有一些限制:

  • 只能使用原始类型:int, double, bool, string 和 string list
  • 它不是用来存储大量数据,因此不适合作为应用程序缓存。

有关Android上共享首选项的更多信息,请访问Android开发人员网站上的共享首选项文档

例子

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

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

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

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

  @override
  _MyHomePageState createState() => new _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();
    _counter = (prefs.getInt('counter') ?? 0) + 1;
    setState(() {
      _counter;
    });
    prefs.setInt('counter', _counter);
  }

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

测试支持

通过运行以下代码,我们可以在我们的测试中使用初始值填充SharedPreferences:

const MethodChannel('plugins.flutter.io/shared_preferences')
  .setMockMethodCallHandler((MethodCall methodCall) async {
    if (methodCall.method == 'getAll') {
      return <String, dynamic>{}; // set initial values here if desired
    }
    return null;
  });

上面的代码应放在测试文件夹下的测试文件中。

读写文件

在某些情况下,将文件读取和写入磁盘可能非常方便。 这可用于跨应用程序启动持续保存数据或从互联网上下载数据并保存以供以后脱机使用。

为了将文件保存到磁盘,我们需要将path_provider插件与dart:io库结合使用。

路线

  • 找到正确的本地路径
  • 创建对文件位置的引用
  • 将数据写入文件
  • 从文件中读取数据

1.找到正确的本地路径

在这个例子中,我们将显示一个计数器。 当计数器发生变化时,我们需要在磁盘上写入数据,以便在应用程序加载时再次读取它。 因此,我们需要问:我们应该在哪里存储这些数据?

path_provider插件提供了一种平台不可知的方式来访问设备文件系统上的常用位置。 该插件当前支持访问两个系统文件位置:

  • 临时目录: 一个临时目录(缓存),系统可以随时清除。 在iOS上,这对应于NSTemporaryDirectory()返回的值。 在Android上,这是getCacheDir()返回的值。
  • 文档目录:应用程序的目录,用于存储只有它可以访问的文件。 只有当应用程序被删除时,系统才会清除目录。 在iOS上,这对应于NSDocumentDirectory。 在Android上,这是AppData目录。

在我们的例子中,我们希望将信息存储在文档目录中! 我们可以像这样找到文档目录的路径:

Future<String> get _localPath async {
  final directory = await getApplicationDocumentsDirectory();
  
  return directory.path;
}

2.创建对文件位置的引用

一旦我们知道在哪里存储文件,我们需要创建一个文件的完整位置的引用。 我们可以使用dart:io库中的File类来实现此目的。

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

3.将数据写入文件

现在我们有一个File可以使用,我们可以使用它来读取和写入数据! 首先,我们将一些数据写入文件。 由于我们正在使用计数器,因此我们只会将整数存储为字符串。

Future<File> writeCounter(int counter) async {
  final file = await _localFile;
  
  // Write the file
  return file.writeAsString('$counter');
}

4.从文件中读取数据

现在我们在磁盘上有一些数据,我们可以阅读它! 再次,我们将使用File类来完成此操作。

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

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

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

测试

为了测试与文件交互的代码,我们需要模拟对MethodChannel的调用。 MethodChannel是Flutter用来与主机平台进行通信的类。

在我们的测试中,我们无法与设备上的文件系统进行交互。 我们需要与我们的测试环境的文件系统进行交互!

为了模拟方法调用,我们可以在我们的测试文件中提供一个setupAll函数。 该功能将在测试执行之前运行。

setUpAll(() async {
  // Create a temporary directory to work with
  final directory = await Directory.systemTemp.createTemp();
  
  // Mock out the MethodChannel for the path_provider plugin
  const MethodChannel('plugins.flutter.io/path_provider')
      .setMockMethodCallHandler((MethodCall methodCall) async {
    // If we're getting the apps documents directory, return the path to the
    // temp directory on our test environment instead.
    if (methodCall.method == 'getApplicationDocumentsDirectory') {
      return directory.path;
    }
    return null;
  });
});

完整例子

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(
    new MaterialApp(
      title: 'Reading and Writing Files',
      home: new FlutterDemo(storage: new 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 new 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 we encounter 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() => new _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() async {
    setState(() {
      _counter++;
    });

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏有趣的django

Django rest framework源码分析(1)----认证

一、基础 1.1.安装 两种方式: github pip直接安装 pip install django-rest-framework 1.2.需要先了解的一...

65711
来自专栏小灰灰

Java 动手写爬虫: 四、日志埋点输出 & 动态配置支持

第四篇, 日志埋点输出 & 动态配置支持 前面基本上实现了一个非常简陋的爬虫框架模型,很多关键链路都没有日志,在分析问题时,就比较麻烦了,因此就有了这一篇博文...

3077
来自专栏潇涧技术专栏

Art of Android Development Reading Notes 13

《Android开发艺术探索》读书笔记 (13) 第13章 综合技术、第14章 JNI和NDK编程、第15章 Android性能优化

992
来自专栏求索之路

Android大厂面试题锦集(BAT TMD JD 小米)

上次写这篇文章的时候也差不多是一年前了,这一年我兜兜转转从android到java又回到android,校招面了很多大厂,阿里、京东、小米、头条、知乎、腾讯、...

4328
来自专栏JackieZheng

RabbitMQ入门-从HelloWorld开始

从读者的反馈谈RabbitMQ 昨天发完《RabbitMQ入门-初识RabbitMQ》,我陆陆续续收到一些反馈。鉴于部分读者希望结合实例来讲 期待下篇详细,最好...

2139
来自专栏编程思想之路

WiFiAp探究实录--功能实现与源码分析

Android虐我千百遍,我待Android如初恋。 ——————编辑于2017-08-02——————— wifi热点说的是wifiAp相...

1.6K9
来自专栏JackieZheng

RabbitMQ入门-从HelloWorld开始

从读者的反馈谈RabbitMQ 昨天发完《RabbitMQ入门-初识RabbitMQ》,我陆陆续续收到一些反馈。鉴于部分读者希望结合实例来讲 期待下篇详细,最好...

2335
来自专栏Java编程技术

SpringBoot之日志文件找不到

做新应用就是这样,会遇到各种问题,昨天刚解决了加载某一个类时候抛出了class is not visible from class loader的问题,今天就有...

981
来自专栏晓晨的专栏

ASP.NET Core 依赖注入(DI)简介

6394
来自专栏双十二技术哥

确认过眼神,这就是你要的路由库

上一篇文章我们谈到了如何实现一个路由库,那本篇文章就给大家推荐一个好用的路由库,来确认下眼神,这就是你要的路由库。

1754

扫码关注云+社区

领取腾讯云代金券