20210109のAndroidに関する記事は2件です。

【Flutter】 無駄なリビルドを防ぐたった1つの方法

Flutter では StatelessWidget や StatefulWidget(以下、Widget) の build() が頻繁に呼ばれる(リビルドされる)ことを、 【Flutter】build() でやってはいけない 3 つのこと では説明しました。

build() が頻繁に呼ばれること自体は Flutter フレームワークとして想定通りの動作ですので、ここに「変な」処理を書かない限り リビルドが原因でパフォーマンスが低下するようなことは通常ありませんが、アプリの規模が大きくなり Widget の入れ子(以下「Widget ツリー」と呼びます)が深くなってくると少しずつ「リビルド範囲が大きすぎる」ためにパフォーマンスが落ちてくる場合があるのも事実です。

この記事では、 UI の変化が必要ない Widget の無駄なリビルドを防いでパフォーマンスを改善する にはどうすれば良いのかを理解するために、 Flutter フレームワークがリビルドを行わない条件を確認しつつ、その条件を満たすための実装について考えていきたいと思います。

インスタンスが変わらなければリビルドは発生しない

まず Flutter フレームワークが Widget をリビルドしない条件ですが、それは リビルド対象の Widget のインスタンスがリビルド前後で同一である 場合です。

通常以下のように SomeWidgetbuild() の中で OtherWidget を生成している場合、

class SomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return OtherWidget();
  }
}

_SomeWidgetStatebuild() が呼び出されると OtherWidgetbuild() もそれに続けて呼び出されます。 もし OtherWidgetbuild() の中でさらに別の Widget が生成されている場合はその Widget の build() も呼び出され、以下同様に Widget ツリーの末端に到達するまで build() が連続で呼ばれます。これを リビルドの伝播 と呼んだりします。

何も考えずに上記のようにコーディングすると、 OtherWidget のインスタンスは _SomeWidgetStatebuild() が呼び出されるたびに新しく生成されるため、前回の build() で生成したインスタンスとは別のものになってしまいます。

しかし、ここにひと工夫入れて 何度 build() が呼ばれたとしても同じ OtherWidget インスタンスを使い回す ことで、 Flutter フレームワークに対してそのインスタンスをリビルドする必要がない、と判断させることができます。1

Widget のインスタンスを使い回すための 3 つのテクニック

では、インスタンスが変わらなければリビルドが発生しないことを、以下のサンプルアプリで確かめていきたいと思います。

count_app.gif

このアプリでは、 + アイコンをタップすると横の数字がカウントアップします。その上には固定の文言が表示されています。

ソースコードは以下のようになっています。(レイアウトを整えるためのコードは省略しています)

labeled_counter.dart
/// カウントアップでリビルドが発生する StatefulWidget
class LabeledCounter extends StatefulWidget {
  @override
  _LabeledCounterState createState() => _LabeledCounterState();
}

class _LabeledCounterState extends State<LabeledCounter> {
  var _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SomeFixedWidget(), // _LabeledCounterState の build() が呼ばれてもリビルドさせないようにしたい
        Row(
          children: [
            Text('$_counter', style: TextStyle(fontSize: 32)),
            IconButton(
              icon: Icon(Icons.add),
              onPressed: () => setState(() {
                _counter++; 
              }),
            ),
          ],
        ),
      ],
    );
  }
}

/// リビルドを発生させたくない Widget
class SomeFixedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('I don\'t want to rebuild this widget.');
  }
}

通常このように記述すると、先ほど説明した通り _LabeledCounterStatebuild() が呼び出されたタイミングで SomeFixedWidget のインスタンスも新しく生成され、 SomeFixedWidgetbuild() が続けて呼び出されてしまいます。

しかし今回のアプリでは、 SomeFixedWidget の内容は状態(_counter の値)によって変化しないため、無駄なリビルドが発生しないように修正していきたいと思います。

SomeFixedWidget のインスタンスが _LabeledCounterState のリビルドごとに変わらなければ SomeFixedWidget のリビルドが発生しないのは先述した通りですが、ここからはそれを実現するための具体的な方法として以下の 3 つを見ていきたいと思います。

