Flutterパッケージ自作とpub.dev公開:依存関係地獄を回避する設計とCI/CD構築

開発チームの規模が拡大し、複数のFlutterアプリ(例えば、ユーザー向けアプリと管理画面用アプリ)を並行して運用し始めると、必ず直面する問題があります。それは「共通コードのコピペ管理」による技術的負債です。先日、私が担当するプロジェクトで、認証ロジックを修正した際にユーザー向けアプリには反映しましたが、管理アプリへの反映を忘れ、本番環境で整合性が取れなくなるという致命的なミスが発生しました。APIクライアントや共通UIコンポーネントをプロジェクト間でコピー&ペーストで使い回す運用は、初期段階では楽ですが、長期的には「依存関係地獄」への入り口となります。本記事では、この問題を根本から解決するために、共通機能をFlutterパッケージとして切り出し、pub.dev(あるいはプライベートリポジトリ)で適切に管理・公開するためのエンジニアリング手法を共有します。

Flutterパッケージ化の必要性と環境要件

今回の検証および実装は、以下の環境で行っています。特にFlutter 3系以降、Dart 3の導入により、パッケージの記述方法や制約(例えば、null safetyの強制やclass modifiersなど)が厳格化されているため、最新のドキュメントに基づいた設計が必要です。

  • OS: macOS Sonoma 14.4 (Apple Silicon) / Windows 11 Pro
  • Flutter SDK: 3.19.0 (Stable Channel)
  • Dart SDK: 3.3.0
  • IDE: VS Code (with Flutter/Dart Extensions)
Context: パッケージ化の主な目的は「再利用性」ですが、同時に「責任分界点の明確化」も重要です。UIパーツとビジネスロジックを密結合させたままパッケージ化しようとすると、呼び出し側の依存が増え、かえって使いにくいライブラリになります。

通常、モノリシックなプロジェクト構成では、lib/utils/lib/widgets/ に汎用コードを配置します。しかし、これらが特定のドメインロジック(例えば、特定のAPIレスポンス型や状態管理ライブラリ)に依存し始めると、他のプロジェクトで再利用する際に不要な依存関係まで引き込むことになります。これを防ぐには、依存関係を極小化した「Pure Dartパッケージ」または「Flutter Plugin」として設計する必要があります。

失敗談:Git Submoduleとローカルパス参照の罠

正式なパッケージ構成に移行する前、私たちは手軽さを優先して「Git Submodule」とpubspec.yamlpath参照を使用しようと試みました。

# 失敗した構成例
dependencies:
  my_shared_lib:
    path: ../shared_libs/my_shared_lib # ローカルパス参照

このアプローチは、開発者のローカルマシン上ではうまく機能しました。しかし、CI/CDパイプライン(GitHub Actions)上でビルドを実行した際、即座に失敗しました。CIランナー上ではディレクトリ構造がローカル環境と異なるため、相対パス ../shared_libs/ が見つからなかったのです。また、Git Submoduleは更新のたびにコミットハッシュを親リポジトリ側で更新する必要があり、チーム開発において「誰かがサブモジュールの更新を忘れてビルドが通らない」という事象が頻発しました。これらは、パッケージ管理システム(Pub)が本来解決してくれるバージョニングの恩恵を捨てている行為でした。

解決策:正規のパッケージ構成とExports戦略

正解は、Flutter公式が推奨するパッケージテンプレートを使用し、明確なバージョニングを行うことです。さらに、外部に公開するAPIと内部実装を厳密に分ける「バレルファイル(Barrel File)」パターンを採用します。

まず、以下のコマンドでパッケージの雛形を作成します。

# Flutterパッケージの作成
flutter create --template=package my_awesome_kit

次に、パッケージの設計において最も重要な「公開APIの制御」を行います。Dartでは、lib/src/ 配下のファイルは慣習的にプライベート扱いとなりますが、言語仕様として外部から隠蔽されるわけではありません。しかし、トップレベルのライブラリファイルで明示的に export することで、利用者がインポートすべきファイルを一つに絞ることができます。

// lib/my_awesome_kit.dart
library my_awesome_kit;

// 内部実装は src フォルダに隠蔽し、必要なものだけを公開する
// これにより、将来的なリファクタリングで内部構造を変えても
// 利用側のコード(import文)に影響を与えない
export 'src/calculator.dart' show AdvancedCalculator;
export 'src/models/result.dart';

// utilsなどは公開しない(内部利用のみとする)
// export 'src/utils/logger.dart'; 

上記のコードでは、show キーワードを使って、AdvancedCalculator クラスのみを公開しています。これにより、パッケージ利用者が誤って内部クラスを使用することを防ぎ、APIの表面積を最小限に保つことができます。これはライブラリの保守性を高めるためのベストプラクティスです。

