Flutterアプリ作成についての覚書
Flutterアプリを作るのでモチベーション維持のために1日1回以上を目標に学んだことを残します。
勉強中のため間違っている可能性もあるので指摘大歓迎です。
現在のDartはNull Safetyらしい。
int x = null
のように通常の宣言ではnullの代入はできないため、nullを代入する必要がある際には int? x = null
のように書くとのこと。
引数の場合、{}
で囲んだ引数を定義している場合は引数に値が指定されない(引数がnullになる)可能性があるので required
を付けることによって引数の指定を強制することで引数にnullが入らないようにする。
カウンターアプリ の例で行くと、 MyHomePage
の引数 title
に対して required
を指定している。
class MyHomePage extends StatefulWidget {
final String title;
MyHomePage({Key? key, required this.title}) : super(key: key);
Flutter で開発する上で設定ファイルがいっぱいあって困る。
pubspec.yaml
がFlutterの設定ファイルで、ios/Podfile
がiOSの設定ファイル、 android/build.gradle
がAndroidの設定ファイルっぽい。
そして、 flutter pub get
で pubspec.yaml
設定ファイルに記述されたパッケージのインストールを行える。
iOSは、ios
ディレクトリに移動した状態で pod install
でSDKのインストールが行える。Andoirdはまだよく分かっていない。
pubspec.lock
や ios/Podfile.lock
がある場合は、一旦それを削除しないとコマンドを実行してもうまくパッケージやSDKのバージョンや構成が再構成されないようで詰まるポイントになりがち。
この辺の設定ファイルの意味の理解を深めていくことで、ビルドの際にエラーで詰まることを少なくしていけると思う。
単に画面に文字を表示するだけのものならこれだけでもいけるっぽい。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!!'),
),
),
);
}
}
StatefulWidget
は createState()
関数によって State
クラスのインスタンスを生成して状態を持つことができる。 StatelessWidget
は文字通り State
クラスを持つことは出来ない。
StatefulWidget
や StatelessWidget
を継承して宣言したクラスには、 build()
関数を使って中身に当たるウィジェットを記述していく。
それにしても、 StatelessWidget
でウィジェットをラップするのは状態が変化するのを管理するためだとして、 StatelessWidget
を使うのはなぜだろう。
ios
ディレクトリ配下の設定をいじる場合には、直接ファイルを編集する場合もあるが、 xcodeで開いて編集した方が早い場合(というかその方法しかない?)がある。
ios
ディレクトリ配下のファイルはあくまでiOS用の設定ファイルなので、Flutterの設定ファイルとは分けて考えておく必要がありそう。Androidについても然りだと思う。
このサンプル を StatefulWidget
を使って書き直すとこんな感じになるっぽい。つまり、 StatelessWidget
は直接中にウィジェットを記述、 StatefulWidget
は State
の中にウィジェットを記述するということらしい。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!!'),
),
),
);
}
}
ページ遷移を学んだ。
Navigatorを使うらしい。push()でページを重ねて、pop()でページを解除する。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ページ1'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return NextPage();
},
),
);
},
child: const Text('ページ2へ進む'),
),
),
);
}
}
class NextPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue,
appBar: AppBar(
title: const Text('ページ2'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('ページ1に戻る'),
),
),
);
}
}
ドロワーメニューはこう作る。
右側のメニューは endDrower
を使う。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ページ1'),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
ListTile(
title: Text('メニュー1'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
title: Text('メニュー2'),
onTap: () {
Navigator.pop(context);
},
),
],
),
),
);
}
}
ウィジェットの大きさを目一杯広げるには Expanded
ウィジェットを利用する。
親に Row
ウィジェットや Column
ウィジェットを指定している場合は、等間隔で並べることができる。 Row
なら横に等間隔、 Column
なら縦に等間隔に並べられる。
また、 flex
プロパティを使うとどの割合に幅を広げるかを指定することができる。例えば、3つ並べた際にそれぞれに flex: 1
を指定すると1対1対1(33.3%,33.3%,33.3%)の等間隔になり、 flex: 1
flex: 1
flex: 2
のように指定すると1対1対2(25%,25%,50%)で並べられる。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Row(
children: <Widget>[
Expanded(child: Container(color: Colors.red, child: Text('A'))),
Expanded(child: Container(color: Colors.blue, child: Text('B'))),
Expanded(child: Container(color: Colors.green, child: Text('C'))),
],
),
),
);
}
}
Flutterのレイアウトに参考になる記事を発見した。
ListTileは基本的にこんな感じの構成になっているっぽい。
中身の指定が child
とか children
じゃないのがたまにあるので困る。
このように書けば左を固定、右を可変というようなレイアウトにすることができる。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Row(
children: [
Container(
width: 100.0,
color: Colors.red
),
Expanded(
child: Container(
color: Colors.green
),
),
],
),
),
);
}
}
Containerウィジェットでできること。
- 一つのウィジェットを子に含めることができる
- 子のウィジェットの配置を指定することができる
- 背景の色を指定することができる
- マージン・パディングを指定することができる
このウィジェットはプロパティでいろいろできるのでわざわざCenterウィジェットなどを使ったりする必要がなくなる。できることを知っておくことで無駄なネストを防げる。
ちょっとだけやりたい形になった。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: ListView(
padding: const EdgeInsets.all(5),
children: [
MyListTile(),
MyListTile(),
MyListTile(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: new Icon(Icons.add),
),
),
);
}
}
class MyListTile extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Row(children: [
Container(
alignment: Alignment.center,
width: 30.0,
child: Column(children: [
Text(
'10',
style: TextStyle(
fontSize: 20,
),
),
Text('月'),
]),
),
Expanded(
child: Container(
padding: EdgeInsets.fromLTRB(20.0, 0, 0, 0),
child: Text('予定')
),
),
]),
),
);
}
}
Scaffoldウィジェットの使い方が微妙にわかりにくい。
AppBar, drawer, body とか、何を使えばどこになにを設置できるのか。
簡単にまとめたけど、これで全部なのかな?
ちなみに他にはbottomSheetという下から出てくるメニューもあるっぽいけどこれ以上はごちゃつくので割愛。
drawerのアイコンを変更する方法が謎に複雑で困る。
Scaffoldウィジェットのキーを取得してappBarのactionプロパティで定義する?のかな。
なぜそんな周りくどいやり方になるのか。。
公式の解説を見てようやく分かった。
actionsは右側のウィジェットを表すのか。。actionsだなんて紛らわしい。なんらかの意味があってそうなっているんだろうけど。
drawerやendDrawerとはどう違ってどう使い分けるべきなのだろう。。
Dartの基本もろくに分かってないままここまで来てたのでちょっとお勉強。
ざっくりと、変数の値を変更しないならfinal、変数の値を変更するならconst、クラス変数はstatic、だと理解した。
Flutter (Dart) での変数や定数の宣言方法
Dartの変数定義時の修飾static/final/const、そしてconst constructorについて
この記事とても綺麗にまとめっていて参考になりそう。
AppBarウィジェットの謎。
例えば、以下のような感じにScaffoldウィジェットのbodyに指定するウィジェットは以下のようにクラスを分離できる。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: MyBody(),
),
);
}
}
class MyBody extends StatelessWidget {
Widget build(BuildContext context) {
return Center(
child: Text('Hello World!!'),
);
}
}
でも、ScaffoldウィジェットのappBarに指定するAppBarウィジェットをクラスに分けて見るとエラーになる。
以下の場合、 line 10 • The argument type 'MyAppBar' can't be assigned to the parameter type 'PreferredSizeWidget?'.
というエラーが出る。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: MyAppBar(),
body: MyBody(),
),
);
}
}
class MyBody extends StatelessWidget {
Widget build(BuildContext context) {
return Center(
child: Text('Hello World!!'),
);
}
}
class MyAppBar extends StatelessWidget {
Widget build(BuildContext context) {
return AppBar();
}
}
どうやら、appBarに対しては PreferredSizeWidget
という特殊なウィジェットを指定する必要があるということらしい。
今回のようにクラスに分けてしまうと普通の Widget
になってしまうのでエラーになってしまうっぽい。
クラスに分けることができないわけではなさそうだが、特殊なクラス実装をしないとうまくできなさそう。
ウィジェットの中身にウィジェットを指定する場合で特に多いパターンは child
と children
。
child
は単一のウィジェットを指定し、 children
には複数のウィジェットを指定する。
実際のコードにすると以下のようになる。
child (例: Containerウィジェットの場合)
Container(
child: Text('Hello World!!'),
);
children (例: Columnウィジェットの場合)
Column(
children: <Widget>[
Text('Hello World!!'),
Text('Hello World!!'),
Text('Hello World!!'),
],
);
Flutterにパッケージ追加するには、 pubspec.yaml
に以下のようにパッケージの記述を追加して flutter package get
する。こういう基本的なこともやるたびに忘れてるから何やるにしても手が止まって困る。
dependencies:
xxxxx: ^1.0
BottomSheet
を使ってモーダルウィンドウを表示するのに困っていたけど、ちゃんと動くサンプルが載ってる記事が見つかった。角丸もしたかったので一石二鳥。ラッキー。
ウェブ上の画像を表示させるにはこうするのか。勉強になる。
Image.network('https://xxxxx/xxx.png')
Card
ウィジェットの shape
プロパティを使うと角丸を調整できるのか。。。
こういう細かいのはすぐに忘れちゃうからこうやって残しておけると助かる。
余白の取り方1つとってもいろいろやり方があるんだなー。
こういうのが分かっていないとレイアウトに苦労するイメージがある。
Flutter2.0では RaisedButton
ではなく ElevatedButton
を使うのが正解らしい。
Containerウィジェットのchildプロパティにウィジェットを指定してしまうと大きさが内容に合わせて縮んでしまうが、 constraints: BoxConstraints.expand()
を指定することで幅を幅が広がってくれる。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
constraints: BoxConstraints.expand(),
decoration: BoxDecoration(color: Colors.blueAccent),
child: Text('A')
),
),
);
}
}
constraints: BoxConstraints.expand()
を使えば、 Expanded
ウィジェットは要らなくなるということはないっぽい。 Row
ウィジェットや Column
ウィジェットを使うときは Expanded
ウィジェットが必要。
ただし、中身には constraints: BoxConstraints.expand()
を指定しないと横幅もしくは縦幅が内容に合わせて縮んでしまうので注意。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
Expanded(
child: Container(
constraints: BoxConstraints.expand(),
decoration: BoxDecoration(color: Colors.blueAccent),
child: Text('A')
),
),
Expanded(
child: Container(
constraints: BoxConstraints.expand(),
decoration: BoxDecoration(color: Colors.greenAccent),
child: Text('B')
),
),
],
),
),
);
}
}
カレンダー表示ができるようになった。
Flutterでライブラリを使ってカレンダーを実装する を参考に実装。
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_calendar_carousel/classes/event.dart';
import 'package:flutter_calendar_carousel/flutter_calendar_carousel.dart'
show CalendarCarousel;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: CalenderExample(),
);
}
}
class CalenderExample extends StatefulWidget {
State<StatefulWidget> createState() {
return _CalenderExampleState();
}
}
class _CalenderExampleState extends State<CalenderExample> {
DateTime _currentDate = DateTime.now();
void onDayPressed(DateTime date, List<Event> events) {
this.setState(() => _currentDate = date);
Fluttertoast.showToast(msg: date.toString());
}
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Calender Example"),
),
body: Container(
child: CalendarCarousel<Event>(
onDayPressed: onDayPressed,
weekendTextStyle: TextStyle(color: Colors.red),
thisMonthDayBorderColor: Colors.grey,
weekFormat: false,
height: 420.0,
selectedDateTime: _currentDate,
daysHaveCircularBorder: false,
customGridViewPhysics: NeverScrollableScrollPhysics(),
markedDateShowIcon: true,
markedDateIconMaxShown: 2,
todayTextStyle: TextStyle(
color: Colors.blue,
),
markedDateIconBuilder: (event) {
return event.icon;
},
todayBorderColor: Colors.green,
markedDateMoreShowTotal: false,
),
)
);
}
}
setState()は更新を通知するための関数。この関数の中で変数を変更することで変更が通知される。
setState()を使わずに変数を変更しても変更が通知されず画面が更新されない。
showDialog()はダイアログを生成するための関数。showDialog()を使うことで画面にダイアログを重ねて表示することができる。 Navigator.pop(context);
でダイアログを解除できる。
地味に便利なの見つけた。
この記事を参考にAlertDialogを使ったシンプルな構成を作った。これをいじって仕組みを勉強しよう。
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
)
);
class MyApp extends StatefulWidget {
State<StatefulWidget> createState() {
return _State();
}
}
class _State extends State<MyApp> {
_close(BuildContext context) {
Navigator.pop(context);
}
Future _showAlertDialog(BuildContext context) async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
content: Text('Hello World!!'),
actions: <Widget>[
ElevatedButton(
child: Text('OK'),
onPressed: () => _close(context),
),
],
);
},
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAlertDialog(context),
child: new Icon(Icons.add),
),
);
}
}
fullscreenDialog: true
を付けるだけで下から競り上がってくるページが作れるみたい。
【Flutter】下から出てくる画面遷移の実装方法
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.blue),
home: MyApp(),
)
);
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ページ1'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return NextPage();
},
fullscreenDialog: true,
),
);
},
child: new Icon(Icons.add),
),
);
}
}
class NextPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ページ2'),
),
);
}
}
main()のところの記述はこれを基本形にしようかな。
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: MyApp(),
)
);
【Flutter】MaterialAppにて最初に起動させたいページの設定方法について
これはいつかお世話になりそうな記事。
ステータス管理にはRiverpodを使う予定だけど、この記事はRiverpodについて綺麗にまとまっている印象。勉強になる。
ステータスを扱う上でウィジェットのベースが Stateless
なのか Statefull
なのかは気にしておいた方がよさそう。
一個前のRiverpodについても HookWidget
は Stateless
、 ConsumerWidget
は Statefull
になってる。中で Statefull
なウィジェットを扱うなら ConsumerWidget
の方が良いとかになるのかな…?
これは勘違いだったみたいで、 HookWidget
の中でも StatefullWidget
は置けるっぽい。単にStateを変更するような処理は出来ないってだけらしい。
一旦ここまで出来た。
別ページでカレンダーを開いて、選択した日付を記憶する。ってとこまで。
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_calendar_carousel/classes/event.dart';
import 'package:flutter_calendar_carousel/flutter_calendar_carousel.dart'
show CalendarCarousel;
void main() => runApp(
ProviderScope(
child: MaterialApp(
home: App(),
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.green),
)
)
);
class App extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('予定'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Calendar();
},
fullscreenDialog: true,
),
);
},
child: new Icon(Icons.add),
),
);
}
}
class Calendar extends HookWidget {
Widget build(BuildContext context) {
final controllerList = [
useTextEditingController(),
useTextEditingController(),
];
final resultState = useProvider(resultProvider);
final resultNotifier = context.read(resultProvider.notifier);
return Scaffold(
appBar: AppBar(
title: const Text('予定登録'),
),
body: Center(
child: Column(
children: <Widget>[
CalendarCarousel<Event>(
onDayPressed: (DateTime date, List<Event> events) {
resultNotifier.setDate(
date: date.toString(),
);
},
weekendTextStyle: TextStyle(color: Colors.red),
weekFormat: false,
height: 420.0,
selectedDateTime: (resultState.resultDate == '')
? DateTime.now() : DateTime.parse(resultState.resultDate),
daysHaveCircularBorder: true,
customGridViewPhysics: NeverScrollableScrollPhysics(),
markedDateShowIcon: true,
markedDateIconMaxShown: 2,
markedDateIconBuilder: (event) {
return event.icon;
},
markedDateMoreShowTotal: false,
),
Text('${resultState.resultDate}'),
],
),
),
);
}
}
final resultProvider = StateNotifierProvider<ResultController, ResultState>(
(_) => ResultController(),
);
class ResultState {
const ResultState(this.resultDate);
final String resultDate;
ResultState copyWith({String? resultDate}) {
return ResultState(resultDate ?? this.resultDate);
}
}
class ResultController extends StateNotifier<ResultState> {
ResultController() : super(const ResultState(""));
void setDate({required String date}) {
state = state.copyWith(resultDate: date);
}
}
flutter_calendar_carousel
パッケージの中身読んでるんだけど、 widget
という変数はウィジェット自体のオブジェクトを表しているのか。 this
みたいなものだと理解した。
StatelessWidget の引数渡しバージョン。
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: MyApp('Hello World!!'),
)
);
class MyApp extends StatelessWidget {
final String contents;
MyApp(this.contents);
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(contents),
),
);
}
}
StatefulWidget の引数渡しバージョン。
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: MyApp('Hello World!!'),
)
);
class MyApp extends StatefulWidget {
final String contents;
MyApp(this.contents);
_MyAppState createState() => _MyAppState(contents);
}
class _MyAppState extends State<MyApp> {
String contents;
_MyAppState(this.contents);
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(contents),
),
);
}
}