alpha Lounge

20%の技術記事とオタクネタ

【Flutter】マージンを等間隔に置けるRowウィジェット【小ネタ】

小ネタサンプルです。

Flutterを書いていて、縦のListViewの中に横にスクロールできるListViewを入れたい、となる時がよくあると思います。以下の図のような実装です。
この場合、横のListViewの高さを固定にする必要があります(viewportエラーが出ます)。

一方、Rowを使えばitemに合わせて高さを柔軟に変更してくれますが、ListViewのようにいい感じにitem間のマージンを空けるのが面倒です。
そこで、マージンを等間隔に置けるRow Widgetを書いてみました。

import 'package:flutter/material.dart';

class EvenlySpacedRow extends StatelessWidget {
  final List<Widget> children;
  final int edge;
  final int spacer;

  const EvenlySpacedRow({
    super.key,
    required this.children,
    required this.edge,
    required this.spacer,
  });

  List<Widget> _buildItem(Widget widget, int position, int length) {
    if (position == 0) {
      return <Widget>[
        SizedBox(
          width: edge.toDouble(),
        ),
        widget,
        SizedBox(
          width: spacer.toDouble(),
        ),
      ];
    } else if (position == length - 1) {
      return <Widget>[
        widget,
        SizedBox(
          width: edge.toDouble(),
        ),
      ];
    } else {
      return <Widget>[
        widget,
        SizedBox(
          width: spacer.toDouble(),
        ),
      ];
    }
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: children
            .asMap()
            .entries
            .map((entry) {
          return _buildItem(entry.value, entry.key, children.length);
        })
            .expand((e) => e)
            .toList(),
      ),
    );
  }
}

使用例はこちら。

See the Pen Untitled by あるふぁ2048 (@alpha2048) on CodePen.

スクロール時の追加読み込みなどは実装してませんが、NotificationListenerなどを追加実装すれば実現できそうです。
そもそもListViewの高さを固定にすることでほとんど解決できるかと思いますが、困ったら使ってみてください。

【Flutter】(3.4.0対応) melosでマルチパッケージプロジェクトを管理する / Flutter multi package app sample with melos

この記事はFlutter Advent Calendar 2022の24日目の記事になります。クリスマスには技術記事で歯向かっていこう。

2024/1/7 melos 3.4.0向けに記事とソースコードを一部アップデートしました。

前置き

以前に Flutterのマルチパッケージの記事を投稿しました。
そもそもFlutterではマルチパッケージ構成を採用することはあまり多くないかもしれません。しかし、大規模なアプリを作る場合にRipositoryを分割してコード管理する場合や、複数のFlutterアプリを同じRipositoryで管理したい、みたいなケースにおいて、マルチパッケージ構成を選択することもあるかと思います。

今回はより実用的なマルチパッケージプロジェクトの構築を目指し、マルチパッケージ構築をサポートする「melos」を試していきます。

melos.invertase.dev

melosはいわゆるMonorepoと呼ばれるような、複数パッケージを持つDartプロジェクトを管理するためのツールです。flutterfireなどの有名ライブラリのプロジェクトにも導入されています。Invertase社が開発しています。

melosは以下のような機能を持ちます。

  • バージョン管理と変更ログの自動生成
  • pub.dev へのパッケージの自動公開
  • ローカル パッケージのリンクとインストール
  • 複数のパッケージで同時にコマンドを実行する
  • ローカル パッケージとその依存関係のリスト

どちらかというとライブラリ開発において力を発揮する印象を受けますが、普通のFlutterアプリ開発においても使える機能があります!

Sample

github.com

app_1app_2の2つのFlutterアプリを作成し、UIを提供するapp_commonパッケージを参照する構築です。

構築手順

