📝

【Flutter】Flutter SDKリポジトリ内で使われている気になるパッケージ10選

に公開

概要

今回はFlutter SDKのリポジトリ内で使っているパッケージの中から、メジャーなものは避けて個人的に気になる面白そうなものを10個ピックアップして試して見ました。

この記事はFlutter Advent Calendar 2025の12月25日分の記事です。

対象

今回対象となるのは以下Flutter公式リポジトリのルートにあるで pubspec.yaml の現時点(2025/12)で使われているパッケージを調べて気になったものをピックアップしてます。

https://github.com/flutter/flutter

動作環境

# Dartでの動作環境時
$ dart --version
Dart SDK version: 3.10.1 (stable) (Tue Nov 18 11:02:31 2025 -0800) on "macos_arm64"
# Flutterでの動作環境時
$ flutter --version
Flutter 3.38.5 • channel stable
Tools • Dart 3.10.4 • DevTools 2.51.1

パッケージ

async

https://pub.dev/packages/async

非同期処理を扱うために、dart:async スタイルに準拠したユーティリティクラス
執筆時のバージョン: 2.13.0

  • 主なユーティリティ
    • AsyncCache

      • 時間のかかる非同期計算の結果を一定期間キャッシュしてくれる

        import 'package:async/async.dart' as async;
        
        Future<void> demoAsyncCache() async {
          var counter = 0;
          final cache = async.AsyncCache<int>(const Duration(seconds: 2));
        
          Future<int> load() async {
            counter++;
            print('  expensive computation... (#$counter)');
            await Future<void>.delayed(const Duration(milliseconds: 100));
            return counter;
          }
        
          final v1 = await cache.fetch(load);
          final v2 = await cache.fetch(load);
        
          print('  v1=$v1, v2=$v2 (キャッシュされているので counter=1 のまま)');
        
          // キャッシュの有効期限切れ後
          await Future<void>.delayed(const Duration(seconds: 3));
          final v3 = await cache.fetch(load);
          print('  v3=$v3 (有効期限切れ後なので再計算)');
        }
        /* 実行結果
          expensive computation... (#1)
          v1=1, v2=1 (キャッシュされているので counter=1 のまま)
          expensive computation... (#2)
          v3=2 (有効期限切れ後なので再計算)
        */
        
    • AsyncMemoizer

      • 非同期操作をメモ化してくれる

        import 'package:async/async.dart' as async;
        
        Future<void> demoAsyncMemoizer() async {
          final memoizer = async.AsyncMemoizer<String>();
        
          Future<String> loadConfig() => memoizer.runOnce(() async {
            print('  実際のロードは一回だけ');
            await Future<void>.delayed(const Duration(milliseconds: 100));
            return 'CONFIG-${DateTime.now().millisecondsSinceEpoch}';
          });
        
          final c1 = await loadConfig();
          final c2 = await loadConfig();
        
          print('  c1=$c1');
          print('  c2=$c2 (同じ値・2回目は中の処理は走らない)');
        }
        /* 実行結果
          実際のロードは一回だけ
          c1=CONFIG-1765349468315
          c2=CONFIG-1765349468315 (同じ値・2回目は中の処理は走らない)
        */
        
    • FutureGroup

      • 一連の futures が完了するまで待機してくれる

        import 'package:async/async.dart' as async;
        
        Future<void> demoFutureGroup() async {
          final group = async.FutureGroup<String>();
          Future<String> fetchUser() async {
            await Future<void>.delayed(const Duration(milliseconds: 300));
            return 'User';
          }
          Future<String> fetchPosts() async {
            await Future<void>.delayed(const Duration(milliseconds: 500));
            return 'Posts';
          }
        
          group.add(fetchUser());
          group.add(fetchPosts());
          // もう追加しないことを宣言
          group.close();
        
          final results = await group.future;
          print('  FutureGroup results=$results');
        }
        /* 実行結果
          FutureGroup results=[User, Posts]
        */
        
      • 他にもStream系のユーティリティが多々あります

boolean_selector

https://pub.dev/packages/boolean_selector

