[読書会]ファイルの読み書き

(当頁はdocs.flutter.dev(https://docs.flutter.dev/cookbook/persistence/key-value)のリファレンスブログです。)

1.概要

 アプリを開発する際、ディスク上にファイルを読み書きする必要がある場合があります。
(例えば、アプリの起動をまたいでデータを保存したり、インターネットからデータをダウンロードしてオフラインで利用するために保存したりするケースが考えられます。)

 モバイルやデスクトップアプリでファイルをディスクに保存するには、path_provider プラグインdart:io ライブラリを組み合わせて使用します。

(注意)

 現在、このレシピはWebアプリには対応していません。
この問題に関する議論は flutter/flutter issue #45296 で確認できます。

2.正しいローカルパスを見つける

 この例では、カウンターの値を表示します。カウンターの値が変更されるたびに、そのデータをディスクに保存し、次回アプリを起動した際にそのデータを読み込むことができます。
では、このデータをどこに保存すればよいでしょうか?

(path_provider パッケージ)

 path_provider パッケージを使用すると、デバイスのファイルシステム内で一般的に使用される場所に、プラットフォームに依存しない方法でアクセスできます。このプラグインでは、現在以下の2つのファイルシステムの場所にアクセスできます。

(1)一時ディレクトリ(Temporary directory)

  • 一時的なデータを保存するためのディレクトリ(キャッシュ)です。このディレクトリ内のデータは、システムが必要に応じていつでも削除する可能性があります。
  • iOS では、このディレクトリは NSCachesDirectory に対応します。
  • Android では、このディレクトリは getCacheDir() が返す値に対応します。

(2)ドキュメントディレクトリ(Documents directory)

  • アプリが専用にアクセスできるファイルを保存するためのディレクトリです。
    このディレクトリのデータは、アプリが削除されない限りシステムによって削除されることはありません。
  • iOS では、このディレクトリは NSDocumentDirectory に対応します。
  • Android では、このディレクトリは AppData ディレクトリに対応します。

 以下の例では、情報を ドキュメントディレクトリ に保存します。
このディレクトリのパスを取得するには以下の様にします。

import 'package:path_provider/path_provider.dart';

Future<String> get _localPath async {
  //getApplicationDocumentsDirectory():
  //( ドキュメントディレクトリのパスを取得
  //  (ドキュメントディレクトリの位置を非同期で取得するための関数です。
  //   取得されたパスは、アプリ専用のデータを保存するために使用されます。))
  final directory = await getApplicationDocumentsDirectory();
  
  return directory.path; //ディレクトリのパスを返す
}

 この手法を使うことで、ユーザーのデータを安全に管理し、アプリの再起動後も簡単にアクセスできるようになります。一時データの場合は一時ディレクトリを、永続的なデータ保存にはドキュメントディレクトリを利用するのが適切です。

3.ファイルの場所を参照する

 ファイルを保存する場所(パス)が分かったら、そのファイルの完全な保存先を参照するオブジェクトを作成します。このとき、Dartの dart:io ライブラリの File クラスを使用します。

(参照オブジェクトの作成方法)

 以下のコードは、ファイルの保存先を参照する方法を示しています。

Future<File> get _localFile async {

  //_localPath:
  //( 保存先のディレクトリパスを取得。
  //  ( これは、前のセクションで定義した、ドキュメントディレクトリのパスを
  //    取得する関数を呼び出しています。
  //    非同期でパスを取得するため、await を使用しています。)
  final path = await _localPath;
  
  //File クラス:
  //( 保存先のパスとファイル名を結合して File オブジェクトを返す。
  //  ( Dartの dart:ioライブラリのクラスで、ファイル操作を行う為に使用する。
  //    コンストラクタに保存先の完全なパス
  //    (例: /path/to/directory/counter.txt)を指定することで、
  //      そのファイルへの参照を作成します。))
  return File('$path/counter.txt');
}

(ポイント)

 この手順では、まだファイルにデータを保存したり読み込んだりしていません。
ここで作成しているのは、あくまで「ファイルへの参照」を表すオブジェクトです。
このオブジェクトを使って、次のステップでファイルへのデータの書き込みや読み取りを行います。

 このように、ファイルの保存先ディレクトリとファイル名を組み合わせて File オブジェクトを作成することで、アプリ内で特定のファイルを操作する準備が整います。

4.ファイルにデータを書き込む

 ファイルの参照を取得したら、そのファイルを使ってデータを読み書きできます。
まずは、データを書き込む方法を見ていきます。

(データの書き込み方法)

 カウンターの値(整数)をファイルに保存します。
この値は整数ですが、’$counter’ という構文を使って文字列として書き込みます。

Future<File> writeCounter(int counter) async {

  //_localFile:
  //( ファイル参照を取得
  //  (以前のステップで定義したファイル参照を取得します。
  //   非同期処理のため、await を使ってファイルオブジェクトを待ちます。)
  final file = await _localFile;

  //file.writeAsString:
  //( ファイルに文字列データを書き込むメソッドです。
  //  引数に文字列を指定します。
  //  この例では、カウンターの値を文字列形式(例: '42')に変換し保存します。)
  return file.writeAsString('$counter');
  //↑ 戻り値:
  //( このメソッドは、ファイルの書き込み操作が完了した後、
  //  操作対象の File オブジェクトを返します。
  //  戻り値を利用することで、書き込み後の操作(例: 確認や後続処理)
  //  に活用できます。) 
}

5.ファイルからデータを読み込む

 ディスク上にデータが保存されたので、それを読み取る方法を説明します。
このステップでは、再び dart:io ライブラリFile クラスを使用します。

(データの読み取り方法)

 保存されたカウンターの値をファイルから読み込み、その値を整数として返します。
ファイルが存在しない場合や読み込み中にエラーが発生した場合には、デフォルト値 0 を返します。

Future<int> readCounter() async {
  try {
    
    //_localFile:
    //( ファイル参照を取得
    //  (保存先ファイルの参照を取得します。  
    //   このファイル参照を使用してデータを読み取ります。))
    final file = await _localFile;

    //file.readAsString:
    //( ファイルを読み込む
    //  (ファイルの内容を文字列として非同期で読み込むメソッドです。
    //   ファイルの中身全体を String として取得します。))
    final contents = await file.readAsString();

    //int.parse:
    //( 読み取った文字列を整数値に変換します。
    //  この例では、ファイルに保存されている文字列(例: '42')を
    //  整数値 42 に変換します。)
    return int.parse(contents);
    
  } catch (e) {
  
    //エラー発生時にはデフォルト値 0 を返す
    return 0;

  }
}

(ポイント)

 ファイルが存在しない、もしくは破損している場合にも対応するため、例外処理 (try-catch) を実装しています。
ファイルから取得したデータが期待した形式であることを前提にしていますが、ファイルが正しくない内容を持つ可能性がある場合はさらに検証を行うことが望ましいです。

(この手順の役割)

 この手順により、アプリの前回終了時に保存したデータを復元できるようになります。
(例えば、カウンターの値をアプリ再起動後も保持することで、ユーザに一貫性のあるエクスペリエンスを提供できます。)

 これで、データの保存(書き込み)と復元(読み取り)の一連の処理が完成します。

6.サンプルコード

(再起動しても上記の値がファイル保存されているので、再度読込表示されます。)

import 'dart:async'; // 非同期処理を扱うためのライブラリ
import 'dart:io'; // ファイル操作を行うためのライブラリ

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; // FlutterのUIコンポーネントを提供
import 'package:path_provider/path_provider.dart'; // デバイス上の特定のディレクトリパスを取得するためのパッケージ

void main() {
  runApp(
    MaterialApp(
      title: 'Reading and Writing Files', // アプリのタイトル
      home: FlutterDemo(storage: CounterStorage()), // アプリのホーム画面に FlutterDemo ウィジェットを設定
    ),
  );
}

// カウンターの値を保存・読み取りするためのクラス
class CounterStorage {
  // アプリ専用のドキュメントディレクトリのパスを取得
  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory(); // ドキュメントディレクトリを取得
    if (kDebugMode) {
      print('保存先ディレクトリ: ${directory.path}');
    } // パスをコンソールに出力
    return directory.path; // ディレクトリのパスを返す
  }

  // 保存するファイルの参照を取得
  Future<File> get _localFile async {
    final path = await _localPath; // ディレクトリパスを取得
    return File('$path/counter.txt'); // ファイルパスを組み立てて File オブジェクトを返す
  }

  // ファイルからカウンターの値を読み込む
  Future<int> readCounter() async {
    try {
      final file = await _localFile; // ファイル参照を取得
      final contents = await file.readAsString(); // ファイルの内容を文字列として読み込む
      return int.parse(contents); // 文字列を整数に変換して返す
    } catch (e) {
      // エラー発生時はデフォルト値 0 を返す
      return 0;
    }
  }

  // ファイルにカウンターの値を書き込む
  Future<File> writeCounter(int counter) async {
    final file = await _localFile; // ファイル参照を取得
    return file.writeAsString('$counter'); // 整数を文字列に変換してファイルに書き込む
  }
}

// アプリのメイン画面を構築する StatefulWidget
class FlutterDemo extends StatefulWidget {
  const FlutterDemo({super.key, required this.storage}); // storage を受け取るコンストラクタ

  final CounterStorage storage; // CounterStorage クラスのインスタンス

  @override
  State<FlutterDemo> createState() => _FlutterDemoState(); // 状態管理用のStateクラスを作成
}

class _FlutterDemoState extends State<FlutterDemo> {
  int _counter = 0; // カウンターの初期値

  @override
  void initState() {
    super.initState();
    // アプリ起動時に保存されたカウンターの値を読み込み
    widget.storage.readCounter().then((value) {
      setState(() {
        _counter = value; // 読み込んだ値をセット
      });
    });
  }

  // カウンターをインクリメントし、ファイルに保存する
  Future<File> _incrementCounter() {
    setState(() {
      _counter++; // カウンターをインクリメント
    });

    // カウンターの値をファイルに保存
    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), // ボタンに表示するアイコン
      ),
    );
  }
}

コメントを残す