alpha Lounge

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

【Flutter】(2023年12月時点) AndroidのImpeller対応状況を確認する

この記事はFlutter Advent Calendar 2023のシリーズ3 24日目の記事です。

今年5月にリリースされたFlutter 3.10で、iOSのデフォルトの描画エンジンがImpellerに切り替わった、という発表がありました。1 Androidも対応するかな?と思いつつ気づけば年末になってしまいましたね。
今回はImpellerについて軽くまとめつつ、AndroidにおけるImpeller対応状況を調べてみます。

Impellerについて

ImpellerはFlutterの新しい描画エンジンです。

docs.flutter.dev

元々FlutterではSkiaという描画エンジンを採用していたのですが、このエンジンの欠点として、初回のアニメーション表示がカクつくという問題があります。2 これは、実行時にコンパイルを行うことによって引き起こされる問題です。事前にコンパイルを行うためのSkSL warmup手法も用意されているのですが、ビルド毎、リリース毎に書き出しを行う必要があり手間がかかります。

Impellerはビルド時にコンパイルすることでパフォーマンスへの影響を抑えています。3

各プラットフォームの対応状況

iOS

表題で述べた通り、iOSではFlutter 3.10からデフォルトの描画エンジンがImpellerに移行しています。

MacOS

MacOSは現状プレビュー版です。以下をInfo.plistに追加すると有効化できます。ここも気になる部分ですが割愛します。

  <key>FLTEnableImpeller</key>
  <true />

Android

さて、本題であるAndroidの対応状況です。

執筆時点での最新版であるFlutter 3.16ではプレビューとして、VulkanがサポートされているAndroid端末で利用可能と書かれています。

As of Flutter 3.16, Impeller is available behind a flag on Android devices that support Vulkan

...あれ?Vulkanがサポートされていない端末では使用できないのか?と疑問になるかと思いますので、確認していきます。

バックエンド処理について

AndroidのImpellerの処理は以下で書かれています。

https://github.com/flutter/engine/blob/main/impeller/docs/android.md

VulkanがサポートされていないAndroid端末ではOpenGL ESで動作するようになっています。さらによく見ると、「Android 10(API 29)以上で、Vulkanのバージョンが1.1、かつExtensionsがサポートされている場合」にVulkanを使用するようです。

公式としては、古いVulkanのサポートはコストがかかるため対応せず、OpenGL ESの処理を改善するようですね。この辺りの安定度の向上のためプレビュー期間が長くなるという想定もされています。4 5

Android端末のVulkanのサポート確認

Android端末のVulkanのバージョン確認方法はいくつかあります。

手っ取り早いのは、vulkan.gpuinfo.orgで検索することです。
Pixelで検索すると以下のバージョンが出てきます。API Versionが対応しているバージョンになります。

バイス名がわからない場合、実機がある場合は以下のアプリでも確認ができます。ただしAndroid 12以上という要件があるため使えない機種も多そうです。

https://play.google.com/store/apps/details?id=de.saschawillems.vulkancapsviewer

Pixel 6aで起動した場合

また、以下のissueのログを見ると、実行時にVulkan非対応時にエラーが出るようですね。実際に私が持っているいくつかの古いスマホでも、試したところ同じログが表示されていました。

[Impeller] Android device that successfully falls back to GLES renderer will print several warning messages · Issue #136059 · flutter/flutter · GitHub

有効化方法

実行時に--enable-impellerオプションで有効化もできますが、以下のようにAndroidManifest.xmlに記述すると有効にできるため、こちらが楽かと思います。

<meta-data
    android:name="io.flutter.embedding.android.EnableImpeller"
    android:value="true" />

終わりに

今回はAndroidのImpeller対応状況をまとめました。現在は既知のバグも多く、まだまだ対応中という印象ですが、今後の動向に注目したいところです。気になる方は有効にして動作を確認してみたり、バグ報告してみるといいかもしれませんね👍

参考

Impeller rendering engine | Flutter

engine/impeller/docs/android.md at main · flutter/engine · GitHub

What’s new in Flutter 3.10. Seamless web and mobile integration… | by Kevin Chisholm | Flutter | Medium

What’s new in Flutter 3.16. Material 3 by default, Impeller preview… | by Kevin Chisholm | Flutter | Nov, 2023 | Medium

Flutterの課題、Early-onset jankとは何か

Impeller engine: is it good enough for Flutter? | Medium

【Flutter】iOSの数字テキストのガタつきをなくして等幅にする【Tips】

この記事はジャンルなしオンラインもくもく会 Advent Calendar 2023の16日目の記事になります。

