SODA Engineering Blog
💄

僕のFlutterパッケージがGitHubで100⭐️になって嬉しかった話

2024/12/17に公開

SODA inc.でsnkrdunkを開発している@natsukazeです!

この記事では、自分が公開したFlutterのプラグインパッケージ[1]Galがリリースから1年半ほど経過し、有難いことにpub.devで320likes、1ヶ月で7万DL、GitHubで100starsと順調に成長してきたので、思い出を交えつつ振り返っていきます!

OSSやプラグインならではの話をしていきますので、是非最後までお付き合い下さい!

いいねが多いと、アドベントカレンダーの景品が貰えるので頑張ります 🤫

何を作ったか


https://pub.dev/packages/gal
https://github.com/natsuk4ze/gal
Gal は、OS 標準の写真アプリ[2]に画像/動画を保存するためのプラグインです。
また、非常にシンプルな設計にしているため、誰でも簡単に実装できるのが特徴です。

main.dart
// 1行で写真アプリにファイルを保存できる
Gal.putImage('image.jpg');

Android / iOS / Windows / Mac / Linux(後述)と標準の写真アプリが存在しない web 以外、全てのプラットフォームに対応しています。

名前の由来

Gallery からとって Gal という名前にしました。可愛い名前で気に入っています。また、pub.dev ではパッケージ名をメインのクラス名にするという慣習があります。パッケージ名を短くすることで、パッケージユーザーが書くコード量を減らす狙いもあります👍

あのLocalSend にも Gal が採用されている

https://github.com/localsend/localsend
LocalSendというクロスプラットフォームで使えるAirDrop的なアプリがあるのですが、そのコア機能にも Gal が使われています。(何を隠そう、当時対応するPRを出したのは僕なんですが🤭)

なぜ作ったか

当時開発していたアプリにて、OS標準の写真アプリにメディアファイルを保存する要件があったのですが、既存のプラグインの品質がプロダクションレベルに耐えうるものではなかったので、自分でプラグインを開発することにしました。また、せっかくならとpub.dev に公開して、こつこつメンテナンスしてみることにしました。

プラグイン運営の話

プラグインを運営する上で、やったことを紹介します。
最初のうちは機能を絞り、ミニマルにするという選択がうまくいったように思えます。

目標

プラグインを運営する上で目標を立てておきました。
判断に迷った時、コントリビュータと意見の衝突が起きた時、これらが指針になります。

  • プロダクションで使える品質にすること
  • 長くメンテナンスされるパッケージにすること

戦略

上記の目標を達成するために、幾つかの戦略を考え、実行しています。

プロダクションで使える品質にするために

1. テストを書く

プロダクションアプリで使うプラグインであれば、最低限のテストは書いておきたいところです。
ただし、4プラットフォームでそれぞれネイティブコードのテストを書くのはあまりに大変なので、別のアプローチをとってみることにしました。

Integration Testを使う

ネイティブコードでそれぞれ Unit Test を書くのではなく、Flutter の Integration Testを使用して一括でテストしています。これによって、言語/プラットフォームによってそれぞれ異なるテスト方法を学習するコストを丸々カットすることができました。

Test の自動化

例によって GitHub Actions で自動化しています。また、GitHub Actions を使用した integration test は unit test 等と比べると実行時間が長く、端末の起動も不安定です。実行時間に関しては AndroidのAVDやFlutter
のSDKをキャッシュすることで、30%程削減できました。端末の起動が不安定なことに関してはある程度割り切って、リトライすることにしています。

2. Exceptionの実装

アプリを開発していると、エンドユーザーに対して何のエラーが起きたかを表示したいことがあります。例えば、権限が不足していた場合は、設定画面で許可するように促したいでしょう。
それを簡単に実現する為に、GalException を定義しています。

 try {
   await Gal.putImage('image.jpg');
 } on GalException catch (e) {
   log(e.type.message);
 }

 enum GalExceptionType {
   accessDenied,
   notEnoughSpace,
   notSupportedFormat,
   unexpected;

   String get message => switch (this) {
         accessDenied => 'Permission to access the gallery is denied.',
         notEnoughSpace => 'Not enough space for storage.',
         notSupportedFormat => 'Unsupported file formats.',
         unexpected => 'An unexpected error has occurred.',
       };
 }

3. ドキュメントの充実

特に OS の権限周りは、バージョンによって動作が異なることが多いので、アプリ開発 における躓きポイントの一つです。開発者の負担を軽減するために、GitHub Wikiを使用して情報をまとめて共有しています