melos公式のGetting Startedを参考に構築します。

  1. Flutterをインストール
  2. melosをインストール

     dart pub global activate melos
    
  3. (3.x系より) プロジェクトのルートにpubspec.yamlを設定

    ベースの設定を作成します。以下はサンプルです。

     name: melos_test_workspace
     environment:
       sdk: ">=3.0.0 <4.0.0"
    

    上記を設定後、dart pub add melos --devを実行してインストール済のmelosをdev_dependenciesとして追加します。

    詳しい説明は以下の通り
    https://melos.invertase.dev/getting-started

  4. プロジェクトのルートにmelos.yamlを作成

    以下はサンプルです。 必須項目はname(プロジェクト名)とpackages(melosで管理するパッケージ群)になります。packagesは任意の複数のフォルダを指定できます。

    name: your_project_name
    packages:
      - packages/**
    

    それ以外にも、SDKの場所の指定(fvmなどを導入している場合などに)やIDE、Gitに関する設定も可能です。各自で必要なものをセッティングしましょう。 Configuration overview

  5. 設定したパッケージの管理場所にパッケージを作成、移動

  6. .gitignoreをルートや各パッケージに設定

機能例

melos bootstrapによる初期設定

Bootstrap Command

melos bootstrapは全てのプロジェクトでpub getを実行してくれます。
また、bootstrapを実行するとIDEの設定も作成してくれます。下記はAndroid Studioの例で、IntelliJ系のRun Configが作成されます。

melosのIntelliJサポート

これ以外にも、後述するスクリプトのフックと合わせて初期設定をまとめることも可能です。

※パッケージ公開周りにおける利点もありますが、今回は省略します。

melos listによる依存関係の可視化

プロジェクト内のパッケージの依存関係を可視化することができます。
特にmelos list --graphによるJSON形式での可視化が便利です。

% melos list --graph
{
  "app_1": [
    "app_common"
  ],
  "app_2": [
    "app_common"
  ],
  "app_common": []
}

スクリプトを設定して実行する

おそらくmelosを使う上で一番よく使う機能になります。
スクリプトを組めるため、これによって各パッケージの操作を用意にできます。

各パッケージで任意のコマンドを実行する

melos execコマンドで、各パッケージで任意のコマンドを実行できます。

例えば、freezedなどを使用している場合で、build_runnerによるファイルの自動生成を行う場合に、一気に実行できるようにします。
dependsOnフィルターを使用してbuild_runnerを含むパッケージに対して実行できるようにします。

scripts:
  build_runner:
    exec: flutter pub run build_runner build --delete-conflicting-outputs
    packageFilters:
      dependsOn: 'build_runner'
    description: "run build_runner in projects including build_runner"

3.0より前のスクリプトはここをクリック

scripts:
  build_runner:
    exec: flutter pub run build_runner build --delete-conflicting-outputs
    select-package:
      depends-on: "build_runner"
    description: "run build_runner in projects including build_runner"

melosのlifecycle系コマンドをフックする

Hooks

bootstrapcleanversionの各コマンドの実行前後に好きなスクリプトを差し込めます。 前に実行したいならpreコマンドを、後に実行したいならpostコマンドを追加します。

例えば、bootstrapの後に前述のbuild_runnerの実行を行うスクリプトは以下のように書けます。

command:
  bootstrap:
    hooks:
      post: melos run build_runner

3.0より前のスクリプトはここをクリック

scripts:
  postbootstrap: melos run build_runner

任意のスクリプト実行

melos runコマンドで、scriptsで定義したスクリプトを動かすことができます。

また、runコマンドは任意のスクリプトを実行できます。
fastlaneなどを用いたCI/CD構築ほか、いろいろ活用できます。

scripts:
  fastlane_script:
    run: |
      cd packages/app_1
      fastlane hoge

おわりに

今回は簡単な例で紹介しましたが、スクリプトを組み合わせることで多彩な管理手法を構築できます。
Flutterで大規模なアプリを構築する際は一考の余地があるかと思います。

今回の記事やサンプルでもし間違っている点があればぜひお知らせください🙏

【Flutter】GitHubのDart/FlutterプロジェクトにDependabot機能がサポートされたぞ

GitHubが10月頭に以下のブログを掲載しました。Dart/FlutterプロジェクトにDependabot機能が追加されました。

github.blog

Dependabotは、依存しているライブラリを解析し、脆弱性があればアラートを投げてくれたり、自動的にバージョンアップのプルリクエストを作成してくれる機能です。npmやbundlerなど多くのパッケージ管理システムをサポートしています。

Dart/Flutterプロジェクトのpubでは、ベータ版として春先には使えるようになっていたようですが、ようやく正式サポートになったようになったようです。
正式サポートによってプライベートリポジトリでも有効化できるようになったようなのでありがたいですね🙏

実例

私のGitHubのサンプルの1つであるStateNotifierTestに、以下のようなアラートが届いていました。httpパッケージ脆弱性があるという通知ですね。最近メンテしていませんでしたが、パブリックリポジトリだと自動的に有効化されるようなのでとても助かります。

今後のために、 Dependabotをセッティングしていきます。

Dependabotの有効化

Insights→Dependency graph→Dependabotと辿り、Dependabotを有効化します。

Dependabot security updates(脆弱性対策)の有効化

Settings→Code security and analysis→Dependabotと辿り、Dependabot security updates設定を有効化します。
前述した脆弱性の不具合などを見つけた際に、自動でアップデートするプルリクを作成してくれます。

Dependabot version updates(バージョン更新)の有効化

自動でライブラリのアップデートを検知してくれるように、dependabot.ymlも設定します。

Settings→Code security and analysis→Dependabotと辿り、Dependabot version updates設定からymlを作成します。pubをスキャンするように設定しておけば、ライブラリのアップデートを検知してプルリクを作成してくれます。

おわりに

個人だけでなく企業でもFlutterを導入しているところが増えてきていますが、ライブラリのアップデート周りに頭が回らないところも多いかと思います。セキュリティ面を担保するためにも、Dependabot機能を積極的に使っていきたいですね💪

【Flutter】Riverpodのプロバイダ修飾子「.autoDispose」と「.family」を理解する

Flutterで人気の状態管理ライブラリ「Riverpod」において、忘れてはいけないプロバイダ修飾子について、備忘録がてら書きます。

プロバイダ修飾子とは、プロバイダを作成する際に付ける修飾子のことで、執筆時点では.autoDispose.familyの2つが用意されています。
公式では以下の使用例があります。

final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
  return fetchUser(userId);
});

.autoDispose

「プロバイダの監視が終わったタイミングで、プロバイダに自動でステートを破棄させることができるようになります。」と公式で書かれています。

通常は作成されたプロバイダはどこからも参照されなくなったとしても破棄されずに残り、ステートも残り続けます。メモリリークの原因になったり、再利用する際に以前のステートが邪魔になってしまうことがあります。

.autoDisposeを使用すると、参照されなくなった(ref.watchしているWidgetが破棄されるなど)場合に破棄され、ステートがリセットされます。メモリリークを未然に防ぐことができ、以前のステートを気にする必要がなくなります。
基本的にWidgetの状態管理をするプロバイダは、Widgetが破棄された時に連動して初期化してほしいので、.autoDisposeを設定しておくとよさそうですね。

.autoDispose内部処理

この修飾子をつけるとProviderはAutoDisposeXXXProviderとして作成されます。例えば、StateNotifier用のAutoDisposeNotifierProviderは以下の通り。

class _AutoDisposeNotifierProvider<Notifier extends StateNotifier<State>, State>
    extends AutoDisposeProviderBase<Notifier> {
...
  @override
  Notifier create(
    covariant AutoDisposeStateNotifierProviderRef<Notifier, State> ref,
  ) {
    final notifier = _create(ref);
    ref.onDispose(notifier.dispose);
    return notifier;
  }
...
}

StateNotifier作成時に自動でdispose処理を登録してくれます。

.family

「プロバイダ外部の値を用いてプロバイダを作成できるようになります。」と公式で書かれています。

任意のデータをinjectしてプロバイダを作成することができます。

Widgetの状態管理としてプロバイダを使う時に、以下のように外部からデータを入れたい場面が結構ありますよね。

  • 詳細画面を表示する際に、fetch用のIDなどをプロバイダに渡したい
    • 公式の使用例でいうと、ユーザー詳細画面でfetch用のuserIdを渡す
  • ListViewやGridViewで表示する要素に、画像などの表示するデータを渡したい

こういった時にデータを渡すことができる修飾子です。

.family内部処理

この修飾子をつけるとProviderはXXXProviderFamilyとして作成されます。例えば、StateNotifier用のStateNotifierProviderFamilyは以下の通り。

class StateNotifierProviderFamily<Notifier extends StateNotifier<State>, State,
    Arg> extends Family<State, Arg, StateNotifierProvider<Notifier, State>> {
  /// {@macro riverpod.statenotifierprovider.family}
  StateNotifierProviderFamily(
    this._create, {
    String? name,
    List<ProviderOrFamily>? dependencies,
  }) : super(name: name, dependencies: dependencies);

  ...

  @override
  StateNotifierProvider<Notifier, State> create(
    Arg argument,
  ) {
    return StateNotifierProvider<Notifier, State>(
      (ref) => _create(ref, argument),
      name: name,
      from: this,
      argument: argument,
    );
  }
  ...
}

深く処理を追っていませんが、渡したデータ(argument)がcreate関数に渡されるところは確認できますね。

コードサンプル

2つのプロバイダ修飾子を使って、公式サンプルのようにユーザー情報を表示する画面を想定して、ViewModelをStateNotifierで作ります。

final userDetailViewModelProvider = StateNotifierProvider.autoDispose
    .family<UserDetailViewModel, AsyncValue<User>, int>(
        (ref, userId) {
  return UserDetailViewModel(userId);
});

class UserDetailViewModel extends StateNotifier<AsyncValue<User>> {
  final int userId;

  UserDetailViewModel(this.userId) : super(const AsyncValue.loading()) {
  // Userモデルのfetch処理とか書く
  }
}

宣言がやや複雑ですが、このようにViewModelを表現することができます。
ちなみにプロバイダ修飾子を2つ続けて使用すると、AutoDisposeXXXProviderFamilyとして作成されます。

参考

プロバイダ修飾子

【JavaScript】iPhoneでカメラ(videoタグ)が動かない時の設定

小ネタです。
Webアプリでカメラ機能を実装する際、PCやAndroidChromeでは動いていたのに、iPhoneではSafariChromeどれでも動かないといった問題に出くわしました。

どうもiPhoneで動かすにはplaysinlineなどの属性が必要になるようです。

結論

<video id="hoge" class="huga"></video>

<video muted autoplay playsinline id="hoge" class="huga"></video>
  • autoplay
    • 自動再生
  • muted
    • 消音設定。iPhoneではこれがないと自動再生が有効にならない。
  • playsinline
    • インライン再生。一部のブラウザでは、これを指定しないと全画面再生になるとか。

これを見ると、埋め込みかつ自動再生というのを明確に指定しないと動かないということですね。覚えておきたい。

参考

Webアプリでカメラ操作したいときに見るメモ(javascript) - Qiita

videoタグで自動再生させるときの注意点!iOSはplaysinlineが必要 | Take-cast

[Javascript] iPhone safariでvideoタグを使った時に、NotAllowedErrorが出た時の回避方法-MYNT Blog

【Android】マルチモジュールプロジェクトにCIを導入する + UseCaseとViewModelにテストを書く【Turbine】

以前に作成したAndroidのCompose + マルチモジュールプロジェクトで、実装時に割愛したテスト導入とCIの整備を行いました。 背景やベースのプロジェクトについては以下をご確認ください。

Jetpack Compose + マルチモジュール (+ Atomic Design)で作るAndroidプロジェクト - alpha Lounge

UseCaseとViewModelにテストを書く

今回はdomainモジュール内のUseCaseと、featureモジュール内のViewModelにテストを導入します。
どちらも共通して、モックライブラリとしてMockKを、assert関数の拡張ライブラリとしてTruthを使用します。

UseCase

UseCaseのテストは、Repositoryをモックして、UseCaseの実行結果を確認するだけです。依存するRepositoryが増えたり別のUseCaseを呼ぶ場合も、モックを追加するだけで柔軟に対応可能です。

JetpackComposeTest/SearchRepositoryUseCaseImplTest.kt at master · alpha2048/JetpackComposeTest · GitHub

    private val query = "test"
    private val page = 1

    private val mockRepository = mockk<SearchRepository> {
        coEvery { search(q = query, page = page) } returns
            SearchRepositoryEntity(
                totalCount = 2,
                incompleteResults = true,
                items = listOf(
                    RepositoryEntity(
                        ...
                    ),
                    ...
            )
    }

    @Test
    fun `正常系のチェック`() = runBlocking {
        val useCase = SearchRepositoryUseCaseImpl(mockRepository)
        val result = useCase.execute(SearchRepositoryUseCaseParam(query, page))

        // なんかテスト書く

        coVerify { mockRepository.search(q = query, page = page) }
    }

ViewModel

ViewModelではUiStateの公開用にStateFlowを使用しているため、検証のためにTurbineというライブラリを使用します。こちらはflow周りのテスト用のライブラリです。

github.com

テストを書く前に、Kotlin公式のテストガイドを参考にメインスレッドを置き換えます。これはviewModelScopeがメインスレッドを使用しているためです。

    @OptIn(DelicateCoroutinesApi::class)
    private val mainThreadSurrogate = newSingleThreadContext("UI thread")

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun setup() {
        Dispatchers.setMain(mainThreadSurrogate)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @After
    fun tearDown() {
        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }

続いてViewModelのテストを書きます。
UseCaseをモックして、ViewModelのpublic関数(ここではsearch関数)をチェックします。この際に、TurbineでUiState用のStateFlowを購読することで、awaitItem()を使用してデータを取り出すことができます。

JetpackComposeTest/HomeScreenViewModelTest.kt at master · alpha2048/JetpackComposeTest · GitHub

    private val searchWord = "test"
    private val param = SearchRepositoryUseCaseParam(
        q = searchWord,
        page = 1
    )

    private val mockUseCase = mockk<SearchRepositoryUseCase> {
        coEvery { execute(param) } returns
            UseCaseResult.Success(
                data = SearchRepositoryEntity(
                    ...,
                )
            )
    }

    @Test
    fun `正常系のチェック`() = runBlocking {
        val viewModel = HomeScreenViewModel(mockUseCase)
        delay(1000)

        viewModel.uiState.test {
            viewModel.search(searchWord)
            delay(1000)

            coVerify { mockUseCase.execute(param) }

            val state1 = awaitItem()
            val state2 = awaitItem()
            val state3 = awaitItem()

            assertThat(state3.state).isInstanceOf(HomeScreenViewModel.UiState.Loaded::class.java)
            // なんかテスト書く

            cancelAndConsumeRemainingEvents()
        }
    }

今回はflowでのテストの紹介でしたが、Stateの公開にLiveDataを使用している場合はcore-testingを使用すると似たようなことができそうです。

star-zero.medium.com

AndroidのマルチモジュールプロジェクトにCIを導入する

プルリクの作成時やプッシュ時に、前項で作成したテストの実行、及びAndroidのlint、ktlintの実行を行い、Dangerでコメントします。GitHub Actionsで組んでいます。

ktlint設定

ktlintの設定はktlint-gradleを使用し、ルートのbuild.gradleから各モジュールに設定を反映します。

JetpackComposeTest/build.gradle at master · alpha2048/JetpackComposeTest · GitHub

subprojects {
    apply plugin: "org.jlleitschuh.gradle.ktlint" // Version should be inherited from parent

    repositories {
        // Required to download KtLint
        mavenCentral()
    }

    // Optionally configure plugin
    ktlint {
        android = true
        debug = true
        reporters {
            reporter "checkstyle"
        }
        ignoreFailures = true
    }
}

GitHub Actions

マルチモジュールプロジェクトではテストやlintの結果がそれぞれのモジュールに出力されるので、下記の記事を参考にそれぞれの結果からDanger実行します。

qiita.com

作成したDangerfileは以下の通り。

JetpackComposeTest/Dangerfile at master · alpha2048/JetpackComposeTest · GitHub

# ktlint
checkstyle_format.base_path = Dir.pwd
Dir["**/build/reports/ktlint/*/*.xml"].each do |file|
  checkstyle_format.report file
end

# android lint
android_lint.skip_gradle_task = true # 事前にビルド実行するのでskip
android_lint.filtering = true
Dir["**/build/reports/lint-results*.xml"].each do |file|
  android_lint.report_file = file
  android_lint.lint(inline_mode: true)