const を使う

まず一番取り入れやすいのが const をつけてインスタンスを生成する方法です。

const は Dart の文法の1つで、 コンパイル時に 1 つだけインスタンスを生成し、何度同じ処理が実行されても 1 つのインスタンスを使い回す という仕組みです。

つまり、 const を付けなかった場合、サンプルアプリの SomeFixedWidget() はこの処理が呼び出されるたびに毎回違うインスタンスを生成しますが、 const をつけた場合、 const SomeFixedWidget() は何度この処理が呼び出されたとしてもコンパイル時に生成された SomeFixedWidget インスタンスを再利用します2

const をつけるためには、インスタンスを生成される SomeFixedWidget クラスに const コンストラクタを用意しておく必要があります。

some_fixed_widget.dart
/// リビルドを発生させたくない Widget
class SomeFixedWidget extends StatelessWidget {
  // const コンストラクタを用意
  const SomeFixedWidget();

  ..以下略..

const コンストラクタが用意できたら、あとは SomeFixedWidget のインスタンスを生成するコードに const をつけるだけです。 先ほどのサンプルアプリのコードは以下のように修正できます。

/// const を使ってリビルドを回避するパターンのサンプル
class UseConstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('const を使う例')),
      body: LabeledCounter(),
    );
  }
}

/// リビルドを発生させたくない Widget
class SomeFixedWidget extends StatelessWidget {
  // const コンストラクタを用意する
  const SomeFixedWidget();

  @override
  Widget build(BuildContext context) {
    return Text('I don\'t want to rebuild this widget.');
  }
}

/// カウントアップでリビルドが発生する StatefulWidget
class LabeledCounter extends StatefulWidget {
  @override
  _LabeledCounterState createState() => _LabeledCounterState();
}

class _LabeledCounterState extends State<LabeledCounter> {
  var _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const SomeFixedWidget(), // const をつけてインスタンスを生成する
        Row(
          ..省略..
        ),
      ],
    );
  }
}

この対策は TextPadding など、 Flutter が用意する Widget でも、その Widget に const コンストラクタが定義されている限り同様に使えます。

Lint でコードをチェックすると「const がついていない」旨の警告が出る場合がありますが、それはこの例のように const コンストラクタを利用するだけで無駄なリビルドを防げる ためです。(単純にインスタンスの生成コストを抑えるのが理由の場合もあります)

State にキャッシュする

別の方法として、 生成した Widget インスタンスを State にキャッシュする という方法も考えられます。

これは StatefulWidget のドキュメントにも記載されているパフォーマンス改善の工夫で、以下のように記載されています。

If a subtree does not change, cache the widget that represents that subtree and re-use it each time it can be used. It is massively more efficient for a widget to be re-used than for a new (but identically-configured) widget to be created.
訳) サブツリーが変化しない場合、そのサブツリーを表す Widget をキャッシュして使い回してください。 Widget を使い回すのは新しい(でも内容は同一の) Widget を再生成するよりも効率的です。

https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html

やり方は単純で、 State クラスのフィールドに Widget を保持する変数を宣言するのと同時にインスタンスを生成してしまうだけです。

class _LabeledCounterState extends State<LabeledCounter> {
  var _counter = 0;

  // State のフィールドで Widget のインスタンスを生成しておく
  final _widgetCache = SomeFixedWidget();

  ..以下略..

ポイントは State のフィールド でこれをやるということです。何度も説明している通り、 Widget は Flutter フレームワークによって何度もリビルドされ、インスタンスが再生成されます。つまり、 Widget のフィールドにキャッシュを置いたとしても、その Widget 自体が破棄&再生成がされてしまうため、キャッシュの意味がなくなってしまうのです。

一方で State は Widget ツリーの構造に変化がない限り破棄されることはありませんので、 State にキャッシュしておくことでより長い間同じインスタンスを使い回せる、というわけです。これは StatelessWidget にはできない、 StatefulWidget だからこそ可能な工夫といえます。

この工夫を取り入れたサンプルアプリのコードは以下のようになります。

use_cache_page.dart
/// State でのキャッシュでリビルドを回避するパターンのサンプル
class UseCachePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('State でキャッシュする例')),
      body: LabeledCounter(),
    );
  }
}