簡略化された Dart 風の文法で書かれたブール式を扱うためのライブラリ。
セレクターには、識別子、括弧、および ||, &&, !, ?: などのブール演算子を含めることができます。有効な Dart 識別子はすべて許可されており、識別子にはハイフンを含めることもできます。例) chrome, chrome || content-shell ,js || (vm && linux)
執筆時のバージョン: 2.1.2

  • サンプル

    import 'package:boolean_selector/boolean_selector.dart';
    
    void main(List<String> args) {
      var selector = BooleanSelector.parse("(x && y) || z");
      print(selector.evaluate((variable) => args.contains(variable)));
    }
    
    • BooleanSelector.parse で文字列を解析し evaluate() で評価する
  • どこで使われているか?

    • Dart の test パッケージの platform selector で、この boolean_selector の構文が使われている

      @TestOn('vm && !chrome')
      void main() {
        // このテストは Dart VM かつ Chrome ではない環境でのみ実行
      }
      
      • ここの 'vm && !chrome' 文字列が、内部的には boolean_selector の構文としてパース・評価される

characters

https://pub.dev/packages/characters

文字列を、ユーザーが認識する文字の連なりとして扱う事ができるパッケージ
執筆時のバージョン: 1.4.1

Dart の String は UTF-16 のコードユニット列なので、絵文字や濁点つきの文字などをsubstring や length で扱うと文字の途中でバラバラにしてしまう事があります。

void main() {
  // 「か」+「濁点」 
  final text = 'が';

  print('text: $text');
  print('code unit length: ${text.length}');

  final firstUnit = text.substring(0, 1);
  final secondUnit = text.substring(1, 2);

  print('first unit: $firstUnit');   // 「か」だけ
  print('second unit: $secondUnit'); // 濁点だけ(表示されない or 変な位置に出る)
}
/* 実行結果
text: が
code unit length: 2
first unit: か
second unit: ゙
*/

上記を実行すると文字が分解されてしまいます。 characters を使うと以下のように1文字として扱うことができます。

import 'package:characters/characters.dart';

void main() {
  final text = 'が';
  final chars = text.characters;

  print('grapheme length: ${chars.length}');
  print('first grapheme: "${chars.first}"');

  // 先頭1文字を切り出す
  final firstGraphemeOnly = text.characters.take(1).toString();
  print('take(1) as grapheme: "$firstGraphemeOnly"');
}
/* 実行結果
grapheme length: 1
first grapheme: "が"
take(1) as grapheme: "が"
*/

flutter_staggered_grid_view

https://pub.dev/packages/flutter_staggered_grid_view

Flutterで様々なグリッドレイアウト(スタッガード、 masonry、 quilted、 wovenなど)を実現するためのコレクションを提供します。
執筆時のバージョン: 0.7.0

  • pubspec.yaml に以下を追加

    dependencies:
      flutter_staggered_grid_view: ^0.7.0
    

