今回も前回に引き続きアプリを作成していきます。
ただ、今回は主にステートフルウィジェットとステートレスウィジェットを触っていきます。
前回から続けて作っているアプリは公式サイトのチュートリアルを参考に作っていますが、この中でステートフルウィジェットが出てきます。
公式の説明では以下の様に書いてありました。
ステートレスウィジェットは不変で、プロパティを変更することができない-全ての値がfinalであるということ
ステートフルウィジェットはそのウィジェットのライフタイムにおいて変化しうる状態を扱います。
(公式チュートリアルより)
なんとなくはわかるんですが、もうちょっと具体的に知りたいと思ってたら公式ドキュメントの"Adding interactivity"というページに以下のような説明で乗っていました。
ウィジェットはステートフルなウィジェットかステートレスなウィジェットに分けられます。ウィジェットが(例えばユーザの操作によって)変化するような場合、それはステートフルである。
ステートレスなウィジェット: 決して変化しない。例えば以下のウィジェットはステートレスなウィジェットであり、ステートレスなウィジェットはStatelessWidgetクラスのサブクラスである。
- Icon
- IconButton
- Text
ステートフルなウィジェット: 動的に変化する。例えばユーザの操作が行われた際やデータを受け取った際に見た目が変化する以下のようなウィジェットはステートフルなウィジェットである。また、ステートフルなウィジェットはStatefulWidgetクラスのサブクラスである。
- Checkbox
- Radio
- Slider
- InkWell
- Form
- TextField
ウィジェットの状態はStateオブジェクトに保存される。ここでいう状態とは、スライダーの今の値やチェックボックスのチェック有無といった変化しうる値によって構成されている。
ウィジェットの状態が変化する時、その状態オブジェクトはsetStateメソッドを呼び出し、フレームワークにウィジェットの再描画を依頼します。
では、ステートフルとステートフルなウィジェットを作っていきます。
IDEで開発している場合には"stful"や"stless"と打てば補完でボイラープレートを生成してくれます。補完してそのままウィジェット名を入力すれば内部処理では何もしないウィジェットの完成です。
// ステートフルウィジェットの実装
class TempStatefulWidget extends StatefulWidget {
@override
_TempStatefulWidgetState createState() => _TempStatefulWidgetState();
}
// ステートフルウィジェットに伴う状態を表すクラスの実装
class _TempStatefulWidgetState extends State<TempStatefulWidget> {
@override
Widget build(BuildContext context) {
return Container(
);
}
}
// ステートレスウィジェットの実装
class TempStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
);
}
}
で試しに状態を持たせてみたのが以下です。
class TempStatefulWidget extends StatefulWidget {
@override
_TempStatefulWidgetState createState() => _TempStatefulWidgetState();
}
class _TempStatefulWidgetState extends State<TempStatefulWidget> {
// 状態(今回は背景色用の色)
var bgColor = Colors.white;
@override
Widget build(BuildContext context) {
return Container(
child: FlatButton(
onPressed: () {
setState(() => bgColor = Colors.red);
},
child: Text('A'),
),
color: bgColor,
);
}
}
class TempStatelessWidget extends StatelessWidget {
// 状態
var bgColor = Colors.white;
@override
Widget build(BuildContext context) {
return Container(
child: FlatButton(
onPressed: () {
bgColor = Colors.red;
},
child: Text('B'),
),
color: bgColor,
);
}
}
なんとなく見て貰えばわかるかと思いますが、共に押されたらbgColorの値を変更するような処理を行うウィジェットを作成しました。
ここでステートレスウィジェットに試しにクラス変数を持たせてみたところ、持たせることができかつ値の変更も行うことができました。(IDEからはステートレスにしない?とかmust_be_immutableを無視するようにアノーテーション入れない?みたいな警告もでっぱなしでした)
ただ、公式ドキュメントにもあるようにsetStateメソッドを実行する必要がありますが、StatelessWidgetクラスはsetStateメソッドを持っていないため実行することができませんでした。そのため、値は変更できるけどウィジェットの再描画ができないため、見た目に値の変更を反映させることができませんでした。
作成したウィジェットをメインとなるウィジェットに入れていきます。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Welcome to Flutter')
),
body: ListView(
children: <Widget>[
TempStatefulWidget(),
Divider(),
TempStatelessWidget(),
],
),
),
);
}
}
ホットリロードすると
こんな感じで表示されて、ステートフルなAをタップするとAの背景が赤くなり、ステートレスなBをタップしても何も変化しないことが確認できるかと思います。
因みにまず作ろうとしていた公式チュートリアルのアプリですが、以下のようなコードで
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
home: RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
@override
_RandomWordsState createState() => _RandomWordsState();
}
class _RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return ListView.builder(
padding: EdgeInsets.all(16.0),
itemBuilder: (context, i){
if (i.isOdd) return Divider();
final index = i ~/ 2;
if (index >= _suggestions.length){
debugPrint('loading word pairs');
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
Widget _buildRow(WordPair pair){
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
}
(因みにこのコードはまんま貼り付けてもenglish_wardsが無いため動きません。もし動かしたければ、パッケージをインストールする必要があります。この記事の下の方に手順を載せておきます。)
起動するとランダムに英単語のリストが表示され、ある程度スクルールすると英単語がLazyロードされ無限にスクロールされるっていう感じなんですが、これだけならばStatefulWidgetでなくても実装できます。
公式チュートリアルでは続きの章でこの要素に状態を持たせる予定だからこのようにしています。
ListView.builderはスクレールされて見えるようになった時にitemBuilderの処理が呼び出されます。ListView自体はStatelessWidgetなので、リストの長さが伸びるっていうのは状態変わってるのでは?とも思いましたが、描画され途中なだけで描画された状態が変化しているわけでは無いという考え方かと思われます。
まとめ
もうちょっと公式チュートリアルの作り方を見ていこうと思っていたのですが、ステートフルとステートレスの方が調べていったらどんどん沼にハマってしまって、結果的に殆どStateに関する記事になりました。
補足:パッケージのインストール方法
公式チュートリアルで使っているenglish_wordsパッケージのインストールを行います。
プロジェクトディレクトリ直下にあるpubspec.yamlファイルのdependenciesにenglish_wordsを追加し、保存すると勝手に始まります。
dependencies:
flutter:
sdk: flutter
english_words: ^3.1.5
あまり無いと思いますが、インストールが行われなかったり、CLIからやりたいというような場合にはコマンドパレットで"Flutter: Get Packages"を選ぶかターミナルで以下のコマンドを実行して下さい。
$ flutter pub get
VScodeではインストールについての右下に表示されますが、すぐ消えてしまって気付けないかもしれません。その場合にはpubspec.lockファイルにenglish_wordsがあることを確認して下さい。(Cmd + fで検索してみて下さい。)
インストールした後にホットリロードしてもパッケージが無いと言われると思います。一度停止して再度ビルドしてみて下さい。