長くメンテナンスされるパッケージにするために

1. ネイティブコードのベストプラクティスは追求しない

私はこのプラグインの開発でDart、Swift、Java、C++を書いています。全ての言語でその時々のベストプラクティスに追従しようとすると、手に負えません。よって、ネイティブコードに関していくつか独自のルールを設けた上で、一般的なベストプラクティスを追わない選択をしました。

ファイルを一つだけにする

気軽にPRを提出できるように、ファイルを一つにしています。見慣れない言語でもファイルが一つなら全体像が把握しやすく、理解が容易になります。これは実装がミニマルだからできることでもあります😌

Kotlin ではなく Java を選択する

近年、Android 開発では基本的にKotlin が選択されていますが、読み書きができる人数でいえば、Java に遠く及びません。これもコントリビューションのハードルを下げるための工夫です。

2. メンテナンスを継続するためなら、後方互換性を犠牲にして良い

Packageとして長くメンテナンスされる為に、後方互換性に関しては犠牲にして良いことにしています。特に、古いOSを無理やりサポートしようとすると、分岐を増やしたり、非推奨のAPIを使わざるを得ない場面が存在します。コードの複雑度を下げることで、メンテナンスしやすくしています。

印象的なPR

去年のある日。突然Linuxをサポートに加えるPRが飛んできました。
https://github.com/natsuk4ze/gal/pull/178

Author であるElletflutter-quillという、パッケージのメンテナーでした。そこでメディアファイルの保存機能をクロスプラットフォームでサポートする為に、flutter-quill-extensionsパッケージで、Galを使うようです。

シンプルな設計が幸いし、コード自体は少なかったものの、自分に Linux の知識がない点で、そのまま Linux をサポートに加えることに不安を感じました。

まず、初めに考えたのは、Ellet に Linux に関するコードを任せることです。
本人と会話したところ、「Linux に関して当分コントリビューションしていく」らしいのですが、急に事情が変わるかもしれません。
当時の Gal は今よりさらに小さく、これから沢山の機能を追加していくことも十分考えられる状況でした。そういったタイミングで Linux のサポートがボトルネックになることはどうしても避けたい思いがありました。

よって、結局は自分が Linux に関して勉強する必要があり、それは当時の私のキャパをオーバーしていました。

他の解決策を探してみたところ、ぴったりなものが見つかりました。

それが、Non-endorsed federated pluginです。

Non-endorsed federated plugin を使えるようにする

Non-endorsed federated plugin を理解する為に、まず federated plugin について。詳しい話は公式を参照して頂くとして、ざっくり言えば、単一の platform interface を定義し、プラットフォーム毎に異なるパッケージとして切り出す方式のことで、公式パッケージでも採用されています。

公式のcameraパッケージ


プラットフォーム毎にパッケージとして公開されている
https://pub.dev/packages/camera_android

これの肝は、プラットフォームパッケージが、実装すべき抽象インターフェースを定義し、プラットフォームの実装を保証できる点にあります。

次に本題のNon-endorsed federated pluginですが、先にendorsed federated pluginを考えるとわかりやすいかもしれません。
例えば、普段cameraプラグインを使用する時、これだけで良いですよね。

pubspec.yml
dependencies:
  camera: ^1.0.0

しかし、先述の通り、cameraプラグインはcamera_android等プラットフォーム毎に異なるパッケージとして分割されています。それにも関わらず、cameraだけの追加で良いのは、camera_androidがendorsed federated pluginとして、cameraに組み込まれているからです。

逆に、Non-endorsed federated plugin の場合、camera が camera_android に依存しないので、完全に分離されています。よって以下のように、プラットフォームのパッケージを pubspec に直接追加することで利用します。

pubspec.yml
dependencies:
  camera: ^1.0.0
  camera_android: ^1.0.0

また、この方式を使うことで、ユーザーはプラットフォーム単位で自前で実装を追加することができます。例えば、android の実装だけ差し替えられたりと柔軟です。

ユーザーとしては一つ手間な反面、プラグイン開発者としては、camera_android がまだ公式にサポートできる品質でない場合等にこの手法は有用です。

さらに、今回の場合 gal が組み込まれるのは flutter_quill_extensions なので、実際のユーザーは flutter_quill_extensions を依存に追加するだけで良いと言うことになり、"依存の追加が手間なこと"は無視できます。

galのplatform_interface
https://github.com/natsuk4ze/gal/blob/01971703fa67d9f9e3c94f6810eb2c10c60a248a/lib/src/gal_platform_interface.dart#L1-L41