このパッケージには以下のGrid Layoutが含まれています。

  • Staggered

    • 各タイルの「幅×高さセル数」を自由に決められる、不揃いタイルのグリッド

    • サンプル
      image1.png

      ソースコード
      import 'package:flutter/material.dart';
      import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
      
      class StaggeredLayoutScreen extends StatelessWidget {
        const StaggeredLayoutScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(title: const Text('Staggered Layout')),
            body: Center(
              // 横方向を4分割したグリッド
              child: StaggeredGrid.count(
                crossAxisCount: 4,
                mainAxisSpacing: 4,  // 縦のすき間
                crossAxisSpacing: 4, // 横のすき間
                children: [
                  StaggeredGridTile.count( // 2x2マス分の大きなタイル
                    crossAxisCellCount: 2,
                    mainAxisCellCount: 2,
                    child: _tile(0),
                  ),
                  StaggeredGridTile.count( // 2x1マス分(横長)
                    crossAxisCellCount: 2,
                    mainAxisCellCount: 1,
                    child: _tile(1),
                  ),
                  StaggeredGridTile.count( // 1x1マス分(普通サイズ)
                    crossAxisCellCount: 1,
                    mainAxisCellCount: 1,
                    child: _tile(2),
                  ),
                  StaggeredGridTile.count(
                    crossAxisCellCount: 1,
                    mainAxisCellCount: 1,
                    child: _tile(3),
                  ),
                  StaggeredGridTile.count(
                    crossAxisCellCount: 4,
                    mainAxisCellCount: 2,
                    child: _tile(4),
                  ),
                ],
              ),
            ),
          );
        }
      
        Widget _tile(int index) {
          return Container(
            color: Colors.blue[(index + 1) * 100],
            child: Center(child: Text('Tile $index')),
          );
        }
      }
      
  • Masonry

    • Pinterest 風の「高さバラバラのカード」を縦に詰めていくレイアウト

    • サンプル
      image2.png

      ソースコード
      import 'dart:math';
      
      import 'package:flutter/material.dart';
      import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
      
      class MasonryLayoutScreen extends StatelessWidget {
        const MasonryLayoutScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          final rnd = Random();
          return Scaffold(
            appBar: AppBar(title: const Text('Masonry Layout')),
            body: MasonryGridView.count(
              crossAxisCount: 2,  // 列数
              mainAxisSpacing: 8, // 縦のすき間
              crossAxisSpacing: 8,
              itemCount: 20,
              itemBuilder: (context, index) {
                final height = 80 + rnd.nextInt(120); // アイテムごとに高さを変える
                return Container(
                  height: height.toDouble(),
                  color: Colors.green[(index % 8 + 1) * 100],
                  child: Center(child: Text('Item $index\n$height px')),
                );
              },
            ),
          );
        }
      }
      
  • Quilted

    • 「大きいタイル + 小さいタイル」を繰り返す“キルト”パターンのレイアウト

    • サンプル
      image3.png

      ソースコード
      import 'package:flutter/material.dart';
      import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
      
      class QuiltedLayoutScreen extends StatelessWidget {
        const QuiltedLayoutScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(title: const Text('Quilted Layout')),
            // Quilted: 「大きい+小さい」タイルをパターンで繰り返す例
            body: GridView.custom(
              padding: const EdgeInsets.all(8),
              gridDelegate: SliverQuiltedGridDelegate(
                crossAxisCount: 4,
                mainAxisSpacing: 4,
                crossAxisSpacing: 4,
                // パターンを反転しながら繰り返す
                repeatPattern: QuiltedGridRepeatPattern.inverted,
                pattern: const [
                  QuiltedGridTile(2, 2), // 2x2 の大きなタイル
                  QuiltedGridTile(1, 1),
                  QuiltedGridTile(1, 1),
                  QuiltedGridTile(1, 2), // 1x2 の横長タイル
                ],
              ),
              childrenDelegate: SliverChildBuilderDelegate(
                (context, index) => Container(
                  color: Colors.orange[(index % 8 + 1) * 100],
                  child: Center(child: Text('Item $index')),
                ),
                childCount: 20,
              ),
            ),
          );
        }
      }
      
  • Woven

    • 幅・高さ比率 (crossAxisRatio / aspectRatio) をパターンで変えつつ、左右に少しずつずらしながら「織物」のようなリズムを作るレイアウト

    • サンプル
      image4.png

      ソースコード
      import 'package:flutter/material.dart';
      import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
      
      class WovenLayoutScreen extends StatelessWidget {
        const WovenLayoutScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(title: const Text('Woven Layout')),
            // Woven: 幅と位置を少し変えてリズム感のあるグリッドにする例
            body: GridView.custom(
              padding: const EdgeInsets.all(8),
              gridDelegate: SliverWovenGridDelegate.count(
                crossAxisCount: 2,
                mainAxisSpacing: 8,
                crossAxisSpacing: 8,
                pattern: const [
                  WovenGridTile(1), // 通常
                  WovenGridTile(
                    5 / 7, // 縦横比(やや縦長)
                    crossAxisRatio: 0.9, // 少し細め
                    alignment: AlignmentDirectional.centerEnd, // 右寄せ
                  ),
                ],
              ),
              childrenDelegate: SliverChildBuilderDelegate(
                (context, index) => Container(
                  color: Colors.purple[(index % 8 + 1) * 100],
                  child: Center(child: Text('Item $index')),
                ),
                childCount: 20,
              ),
            ),
          );
        }
      }
      
  • Staired

    • タイルごとにサイズ比率を変え、かつ少しずつずらして「階段状」に配置するレイアウト

    • サンプル
      image5.png

      ソースコード
      import 'package:flutter/material.dart';
      import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
      
      class StairedLayoutScreen extends StatelessWidget {
        const StairedLayoutScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(title: const Text('Staired Layout')),
            // Staired: 大小タイルを階段状にずらして並べる例
            body: GridView.custom(
              padding: const EdgeInsets.all(8),
              gridDelegate: SliverStairedGridDelegate(
                crossAxisSpacing: 16, // 横方向のずれを作る間隔
                mainAxisSpacing: 16,
                startCrossAxisDirectionReversed: true, // 階段の向きを反転
                pattern: const [
                  StairedGridTile(0.5, 1.0),
                  StairedGridTile(0.5, 0.75),
                  StairedGridTile(1.0, 2.0),
                ],
              ),
              childrenDelegate: SliverChildBuilderDelegate(
                (context, index) => Container(
                  color: Colors.red[(index % 8 + 1) * 100],
                  child: Center(child: Text('Item $index')),
                ),
                childCount: 20,
              ),
            ),
          );
        }
      }
      
  • Aligned

    • 「同じ行の中では一番背の高いカードに合わせて高さを揃える」CSS Grid 風レイアウト

    • サンプル
      image6.png

      ソースコード
      import 'dart:math';
      
      import 'package:flutter/material.dart';
      import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
      
      class AlignedLayoutScreen extends StatelessWidget {
        const AlignedLayoutScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          final rnd = Random();
          return Scaffold(
            appBar: AppBar(title: const Text('Aligned Layout')),
            // Aligned: 行ごとに一番高いアイテムに合わせて高さをそろえる例
            body: AlignedGridView.count(
              crossAxisCount: 3,
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
              itemCount: 20,
              itemBuilder: (context, index) {
                final lines = 1 + rnd.nextInt(3); // 行数を変えて高さに差をつける
                return Container(
                  padding: const EdgeInsets.all(8),
                  color: Colors.teal[(index % 8 + 1) * 100],
                  child: Text('Item $index\n' * lines),
                );
              },
            ),
          );
        }
      }
      