FlutterでiOSアプリを作った際、何やら数字の横幅の表示がガタついているな、と思ったことはありませんか?
気のせいかと思いきや、実はガタついています。以下のように、数字によって横幅が異なっています。

今回はこの現象を修正するtipsです。

Sample Code

iOSで数字テキストが崩れる件の直し方 · GitHub

説明

TextStyleにFontFeature.tabularFigures()を指定します。これは等幅(monospace)で表示する設定です。これを適用すると、以下の図のようにガタつきがなくなり等幅で表示されます。
また、横幅を変化するためのFontFeature.proportionalFigures()という設定も用意されていますね。

monoさんがpostされていましたが、どうやらAndroidでは発生しないようですね。この記事を作るためにDartPadで確認していたのですが、そちらでも再現しませんでした。iOS側の実装を深掘りすれば詳しく分かるかもしれませんが、今回は割愛します🙏

参考

FontFeature.tabularFigures constructor - FontFeature - dart:ui library - Dart API

zenn-contents/articles/flutter-textstyle-monospace.md at main · hikiit/zenn-contents · GitHub

古いXcodeで新しいiOSを対応させる方法【Tips】

今開発を行っているアプリが様々な要因で新しいXcodeを使えなかった時、古いXcodeが対応していないiOSが入ったiPhoneは使えないのか?と思うかもしれませんが、実は対処方法があります。

iOS16以下

IOS16以下ではDeveloperDiskImageというのをXcode内に配置することで可能です。ファイルと配置方法は以下のリポジトリを参照してください。iOS16.6まで取得できます。(iOS16.7はまだ用意されていません。)

github.com

iOS17

iOS17以上ではDeveloperDiskImagesは使われず、「CoreDevice」という仕組みが使われています、これはプラットフォーム毎に管理され、新しいXcodeをインストールすることで更新されるようになっています。

CoreDeviceを有効にするには以下のコマンドを叩きます。

defaults write com.apple.dt.Xcode DVTEnableCoreDevice enabled

これで、Xcodeの最新版をインストールすると、古いXcodeでもサポートが追加されます。

また、CoreDeviceのサポートはXcode 14.3以上となっているようですので、Xcode 14.2以下でしか動かない場合は頑張ってアップデートしましょう。

参考資料:
https://developer.apple.com/forums/thread/730947 https://stackoverflow.com/questions/76412754/how-to-run-on-ios-17-device-using-xcode-14

【Flutter】RepaintBoundaryによるパフォーマンス向上について【Tips】

Flutterのパフォーマンスを上げる方法の一つである「RepaintBoundary」Widgetについて軽くメモ。

説明

公式の資料はこちら。こちらに説明全て書いてありますが😇

api.flutter.dev

FlutterはWidgetが再描画されると、関連する同じレイヤーのWidgetが再描画されます。状況によっては再描画の必要のないWidgetまで再描画され、パフォーマンスの低下を招きます。 RepaintBoundaryを使用すると、子Widgetの描画を分離させ、再描画を防ぐことができます。

アプリを作っている側としては、基本的に「再描画のタイミングが異なるWidgetを囲むことでパフォーマンスを上げる」と認識しておけばOKかと思います。


具体的には以下のような活用がありました。

CustomPaintへの使用

任意の図形を描画できるCustomPaint Widgetですが、再描画が走るとパフォーマンスに影響が出ることがあります。RepaintBoundaryを使用して描画処理を切り離します。

 
  Widget buildCustomPaint() {
    return RepaintBoundary(
      child: CustomPaint(
        painter: ExpansivePainter(),
        isComplex: true,
        willChange: false,
      ),
    );
  }

アニメーション内部のWidgetを囲む

Flutterのベンチマークで行っている手法です。アニメーションを行うWidgetで、アニメーション内のWidgetの再描画を防ぐために使用します。

// https://github.com/flutter/flutter/blob/1704d4f5f9c2f53decd1d526a8a41dfa06d1e484/dev/benchmarks/macrobenchmarks/lib/src/color_filter_and_fade.dart#L63

final Widget fadeTransition = FadeTransition(
  opacity: _opacityAnimation,
  // This RepaintBoundary is necessary to not let the opacity change
  // invalidate the layer raster cache below. This is necessary with
  // or without the color filter.
  child: RepaintBoundary(
    child: column,
  ),
);

まとめ

通常の開発ではRepaintBoundaryを使う機会はあまりないかもしれませんが、パフォーマンスが気になるとなった時に、上記の観点を意識して試してみるとよさそうです👍

