KINTO Tech Blog
Flutter

Flutterのマルチパッケージの中でローカルのJSONを読み込みたい!

Cover Image for Flutterのマルチパッケージの中でローカルのJSONを読み込みたい!

はじめに

Flutterのマルチパッケージプロジェクトでは、アセット管理、特にローカルJSONファイルの読み込みが課題となることがあります。
通常のシングルパッケージプロジェクトとは異なるアプローチが必要となり、開発者を悩ませることがあります。
この記事では、Flutterのマルチパッケージプロジェクトにおいて、ローカルのJSONファイルを効果的に読み込む方法について詳しく解説します。

この記事は KINTOテクノロジーズアドベントカレンダー2024 の23日目の記事です🎅🎄

今回用意したテストプロジェクト

今回、調査のため、マルチパッケージで管理する簡潔なプロジェクトを用意しました。
このプロジェクトは、以下のような構成になっています。

🎯 Dart SDK: 3.5.4
🪽 Flutter SDK: 3.24.5
🧰 melos: 6.2.0
├── .github
├── .vscode
├── app/
│   ├── android/
│   ├── ios/
│   ├── lib/
│   │   └── main.dart
│   └── pubspec.yaml
├── packages/
│   ├── features/
│   │   ├── assets/
│   │   │   └── sample.json
│   │   ├── lib/
│   │   │   └── package1.dart
│   │   └── pubspec.yaml
│   ├── .../
├── analysis_options.yaml
├── melos.yaml
├── pubspec.yaml
└── README.md

Assetにあるファイルを読み込む

一般的なシングルパッケージでのAssetの読み込みは、以下のようにすればできるという説明はよく見かけます。

pubspec.yaml
flutter:
  assets:
    - assets/ # アセットフォルダを指定
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('assets/sample.json');
}

公式も同じ様な説明をしています。
https://docs.flutter.dev/ui/assets/assets-and-images

実際にAssetからJSONファイルを読み込んでTextにString表示するだけのWidgetを作ってみました。

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

class LocalAssetPage extends StatefulWidget {
  const LocalAssetPage({super.key});

  
  LocalAssetPageState createState() => LocalAssetPageState();
}

class LocalAssetPageState extends State<LocalAssetPage> {
  String _jsonContent = '';

  
  void initState() {
    super.initState();
    _loadJson();
  }

  Future<void> _loadJson() async {
    final response = await rootBundle.loadString('assets/sample.json');
    final data = await json.decode(response);
    setState(() {
      _jsonContent = json.encode(data);
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Local Asset Page'),
      ),
      body: Center(
        child: _jsonContent.isEmpty
            ? const CircularProgressIndicator()
            : Text(_jsonContent),
      ),
    );
  }
}

しかし、マルチパッケージでのAssetの読み込みは、この方法ではうまくいきません。

Asset load error!





🤨



flutter_genを使ってAssetを読み込む

大抵の場合、マルチパッケージでのAssetの読み込みは、flutter_genを使って解決できます。
flutter_genは、AssetやLocalization等のパスからコード生成をして、タイプセーフにアセットの読み込みが実現できるツールで、マルチパッケージのAssetの読み込みもサポートしています。

https://github.com/FlutterGen/flutter_gen

flutter_genでマルチパッケージのAssetを読み込むには、以下の設定が必要になります。

pubspec.yaml
flutter_gen:
  assets:
      outputs:
        package_parameter_enabled: true

この設定を入れて、flutter_genを実行すると、マルチパッケージのAssetを読み込むためのコードが生成されます。

flutter_gen generated codes

そこで生成されたコードを使ってタイプセーフにAssetを読み込むことができます。
上記の例をflutter_genを使って書き換えると、以下のようになります。

import 'package:{YOUR_PACKAGE_NAME}/gen/assets.gen.dart';

Future<String> loadAsset() async {
  return await rootBundle.loadString(Assets.sample);
}

実際に以下の様なコードを書くことで、マルチパッケージのAssetを読み込むことができます。

import 'dart:convert';

+ import 'package:feature_flutter_gen_sample/gen/assets.gen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

class FlutterGenSamplePage extends StatefulWidget {
  const FlutterGenSamplePage({super.key});

  
  FlutterGenSamplePageState createState() => FlutterGenSamplePageState();
}

class FlutterGenSamplePageState extends State<FlutterGenSamplePage> {
  String _jsonContent = '';

  
  void initState() {
    super.initState();
    _loadJson();
  }

  Future<void> _loadJson() async {
+   final response = await rootBundle.loadString(Assets.sample);
-   final response = await rootBundle.loadString('assets/sample.json');
    final data = await json.decode(response);
    setState(() {
      _jsonContent = data.toString();
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FlutterGen Sample'),
      ),
      body: Center(
        child: _jsonContent.isNotEmpty
            ? Text(_jsonContent)
            : const CircularProgressIndicator(),
      ),
    );
  }
}