pool

https://pub.dev/packages/pool

poolパッケージは、限られたリソースのプールを簡単に管理するためのPoolクラスを提供します。
執筆時のバージョン: 1.5.2

各ユーティリティ

  • Poolクラス
    • Pool(this._maxAllocatedResources, {Duration? timeout})
      • _maxAllocatedResources: 一度に割り当てることができるリソースの最大数
      • timeout : 設定されている場合、指定された時間が経過しても何も操作が行われなければ、保留中のすべての future は TimeoutException をスローする
    • 使用例
      • final pool = new Pool(10, timeout: new Duration(seconds: 30));
      • 一度に10個のリソースしか割り当てないプールを作成、全てのリソースがチェックアウトされた状態で30秒間操作がない場合エラーを発生させる
  • withResource
    • コールバックの前にPoolのリソースを確保し、終了後に自動で release してくれる

      Future<void> demoWithResource() async {
        final pool = Pool(2); // 同時に最大 2 つまで
      
        Future<void> heavyTask(int id) async {
          print('  [start ] task $id');
          await Future<void>.delayed(const Duration(seconds: 1));
          print('  [finish] task $id');
        }
      
        // Pool.withResource は「このコールバックの間だけリソースを借りる」という意味
        pool.withResource(() => heavyTask(1));
        pool.withResource(() => heavyTask(2));
        pool.withResource(() => heavyTask(3));
      
        await pool.close();
      }
      /* 実行結果
        [start ] task 1
        [start ] task 2
        [finish] task 1
        [start ] task 3
        [finish] task 2
        [finish] task 3
      */
      

shelf

https://pub.dev/packages/shelf

Shelfは、Webサーバーやその一部を簡単に作成・構成できるようにするツールです。
執筆時のバージョン: 1.4.2

