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

原创
2018/04/09 20:43
阅读数 3.2W

将键值数据存储在磁盘上

如果我们有一小部分我们想要保存的键值,我们可以使用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');

Settergetter方法适用于所有原始类。

支持的类型

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

  • 只能使用原始类型: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 评论
1 收藏
0
分享
返回顶部
顶部