end

# junit
Dir["**/build/test-results/*/*.xml"].each do |file|
  junit.parse file
  junit.show_skipped_tests = true
  junit.report
end

完成したYAMLファイルは以下の通り。あまり最適化はできてないですが、ベースはこれで動くと思います。

JetpackComposeTest/check_pr.yml at master · alpha2048/JetpackComposeTest · GitHub

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: set up JDK 11
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '11'
          cache: gradle
  check:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '2.7'
          bundler-cache: true
      - name: Build with Gradle
        run: ./gradlew assembleDebug
      - name: run UnitTest
        continue-on-error: true
        run: ./gradlew testDebugUnitTest
      - name: run androidLint
        run: ./gradlew lintDebug
      - name: run ktlintCheck
        run: ./gradlew --no-daemon ktlintCheck
      - name: run danger
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gem install bundler
          bundle update
          bundle install
          bundle exec danger

デフォルトの状態だとktlintが自動で直せない部分(max lengthとか)は結構怒られるので、そのあたりのチューニングは必要そうです😢

おわりに

Turbineは初めて使いましたが、簡単にflowのテストが作成できるのでとてもいい感じ👍 今後のアップデートにも期待です。

CIについては、運用中のアプリであればリリースビルドを作ってFirebase Distributionなどで配布するworkflowを作るとかしたいですね。