Dart の HTTP サーバを「Handler」と「Middleware」で組み立てるための軽量フレームワークで、 Request -> Future<Response> 関数を積み重ねていくだけなので、挙動が理解しやすいです。

  • Echo Serverのサンプル

    import 'package:shelf/shelf.dart';
    import 'package:shelf/shelf_io.dart' as shelf_io;
    
    void main() async {
      var handler =
          const Pipeline().addMiddleware(logRequests()).addHandler(_echoRequest);
    
      var server = await shelf_io.serve(handler, 'localhost', 8080);
    
      // Enable content compression
      server.autoCompress = true;
    
      print('Serving at http://${server.address.host}:${server.port}');
    }
    
    Response _echoRequest(Request request) =>
        Response.ok('Request for "${request.headers}"');
    

    上記を実行してアクセスすると以下の様になります。

    $ curl http://localhost:8080
    Request for "{user-agent: curl/8.7.1, accept: */*, host: localhost:8080}"
    
  • Routing + JSON + POST Bodyサンプル

    import 'dart:convert';
    
    import 'package:shelf/shelf.dart';
    import 'package:shelf/shelf_io.dart' as shelf_io;
    
    void main() async {
      var handler = const Pipeline()
          .addMiddleware(logRequests())
          .addHandler(_router);
    
      var server = await shelf_io.serve(handler, 'localhost', 8080);
    
      // Enable content compression
      server.autoCompress = true;
    
      print('Serving at http://${server.address.host}:${server.port}');
    }
    
    Future<Response> _router(Request request) async {
      final path = request.url.path;
      final method = request.method;
    
      // GET /
      if (path.isEmpty && method == 'GET') {
        return Response.ok('Hello Shelf!');
      }
    
      // GET /json
      if (path == 'json' && method == 'GET') {
        final body = jsonEncode({
          'message': 'Hello JSON',
          'time': DateTime.now().toIso8601String(),
        });
        return Response.ok(
          body,
          headers: {'content-type': 'application/json; charset=utf-8'},
        );
      }
    
      // POST /echo (ボディをそのまま返す)
      if (path == 'echo' && method == 'POST') {
        final body = await request.readAsString();
        return Response.ok('you posted: $body');
      }
    
      // その他は 404
      return Response.notFound('Not Found');
    }
    

    実際のアクセス結果がこちら

    $ curl http://localhost:8080
    Hello Shelf!
    $ curl http://localhost:8080/json
    {"message":"Hello JSON","time":"2025-12-15T21:11:06.678950"}
    $ curl -X POST -d "Hello from curl" localhost:8080/echo
    you posted: Hello from curl
    

stack_trace

https://pub.dev/packages/stack_trace

このライブラリは、Dart実装によって生成されたスタックトレースの解析、検査、操作を可能にします。また、ネイティブのStackTrace実装よりも読みやすい形式でスタックトレースの文字列表現を生成する関数も提供します
執筆時のバージョン: 1.12.1

サンプルのプロジェクトを作成して試してみます。

dart create stack_trace_example
  • 未使用時のStackTrace

    • ソースコード (bin/stack_trace_example.dart)

      import 'dart:async';
      
      void main() {
        _scheduleAsync();
      }
      
      void _scheduleAsync() {
        Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
      }
      
      void _runAsync() {
        throw 'oh no!';
      }
      
    • 出力

      $ dart run
      Building package executable...
      Built stack_trace_example:stack_trace_example.
      Unhandled exception:
      oh no!
      #0      _runAsync (file:///Users/xxxxx/stack_trace_example/bin/stack_trace_example.dart:12:3)
      #1      _scheduleAsync.<anonymous closure> (file:///Users/xxxxx/stack_trace_example/bin/stack_trace_example.dart:8:52)
      #2      Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:948:45)
      #3      Future._propagateToListeners (dart:async/future_impl.dart:977:13)
      #4      Future._complete (dart:async/future_impl.dart:711:7)
      #5      new Future.delayed.<anonymous closure> (dart:async/future.dart:436:16)
      #6      Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
      #7      _Timer._runTimers (dart:isolate-patch/timer_impl.dart:423:19)
      #8      _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:454:5)
      #9      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)
      
  • 使用した場合

    • pubspec.yaml

      dependencies:
        stack_trace: ^1.12.1
      
    • ソースコード

      import 'dart:async';
      
      import 'package:stack_trace/stack_trace.dart';
      
      void main() {
        Chain.capture(_scheduleAsync);
      }
      
      void _scheduleAsync() {
        Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
      }
      
      void _runAsync() {
        throw 'oh no!';
      }
      
    • 出力

      $ dart run
      Building package executable...
      Built stack_trace_example:stack_trace_example.
      Unhandled exception:
      oh no!
      bin/stack_trace_example.dart 14:3                             _runAsync
      bin/stack_trace_example.dart 10:52                            _scheduleAsync.<fn>
      package:stack_trace/src/stack_zone_specification.dart 127:36  StackZoneSpecification._registerUnaryCallback.<fn>.<fn>
      package:stack_trace/src/stack_zone_specification.dart 207:15  StackZoneSpecification._run
      package:stack_trace/src/stack_zone_specification.dart 127:24  StackZoneSpecification._registerUnaryCallback.<fn>
      dart:async/zone.dart 1538:47                                  _rootRunUnary
      dart:async/zone.dart 1429:19                                  _CustomZone.runUnary
      dart:async/future_impl.dart 948:45                            Future._propagateToListeners.handleValueCallback
      ...
      

      通常のStackTraceより詳細な情報が追加され、見やすくなっていますが若干長いので Chain#terse を使うと簡潔にしてくれます。

      # 使い方
      #void main() {
      #  Chain.capture(_scheduleAsync, onError: (error, chain) => print(chain.terse));
      #}
      
      $ dart run
      Building package executable...
      Built stack_trace_example:stack_trace_example.
      bin/stack_trace_example.dart 14:3   _runAsync
      bin/stack_trace_example.dart 10:52  _scheduleAsync.<fn>
      ===== asynchronous gap ===========================
      dart:async                          Future.then
      bin/stack_trace_example.dart 10:40  _scheduleAsync
      package:stack_trace                 Chain.capture
      bin/stack_trace_example.dart 6:9    main
      