/// カウントアップでリビルドが発生する [StatefulWidget]
class LabeledCounter extends StatefulWidget {
  @override
  _LabeledCounterState createState() => _LabeledCounterState();
}

class _LabeledCounterState extends State<LabeledCounter> {
  var _counter = 0;

  // State のフィールドで Widget のインスタンスを生成しておく
  final _widgetCache = SomeFixedWidget();

  @override
  Widget build(BuildContext context) {
    // レイアウト関連の記述は省略しています
    return Column(
      children: [
        _widgetCache, // _widgetCache を使い回す
        Row(
          ..省略..
        ),
      ],
    );
  }
}

もちろん、 const コンストラクタが使える場合は const を優先して使った方が、たとえ State が破棄されたとしても同じインスタンスが使いまわされるため効率的です。記述も少なく不具合も起こりづらいです。

何らかの理由で const が使えない場合(たとえば状況に応じてキャッシュする Widget の引数を変えたい場合など)はこの方法を検討してみてください。

外から渡す

3 つめに説明するのが、「外から渡す」という方法です。今まではサンプルアプリの LabeledCounter 内で完結する方法でしたが、この方法では LabeledCounter を使う クラスにも手を入れます。

つまり、 LabeledCounter クラスのコンストラクタに「使いまわしたい Widget」を受け取るための引数を用意し、それをフィールドに保持して使い回す、という方法です。

この方法が他の 2 つと違う点は、 LabeledCounter を使う側が自由に固定表示部分の Widget を指定できる、という点です。

例えば今回のサンプルアプリの場合、 LabeledCounter クラスを以下のように修正します。

class LabeledCounter extends StatefulWidget {
  // リビルドさせたくない Widget を外から受け取る
  final Widget label;

  const LabeledCounter({Key key, this.label}) : super(key: key);

  ..以下略..

そして、この LabeledCounter クラスを使う側で、以下のように SomeFixedWidget を生成して引数に渡します。

/// LabeledCounter を利用する Widget
class UseInjectionPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('外からインスタンスを渡す例')),
      body: LabeledCounter(
        label: SomeFixedWidget(), // ここで SomeFixedWidget インスタンスを生成して渡す
      ),
    );
  }
}

こうすることで、 LabeledCounter を利用する UseInjectionPage がリビルドされない限りは(つまり + ボタンがタップされただけの場合は) SomeFixedWidget インスタンスが再生成されることはないため、何度 _LabeledCounterStatebuild() が呼ばれたとしてもインスタンスが変わることはなく、 SomeFixedWidget のリビルドも発生しない、というわけです。

この方法を取り入れると、サンプルアプリは以下のように修正できます。

use_injection_page.dart
/// 外から Widget のインスタンスを渡してリビルドを回避するパターンのサンプル
class UseInjectionPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('外からインスタンスを渡す例')),
      body: LabeledCounter(
        label: SomeFixedWidget(), // ここで [SomeFixedWidget] インスタンスを生成して渡す
      ),
    );
  }
}

/// カウントアップでリビルドが発生する [StatefulWidget]
class LabeledCounter extends StatefulWidget {
  // リビルドさせたくない Widget を外から受け取る
  final Widget label;

  const LabeledCounter({Key key, this.label}) : super(key: key);

  @override
  _LabeledCounterState createState() => _LabeledCounterState();
}

class _LabeledCounterState extends State<LabeledCounter> {
  var _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        widget.label, // 外から受け取った Widget を使い回す
        Row(
          ..省略..
        ),
      ],
    );
  }
}

まとめ

リビルドが発生した(つまり build() が呼ばれた) Widget の中に StatelessWidget / StatefulWidget があるとき、それらも続けてリビルドされるかどうかは インスタンスが同一かどうか で決定します。

そのため、親の Widget の状態の変化に応じて自身の UI を変化させる必要がないのであれば、この記事で説明したような方法を使って同一のインスタンスを使い回すことで、無駄なリビルドを防ぐことが可能です。