pubspec.yamlの最適化と依存解決

パッケージを pub.dev に公開する場合、pubspec.yaml のメタデータは検索順位や信頼性スコア(Pub Points)に直結します。必須項目を埋めるだけでなく、バージョニング戦略を明確にする必要があります。

name: my_awesome_kit
description: A high-performance calculation library for financial apps.
version: 1.0.0 # Semantic Versioning (Major.Minor.Patch)
homepage: https://github.com/username/my_awesome_kit
repository: https://github.com/username/my_awesome_kit # 必須:ソースコードへのリンク

environment:
  sdk: '>=3.0.0 <4.0.0'
  flutter: '>=3.3.0'

dependencies:
  flutter:
    sdk: flutter
  # 依存ライブラリは最小限に留める
  # バージョン指定はキャレット(^)を使い、互換性を維持する
  intl: ^0.19.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

特に environment のSDKバージョン指定は重要です。ここで古いバージョンをサポートしすぎると、最新のDart機能(RecordsやPatternsなど)が使えなくなります。逆に新しすぎると、既存のアプリに導入できなくなります。ターゲットとする層に合わせて慎重に設定してください。

手法導入容易性保守コストチーム開発適性CI/CD安定性
コピペ運用極大(バグ温床)不適
Git Submodule高(ハッシュ管理)低(運用ミス多発)中(要設定)
Pubパッケージ低(バージョン管理)最適

表からも分かる通り、初期設定のコストを支払ってでもPubパッケージ化を行うことで、長期的な保守コストとCI/CDの安定性が劇的に改善します。特に「バージョン固定」ができる点は、本番環境の安定稼働において必須条件です。

Check Official Documentation

公開前のチェックとコマンド操作

パッケージが完成したら、公開の準備です。しかし、いきなり publish コマンドを叩くのは危険です。以下の手順で厳格なチェックを行います。

Performance Warning: パッケージに不要なアセット(大きな画像やテスト用のダミーデータ)を含めないでください。パッケージサイズが肥大化すると、利用者のアプリサイズに悪影響を与えます。.pubignore ファイルを作成し、不要なファイルを除外設定してください。
  1. 静的解析の通過: flutter analyze を実行し、Warningをゼロにします。
  2. ドライラン: 実際にアップロードせずに検証を行います。
    dart pub publish --dry-run
  3. LICENSEファイルの確認: オープンソースライセンス(MITやApache 2.0など)が含まれていないと、Pub Pointsが大幅に下がります。
  4. CHANGELOG.mdの更新: バージョンごとの変更点を記述します。
# 最終的な公開コマンド
# 初回はブラウザ認証が求められます
dart pub publish

一度公開したバージョン(例: 1.0.0)は、二度と上書きできません。修正が必要な場合は、必ずバージョン番号を上げて(例: 1.0.1)再公開する必要があります。これがパッケージ管理における「不変性(Immutability)」の原則であり、依存関係の地獄を防ぐための最も強力な機能です。

エッジケース:Federated PluginとPlatform Interface

もし作成するパッケージが、iOSやAndroidのネイティブ機能(カメラ、GPSなど)に依存する場合、単なるDartパッケージではなく「Plugin」として開発する必要があります。さらに、最近のFlutterコミュニティでは「Federated Plugins」という構成が推奨されています。

これは、my_plugin_platform_interface(共通インターフェース)、my_plugin_android(Android実装)、my_plugin_ios(iOS実装)、my_plugin(利用者が使うファサード)のように、パッケージをプラットフォームごとに分割する手法です。これにより、LinuxやWindows対応を追加する際、既存のパッケージを壊さずに拡張可能になります。単純なロジック共有であればここまでする必要はありませんが、ハードウェア機能に触れる場合は、この設計を検討しないと後々リファクタリングが困難になります。

Best Practice: プライベートな社内利用のみで公開したくない場合は、独自のPubサーバーを立てるか、Gitリポジトリのタグ指定で依存解決を行う方法があります。pubspec.yamlgit: url: ... ref: v1.0.0 と指定することで、擬似的にバージョン管理されたプライベートパッケージとして運用可能です。

結論

Flutterパッケージの自作と公開は、単なるコードの整理整頓以上の価値があります。それは、チーム内でのコードの責任範囲を明確にし、API設計のスキルを向上させ、バージョン管理による堅牢な開発プロセスを強制するからです。最初は小さなUtility関数の集合体から始めてみてください。それがやがて、プロジェクト全体の生産性を支える重要な資産となるはずです。

Post a Comment