retry

https://pub.dev/packages/retry

このパッケージは、非同期関数のリトライを簡単に行う方法を提供します。これは、接続の切断や一時的なサーバの過負荷など、断続的に発生するエラーによるクラッシュを回避するのに役立ちます。
執筆時のバージョン: 3.1.2

「Exponential Backoff」(再試行(リトライ)までの待機時間を「1秒、2秒、4秒、8秒…」のように指数関数的に増やしていく)を伴うリトライ処理はシンプルに以下の様に書けます。

Future<void> demoSimpleRetry() async {
  final random = Random();
  var callCount = 0;
  final result = await retry<String>(
    () async {
      callCount++;
      print('  try #$callCount');
      await Future<void>.delayed(const Duration(milliseconds: 200));
      if (random.nextBool()) {
        throw TimeoutException('temporary timeout');
      }
      return 'success!';
    },
    retryIf: (e) => e is TimeoutException, // TimeoutException のときにリトライする
  );

  print('  result = $result\n');
}
/* 実行結果
  try #1
  try #2
  result = success!
*/

ちなみに待機時間が大体どれくらい増えるか確認するには以下で確認ができます。

  const r = RetryOptions(); // デフォルト設定
  for (var attempt = 1; attempt <= r.maxAttempts - 1; attempt++) {
    final delay = r.delay(attempt);
    print('  attempt #$attempt -> delay = $delay');
  }
  /* 実行結果
    attempt #1 -> delay = 0:00:00.362201
    attempt #2 -> delay = 0:00:00.828308
    attempt #3 -> delay = 0:00:01.402243
    attempt #4 -> delay = 0:00:03.631579
    attempt #5 -> delay = 0:00:06.643290
    attempt #6 -> delay = 0:00:14.554009
    attempt #7 -> delay = 0:00:27.117237
  */

file_testing

https://pub.dev/packages/file_testing

package:file と連携するためのテストユーティリティ。
執筆時のバージョン: 3.0.2

使えるユーティリティ

  • exists
    • FileSystemEntity.existsSync() が true になるものをマッチ
  • isFile / isDirectory / isLink / isFileSystemEntity / isFileStat
    • それぞれ File, Directory, Link, FileSystemEntity, FileStat 型かどうかをマッチ
  • hasPath(path)
    • entity.path == path になるかをマッチする Matcher を返す
  • isFileSystemException([osErrorCode])
    • FileSystemException であること(必要なら OS エラーコードも)をチェックする Matcher
  • throwsFileSystemException([osErrorCode])
    • 「関数 or Future が FileSystemException を投げる」ことを検証する Matcher
  • expectFileSystemException(osErrorCode, callback)
    • callback が指定された OS エラーコードを持つ FileSystemException を投げることを expect 相当でチェックするヘルパー

実際にテストを書いて試してみます。 pubspec.yaml は以下の様に設定してまうす。

dependencies:
  file: ^7.0.1

dev_dependencies:
  test: ^1.25.6
  file_testing: ^3.0.2

