Open48

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 getpubspec.yaml 設定ファイルに記述されたパッケージのインストールを行える。
iOSは、ios ディレクトリに移動した状態で pod install でSDKのインストールが行える。Andoirdはまだよく分かっていない。

pubspec.lockios/Podfile.lock がある場合は、一旦それを削除しないとコマンドを実行してもうまくパッケージやSDKのバージョンや構成が再構成されないようで詰まるポイントになりがち。

この辺の設定ファイルの意味の理解を深めていくことで、ビルドの際にエラーで詰まることを少なくしていけると思う。

あっきーあっきー

単に画面に文字を表示するだけのものならこれだけでもいけるっぽい。

main.dart
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!!'),
        ),
      ),
    );
  }
}
あっきーあっきー

StatefulWidgetcreateState() 関数によって State クラスのインスタンスを生成して状態を持つことができる。 StatelessWidget は文字通り State クラスを持つことは出来ない。
StatefulWidgetStatelessWidget を継承して宣言したクラスには、 build() 関数を使って中身に当たるウィジェットを記述していく。

それにしても、 StatelessWidget でウィジェットをラップするのは状態が変化するのを管理するためだとして、 StatelessWidget を使うのはなぜだろう。

あっきーあっきー

ios ディレクトリ配下の設定をいじる場合には、直接ファイルを編集する場合もあるが、 xcodeで開いて編集した方が早い場合(というかその方法しかない?)がある。
ios ディレクトリ配下のファイルはあくまでiOS用の設定ファイルなので、Flutterの設定ファイルとは分けて考えておく必要がありそう。Androidについても然りだと思う。

あっきーあっきー

このサンプルStatefulWidget を使って書き直すとこんな感じになるっぽい。つまり、 StatelessWidget は直接中にウィジェットを記述、 StatefulWidgetState の中にウィジェットを記述するということらしい。

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'))),
          ],
        ),
      ),
    );
  }
}

あっきーあっきー

ListTileは基本的にこんな感じの構成になっているっぽい。
中身の指定が child とか children じゃないのがたまにあるので困る。

LIstTile

あっきーあっきー

このように書けば左を固定、右を可変というようなレイアウトにすることができる。

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という下から出てくるメニューもあるっぽいけどこれ以上はごちゃつくので割愛。

Scaffoldウィジェット

あっきーあっきー

公式の解説を見てようやく分かった。
actionsは右側のウィジェットを表すのか。。actionsだなんて紛らわしい。なんらかの意味があってそうなっているんだろうけど。
drawerやendDrawerとはどう違ってどう使い分けるべきなのだろう。。

AppBar class

あっきーあっきー

この問題なんとなく理解した。
AppBarのleadingやactionsはヘッダーに直接アイコンメニューを設置する時に使うんだと思われる。
drawerやendDrawerを使うのはハンバーガーメニューの中にメニューを設置したい時に使うということだと思う。これについてもdrawerやendDrawerはハンバーガーメニューで使うことを想定しているからアイコンを変更するような考慮はされていないわけか。

なので、今自分がやりたいのはヘッダーの右側に設定アイコンを設置して設定画面に遷移させたいわけなので、AppBarのactionsにウィジェットを設置してメニューを表示するのが正解っぽい。

あっきーあっきー

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 になってしまうのでエラーになってしまうっぽい。
クラスに分けることができないわけではなさそうだが、特殊なクラス実装をしないとうまくできなさそう。

あっきーあっきー

ウィジェットの中身にウィジェットを指定する場合で特に多いパターンは childchildren
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
あっきーあっきー

ウェブ上の画像を表示させるにはこうするのか。勉強になる。

Image.network('https://xxxxx/xxx.png')
あっきーあっきー

Card ウィジェットの shape プロパティを使うと角丸を調整できるのか。。。
こういう細かいのはすぐに忘れちゃうからこうやって残しておけると助かる。

あっきーあっきー

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でDialogの表示を変更できなかったお話

ステータスを扱う上でウィジェットのベースが Stateless なのか Statefull なのかは気にしておいた方がよさそう。
一個前のRiverpodについても HookWidget StatelessConsumerWidgetStatefull になってる。中で 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),
      ),
    );
  }
}