2021/4/1 Flutter : Read and write filesの訳

null safety article

Read and write files

場合によってはファイルをディスクに書き込んだり、ディスクからファイルを取得して読み取ることが必要な場合があります。

たとえば、アプリの起動後もデータを保持したり、インターネットからデータをダウンロードして後でオフラインで使用できるように保存したりする必要がある場合があります。

ファイルをディスクに保存するには、path_provider プラグインをdart:ioライブラリと組み合わせます。

このレシピでは、次の手順を使用します。

  1. Find the correct local path.
  2. Create a reference to the file location.
  3. Write data to the file.
  4. 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.

上記のエラーが発生することがあるようです。というか出ました。

[Solved] No implementation found for method getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider

上記ページの一つ目の解決法、「アプリを閉じて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ではなく(リセットされず)、そのファイルから読み取った数字が画面に表示されます。

readCounterなどのファイル読み取り・書き込みは非同期関数なのですが、
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

コメントを残す

メールアドレスが公開されることはありません。