もしこの記事をご覧になった方、参考になったらぜひブクマやシェアお願いします🙏

【Flutter】GetXを使ってMVVMでポケモン図鑑を作る

こんにちは、Flutter民です✌

自分は今までproviderRiverpodをメインに使用していたのですが、ここ最近GetXの評価が上がっており、状態管理ライブラリではこちらも主流になりつつあるようです。
出始めの頃は色々あって使用が忌避されていた1のですが、現在は安定しており使い勝手も向上しているようです。

今回はGetXを使ってシンプルなポケモン図鑑アプリを作成してみます。

Sample

github.com

解説

アーキテクチャはMVVM構成です。
GetXControllerを使用してViewModelを作成して、そこからRepositoryのAPIをfetchします。

StateはRx型で管理し、View側からObxで更新を取得します。
Stateは今回もfreezedを使用しています。

// ViewModel側
Rx<PokeListViewModelData> state = const PokeListViewModelData.loading().obs;

Future<void> fetch() async {
  state.value = const PokeListViewModelData.loading()

  repository.fetchPokemonList().then((res) async {
    state.value = PokeListViewModelData.loaded(response: res);
  }).catchError((_) {
    state.value = const PokeListViewModelData.error();   });
}
// View側
final PokeListViewModel viewModel = Get.put(PokeListViewModel());

Obx(() {
    return viewModel.state.value.map(
      // 省略
    );
  }),
);

画面遷移はGet.toなどを使えば、引数も通常通りに渡すだけなので簡単に実装できます。

Get.to(PokeDetailPage(pokemon: pokemon)),

おわりに

触ってみるまでは半信半疑だったのですが、とても簡単かつシンプルにMVVM構成を実現できました。
今回使用したライブラリの範囲ではRiverpodやChangeNotifierと比較してもあまり変わらず、GetX自体に強く依存しているという印象もありません。
Flutterにおける今後の状態管理ライブラリの候補の一つとして検討しても良さそうですね。


  1. 当時は破壊的変更が多くて安定してなかった?とかが原因だった気がする...あんまり詳しく覚えていません😢