はじめに
Flutterについての備忘録です。
もろもろコードや文法などを記載していきます
なにか間違っていたり、より「こっちのほうがいいよ!」って場合はコメント等お願いします。
備忘録なので解説が雑な部分があります。ご注意ください。
なお以下の記事の続編です
①
②
③
アジェンダ
StatelessWidgetとStatefulWiget
今回は最序章でも軽く触れていたStateless,Statefulについてです。
StatelessWiget
- 状態を持たないウィジェット
- 一度描画される(初期化)されると見た目が変わらない
- 例えば固定テキストや、アイコンや画像etc...
StatefulWiget
- 状態を持つウィジェット
- ユーザの操作や経過などによって状態が変化するものであり動的なもの
- 例としてテキストボックス、タイマーやカウンターetc...
2つのウィジェットの基本構文
親クラスであるStatelessWidget
を継承して状態を持たない「Stateless」
基本構文 (StatelessWidget)
class クラス名 extends StatelessWidget{
const クラス名({Key?key}) : super(key:key); // コンストラクタ
@override
Widget build(BuildeContext context){
return const Scaffold();
}
}
- ①この部分はコンストラクタとなりクラスのメンバ変数の初期化を行う
const クラス名({Key?key}) : super(key:key); // コンストラクタ
- ② この部分はBuild(画面の描画,画面構築)
@override
Widget build(BuildeContext context){
return const Widget( // UIを記述);
}
基本構文(StatefulWidget)
- ① シンプルにStatefulを継承して動的なクラスの管理ができるクラスと明示する
class クラス名 extends StatefulWidget{
const クラス名({Key?key}) : super(key:key);
@override
State<クラス名> createState() => _クラス名State();
}
class _クラス名State() extends State<クラス名> {
// ここに動的に変化する関数などを記述
@override
Widget build(BuildContext context){
return Widget();
}
}
- ②コンストラクタでメンバ変数を初期化
const クラス名({Key?key}) : super(key:key);
- ③ この書き方は関数呼び出しの書き方の省略形となっている、ここで描画を呼び出し
これと
@override
State<クラス名> createState() => _クラス名State();
}
これは
State<クラス名> createState(){
return _クラス名State()
}
同義です。
④ 画面構築
class _クラス名State() extends State<クラス名> {
// ここに動的に変化する関数などを記述
@override
Widget build(BuildContext context){
return Widget();
}
画面描画タイミングが3つある
- 初回画面構築時
- 親ウィジェット更新時
- 状態更新時
初回画面構築時
コンストラクタ → createState() // 状態管理 → initState() // 初めて生成のとき一度のみ生成(ここに初期化を記述) → didChangeDependencies() // 依存関係などが更新されたときに呼び出される → build() // 描画
親ウィジェット更新時(新しい設定での更新時)
コンストラクタ → didChangeDependencies() // 依存オブジェクト変更のため → build()
状態更新時(ウィジェットの状態が変更した時画面を更新したいとき)
setState() // 変更を検知して更新を指示する特殊なメソッド → build()
InteractiveViewerについて
拡大,縮小や左右に移動できる要素(通称:パン)などを可能にするウィジェットです。
子ウィジェットを先ほどの要素などを追加できるような機能を提供してくれます。
基本的なメソッドやそれらの機能、引数については公式のリファレンスを読むとより分かりやすいかなと思います。
指定できる引数について(抜粋)
boundaryMargin
→ 子ウィジェットの周りマージンを置く。指定しておくと子ウィジェットの範囲外をスクロールを制限をかけれたりする(無限スクロールも現実可能に)
constrained
→ 画面外に出ている子ウィジェットにスクロールできるかできないかの制約を追加できる。(引数:bool型 )
より詳しい内容(サンプルコードを動かして「true」「false」の挙動を見てみるのがおすすめ)
maxScale
minScale
→ 拡大,縮小の最大値と最小値についての制約を付けられる
今回紹介で使ったものを使用例としてソースを残しておきます
使用例
InteractiveViewer(
boundaryMargin: EdgeInsets.all(double.infinity), // 無限スクロール可能に
constrained: false,
minScale: 0.5,
maxScale: 2.0,
child: container ( // 好きな形を定義
)
)
今回の例ではそのまま InteractiveViewer
を使用していますが、普段はbody
ウィジェットに入れたりするのが普通です。
また
Stackについて
どこかにウィジェットを固定しておいておきたいときやウィジェット同士を重ねて遊びたいときにStackウィジェットを使用することによって実現できるらしいです
基礎①
最初にコンテナをチルドレンとして持たせてそれらを重ねて表示するようなプログラムを書いてみます。
画面例①
このような形で (配色が気持ち悪い) 3つのコンテナを重ねて簡単に表示することができます
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(
children: [
Container(
color: Colors.black,
width: 300,
height: 300,
),
Container(
color: Colors.red,
width: 200,
height: 200,
),
Container(
color: Colors.white,
width: 100,
height: 100,
)
], // children
),
);
}
}
基礎②(Alignウィジェット)
AlignmentクラスでStackを中心として子ウィジェットの位置取りを制御できる。
ちなみAlignは画面にとって相対的な位置を指定するようになっている
前回の例も含めStack
はbody
内にある。すなわちbody内の右端に生成されるということ
今回は黒コンテナと赤のコンテナを子ウィジェットとして
赤コンテナを右下に表示するようなものを作成してみました。
黒はディフォルトです
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(children: [
Container(
color: Colors.black,
width: 300,
height: 300,
),
Align(
alignment: Alignment.bottomRight,
child: Container(
color: Colors.red,
width: 200,
height: 200,
), // Container
), // Align
]), // Stack
);// Scaffold
}
}
基礎③(Positionedウィジェット)
ここではPositionedウィジェットを利用して描画の優先(前に表示とか後ろに表示)などを定義してみたいと思います。
またPositioned
は画面にとって 絶対的な位置
を指定するようになっている。
先に記述されたものが下に行きます。
またPositioned
には子ウィジェットを持つことができ、そこに制約を与えることができます(位置などの)
以上のような例で実際に作ってみましょう。
今回出力する画面例
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(children: [
Positioned(
top: 50,
left: 20,
height: 250,
width: 250,
child: Container(
width: 150,
height: 150,
color: Colors.red,
child: const Text("①"),
),
),
Positioned(
top: 130,
left: 50,
height: 250,
width: 250,
child: Container(
width: 150,
height: 150,
color: Colors.green,
child: const Text("②"),
),
),
Positioned(
top: 210,
left: 80,
height: 250,
width: 250,
child: Container(
width: 150,
height: 150,
color: Colors.purple,
child: const Text("③"),
),
),
]),
);
}
}
このように先に書いた (配色がきれいな) コンテナ達が並んでますよね
応用例?
スクロールや画面をサイズを変更しても固定で同じ場所に表示し続けるやり方を紹介します。
以下のイメージ図のようにどれだけ画面を拡大などしてもコンテナを画面の固定の場所に表示し続ける方法をご紹介します。
イメージ図
通常 | 画面を大きくしたとき |
---|---|
ここで勘が良い方はお気づきかもしれないですが、
実はめちゃくちゃ簡単で 画面に取っての相対位置を指定
で済むのです
つまり
Align
を利用すればいいだけです。
実際の実行画面
通常 | 画面を大きくしたとき |
---|---|
このように相対的な位置なので画面に対して右端を指定できていることがわかります。
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(children: [
Align(
alignment: Alignment(1, 0),
child: Container(
color: Colors.purple,
width: 100,
height: 100,
child: const Center(
child: Text("コンテナ"),
),
),
),
]),
);
}
}
GestureDetectorウィジェット
ボタンを使用せずに好きなコンテナなどをボタンのように扱うためのウィジェットです。
今回作成したい機能のイメージ図
という感じでStack
の子ウィジェットにGestureDetector
を入れてその子ウィジェットのコンテナをボタン化したいというものです。
ボタン押下前 | ボタン押下後 |
---|---|
※ 前紹介したものと同じようにコンテナは相対的に真ん中に表示されるソースコードです(画像の切り取り方の問題)
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 状態を保持する変数
String tapText = "タップして";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(children: [
GestureDetector(
onTap: () {
setState(() {
// テキストを更新
tapText = "タップ済み";
});
},
child: Align(
alignment: const Alignment(1, 0),
child: Container(
color: Colors.purple,
width: 100,
height: 100,
child: Center(
child: Text(
tapText,
style: const TextStyle(color: Colors.white),
),
),
),
),
),
]),
);
}
}
解説は深掘りしませんが、ほぼボタンと同じでコンテナが押された際にメンバ変数をsetStateで更新している形になっています。
Visibilityウィジェットについて
ボタンをや指定のコンテナをタップしたらそのコンテナやボタンを非表示にできるウィジェットを紹介します。
今回作成するもののイメージ図
ボタン押下前 | ボタン押下後 |
---|---|
コード例の前に解説。
今回はこれまでStack
の子ウィジェットとして色々機能を載せてきたコンテナを利用します。これも実はすごく簡単で、Visibility
の子ウィジェットとしてStackを子ウィジェットにすればいいだけです。
もしかしたらもっといい方法もあるのかも?
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 状態を保持する変数
String tapText = "タップして";
bool _isButtonVisible = true; // ボタンの状態
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Visibility(
visible: _isButtonVisible,
child: Stack(children: [
GestureDetector(
onTap: () {
setState(() {
// テキストを更新
tapText = "タップ済み";
_isButtonVisible = false;
});
},
child: Align(
alignment: const Alignment(1, 0),
child: Container(
color: Colors.purple,
width: 100,
height: 100,
child: Center(
child: Text(
tapText,
style: const TextStyle(color: Colors.white),
),
),
),
),
),
]),
),
);
}
}
応用例①(StackとGestureDetectorの組み合わせ)
今回上の基礎2つを組み合わせて (3つ必要だと思ったけど考えてみれば2つでいいやんってなった) コンテナ①を表示して,コンテナ②をタップしたらコンテナ②を表示をするような感じの表示非表示の切り替えをしてみたいと思います。
今回作成する画面のイメージ図
コンテナ①押下前 | ボタン押下後 |
---|---|
みたいな感じの設計です。
今回の考え方として
コンテナ①のフラグを管理することが一番大事です。
フローチャートにするほどでもないですが簡単にフローチャートにしてみました。
今回わかりやすさ優先でフローチャートにしたため実際の描画タイミングやif文,更新のタイミングが異なります。公式リファレンスや描画の記事などをご参照ください。
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 状態を保持する変数
String tapText1 = "タップして";
String tapText2 = "コンテナ②";
bool isContainer1Visible = true; // コンテナ①の表示状態
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(
children: [
// もしコンテナ1のフラグが立っていたらコンテナ①を表示処理
if (isContainer1Visible)
GestureDetector(
onTap: () {
// コンテナがタップされたら
setState(() {
// コンテナ②を表示する(フラグを下げるとコンテナ②が表示される)
isContainer1Visible = false;
});
},
child: Align(
alignment: const Alignment(1, 0),
child: Container(
color: Colors.purple,
width: 100,
height: 100,
child: Center(
child: Text(
tapText1,
style: const TextStyle(color: Colors.white),
),
),
),
),
),
// コンテナ①のフラグが立っていなかったら(コンテナ②の表示処理)
if (!isContainer1Visible)
GestureDetector(
onTap: () {
setState(() {
// コンテナ①を表示する
isContainer1Visible = true;
});
},
child: Align(
alignment: const Alignment(1, 0),
child: Container(
color: Colors.green,
width: 100,
height: 300,
child: Center(
child: Text(
tapText2,
style: const TextStyle(color: Colors.white),
),
),
),
),
),
],
),
);
}
}
応用②(InteractiveViewerとの組み合わせ)
ここではStack
の下にひょうじされるコンテナを無限スクロール可能として応用①と組み合わせをしてみようと思います。
このようなものを作ります。
基本的にStack
の考え方で
『 先に書いたものは下に来る 』 性質を利用して棲み分けを定義していきます。
これにより先にスクロールできる領域のコンテナをInteractiveViewer
で定義して
後で応用①で利用したコードを定義します。
コード例
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isContainer1Visible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(
children: [
// インタラクティブエリア
InteractiveViewer(
boundaryMargin: const EdgeInsets.all(500), // 十分に余裕を持たせる
minScale: 0.5,
maxScale: 2.0,
child: Stack(
children: [
// 背景
Container(
color: Colors.grey[600],
width: 2000,
height: 2000,
),
],
),
),
// コンテナ①
if (isContainer1Visible)
GestureDetector(
onTap: () {
setState(() {
isContainer1Visible = false;
});
},
child: Align(
alignment: const Alignment(1, 0),
child: Container(
color: Colors.purple,
width: 100,
height: 100,
child: const Center(
child: Text(
"タップして",
style: TextStyle(color: Colors.white),
),
),
),
),
),
// コンテナ②
if (!isContainer1Visible)
GestureDetector(
onTap: () {
setState(() {
isContainer1Visible = true;
});
},
child: Align(
alignment: const Alignment(1, 0),
child: Container(
color: Colors.green,
width: 100,
height: 300,
child: const Center(
child: Text(
"コンテナ②",
style: TextStyle(color: Colors.white),
),
),
),
),
),
],
),
);
}
}
参考記事等