- 投稿日:2021-01-07T22:58:33+09:00
【Flutter】build() でやってはいけない 3 つのこと
Flutter で仕事したい人のための Widget 入門 で説明した通り、Flutter では基本的に
StatelessWidget
やStatefulWidget
を継承したクラスでbuild()
をオーバーライドし、そこに UI を構築する処理を書いていきます。 (厳密には、 StatefulWidget の場合はState
クラスのbuild()
)例↓
login_page.dartclass LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ TextFormField( decoration: InputDecoration( hintText: 'chooyan@example.com', ), ), const SizedBox(height: 32), ElevatedButton( onPressed: () {}, child: Text('ログインメールを送る'), ), ], ), ); } }初めて Flutter を触る人にとっては、この
build()
を そのページの「初期化処理」と捉えてしまいがち なのではないかと思います。自分もそうでした。しかし、
build()
を「初期化処理」だと考えて「やってはいけないこと」を書いてしまうと、思わぬパフォーマンスの低下や不具合を引き起こしてしまう可能性があります。この記事では、そんな
build()
でやってはいけない 3 つのパターン を理由つきで説明していきます。
build()
は何度でも呼ばれるまず知っておかなければならないのは、
build()
は何度でも呼ばれる、ということです。最初の1回だけではありません。そもそも Flutter は、「状態」が変わるたびに何度も Widget を破棄し、新しい「状態」を使って Widget を作り直すことで画面を変化させる 設計になっています。
例えば Flutter プロジェクトを新規作成した時にテンプレートとして作られる「カウンターアプリ」では、
FloatingActionButton
として配置されたカウントアップボタンをタップするたびに_counter
(状態を表す変数)をインクリメントして_MyHomePageState
の中のbuild()
メソッドが呼ばれます。
_MyHomePageState
のbuild()
では、以下のように_counter
変数の値をText
で画面に表示するようにコーディングされていますので、Text( '$_counter', style: Theme.of(context).textTheme.headline4, ),
build()
が呼ばれるたびにその時点での_counter
の値を持ったText
が作り直され、画面の表示内容が変化する仕組みになっています。
build()
が呼ばれるタイミングそんな
build()
が呼ばれるタイミングは様々です。一番分かりやすいのは、
StatefulWidget
(が生成するState
)でsetState()
が呼ばれたときです。カウンターアプリでも、 FloatingActionButton がタップされた時に呼び出される
_incrementCounter
メソッドにはvoid _incrementCounter() { setState(() { _counter++; }); }とコーディングされていて、
setState()
を呼び出すことと、_counter
をインクリメントすることが書かれています。Flutter では、この
setState()
内に書いた処理を実行したあとにbuild()
を呼び出す1作りになっているため、新しくbuild()
で作られる Text にはインクリメントした後の_counter
の値がセットされる、というわけです。他にも、
MediaQuery
やProvider
など、InheritedWidget
が保持する値が更新された場合や、同様のことが Widget ツリーの先祖で発生してbuild()
が伝播してきた場合など、いろいろなタイミングがあります。2特にアニメーションが発生する場合、60 fps、つまり 16 ミリ秒に 1 回という速度(参考)で毎フレームこの
build()
が呼ばれる場合もあります。とにかく、
build()
は様々なタイミングで、高速に何度でも呼ばれる可能性がある 、ということを忘れてはいけません。build() でやってはいけない 3 つのこと
build()
が高速に何度でも呼ばれる可能性がある ということを踏まえると、以下のような処理はそこに書いてはいけないことが見えてきます。1. 初期化処理
build()
は画面が出ている間(場合によっては画面に見えていない間も)何度でも呼ばれる可能性があります。そのため、状態を管理する変数を初期化するようなコードは書いてはいけません。int _count; @override Widget build(BuildContext context) { _count = 0; // 0で初期化 return Container( child: (省略) ); }このように書いてしまうと、何かのきっかけで
build()
が呼ばれ直した瞬間にそれまでのカウントが 0 に戻ってしまいます。また、 Firebase やローカルのデータベースとの接続処理であったり、表示するデータの取得処理などもここに書いてしまうと繰り返し呼び出されてバグの原因につながります。サーバーにアクセスする処理であればサーバーの負荷にも影響が出てしまうでしょう。
このような初期化処理は、基本的に
State
クラスが持っているinitState()
をオーバーライドするなど、確実にその画面が初めて生成されるタイミングに 1 回だけ呼ばれる場所に書くのが良いでしょう。2. ひとつだけあれば良いオブジェクトの生成
たとえば GoogleMaps やアニメーション関係の Controller であったり、一度生成してキャッシュしておく Widget の生成処理などはここに書かいてはいけません。
// 良くない例 class MapSampleState extends State<MapSample> { Completer<GoogleMapController> _controller; @override Widget build(BuildContext context) { // build ごとに mapController を生成してはいけない _controller = Completer(); return GoogleMap( (省略) onMapCreated: (GoogleMapController controller) { _controller.complete(controller); }, ); } }// 良くない例 class FixedComponentState extends State<FixedComponent> { List<Widget> cachedMenu; @override Widget build(BuildContext context) { // 使い回す Widget をここで生成してはいけない cachedMenu = [ Text('メニューその1'), Text('メニューその2'), Text('メニューその3'), ]; return Column( children: cachedMenu, ); } }Controller が複数生成されてしまってその Controller 自体の初期化処理が何度も無駄に実行されてしまったり、せっかくキャッシュしておいた Widget が何度も再生成 & 上書きされてキャッシュの意味がなくなってしまいます。
このような場合は、例えば以下のようにフィールドの宣言と同時にインスタンスの生成までやってしまうと良いでしょう。
// 改善例 (google_maps_flutter 公式のサンプルコード通り) class MapSampleState extends State<MapSample> { Completer<GoogleMapController> _controller = Completer(); @override Widget build(BuildContext context) { return GoogleMap( (省略) onMapCreated: (GoogleMapController controller) { _controller.complete(controller); }, ); } }// 改善例 class FixedComponentState extends State<FixedComponent> { // FYI: 可能な限り const もつけるとなお良い List<Widget> cachedMenu = [ Text('メニューその1'), Text('メニューその2'), Text('メニューその3'), ]; @override Widget build(BuildContext context) { return Column( children: cachedMenu, ); } }3. その他重い処理
その他、多量な計算や通信処理、ファイルの読み書きなど、重い処理を書けば書くほど画面の変化を端末に反映させるのが滞ってしまいます。なるべく
build()
には Widget の生成以外の処理を書かないようにしましょう。もし重い処理を書く必要があるのであれば、やはり
initState()
など適切な場所でasync
つきのメソッドにまとめて処理が終わり次第setState()
で結果だけを反映させるような工夫を入れます。処理が終わるまでは CircularProgressIndicator でクルクルを表示するなどすると良いでしょう。class ArticleListState extends State<FixedComponent> { List<Article> articles; Future<void> _loadArticles() async { final result = await ArticleLoader().all(); // 記事を全件取得する処理 setState(() { articles = result; } } @override void initState() { _loadArticles(); super.initState(); } @override Widget build(BuildContext context) { // articles が null ならクルクルを表示、あれば ArticleList を表示 return articles == null ? CircularProgressIndicator() : ArticleList(articles); } }まとめ
Flutter を入門すると、まずソースコードを書き始めるのが
build()
の中(StatelessWidget でも StatefulWidget でも)な関係上、どうしてもここが「最初に1回」やる処理を書く場所だと誤解してしまいがちです。しかし実際は、 Flutter の仕組みとして Widget は使い捨てであり、
build()
は何度でも呼び出されます。このことを初学者はまず知る必要があるでしょう。公式ドキュメントにも、以下のように記述されています。This method can potentially be called in every frame and should not have any side effects beyond building a widget.
訳) このメソッドは毎フレーム呼び出される可能性があり、Widget を構築する以外の副作用を発生させてはいけません。これは Flutter の根本的な設計の都合ですので、 Provider などの状態管理パッケージを使う場合でも同様の発想が必要になります。3
「1回だけやれば良い処理や重い処理は
build()
に書いてはいけない、なぜならbuild()
は高速に連続で呼び出される可能性があるから」 ということを頭に置いておくだけでも、 Flutter アプリ開発をする上で発生する多くの不具合を回避できるでしょう。(2021.1.10 追記)
この記事の内容を気をつけていても、アプリの規模が大きくなるのにしたがってbuild()
による画面の更新が重くなってしまう場合があります。そんな時に使えるテクニックと考え方を以下の記事に書いてみましたので、読んでみてください。【Flutter】 無駄なリビルドを防ぐたった1つの方法
(追記ここまで)併せて読みたい
この記事の内容をさらに詳しく理解する手助けになる記事です。それぞれの記事には公式サイトも参照先に挙げられていますので、そこまで読めればだいぶ理解を深められるでしょう。
記事中に出てきた
InheritedWidget
の役割や使い方を解説した記事です。 InheritedWidget は実際にアプリ開発者が直接使うことは少ない Widget のため参考資料も少なめですが、この記事は実際に動作するコードつきで段階的に説明してくれているので、 InheritedWidget というものを理解する上でとても勉強になると思います。InheritedWidget の中身にフォーカスした記事です。Flutter の実装なども読みながらその仕組みを追っていきます。なお、↑の記事とは違い「どうやって使うのか」には一切触れていません。
setState()
の実装を追いながらその仕組みを説明した記事です。フレームワーク内部のソースコードに興味のある方は読んでみると、 Flutter のソースコードの追い方などが見えてくるかもしれません。Flutter の Widget が画面に表示されるまでの Flutter フレームワークの流れを詳しく解説した記事です。ちょっと長くて内容も「慣れてきた人」向けではありますが、いつかは読んでおくととても良い勉強になります。
- 投稿日:2021-01-07T21:00:04+09:00
[Swift] スタティックプロパティ(static)を使ってみて分かったこと
はじめに
今回は、スタティックプロパティを理解するためにインスタンスプロパティも一緒に動かしてみたので、
比較しながら理解を深めていけたらなと思います。試してみたコードの説明
比較のためにインスタンスプロパティaとスタティックプロパティbを用意します。
関数getは、aとbの値の変更内容を表示するためのものです。struct Sample { var a = 0 static var b = 0 func get() -> String { return "aが\(a)、bが\(Sample.b)" } }インスタンスプロパティaを変更してみた
//3種類のインスタンスを作成します。 var S1 = Sample() var S2 = Sample() var S3 = Sample() //もちろん結果は同じです。 S1.get() //"aが0、bが0" S2.get() //"aが0、bが0" S3.get() //"aが0、bが0" //S2だけ変更を加えてみます。 S2.a = 3 //S2.aの部分のみが変更されています。 S1.get() //"aが0、bが0" S2.get() //"aが3、bが0" S3.get() //"aが0、bが0" //変数aにアクセスしようとすると、S1.aかS2.aかS3.aになる。 //インスタンス化したものに紐づいてアクセスするので、値の変更があった場合はそのインスタンスごとに変更になる。 //結果を見てもS2.aだけ変更されている事がわかる。スタティックプロパティbを変更してみた
Sample.b = 6 //全てのbが変更されている。 S1.get() //"aが0、bが6" S2.get() //"aが3、bが6" S3.get() //"aが0、bが6" //staticプロパティのbにアスセスするには、Sample.bになる。S1.bとアクセスする事はできない。 //インスタンスではなく、元の型自身に紐づいているプロパティである事がわかる。 //変更結果をみても、元になる型の内容を変更していることになるので、全ての値が変更されている事がわかる。
- 投稿日:2021-01-07T20:21:34+09:00
コピペで乗り切るエンジニアがSwiftUIを勉強してみた(環境構築編)
こんにちは!
転職をして会社で扱うPCがMacだったので、
社畜の私はMacBook Proを買いました。この時期、M1搭載のMacがYoutubeで色々話題ですが、
何か分からないけど使えないソフトとかあったらどうしよう。。。と
Intel CoreのMacを買いました。さて、せっかくMacを買ったということで、
MacでできてWindowsでできないことといえば、そう! iOSアプリの開発!
何も分かりませんが、やってみないと分からないということで、
まずは環境構築をやってみます。環境構築
Swift UIを使えるように実装していきます。
Xcodeのインストール
「App Store」より「Xcode」をインストールしていきます。
これでバッチリです!!(たぶん)
テストコードを作る
さて、実行環境ができたので
テストコードといえば、「Hello world」の実行ですね。
(「Hello world」と画面に表示するだけ)「Create a new Xcode project」をクリックします。
次に「iOS」->「App」を選択し、「Next」をクリックします。
次に以下を指定して「Next」をクリックします。
* Product Names
プロジェクトの名前。アプリ名の初期値にもなる。
* Team
Apple Developer Web サイトを利用できるユーザーや組織名。
(勉強用ということで適当でー)
* Organization Identifier
任意の値でOK。ただし、iOS アプリの配布を想定した名前。
(こちらも適当でー)
* Interface
「SwiftUI App」を指定する。
* Language
「Swift」を指定する。
そうすると。。。
プロジェクトが立ち上がるので、そのまま「▶️」をクリックします。
しばらく待っていると、シミュレーターの画面に「Hello, world!」が!
おぉーー!これでOK!!(きっと)
次回は「Hello world」のソースについて
ふわっと理解で触れていきます。
- 投稿日:2021-01-07T16:20:53+09:00
iOS Simulator 実物大にする方法
- 投稿日:2021-01-07T14:41:15+09:00
iOS14.3のiOSシミュレータでNavigationBarが黄色くなってしまう場合の対処方法
問題点
iOS14.3のiOSシミュレータでiOSアプリをビルドして起動すると、NavigationBarなどの表示が本来は半透明のところが黄色くなってしまいます。
全体の雰囲気がわからなくなるので元にもどしたい。スクリーンショット
対処法
- iOSシミュレータのメニューからGPUの設定を変更する
- 変更前 [File] -> [GPU Selection] -> [Automatic]
- 変更後 [File] -> [GPU Selection] -> [Prefer Integrated GPU]
- iOSシミュレータを再起動する
変更後
- 下記のスクリーンショットのように元の半透明に戻っていることが確認できます
スクリーンショット
- 投稿日:2021-01-07T08:11:24+09:00
[Swift5]コードでダークモード(暗い外観)の適用を回避する方法
- 投稿日:2021-01-07T01:58:29+09:00
iOS14でtrackingAuthorizationStatusのメモ
環境
- Xcode12.3
- iOS 14.2 iPhone SE
条件
トラッキング要求の動き
端末 トラッキング要求 アプリ status 許可 未 - notDetermined 済 許可 authorized 拒否 denied 拒否 未 - denied 済 許可 denied 拒否 denied 想定通り。
トラッキング要求を出していない状態で、端末設定を拒否から許可に戻すとちゃんとnotDeterminedに戻りますトラッキング要求を許可後の動き
アプリでトラッキング要求を許可した後に端末設定を拒否にした場合、こんなダイアログが出てくる
トラッキング要求をしていない場合や拒否した場合はダイアログがでない
「Appがトラッキングを続けることを許可する」を選ぶと、こんな画面なので端末で拒否しても取れるのかなと思ったらそんなことはなかった。
端末設定で再許可時のstatusをそのままにしてくれる動きのようです
ダイアログ status 端末設定で再許可時のstatus Appがトラッキングを続けることを許可する denied authorized 「Appにトラッキングの停止を要求」 denied denied
端末拒否時のトラッキング要求の動き
また、端末で拒否時にトラッキング要求をするとdeniedが設定されて端末設定を許可に戻してもdeniedのままになります。
トラッキング要求を出していなければnotDeterminedに戻ります端末拒否時
トラッキング要求 status 端末許可に変更後のstatus 未 denied notDetermined 済 denied denied