null safety article
Contents
Read and write files
場合によってはファイルをディスクに書き込んだり、ディスクからファイルを取得して読み取ることが必要な場合があります。
たとえば、アプリの起動後もデータを保持したり、インターネットからデータをダウンロードして後でオフラインで使用できるように保存したりする必要がある場合があります。
ファイルをディスクに保存するには、path_provider
プラグインをdart:io
ライブラリと組み合わせます。
このレシピでは、次の手順を使用します。
- Find the correct local path.
- Create a reference to the file location.
- Write data to the file.
- Read data from the file.
1. Find the correct local path
この例では、カウンターを表示します。カウンターが変更されたら、ディスクにデータを書き込んで、アプリの読み込み時に再度読み取ることができるようにします。このデータをどこに保存する必要がありますか?
このpath_provider
パッケージは、デバイスのファイルシステムで一般的に使用される場所にアクセスするためのプラットフォームに依存しない方法を提供します。プラグインは現在、2つのファイルシステムの場所へのアクセスをサポートしています。
一時ディレクトリ(Temporary directory)
システムがいつでもクリアできる一時ディレクトリ(キャッシュ)。iOSでは、これはNSCachesDirectory
に対応します。Androidでは、getCacheDir()
によって返される値です 。
ドキュメントディレクトリ(Documents directory)
アプリだけがアクセスできるファイルを保存するためのアプリのディレクトリ。アプリが削除された場合にのみ、システムはディレクトリをクリアします。iOSでは、これはNSDocumentDirectory
に対応します。Androidの場合、これはAppData
ディレクトリです。
この例では、documentsディレクトリに情報を保存します。ドキュメントディレクトリへのパスは次のように見つけることができます。
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
2. Create a reference to the file location
ファイルを保存する場所がわかったら、ファイルの完全な場所への参照を作成します。これを実現するために、dart:io
ライブラリのFileクラスを使用できます。
Future<File> get _localFile async {
final path = await _localPath;
return File('$path/counter.txt');
}
3. Write data to the file
操作するファイルへアクセスできるようになりましたので、それを使用してデータの読み取りと書き込みを行います。まず、ファイルにデータを書き込みます。カウンターは整数ですが、'$counter'
構文を使用して文字列としてファイルに書き込まれます。
Future<File> writeCounter(int counter) async {
final file = await _localFile;
// Write the file.
return file.writeAsString('$counter');
}
4. Read data from the file
ディスクにデータが保存されたので、それを読み取ることができます。もう一度、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 encountering an error, return 0.
return 0;
}
}
Testing
ファイルと相互作用するコードをテストするには、ホストプラットフォームと通信するMethodChannel
クラスへの呼び出しをモックする必要があります。セキュリティ上の理由から、デバイス上のファイルシステムと直接対話することはできないため、テスト環境のファイルシステムと対話します。
メソッド呼び出しをモックするには、テストファイルでsetupAll()
関数を提供します。この関数は、テストが実行される前に実行されます。
setUpAll(() async {
// Create a temporary directory.
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 you're getting the apps documents directory, return the path to the
// temp directory on the test environment instead.
if (methodCall.method == 'getApplicationDocumentsDirectory') {
return directory.path;
}
return null;
});
});
Complete example
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( MaterialApp( title: 'Reading and Writing Files', home: FlutterDemo(storage: CounterStorage()), ), ); } class CounterStorage { Future<String> get _localPath async { final directory = await getApplicationDocumentsDirectory(); return directory.path; } Future<File> get _localFile async { final path = await _localPath; print('ファイルのパス:$path'); return File('$path/counter.txt'); } Future<int> readCounter() async { try { final file = await _localFile; // Read the file final contents = await file.readAsString(); return int.parse(contents); } catch (e) { // If encountering 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 { const FlutterDemo({Key? key, required this.storage}) : super(key: key); final CounterStorage storage; @override _FlutterDemoState createState() => _FlutterDemoState(); } class _FlutterDemoState extends State<FlutterDemo> { int _counter = 0; @override void initState() { super.initState(); widget.storage.readCounter().then((int value) { setState(() { _counter = value; }); }); } Future<File> _incrementCounter() { setState(() { _counter++; }); // Write the variable as a string to the file. return widget.storage.writeCounter(_counter); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Reading and Writing Files'), ), body: Center( child: Text( 'Button tapped $_counter time${_counter == 1 ? '' : 's'}.', ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
(2021/8/20追記)
iPhone実機で実行すると
No implementation found for method getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider in a flutter.
上記のエラーが発生することがあるようです。というか出ました。
上記ページの一つ目の解決法、「アプリを閉じてflutter run で実行」でエラーが出なくなりました。
結局実行するとただのカウンターなわけですが、
Future<File> get _localFile async { final path = await _localPath; print(path); return File('$path/counter.txt'); }
print文でpathの中身を見てみると、
flutter: /Users/userno1/Library/Developer/CoreSimulator/Devices/8A1217F9-EE81-4AB2-81EE-97109119A5FB/data/Containers/Data/Application/24A414F3-A415-4E3F-90F2-8E534CAFA2F1/Documents
のように表示され、実際その場所に行ってみると確かにcounter.textファイルにカウンターの数字が書き出されています。
ですので、ホットリスタートをした時も、カウンターの数字は0ではなく(リセットされず)、そのファイルから読み取った数字が画面に表示されます。
Future<File> _incrementCounter() { setState(() { _counter++; }); // Write the variable as a string to the file. return widget.storage.writeCounter(_counter); }
とりあえずsetStateで状態を変更して、画面自体は同期的にリビルドして、そのあと非同期処理をawaitで待つことはしていない。なのでbuildメソッド内でFutureBuilderを使っていない、ということですね。
サンプルなのでエラーハンドリングも無い、と。
参考
https://flutter.dev/docs/cookbook/persistence/reading-writing-files