package:file の MemoryFileSystem を使って実ファイルを作らずにテストを書いてみました。

  • 実際のテスト

    import 'package:file/file.dart';
    import 'package:file/memory.dart';
    import 'package:file_testing/file_testing.dart';
    import 'package:test/test.dart';
    
    void main() {
      late MemoryFileSystem fs;
    
      setUp(() {
        fs = MemoryFileSystem();
        fs.file('/foo.txt').writeAsStringSync('hello');
        fs.directory('/dir').createSync();
        fs.link('/link-to-foo').createSync('/foo.txt');
      });
    
      group('基本の型系 Matcher', () {
        test('exists / isFile / isDirectory / isLink / isFileSystemEntity', () {
          final file = fs.file('/foo.txt');
          final dir = fs.directory('/dir');
          final link = fs.link('/link-to-foo');
    
          // /foo.txt が存在するファイルである
          expect(file, exists);
          expect(file, isFile);
          expect(file, isFileSystemEntity);
    
          // /dir が存在するディレクトリである
          expect(dir, exists);
          expect(dir, isDirectory);
          expect(dir, isFileSystemEntity);
    
          // /link-to-foo がシンボリックリンク
          expect(link, exists);
          expect(link, isLink);
          expect(link, isFileSystemEntity);
        });
    
        test('isFileStat', () {
          final stat = fs.file('/foo.txt').statSync();
          expect(stat, isFileStat);
        });
    
        test('hasPath', () {
          final file = fs.file('/foo.txt');
          final dir = fs.directory('/dir');
    
          // パスが /foo.txt である File
          expect(file, hasPath('/foo.txt'));
    
          // パスが /dir である Directory
          expect(dir, hasPath('/dir'));
        });
      });
    
      group('FileSystemException 系', () {
        /// 必ず FileSystemException を投げる処理
        void throwNoSuchFile() {
          fs.file('/no/such/file').readAsStringSync();
        }
    
        test('isFileSystemException', () {
          try {
            throwNoSuchFile();
            fail('例外が発生するはず');
          } on FileSystemException catch (e) {
            // FileSystemException であること
            expect(e, isFileSystemException());
          }
        });
    
        test('throwsFileSystemException (関数)', () {
          // コールバックが FileSystemException を投げること
          expect(() => throwNoSuchFile(), throwsFileSystemException());
        });
    
        test('throwsFileSystemException (Future)', () async {
          Future<void> asyncThrow() async {
            throwNoSuchFile();
          }
    
          // Future が FileSystemException で失敗すること
          expect(asyncThrow(), throwsFileSystemException());
        });
    
        test('expectFileSystemException + ErrorCodes.ENOENT', () {
          // こちらは元のままで OK(実際に FileSystemException が投げられるパターン)
          expectFileSystemException(ErrorCodes.ENOENT, () {
            fs.directory('/no/such/dir').resolveSymbolicLinksSync();
          });
        });
      });
    }
    
    

quiver

https://pub.dev/packages/quiver

多くのDartライブラリをより簡単に、より便利に使えるようにしたり、追加機能を提供したりする、Dart用の便利なユーティリティライブラリ集
執筆時のバージョン: 3.2.2

以下主なユーティリティになります。

quiver.async

Future / Stream / タイマーなど、非同期処理を扱うためのユーティリティ

  • 一定間隔で残り時間を流してくれるCountdownTimerのサンプル
import 'package:quiver/async.dart';

void main() {
  final timer = CountdownTimer(
    const Duration(seconds: 3), // 合計時間
    const Duration(seconds: 1), // tick 間隔
  );

  timer.listen(
    (event) {
      print('残り: ${event.remaining.inMilliseconds} ミリ秒');
    },
    onDone: () {
      print('終了!');
    },
  );
}
/* 実行結果
残り: 1992 ミリ秒 ※ 少しの誤差あり
残り: 998 ミリ秒
残り: -1 ミリ秒
終了!
*/

quiver.cache

非同期アクセス前提のキー→値キャッシュ(Cache / MapCache)

  • 非同期 API で get/set する Map ベースのキャッシュサンプル
import 'package:quiver/cache.dart';

Future<void> main() async {
  final cache = MapCache<String, int>();

  await cache.set('user:1', 42);
  final value = await cache.get('user:1');

  print(value); // 42

  // LRU キャッシュにしたい場合
  final lruCache = MapCache<String, int>.lru(maximumSize: 2);
  await lruCache.set('a', 1);
  await lruCache.set('b', 2);
  await lruCache.get('a'); // a を最近アクセスに
  await lruCache.set('c', 3); // 古い b が追い出される
  print(await lruCache.get('b')); // null
}
/* 実行結果
42
null
*/