とはいえ、無駄なリビルドを防ぐためにこれらの工夫を「入れなければならない」かどうかは状況次第です。 Flutter はビルドを大量に行ってもパフォーマンスが落ちないように工夫されているフレームワークですので、わざわざこれらの工夫を入れて(記述を増やして)リビルド範囲を狭めようと努力するのは、アプリの規模が大きくなり Widget ツリーが深くなってリビルドごとに画面の更新がカクつくようになった時に検討する程度で良いと思います。(1つ目の const をつける方法はほぼデメリットなくさっと行えるので習慣化すると良いと思いますが)

今回のサンプルアプリは以下のリポジトリに PUSH してあります。

chooyan-eng/prevent_rebuild_sample | GitHub

Flutter 自体のソースコードが読めるようになるとさらに詳しくこのあたりの仕組みがわかるかと思いますが、まずはとりあえずできることとして今回紹介したサンプルプロジェクトを上記の GitHub から落とすなり自分で書いてみるなりして実行し、手元でブレークポイントを張りながら動作確認してみると良いでしょう。この記事には書ききれなかった発見があると思います。


  1. なぜそのような挙動になるのか、興味のある方はまず Element について理解した上で、 Flutter のソースコードの Element.updateChildren あたり の処理を追ってみると良いでしょう。 

  2. 当然、引数を変えてインスタンスを生成するようなことはできません。詳しくは Language tour | Dart を確認してください。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2021年、業務アプリ開発歴17年が初めてFlutter Firebase Dartを触りたくなった話