ファイルのパスが構造化されるので、とてもキレイですね!
チーム開発のAsset管理はこれで安心です。

ですが、なるべくツールに頼らない方法を取りたい場合も多々ありますよね?
次はその方法についてもお話します。

flutter_genを使わないでAssetを読み込む

flutter_genを使わずにマルチパッケージのAssetを読み込む方法ももちろんあります。
その場合は、以下のルールでパスを指定することでAssetを読み込むことができます。

  • パッケージ名は、そのassetが格納されているPackageのpubspec.yamlのnameに指定した名前になります。
  • フォルダパスは、そのPackageのpubspec.yamlのassetsに指定したパスになります。
pubspec.yaml
name: local_asset

...

flutter:
  assets:
    - assets/
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

class LocalAssetPage extends StatefulWidget {
  const LocalAssetPage({super.key});

  
  LocalAssetPageState createState() => LocalAssetPageState();
}

class LocalAssetPageState extends State<LocalAssetPage> {
  String _jsonContent = '';

  
  void initState() {
    super.initState();
    _loadJson();
  }

  Future<void> _loadJson() async {
+   final response = await rootBundle.loadString('packages/local_asset/assets/sample.json');
-   final response = await rootBundle.loadString('assets/sample.json');
    final data = await json.decode(response);
    setState(() {
      _jsonContent = json.encode(data);
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Local Asset Page'),
      ),
      body: Center(
        child: _jsonContent.isEmpty
            ? const CircularProgressIndicator()
            : Text(_jsonContent),
      ),
    );
  }
}

このようにして、flutter_genを使わずにマルチパッケージのAssetを読み込むことができます。
小規模プロジェクトや、個人開発ではこの方法でも十分に対応できそうです。

このパスの作成ルールが理解できておらず、ファイルの相対パスを指定したり、パスの設定を色々工夫してみたりしましたが、
そもそもエラーでビルドできなかったりととても苦戦しました...

pubspec.yamlで指定するパスについて

ローカルでアセットを管理する時に、pubspec.yamlで指定するパスについても注意が必要です。
assetsのパスは、pubspec.yamlからの相対パスで指定しますが、

  • /assets
  • /assets/{サブフォルダ}
    の扱いにも注意が必要です。

以下の様にJSONファイルをサブフォルダに移動した場合、

├── packages/
│   ├── features/
│   │   ├── assets/
│   │   │   └── jsons/
│   │   │       └── sample.json  <<< HERE
│   │   ├── lib/
│   │   │   └── package1.dart
│   │   └── pubspec.yaml
│   ├── .../

僕はてっきり /assets と指定すれば、/assets/{サブフォルダ} 以下のファイルも読み込めると思っていましたが、
loadStringのパスを packages/local_asset/assets/jsons/sample.json に変更しても読み込めなくなります。
サブフォルダに移動した場合は、以下の様にサブフォルダを明示的に指定する必要があります。

pubspec.yaml
flutter:
  assets:
    - /assets/jsons/

これで、packages/local_asset/assets/jsons/sample.json をロードできる様になりました。
サブフォルダで細かくアセットを管理するのであれば、サブフォルダの指定もpubspec.yamlに追加するのを忘れない様にしないといけません。

ちなみに、ここまで assets フォルダで話をしていましたが、このフォルダ名も変更できます。
pubspec.yamlと実際のパス構成が合っていれば、 assets フォルダ以外でも問題なく読み込めます。

まとめ

今回はFlutterのマルチパッケージの中でローカルのJSONを読み込む方法についてお話しました。
普段はiOSの開発がメインなので、簡単にできるだろうと思っていたのですが、意外と苦労しました。
マルチパッケージ下での開発手法はまだあまり情報がない様なので、今後もこういった情報があれば共有していきたいと思います。

Facebook

関連記事 | Related Posts

We are hiring!

【PdM】my route開発G/東京

my route開発グループについてmy route開発グループは、my routeに関わる開発・運用に取り組んでいます。my routeの概要 my routeは、移動需要を創出するために「魅力ある地域情報の発信」、「最適な移動手段の提案」、「交通機関や施設利用のスムーズな予約・決済」をワンストップで提供する、スマートフォン向けマルチモーダルモビリティサービスです。

プロダクトデザイナー/my route開発G/東京

my route開発グループについてmy route開発グループは、my routeに関わる開発・運用に取り組んでいます。my routeの概要 my routeは、移動需要を創出するために「魅力ある地域情報の発信」、「最適な移動手段の提案」、「交通機関や施設利用のスムーズな予約・決済」をワンストップで提供する、スマートフォン向けマルチモーダルモビリティサービスです。