Roadmap.shで学ぶFlutter - Advanced Dart - [Core Libraries⑤:io]

# 導入
[roadmap.shのFlutter](https://roadmap.sh/flutter)の`Advanced Dartについて学習を進めていきます。

今回は`dart:io`ライブラリです。
このライブラリは、ファイル、ディレクトリ、プロセス、ソケット、WebSocket、HTTPクライアント、サーバーを扱うためのAPIを提供してくれます。

https://dart.dev/libraries/dart-io

# dart:io

:::note warn
`dart:io`ライブラリは、ウェブアプリではないのFlutterアプリ、コマンドラインスクリプト、サーバーでのみインポートして使用できます。
ウェブアプリでは使用できません。
:::


`dart:io`ライブラリは一般的に**非同期API**を実装することが推奨されています。同期処理の場合、その処理がアプリケーションをブロックしてしまい、スケールさせることが難しくなる可能性があるためです。
そのため、ほとんどの操作は`Future`や`Stream`オブジェクトを介して結果を返します。

同期メソッドも`dart:io`ライブラリに一応存在はしており、メソッド名に「Sync」サフィックスが付いています。

`dart:io`ライブラリも`dart:math`などと同様に、使用する際には、以下のようにインポートが必要になります。

```dart
import 'dart:io';
```

## ファイルとディレクトリの操作

今回のライブラリを使用すると、コマンドラインアプリでファイルの読み書きやディレクトリのブラウズが可能です。
ファイルの内容を読み込む方法は2通りあります。
- すべてを一度に読み込む
    - ファイルの内容をすべて格納できるだけのメモリが必要
- ストリームを使用して読み込む
    - ファイルが非常に大きい場合や、読み込みながら処理したい場合は、`Stream`を使用するのが良い

### テキストとしてファイルを読み込む

UTF-8でエンコードされたテキストファイルを読み込む場合、`readAsString()`を使用して**ファイル全体を文字列**として読む込むことができます。逆に行ごとに取得したい場合は、`readAsLines()`を使用することで実現可能です。
いずれの場合も、`Future`オブジェクトが返却されます。

```dart
void main() async {
  var config = File('config.txt');

  // ファイル全体を1つの文字列に格納
  var stringContents = await config.readAsString();
  print('ファイルは${stringContents.length}文字の長さです。');

  // ファイルの各行をそれぞれの文字列に格納
  var lines = await config.readAsLines();
  print('ファイルには${lines.length}行あります。');
}
```

### バイナリとしてファイルを読み込む

ファイル全体をバイトのリストとして読み込むことも可能です。
`readAsBytes()`の呼び出しは`Future`を返却します。
結果が利用可能になった時点でその結果を提供してくれます。

```dart
void main() async {
  var config = File('config.txt');

  var contents = await config.readAsBytes();
  print('ファイルは${contents.length}バイトの長さです。');
}
```

### エラーハンドリング

エラーが発生した際、未処理の例外にならないようにするには、`Future`に`catchError`ハンドラを登録するか、非同期関数内で`try-catch`を使用します。

```dart
void main() async {
  var config = File('config.txt');
  try {
    var contents = await config.readAsString();
    print(contents);
  } catch (e) {
    print(e);
  }
}
```

### ストリームでファイル内容を読み込む

`Stream`を使用してファイルを少しずつ読み込むことができます。
`Stream` APIや、Dartの非同期サポートの一部である`await for`を使用できます。

```dart
import 'dart:io';
import 'dart:convert';

void main() async {
  var config = File('config.txt');
  Stream<List<int>> inputStream = config.openRead();

  var lines = utf8.decoder.bind(inputStream).transform(const LineSplitter());
  try {
    await for (final line in lines) {
      print('ストリームから${line.length}文字を取得');
    }
    print('ファイルが閉じられました');
  } catch (e) {
    print(e);
  }
}
```

### ファイル内容の書き込み

データをファイルに書き込むには、`IOSink`を使用します。
`File`の`openWrite()`メソッドを使用して、書き込み可能な`IOSink`を取得します。
`FileMode.write`は、デフォルトのモードであり、ファイル内の既存のデータを完全に上書きします。

完全上書きでなく、ファイルの末尾にデータを追加したい場合は、オプションの`mode`パラメータで`FileMode.append`を指定します。

```dart
var logFile = File('log.txt');

// ファイルに完全上書きしたい場合
var sink = logFile.openWrite();
sink.write('ファイルがアクセスされました ${DateTime.now()}\n');
await sink.flush();
await sink.close();

// 末尾に追加したい場合
var sink = logFile.openWrite(mode: FileMode.append);
```

バイナリデータを書き込むには、`add(List<int> data)`を使用します。

## ディレクトリ内のファイル一覧を取得

次は特定のファイルに対するI/Oではなく、ディレクトリに対する操作についてです。
ディレクトリ内のすべてのファイルとサブディレクトリを検索する操作は非同期になります。list()`メソッドは、ファイルやディレクトリが見つかるたびにオブジェクトを出力してくれる`Stream`を返却します。

```dart
void main() async {
  var dir = Directory('tmp');

  try {
    var dirList = dir.list();
    await for (final FileSystemEntity f in dirList) {
      if (f is File) {
        print('ファイルを発見: ${f.path}');
      } else if (f is Directory) {
        print('ディレクトリを発見: ${f.path}');
      }
    }
  } catch (e) {
    print(e.toString());
  }
}
```

## HTTPクライアントとサーバー

`dart:io`ライブラリは、コマンドラインアプリがHTTPリソースにアクセスしたり、HTTPサーバーを実行するためのクラスを提供します。
これを利用することで、HTTPサーバーアプリケーションをFlutterで作成することが可能になります。

### HTTPサーバー

`HttpServer`クラスは、ウェブサーバーを構築するための低レベルの機能を提供します。リクエストハンドラのマッチング、ヘッダーの設定、データのストリーミングなどが可能です。

次のサンプルウェブサーバーは、シンプルなテキスト情報を返します。このサーバーはポート8888で`127.0.0.1`（localhost）を監視し、`/dart`パスへのリクエストに応答します。他のパスへのリクエストには、ステータスコード404（ページが見つかりません）で応答します。

```dart
void main() async {
  final requests = await HttpServer.bind('localhost', 8888);
  await for (final request in requests) {
    processRequest(request);
  }
}

void processRequest(HttpRequest request) {
  print('リクエストを受信: ${request.uri.path}');
  final response = request.response;
  if (request.uri.path == '/dart') {
    response
      ..headers.contentType = ContentType(
        'text',
        'plain',
      )
      ..write('サーバーからの挨拶');
  } else {
    response.statusCode = HttpStatus.notFound;
  }
  response.close();
}
```

### HTTPクライアント

`dart:io`を使用して直接HTTPリクエストを行うのは避けた方が良いとされています。`dart:io`の`HttpClient`クラスはプラットフォーム依存であり、単一の実装に結びついています。代わりに、<font color='red'>`package:http`のような高レベルのライブラリを使用することが推奨されています。</font>

# 終わりに
今回は`dart:io`ライブラリについて学習しました。File操作によく利用されるライブラリですが、HTTP通信にも利用できることは言葉上でしか理解できていなかったので、為になりました。ただ、モバイルアプリとして開発する場合は、Flutterはクライアントとなるので、終盤に書いた通り、`package:http`のようなライブラリを使用した方が良さそうです。
その点も含めて、今回学習できたことはプラスになったと思うので、今後もこういったCoreライブラリを活用していきたいです。