筆者のスペック:Delphi の人です。業務アプリたるやみたいなことを普段は考えています。

  • ERP業務アプリケーション開発17年
  • 言語:Java, JavaScript (Delphi, COBOL, C++, SQL?, HTML?, CSS?, 趣味では色々
  • フレームワーク?:Spring, jQuery,
  • 他:Windows, UNIX全般, LINUX, データベースは何でも

で3層構造を主に触って食ってきました。タイトルだけは今時風にしてみました。
以下 Web制作を諦めた初心者が半年でアプリをリリースした話(Flutter) を読んで全く単語が頭に入ってこなかったのと、頭から入りたい質として「あーん?こいつら信用できるん?」なんて、浮かんだ疑念を解き明かすために書きます。

結論

  • Flutter
  • Firebase
  • Dart

を使ってバックエンドはNginx/Django/PostgreSQL あたり、で問題なく(というより最適だ位に)モバイルアプリは作れそうだ、と理解しました。
冒頭から手のひらを反すような結論になります。
三連休に本気出したらFlutterでアプリを作成できるか検証してみた を見つけたからこの3連休で本気出すか...どうかは約束しない。

以下、枯れた技術だけは知っているけれどイマドキの単語は全く、という方の、2021年のお供になればと思い書きます。

わからなかった単語1: Flutter

Flatter

https://ejje.weblio.jp/content/flatter

主な意味: おべっかを使う、おもねる、お世辞を言う

ではない。(本当に間違えた。)

Flutter

公式:https://flutter.dev/
Wikipedia: https://ja.wikipedia.org/wiki/Flutter

Flutter (フラッター) は、Googleによって開発されたフリーかつオープンソースのモバイルアプリケーションフレームワークである。FlutterはAndroidやiOS向けのアプリケーションの開発に利用されている。

学生の個人開発にもFlutterがオススメな理由(リリース例あり) があるが

全てDartのみで書ける
Storyboard, XML, CSSを触る必要がありません

Dartのみで書けるってどういう意味やねん。状態であった。

わからなかった単語2: Dart

Dart

公式:https://dart.dev/
Wikipedia: https://ja.wikipedia.org/wiki/Dart

Dart(ダートもしくはダーツ。当初は Dash と呼ばれていた)はGoogleによって開発されたウェブ向けのプログラミング言語である。ウェブブラウザ組み込みのスクリプト言語であるJavaScriptの代替となることを目的に作られた。

Flutter入門のためのDart入門
なるほど、FlutterとDartの組み合わせはどうやらテッパンらしい。

わからなかった単語3: Firebase

Firebase

公式: https://firebase.google.com/
Wikipedia: https://ja.wikipedia.org/wiki/Firebase

Firebase(ファイアベース)は、2011年にFirebase, Inc.が開発したモバイル・Webアプリケーション開発プラットフォームで、その後2014年にGoogleに買収された。

Web制作を諦めた初心者が半年でアプリをリリースした話(Flutter)に戻ると「Flutter×Firebaseで作成しました」とあったわけだが、Firebase、これも分からなかった。

Firebaseで1時間で簡単なWebチャットアプリが作れるハンズオン
1時間で作れる、そんなに嬉しいの?というか拡張性、信頼性、あるの?

わからなかった背景: 何でそんなフレームワーク達を使うの?

この疑問は端的には以下2つから来ている。

  1. そんな新しいフレームワーク信用できるの?
  2. えっ、また新しい言語覚えるのツライ...

疑問1. Flutterは信用できるのか

Flutterがいい理由 にある。

Androidの開発をJavaやkotlinの完全ネイティブで開発すると Activity、Fragment、Viewと複数のライフライクルを管理することになり、わかりづらくデバッグしづらいバグが発生します。

  • Flutterは複雑なライフサイクルがラッピングされているためシンプル
  • AndroidとiOSで共通のソースコードで実装できる
  • 標準のUIライブラリでAndroidとiOS両方対応している。

疑問2. 新しい言語覚えるのツライ

同記事で引用されている Android開発はFlutterでやる方がいい説 が私にはわかりやすかった。
良くも悪くもDart言語 であるということはすなわち

Dart言語の文法は、ほとんどJavaです。JavaScriptに近いかもしれませんが、Javaを知っている人の学習コストは限りなくゼロに近い

らしい。【翻訳記事】なぜFlutterにおいてDartを使用するのか? が詳しいので更に読むと良い。

結論:アーキテクチャ選定のガイドラインに則り

以上より Flutter, Firebase, Dart、それだけ話題になっていることがわかった。
使えるのではないかなとようやく理解できました。

ガイドラインとは

技術選定/アーキテクチャ設計で後悔しないためのガイドライン という記事を昨年末読んだ。

  • そもそもアーキテクチャ・技術選定に時間をかけるべきか。
  • あなたの技術的モチベーションをどの程度考慮したほうがいいのか。

今回私が Flutter, Firebase, Dart に興味を持った動機は、「趣味でアプリ開発できるかな?」程度。上記を考えると「時間はかけないべき」だし「私のモチベーションだけを考慮すればいい」。しかし多少将来を考えると、少しでもほかの誰かも触れたり、中身に興味を持てたり、あるいは日常業務(記事冒頭参照)に学びを還元できる要素があったほうがいい。

アーキテクチャ選定のガイドライン まとめ には以下のようにある。

最も大事なのは、そのような「失敗」をリカバリーできる体制やチーム、組織を作り上げることです。そのためには、その時考えたことや予測をしっかりと文書化し残していくことが大事になります。

そういう意味で当記事は「Flutter Firebase Dart」などという私にとっては初見のマイナー扱いだった技術たち。それらが、意外と強固で信頼性に足りそうだという記録として残したく書いたということになります。

補足:Dart言語の人気

2019年にわざわざ学ばなくてもいいプログラミング言語 で堂々1位。
疑問はStack Overflowで解決しながら淡々と利用されている、そういう言語であり、それを目指している

Kotlin: https://kotlinlang.org/ はそれとして。
KotlinとJavaができる人向けDart速習 なんてのもあるし、まあJavaがベースにあるヒトにとってはどちらでも、かじってみてもいいのかなという印象です。
一方で もしあなたが急にAndroidアプリを業務で作るはめになった場合の選択肢(2021年初頭版)。あ、業務で作るとなるとそれはまた別の話よね、とおもったので念のためここに追記。

以上2021年、(比較的)おっさん向けのモバイルアプリ「完全に理解したい」ための記事でした。
その後のHello, My Flutter Worldの記録はここにメモった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む