quiver.check

checkArgument / checkState など、前提条件・状態チェック用ヘルパー

  • 0除算を防いだり・状態チェックをするサンプル
import 'package:quiver/check.dart';

int divide(int a, int b) {
  checkArgument(b != 0, message: 'b must not be 0');
  return a ~/ b;
}

void changeState(bool ready) {
  checkState(ready, message: 'Not ready yet');
}

void main() {
  divide(10, 0); // ArgumentError が投げられる
  changeState(false); // StateError が投げられる
}
/* 実行結果
Unhandled exception:
Invalid argument(s): b must not be 0

Unhandled exception:
Bad state: Not ready yet
*/

quiver.collection

Multimap / BiMap / LruMap など、コレクション系の追加データ構造

  • Multimapのサンプル
import 'package:quiver/collection.dart';

void main() {
  final tags = Multimap<String, String>();

  tags.add('fruit', 'apple');
  tags.add('fruit', 'banana');
  tags.add('drink', 'water');

  print(tags['fruit']);
  print(tags['drink']);
}
/* 実行結果
[apple, banana]
[water]
*/

quiver.core

Optional や hashObjects など、汎用的な小さなコアユーティリティ

  • 自前クラスの hashCode 実装を安全に書くためのヘルパーのサンプル
import 'package:quiver/core.dart';

class Point {
  final int x;
  final int y;

  Point(this.x, this.y);

  @override
  bool operator ==(Object other) =>
      other is Point && other.x == x && other.y == y;

  @override
  int get hashCode => hash2(x, y); // or hashObjects
}

void main() {
  final a = Point(1, 2);
  final b = Point(1, 2);

  print(a.hashCode);
  print(b.hashCode);
  print(a == b);
}
/* 実行結果
472247166
472247166
true
*/

quiver.iterables

Iterable を連結・分割・生成するためのヘルパー(partition / merge など)

  • range のサンプル
import 'package:quiver/iterables.dart';

void main() {
  final nums = range(1, 10); // 1〜9
  for (final n in nums) {
    print(n);
  }
  final down = range(10, 0, -2); // step を -2 に
  print(down.toList());
}
/* 実行結果
1
2
3
4
5
6
7
8
9
[10, 8, 6, 4, 2]
*/

quiver.pattern

Pattern / RegExp / Glob を扱うためのユーティリティ

  • 正規表現とファイルパスの簡単なマッチサンプル
import 'package:quiver/pattern.dart';

void main() {
  // 正規表現
  print(matchesFull(RegExp(r'\d+'), '123'));
  print(matchesFull(RegExp(r'\d+'), '123a'));
  // ファイルパスの簡単なマッチ
  final glob = Glob('lib/**/*.dart');
  print(glob.hasMatch('lib/src/main.dart'));
  print(glob.hasMatch('test/main_test.dart'));
}
/* 実行結果
true
false
true
false
*/

quiver.strings

文字列の判定・整形・比較用ユーティリティ(isBlank / center など)

  • isBlank / equalsIgnoreCase / center サンプル
import 'package:quiver/strings.dart';

void main() {
  print(isBlank('   '));
  print(isBlank('hello'));
  print(equalsIgnoreCase('Abc', 'aBC'));
  print(center('hi', 6, '*'));
}
/* 実行結果
true
false
true
**hi**
*/

quiver.time

Clock や aSecond など、時間・期間を扱いやすくするユーティリティ

  • Clock を使ったサンプル
import 'package:quiver/time.dart';

void main() {
  final clock = Clock();
  final now = clock.now();
  final tomorrow = now.add(aDay); // 1日後
  final in30Seconds = now.add(aSecond * 30);
  print('now:       $now');
  print('tomorrow:  $tomorrow');
  print('in 30 sec: $in30Seconds');
}
/* 実行結果
now:       2025-12-17 07:58:08.483232
tomorrow:  2025-12-18 07:58:08.483232
in 30 sec: 2025-12-17 07:58:38.483232
*/

まとめ

今回メジャーなパッケージ以外をピックアップしたら、Dartの便利系パッケージが多くなりましたが、どれも実用的で、プロジェクトに組み込みやすいものばかりですので、活用できそうだなと思いました。
手間かかりますが、こうやって一つ一つ試してみると、パッケージの理解が深まりますし、実際に使うイメージも湧きやすくなりますのでオススメです。

Discussion