システム開発部のTです。
今回、Flutterでアプリを開発するうえで、覚えておきたいライフサイクルについて書いていきたいと思います。
個人的な所感としては、AndroidやiOSと比較してちょっと複雑かと思いました。
それでは、以降見ていきましょう!
概要
Flutterのライフサイクルは主に3つの種類があります。
大まかに以下のようになるかと思います。
上記でも分かる通り、StatefulWidgetのライフサイクルだけだと、画面遷移時のハンドリングができないです。
以降にて、上記のライフサイクルの詳細な動きを書いていきたいと思います。
アプリケーションのライフサイクル
アプリケーションのライフサイクルの取得には、以下のようにMixinsのWidgetsBindingObserverを追加することでdidChangeAppLifecycleStateから取得可能です。
あらかじめ、initState()でWidgetsBinding.instance.addObserver(this);
、およびdispose()でWidgetsBinding.instance.removeObserver(this);
を実装します。
PageA.dart
class PageA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _PageA(context: context,);
}
}
class _PageAWidget extends StatefulWidget {
late BuildContext context;
_PageAWidget({required this.context});
@override
_PageAWidgetState createState() {
return _PageAWidgetState(this.context);
}
}
class _PageAWidgetState extends State<_PageAWidget> with WidgetsBindingObserver { <-- これを追加
@override
Widget build(BuildContext context) {
return Material(
child: Container(
height: MediaQuery.of(context).size.height,
padding: EdgeInsets.all(32.0),
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, "/pageB");
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("pageA"),
],
),
)
)
)
);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this); <-- コード追加
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this); <-- コード追加
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) { // <-- これを追加することで、状態取得可能になる
if (state == AppLifecycleState.resumed) {
} else if (state == AppLifecycleState.paused) {
} else if (state == AppLifecycleState.detached) {
} else if (state == AppLifecycleState.inactive) {
}
}
}
上記見てもらうとわかりますが、ライフサイクルのステータスとして、以下の4種類に分けられます。
状態 | 内容 | 備考 |
---|---|---|
inactive | 以下で説明するresumed、pausedの前に、この状態になる。 | 自分のAndroid環境では、resumedの前でinactiveにならなかった・・・。 |
resumed | アプリがバックグラウンドからフォアグラウンドに切り替わるとき、Android、iOSともに、ホームボタン押下でホーム画面が呼び出されるタイミングでこの状態となる。 | |
paused | アプリがフォアグラウンドからバックグラウンドに切り替わるときに、Android、iOSともに、ホーム画面からアプリに復帰するときにこの状態となる。 | |
detached | アプリ終了時、Androidの戻るボタンでアプリを終了する場合、この状態となる。 | iOSの場合、戻るボタンが存在しないため、この状態にはならない。 |
上記がアプリのライフサイクルになるが、アプリの起動をハンドリングすることはできない・・・。
起動時に「insctive」「resumed」の状態になるかと思いきや・・・、didChangeAppLifecycleStateすらコールされないので、起動時をハンドリングするためには、以降のライフサイクルを利用するしかなさそうです。
以上がアプリケーションのライフサイクルとなります。
StatefulWidget(画面)のライフサイクル
先程はアプリケーションが管理するライフサイクルでしたが、次はStatefulWidgetのライフサイクルになります。StatefulWidgetで画面のレイアウトも生成することになるため、画面制御したい場合、本ライフサイクルを利用することになります。
まずは、ライフサイクルの実装内容を見ていきましょう。
PageA.dart
class PageA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _PageA(context: context,);
}
}
class _PageAWidget extends StatefulWidget {
late BuildContext context;
_PageAWidget({required this.context});
@override
_PageAWidgetState createState() {
return _PageAWidgetState(this.context);
}
}
class _PageAWidgetState extends State<_PageAWidget> {
@override
void initState() {
super.initState();
print("${this} initState() _StateLifecycle.created");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("${this} didChangeDependencies() _StateLifecycle.initialized");
}
@override
void didUpdateWidget(covariant T oldWidget) {
super.didUpdateWidget(oldWidget);
print("${this} didUpdateWidget() _StateLifecycle.ready");
}
@override
void dispose() {
print("${this} dispose() _StateLifecycle.defunct");
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
child: Container(
height: MediaQuery.of(context).size.height,
padding: EdgeInsets.all(32.0),
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, "/pageB");
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("pageA"),
],
),
)
)
)
);
}
}
各状態によって、上記の種類のコールバックメソッドがコールされます。
コールバックメソッド | 内容 | 備考 |
---|---|---|
initState() | 初回Widget生成時にコールされる。内部的には初期化前のため、BuildContextは利用不可。 | 初回のみコールされる |
didChangeDependencies() | initState()後、上記と同様初回のみコールされる。 Widgetの初期化が終了しているため、BuildContextは利用可能。 そのため、BuildContext利用時は、ここで初期設定をすること。 | 初回のみコールされる |
didUpdateWidget() | 初回Widget生成時はコールされない。 親のWidgetの内容が変更された場合、本メソッドがコールされる。 | 再構築時にコールされる |
build() | 上記コールバック後にコールされる。 | 初回、再構築ともにコールされる |
dispose() | Widget破棄時にコールされる。Android、iOSでは戻る操作で画面を終わらせるタイミングでコールされることになる。 | 終了時にコールされる |
上記を見てもらうとわかりますが、初回構築時、再構築時、終了時にコールされることがわかるかと思います。
ただ、これでも画面遷移を実装したときに、上記のみだと画面遷移のハンドリングはできません。
画面遷移で画面がコールされたときに、initState()〜build()がコールされます。
次に別画面に遷移する場合、呼び出し元の画面はNavigator.push()だと画面破棄されることはないので、dispose()がコールされません。また、アプリケーションのライフサイクルでも、アプリ自体がバックグラウンドにならないため、ここまでのライフサイクルだけでは、画面遷移をすべてサポートできていないのです。
そのため、次に画面遷移時のコールバックメソッドを紹介します。
画面遷移時のライフサイクル
本件のライフサイクルを理解することで、やっと画面のライフサイクルを理解することになるかと思います。
ここも複雑なので、サンプルアプリ作るなりして、理解を深めていただきたいです。
画面遷移のハンドリングとして、RouteObserverを利用する方法と、NavigatorObserverを利用する方法の2種類がありますが、本件では個人的に実装したRouteObserverを利用した方法を説明していきたいと思います。
前準備として、main.dartの外部変数にRouteObserver<PageRoute>
の値routeObserver
を用意します。外部変数として定義した理由は、以降で紹介する PageA画面からも参照するためになります。
そして、最上位のStatelessWidgetにて、MaterialApp
のnavigatorObservers
に上記で定義した値を設置します。
main.dart
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>(); // <-- ここでrouteObserverを定義
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
initialRoute: '/pageA',
routes: {
'/pageA' => PageA(),
'/pageB' => PageB(),
},
navigatorObservers: [
routeObserver, // <-- ここにrouteObserverを設置
],
);
}
}
画面遷移のライフサイクルの取得には、以下のようにMixinsのRouteAwareを追加することで、以降で説明するコールバックメソッドを実装できます。
didChangeDependencies()でrouteObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
、およびdispose()でrouteObserver.unsubscribe(this);
を実装します。
PageA.dart
class PageA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _PageA(context: context,);
}
}
class _PageAWidget extends StatefulWidget {
late BuildContext context;
_PageAWidget({required this.context});
@override
_PageAWidgetState createState() {
return _PageAWidgetState(this.context);
}
}
class _PageAWidgetState extends State<_PageAWidget> with RouteAware {
@override
Widget build(BuildContext context) {
return Material(
child: Container(
height: MediaQuery.of(context).size.height,
padding: EdgeInsets.all(32.0),
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, "/pageB");
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("pageA"),
],
),
)
)
)
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
@override
void didPush() {
print("${this} RouteAware didPush");
super.didPush();
}
@override
void didPop() {
print("${this} RouteAware didPop");
super.didPop();
}
@override
void didPopNext() {
print("${this} RouteAware didPopNext");
super.didPopNext();
}
@override
void didPushNext() {
print("${this} RouteAware didPushNext");
super.didPushNext();
}
}
PageB.dart
class PageB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _PageB(context: context,);
}
}
class _PageBWidget extends StatefulWidget {
late BuildContext context;
_PageBWidget({required this.context});
@override
_PageBWidgetState createState() {
return _PageBWidgetState(this.context);
}
}
class _PageBWidgetState extends State<_PageBWidget> with RouteAware {
@override
Widget build(BuildContext context) {
return Material(
child: Container(
height: MediaQuery.of(context).size.height,
padding: EdgeInsets.all(32.0),
child: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("pageB"),
],
),
)
)
)
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
@override
void didPush() {
print("${this} RouteAware didPush");
super.didPush();
}
@override
void didPop() {
print("${this} RouteAware didPop");
super.didPop();
}
@override
void didPopNext() {
print("${this} RouteAware didPopNext");
super.didPopNext();
}
@override
void didPushNext() {
print("${this} RouteAware didPushNext");
super.didPushNext();
}
}
上記のように実装することで、画面遷移時にdidPush()、didPop()、didPushNext()、didPopNext()がコールされるようになります。
上記の4種類のメソッドは、以下のタイミングでコールされます。
本件では、画面Aから画面Bをコールした場合を例として説明します。
コールバック メソッド | 内容 | 備考 |
---|---|---|
didPush() | 画面AにてNavigator.push()で指定された画面、つまり呼び出された画面側(画面B)にてコールされるメソッド。 | A→(push)→Bの場合、Bでコールされる |
didPop() | 画面BからNavigator.pop()を実行したときに、画面Bにてコールされるメソッド。 | B→(pop)→Aの場合、Bでコールされる |
didPushNext() | 画面AからNavigator.push()を実行したときに、画面Aにてコールされるメソッド。 | A→(push)→Bの場合、Aでコールされる |
didPopNext() | 画面BにてNavigator.pop()を実行したときに、画面Aにてコールされるメソッド。 | B→(pop)→Aの場合、Aでコールされる |
上記まとめてみましたが、いかがでしょうか?
各位におかれましては、実際に実装してみて動きを確認したほうがいいかと思います。
私自身も、ログ埋め込むなどで動き追いながら、上記まとめたところです。
(正直、訳わからなくなる・・・)
以上、アプリケーション、画面のライフサイクルになります。
まとめ
個人的な所感ですが、AndroidやiOSと比較してライフサイクルの敷居が高いように見えました。
現状、個人的にアプリ作っているなかで、本件で紹介したライフサイクルの知識が必要になったので、勉強していきましたが、これ以外にも必要となるコールバックがあるのかもしれないので、今後も随時記事にしていければと思っています。
それにしても・・・、楽に実装できる手段あれば教えてほしいなぁ・・・って思う今日このごろです。
以上、ありがとうございました!