gal_linux の実装
https://github.com/EchoEllet/gal-linux/blob/d4ce6e94bf749d5a19130a27ebf710bc5975fd9c/lib/src/gal_linux.dart#L1-L39

それを利用するflutter_quill_extensions
https://github.com/singerdmx/flutter-quill/blob/e7d3391cdbb7a2197891800cd9819443ff6555b0/flutter_quill_extensions/pubspec.yaml#L47-L48

また、将来的にあるgal_linuxの動作が安定していることがわかったり、自分にLinuxの知識がついたタイミングで endorsed federated plugin として gal に組み込んでしまえば良いだけです。更に言えば他の Linux 実装をパッケージ化する人が現れたら、一番よさそうなものを選ぶこともできます。

導入の結果

あれからちょうど一年程経ちましたが、今でもこの選択には非序に満足しています。自分は Linux に関して気にしなくて良いですし、Ellet は僕のレビューを待ったりせず、自由に gal_linux を運営することができます。

しかも、つい先日flutter_quill(extensions)はdevにてGalを含んだいくつかのプラグインを自前のネイティブコードにリプレイスする変更をしました。

主な理由は、デスクトップの場合、写真アプリではなく、通常のフォルダに保存する方が一般的だということのようです。
もしこれが stable になれば、Ellet が gal_linux をメンテナンスしていく動機は失われたと言っても過言ではない訳で。となると自分の選択は正しかったのかなと。

これが Flutterにおけるプラグイン運営の難しさでもあります。これが iOS のライブラリであれば基本的に使用者も Swift 等の経験者なのでコントリビューションが可能です。ただし、Flutter プラグインの場合、使用者は基本的に Flutter エンジニアであって、現役で Swift や C++も書けるみたいな人は多くありません。よって少数のコントリビューターに依存する構造になりがちです。
もしあの時安易に Linux をリポジトリに加えていたら、間違いなく Linux の実装がボトルネックになって、新機能の追加等に影響が出ていたことでしょう。

また、flutter_quillに関しては、もはやオーナーではなくElletが一人でメンテナンスしているようで、彼が就職なりでコントリビューションができなくなった時、ネイティブコードの運営はかなり大変なんじゃないかなと思ったりしてます。(Elletは確か学生)

印象的なissue

https://github.com/natsuk4ze/gal/issues/198

要するに、「Androidで画像を32回保存するとエラーになる」と言うissueです。

何それって感じですよね🫨 また、この時点でピンとくる方はAndroid経験者でしょう。

エラー内容を元に調べたところによると。まず、スマホや、PCに関わらず、同じ名前のファイルを保存しようとすると、自動的に重複が回避されますよね。例えば、image(1).pngみたいに。

で、Android公式のMediaStoreではその重複回避が(31)までしか作成されないように制限をかけるjavaコードが存在しました。

FileUtils.java
// Generate names like "foo (2)"
return new Iterator<String>() {
    int i = 0;
    @Override
    public String next() {
        final String res = (i == 0) ? name : name + " (" + i + ")";
        i++;
        return res;
    }
    @Override
    public boolean hasNext() {
        return i < 32;
    }
};

仕方ないので、自前で重複チェックを実装して修正

https://github.com/natsuk4ze/gal/pull/199

正直、重複チェックなんてファイルマネージャー側でよしなにやってよ!って思いました🙄

まとめ

やっててよかった☺️

顔も知らない海外の人との開発が楽しい

単純にワクワクします。

成長を見るのが楽しい

pub.devの👍や、GitHubの⭐️が増えるのは嬉しいです。
また、GitHub のインサイトには Dependecy Graph といって、自分のパッケージに依存しているPublicリポジトリを一覧で確認できる機能があります。

見てみると、沢山スターがついているリポジトリでも使われていることがわかります。自分のパッケージを使用して、どんなアプリが作られているかを見にいくことが密かなマイブームです🤫(中国の大学の学内コミュニティアプリに使われてたりする)

おまけ

GitHubで公開している他のアウトプットも、一部ですが載せておきます!

Flutterでホロエフェクト サイネージ的なやつ 振動を使ったナビアプリ

それぞれの詳細は別の機会に紹介できればと思います!

最後まで読んでいただきありがとうございます! では👋

脚注
  1. プラグインパッケージとは ↩︎

  2. iOSならPhotos、AndroidならGoogle Photos等 ↩︎

SODA Engineering Blog
SODA Engineering Blog

Discussion