参考資料:
Dive in to DevTools - YouTube
Flutter - Using RepaintBoundary Examples - Woolha
Flutter-タイマーアプリケーションを作ってみる6 | Take4-blue
FlutterのRaster Cacheを追ってみる

新型コロナに罹ったので対処回復メモ

最近新型コロナがまた流行りだしてる中、7月末に私も罹っていました。
何かの役に立つかと思い、経過と対処をメモっておきました。一人暮らしの方の参考にでもなれば。
ちなみに、ワクチンは1年前までに3回受けていました。

経過

発症0日目

午前4時頃に猛烈な暑さを感じて起床。熱を計ったら38度超。
前日出社していたこともあり、午前中に急いで外来に向かった。検査で新型コロナ陽性と判明。

0日目は熱が激しいだけでまだ動けたほうだった。この日のうちに今週分の業務や予約などのキャンセルを行った。 また、置き配を使ってスポーツドリンクやゼリー、冷却シートなどを注文した。Uber Eatsや出前館ですぐに注文できるため、普段使わない場合でもセッティングしておくとGood。

発症1日目

症状が酷くなり、激しく咳込むようになった。
解熱剤を使ってもほとんど熱が下がらないため、ゼリーとスポーツドリンクを何とか口に入れてやり過ごした。

発症2日目

解熱剤が効くようになり、平熱近くまで下がってる間は多少マシになるが、まだ寝てないとしんどい。少し固形物も食べるようにした。

発症3日目

ようやく熱が下がる。喉の痛みや咳が残り、疲労感も高かったので、気をつけながら家を片付けるなどした。

発症4〜5日目

喉の痛み、咳も回復傾向にあるものの全快にはならず。 体力を戻すため、ちょうど時期のうなぎなどを食べつつ、食事を戻すようにした。

その後

翌週は完全リモートワークで稼働。喉の痛みは早く取れたが、ふとした時に咳が止まらなくなる状態が続く。執筆時点でも少し不安がある状態。

まとめ

とにかく初動の対応が大事だなと感じました。体調が悪くなったら我慢せず早めに病院に行きましょう。 症状が本格化すると動くのが厳しくなるので、日頃から準備をしておくのも大切です。

JetBrains民によるGithub Copilot (+ChatGPT)の所感

私は何を開発するにしても、ほとんどJetBrains社が開発したIntelliJ IDEAと、IntelliJがベースのAndroid Studioを使っています。

先日、join先の会社でGithub Copilotについて話題になり、使用についての整備が始まったため、私も重い腰を上げてIntelliJAndroid Studioに入れてみました。

しかし..

Copilotから出てくる提案が気に入らないので、ESCで消す
→再び入力して、IntelliJのsuggestを貰おうとする
→再びCopilotが気に入らない提案をしてくる
→ESCで消す →以下ループ...

この現象が何度か続いたため、結局Copilotは消してしまいました😢

今は使わないとなった所感について

Github Copilotは周りのコードに合わせた提案を次々としてくれるため、使いこなせれば非常に強い武器です。 VSCodeAtomなど、通常のエディタで開発をするのが好きな方は使って損はないでしょう。

一方、IntelliJは高等なIDEということもあり、元から備わっているsuggestが優秀なので、そちらとバッティングすることがあります。

IntelliJのsuggestで書くのが好き

個人的にはこのsuggestありきでコードを組み立てていくコーディングスタイルに慣れているため、Copilotによってむしろそれが阻害されてしまっていると感じたことが、使えないと感じた大きな要因かなと思います。

どうなったらCopilotを使いたいか

  • Copilot自体の精度の向上
  • IntelliJのsuggestといい感じに混ざり合う

結局これしかなさそうですね..
Copilotに細かいコードまで完全に任せられるか、もしくはプラグインが改善されて、suggestとの共存がいい感じになったら使おうかなと思います。


余談: ChatGPTについて

余談で話すには物足りなさそうなので軽く取り上げますが、ChatGPTはいいですね👍
何もないところからコードを提案したり手順を示して欲しいといったケースにおいて大変重宝します。エンジニア初学者の方が何もわからない状態から何かするための道標にしたり、個人開発などでなかなか手が回っていない箇所を助けてもらったり、できることが無限大になった印象です。

私も使い始めなので、もっと有効な使い方、プロンプトを探していきたいと思います👀

AWS CopilotでAPI作りたいと書いたら、手順からわかりやすく教えてくれる

【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の高さを固定にすることでほとんど解決できるかと思いますが、困ったら使ってみてください。