- 投稿日:2021-01-09T22:18:08+09:00
[Xcode]README.mdファイルの追加方法
表題の通りXcodeにREADME.mdファイルを追加する方法について投稿します。
検索をかけたところ最近の記事にありつけなかったので、ぜひ2021年最新版として参考にしていただければと思います!
追加手順(シンプル)
Xcodeナビゲーションバー(Macディスプレイ最上部のメニューバー)ののFileを選択
↓
Newを選択
↓
Fileを選択
↓
すると画像のようにファイルを選択するメニューが出てくるので、Markdown Fileを選択してNext
↓
そのままページが切り替わるのでCreateを選択これでGitにコミットすればGithubのリポジトリでREADMEに加えた編集内容を見ることができます。
- 投稿日:2021-01-09T21:55:19+09:00
UITextViewの一部のテキストに背景色を角丸でつける
UITextViewの一部のテキストに背景色を角丸でつける方法です。
背景色をつける
UITextView のテキストの一部に背景色をつけたい場合は、NSAttributedString + NSAttributedString.Key.backgroungColor 属性を使います。
let text = "あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、..." let attributedText = NSMutableAttributedString(string: text) attributedText.addAttributes([.backgroundColor: UIColor.cyan], range: NSRange(location: 2, length: 7)) let textView = UITextView() textView.attributedText = attributedText背景色を角丸でつける
この背景色の部分に角丸をつけたい場合は、NSLayoutManager の fillBackgroundRectArray メソッドをオーバーライドして背景を角丸で描画するカスタムクラスを作り、UITextViewに適用します。
TextLayout.swiftimport UIKit class LayoutManager: NSLayoutManager { var backgroundCornerRadius: CGFloat = 0 override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) { guard let context = UIGraphicsGetCurrentContext() else { super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color) return } context.saveGState() defer { context.restoreGState() } context.setFillColor(color.cgColor) for rectIndex in 0..<rectCount { let rect = rectArray.advanced(by: rectIndex).pointee let path = UIBezierPath(roundedRect: rect, cornerRadius: backgroundCornerRadius) context.addPath(path.cgPath) context.fillPath() } } }let layoutManager = LayoutManager() layoutManager.backgroundCornerRadius = 5 let textView = UITextView() textView.textContainer.replaceLayoutManager(layoutManager)背景色を角丸でつける(改行も考慮する)
上記の方法の場合、背景色のつくテキストが改行されると次のように折り返しの部分も角丸になってしまいます。
そこで、改行も考慮して fillBackgroundRectArray メソッドを実装してみます。実装のポイントは、
- fillBackgroundRectArray メソッドはテキスト内の backgroundColor 属性ごとに呼び出される
- 一つの backgroundColor 属性に対して、テキストが改行される場合は、入力の矩形
rectArray
,rectCount
が複数になる- 複数の矩形がある場合は、最初の矩形は左側のみ角丸をつけ、最後の矩形は右側のみ角丸をつけ、それ以外は角丸をつけない
という感じです。
TextLayout.swiftclass LayoutManager: NSLayoutManager { var backgroundCornerRadius: CGFloat = 0 override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) { guard let context = UIGraphicsGetCurrentContext() else { super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color) return } context.saveGState() defer { context.restoreGState() } context.setFillColor(color.cgColor) let cornerRadii = CGSize(width: backgroundCornerRadius, height: backgroundCornerRadius) for rectIndex in 0..<rectCount { let rect = rectArray.advanced(by: rectIndex).pointee let corners: UIRectCorner if rectCount == 1 { corners = .allCorners } else if rectIndex == 0 { corners = [.topLeft, .bottomLeft] } else if rectIndex == rectCount - 1 { corners = [.topRight, .bottomRight] } else { corners = [] } let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: cornerRadii) context.addPath(path.cgPath) context.fillPath() } } }いい感じになりました
- 投稿日:2021-01-09T21:52:08+09:00
flutter podfile関連 環境構築メモ
cache消しとく
sudo rm -rf cacheそしたらいつも通りする
flutter pub upgrade flutter pub get flutter runきちんとpod installする
出てくるエラー
Error output from CocoaPods: ↳ [!] Automatically assigning platform `iOS` with version `8.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`. Error: CocoaPods's specs repository is too out-of-date to satisfy dependencies. To update the CocoaPods specs, run: pod repo update対処①
1.Go to /ios folder inside your Project.
2.Delete Podfile.lock
3.Run pod install --repo-update
4.Run flutter clean
5.Once complete, rebuild your Flutter application: flutter run出てくるエラー
[!] CocoaPods could not find compatible versions for pod "sqflite": In Podfile: sqflite (from `.symlinks/plugins/sqflite/ios`) Specs satisfying the `sqflite (from `.symlinks/plugins/sqflite/ios`)` dependency were found, but they required a higher minimum deployment target. [!] Automatically assigning platform `iOS` with version `8.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syn対処②
platform : ios, '13.0'にしてpod install 実行
https://github.com/fluttercommunity/flutter_workmanager/issues/106
podfile# Uncomment this line to define a global platform for your project platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end対処③
ios>Flutter>Flutter.framework>Release.xcconfigで以下の2行を追加
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"Release.xcconfig#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
- 投稿日:2021-01-09T20:32:16+09:00
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた②
前回の続き↓
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた①はじめに
前回の記事を読んでない方に、簡単に説明するとiOSアプリで位置情報を取得するときに
配慮する点をまとめた内容となっています。前回までのあらすじ
前回、
端末の位置情報サービスの有無
のチェックは
アプリをインストール後の初回起動のみ
しかされないと書きました。初回起動時に位置情報サービスが
オフ
になっていたらこんなアラートを表示します↓
このチェックは
初回起動のみ
なので、2回目以降の起動時はアラートを表示するように改修しましたね↓
今回は、バックグラウンド状態
からフォアグラウンド状態
にアプリを切り替えた時にも
端末の位置情報サービスの有無
をチェックするようにしていきましょう。バックグラウンド状態から戻った時にチェックする(端末)
アプリが、
バックグラウンド状態
からフォアグラウンド状態
に切り替わった時に
端末の位置情報サービスの有無
をチェックするように処理するには
AppDelegate
のapplicationWillEnterForeground(_ application: UIApplication)にて行います。※
AppDelegate
の呼ばれる順番は、こちらの記事が非常に分かりやすかったので載せておきます↓
iOSアプリのライフサイクルAppDelegate.swift// アプリがフォアグラウンド状態に入ろうとしている時に呼ばれるメソッド func applicationWillEnterForeground(_ application: UIApplication)ですが、アラートを表示したいのでViewControllerのクラス内で処理を行いたいですね。
アプリの状態が変わったことを
AppDelegate
以外で感知して
ある処理を行いたい場合は、NotificationCenterを使用します。※こちらの記事を参考にして実装しました↓
[Swift3.0] NotificationCenter を使ってアプリの状態に応じた処理を行うViewController// ビューが表示される直前に呼ばれるメソッド // 初回表示以外にもバックグラウンド復帰、タブ切り替えなどにも呼ばれる // まだビューが表示されていないため、計算コストの高い処理は避ける override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } @objc private func willEnterForeground() { // バックグラウンド状態から戻ってきた時に端末の位置情報サービスがオフの場合 if !CLLocationManager.locationServicesEnabled() { // アラート表示 Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } }ビルドしてみると、
バックグラウンド状態
から戻った時にもアラートが表示されるようになりました↓
これで、端末の位置情報サービスの有無を起動時
とバックグラウンド状態から戻った時
に
チェックするようになりましたね。後、チェックしなければいけないのは
アプリの位置情報サービスの有無
です。アプリの位置情報サービスの有無をチェックする
位置情報を取得する際には、ユーザーに許可を貰わなければ取得出来ません。
なので、アプリはこのようなアラートを表示してユーザーに許可をリクエストします↓
ですが、もし、ユーザーが許可しない
を選択したらどうでしょうか?
位置情報が必要なアプリの場合、許可を貰わなければ困りますよね。なのでユーザーが、
許可しない
を選択した場合にアラートなどを表示したりして
アプリの位置情報取得の許可
を促さなければいけません。前回のソースコードを例に説明します。
※ちょっと長いので一部抜粋今回も、アラートの表示はこちらの方法で表示します↓
UIAlertControllerをファイルを分けて実装してみるViewControllerimport UIKit import CoreLocation override func viewDidLoad() { super.viewDidLoad() locationManager.requestWhenInUseAuthorization() locationManager.delegate = self } extension ViewController: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus switch status { case .authorizedAlways, .authorizedWhenInUse: manager.startUpdatingLocation() case .notDetermined: manager.requestWhenInUseAuthorization() // 許可しない場合 case .denied: // アラートを表示して、アプリの位置情報サービスをオンにするように促す Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "") case .restricted: break default: break } } }
許可しない
を選択した場合、アラートがちゃんと表示されました。
これでも、ちゃんとユーザーに対して
アプリの位置情報取得の許可
を促していますが
ここで、もう少し配慮してアラートのOK
を選択したら設定アプリ
に画面遷移するようにしましょう。設定アプリに画面遷移する
設定アプリ
に画面遷移するメリットとしてアプリの位置情報取得の許可
がスムーズになり
ユーザーの手間が省かれることです。画面遷移する方法は、こちらの記事が分かりやすかったので載せておきます↓
Swift5を使ってURLスキームで設定画面に遷移する方法この方法を用いて、先ほどのソースコードに追加していきます。
ViewControllerimport UIKit import CoreLocation override func viewDidLoad() { super.viewDidLoad() locationManager.requestWhenInUseAuthorization() locationManager.delegate = self } extension ViewController: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus switch status { case .authorizedAlways, .authorizedWhenInUse: manager.startUpdatingLocation() case .notDetermined: manager.requestWhenInUseAuthorization() // 許可しない場合 case .denied: // アラートを表示して、アプリの位置情報サービスをオンにするように促す // ユーザーに対して分かりやすようにmessageで、OKを選択すると設定アプリに画面遷移することを伝える Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in // OKを選択した後、設定アプリに画面遷移する UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) } case .restricted: break default: break } } }こちらをビルドしてみると、ちゃんと
設定アプリ
に画面遷移しました。
ですが、ここでも落とし穴があります。
この方法で、設定アプリに移動したのはいいけど
アプリの位置情報取得の許可
をしないまま戻ったとしましょう。このままだと、アプリは何もチェックしません。
この場面でも、アプリが
バックグラウンド状態
から戻った時に
アプリの位置情報取得の有無
をチェックしなければいけません。またまた記事が長くなりそうなので、今回はここら辺で終わります。
ここ間違っているよー!というのがありましたら、気軽にコメントして下さい。最後まで読んで下さって、ありがとうございます!
ここまでのソースコードを下に載せておきます↓ソースコード
ViewControllerimport UIKit import CoreLocation class ViewController: UIViewController { var locationManager: CLLocationManager = { var locationManager = CLLocationManager() locationManager.distanceFilter = 5 locationManager.distanceFilter = 5 return locationManager }() override func viewDidLoad() { super.viewDidLoad() locationManager.requestWhenInUseAuthorization() locationManager.delegate = self } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } @objc private func willEnterForeground() { if !CLLocationManager.locationServicesEnabled() { Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !CLLocationManager.locationServicesEnabled() { Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } } } extension ViewController: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus switch status { case .authorizedAlways, .authorizedWhenInUse: manager.startUpdatingLocation() case .notDetermined: manager.requestWhenInUseAuthorization() case .denied: Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) } case .restricted: break default: break } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let gps = manager.location?.coordinate else { return } manager.stopUpdatingLocation() let lat = gps.latitude let lng = gps.longitude print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))") } }Alertimport UIKit final class Alert { static func okAlert(vc: UIViewController,title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) { let okAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) okAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler)) vc.present(okAlertVC, animated: true, completion: nil) } }
- 投稿日:2021-01-09T18:49:13+09:00
【Flutter】 無駄なリビルドを防ぐたった1つの方法
Flutter では StatelessWidget や StatefulWidget(以下、Widget) の
build()
が頻繁に呼ばれる(リビルドされる)ことを、 【Flutter】build() でやってはいけない 3 つのこと では説明しました。
build()
が頻繁に呼ばれること自体は Flutter フレームワークとして想定通りの動作ですので、ここに「変な」処理を書かない限り リビルドが原因でパフォーマンスが低下するようなことは通常ありませんが、アプリの規模が大きくなり Widget の入れ子(以下「Widget ツリー」と呼びます)が深くなってくると少しずつ「リビルド範囲が大きすぎる」ためにパフォーマンスが落ちてくる場合があるのも事実です。この記事では、 UI の変化が必要ない Widget の無駄なリビルドを防いでパフォーマンスを改善する にはどうすれば良いのかを理解するために、 Flutter フレームワークがリビルドを行わない条件を確認しつつ、その条件を満たすための実装について考えていきたいと思います。
インスタンスが変わらなければリビルドは発生しない
まず Flutter フレームワークが Widget をリビルドしない条件ですが、それは リビルド対象の Widget のインスタンスがリビルド前後で同一である 場合です。
通常以下のように
SomeWidget
のbuild()
の中でOtherWidget
を生成している場合、class SomeWidget extends StatelessWidget { @override Widget build(BuildContext context) { return OtherWidget(); } }
_SomeWidgetState
のbuild()
が呼び出されるとOtherWidget
のbuild()
もそれに続けて呼び出されます。 もしOtherWidget
のbuild()
の中でさらに別の Widget が生成されている場合はその Widget のbuild()
も呼び出され、以下同様に Widget ツリーの末端に到達するまでbuild()
が連続で呼ばれます。これを リビルドの伝播 と呼んだりします。何も考えずに上記のようにコーディングすると、
OtherWidget
のインスタンスは_SomeWidgetState
のbuild()
が呼び出されるたびに新しく生成されるため、前回のbuild()
で生成したインスタンスとは別のものになってしまいます。しかし、ここにひと工夫入れて 何度
build()
が呼ばれたとしても同じOtherWidget
インスタンスを使い回す ことで、 Flutter フレームワークに対してそのインスタンスをリビルドする必要がない、と判断させることができます。1Widget のインスタンスを使い回すための 3 つのテクニック
では、インスタンスが変わらなければリビルドが発生しないことを、以下のサンプルアプリで確かめていきたいと思います。
このアプリでは、 + アイコンをタップすると横の数字がカウントアップします。その上には固定の文言が表示されています。
ソースコードは以下のようになっています。(レイアウトを整えるためのコードは省略しています)
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.'); } }通常このように記述すると、先ほど説明した通り
_LabeledCounterState
のbuild()
が呼び出されたタイミングでSomeFixedWidget
のインスタンスも新しく生成され、SomeFixedWidget
のbuild()
が続けて呼び出されてしまいます。しかし今回のアプリでは、
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( ..省略.. ), ], ); } }この対策は
Text
やPadding
など、 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
インスタンスが再生成されることはないため、何度_LabeledCounterState
のbuild()
が呼ばれたとしてもインスタンスが変わることはなく、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 から落とすなり自分で書いてみるなりして実行し、手元でブレークポイントを張りながら動作確認してみると良いでしょう。この記事には書ききれなかった発見があると思います。
なぜそのような挙動になるのか、興味のある方はまず
Element
について理解した上で、 Flutter のソースコードの Element.updateChildren あたり の処理を追ってみると良いでしょう。 ↩当然、引数を変えてインスタンスを生成するようなことはできません。詳しくは Language tour | Dart を確認してください。 ↩