LoginSignup
88
89

More than 1 year has passed since last update.

初心者がFlutterでアプリ開発するための知見まとめ

Last updated at Posted at 2020-03-11

はじめに

日頃、Flutter情報を纏めていたNotionというツールに情報突っ込みすぎて、「更に使いたいなら有料プランへどうぞ」的なのが出てきたのでこちらに情報を整理しつつ退避して、Notionを軽くしようと思います。

ちまちま纏めていきますので、末永くお付き合いください。

また、初心者ゆえの情報の誤り等がある可能性があることは念頭に置いて御覧ください。(誤りがありましたらご指摘いただければよろこんで修正致します)

Flutterのはじめかた

環境構築的なことは死ぬほど情報があるのでここでは書きません。
もし、あなたがAndroidアプリを作ったことがあるのであれば、以下の記事をご覧いただければ大体の感覚は掴めるのではないでしょうか。

Android開発者のためのFlutter説明文がためになったので全訳&要約 - Qiita https://qiita.com/coka__01/items/db6d482af1a1994aaa73

と、丸投げしましたが、まずは以下のテンプレで初めてみては如何でしょうか。

main()がアプリのエントリポイントになりますので、必須の関数になります。

import 'package:flutter/material.dart';

void main() {
 runApp(App());
}

class App extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     debugShowCheckedModeBanner: false,  // アプリ右上に表示されるDebugの帯を付けるか否か
     home: Scaffold(
       appBar: AppBar(title: Text("サンプル")) // ヘッダー
       body: Center(child: Text("サンプルアプリ")),
     ),
   );
 }
}

これでビルドしてみてください。味気ないアプリが動くと思います^^;

Flutterの特徴として、Hot Reloadというものがあり、一度、ビルドして動作させたあとはコードを弄ってSaveするたびにエミュレータ/実機に変更が反映されます。(ただし、ネストが深くなったりするとHot Reloadが動作しない場合がありますのでご注意ください)

以下、わかり易い説明がありましたのでこちらから抜粋します。

App Widget はMaterialデザインを作るための MaterialApp Widget をもっており、MaterialApp Widget は モバイルアプリの基本的な構造を持つ Scaffold Widget からなります。Scaffold Widget の body プロパティは アプリ( Scaffold Widget )のメインコンテンツの Widget を 表しています。今回はこのbodyプロパティは 子(childプロパティ)の Widget を真ん中に配置する位置指定の Center Widget を持っており、Center Widget は 文字列を表示する Text Widget を持っているという入れ子構造になっています。言葉で説明するとややこしいですが、図にするとイメージしやすいです。

この後、ヘッダーとフッターをつくっていきますが、Widget を 入れ子にして UI 作っていくということを念頭に置いておくと後の流れがわかりやすいと思います。
 ちなみに、記事の途中ですが、どんな Widget があるのか詳細を知りたい方は個人的に下記のページをおすすめします。私もこの記事の作成にあたり、どの Widget を使うかで参考にさせていただきました。

Flutter:Widget一覧

Flutterプロジェクトにおける各ファイル/フォルダの意味

Flutterの環境構築が終わってさてFlutterアプリの雛形を作ると出てくるフォルダ/ファイル類。おおざっぱに以下に整理します。

pubspec.lock

パッケージ管理ファイルのpubspec.yamlでは管理しきれない依存関係を管理します。

コードを見る必要はありませんが、触る必要もありません。

下手に触ってしまうと、アプリを動かすためのパッケージをあやまって消した時に同じパッケージ構成を再現できないことがあります。

pubspec.yamlは人が見て編集する用、pubspec.lockpubspec.yamlよりもさらに詳細な依存関係が管理されています。(そんなの入れたっけ的なやつが大半です)

.packages

.packagesファイルには、アプリケーションで使用される依存関係のリストが含まれています。

Flutterはどこにパッケージを置いているのか気になっていましたが、隠しファイルにキャッシュしているようですね〜

このファイルは基本的に見る必要はありません、

.metadata

このファイルはこのFlutterプロジェクトのプロパティを追跡します。

Flutterツールで機能を評価し、アップグレードなどを実行するために使用されます。

このファイルはバージョン管理されている必要があり、手動で編集しないでください。

と、ファイル自体に説明が書かれていますが、なんなんですかね〜

Dart記法

いきなり丸投げですが、以下でも読んでいただいたほうがてっとり速いかなと思います。

Dartの基本文法を振り返る - Qiita https://qiita.com/sileader/items/875f7f1fdc3995ceafda

私が知らなかったところと特徴的なところだけ抜粋します。
まずはdynamicとvarについて。

Dartの型の表記にはdynamicvarもあります。
dynamicは動的型でListなどのように書きたい時にも使えます。
varは型推論を行い代入時の型になります。なので、再代入時に別の型の値を入れるとエラーになります。

動的型を乱用することは良いとは思いませんが、動的型を使わないと記述不可能な場面は出てくると思いますので、ここぞという時に使うと良いと思います。

finalとconst。constはちょーつよいfinalって認識。constは初期値がないとエラーになります。

Dartは型の修飾子としてfinalconstが存在しています。
finalはJavaのfinalと同じ意味です。つまり再代入不可であることを示します。
constはコンパイル時定数。つまり、「再代入不可 + そのメモリ領域の書き換え不可」を表します。

名前付きコンストラクタ。あまり使いみちは分かっていない…

class Klass {
  Klass.fromString(String f);
}

ファクトリ。こちらも効果的な使い方は不明。 Singletonを定義できるので、2つとインスタンスを作られたくない処理を書くときに(共通設定画面系とか)。

class Klass {
  Klass._private(); // 非公開な名前付きコンストラクタ
  factory Klass() {
    var instance = Klass._private(); // newは省略できる
    return instance;
  }
}

定数コンストラクタ。

class Klass {
  final String name;
  const Klass(this.name); // this.nameのようにするとnameという引数で受けそのままthis.nameに代入できる
}

ちなみに、クラスのフィールドに値を代入する際は、コンストラクタの引数を活用する(initializing formalというらしい)。
以下のようにコンストラクタの引数を直接フィールドに代入できます。

initializingFormalの例

class Point {
  num x, y;
  Point(this.x, this.y);
}

以下のようにfinalなフィールドはコンストラクタのbodyで初期化できないため、Initializerという機能を使う。Initializerはコロンに続いてコンマ区切りの初期化処理を書くことができる。

class Dog {
  String name;
  final Owner owner;

  Dog(this.name): owner = Owner('John');
}

Initializerを使って、別のコンストラクタにリダイレクトすることもできる。

class Dog {
  String name;
  final Owner owner;

  Dog(this.name): this.owner = Owner('John');

  Dog.pochi(): this.name = 'Pochi',
               this.owner = Owner('John');

  Dog.taro(String name): this(name);
}

繰り返し

forはフツー?なのでそれ以外を。

forEach文

List<int> list = [1, 2, 3];
Map map = { 1 : 'first',
            2 : 'second',
            3 : 'third',
}; 
// forEach
list.forEach((int value){print(value);});
map.forEach((key,value){print('$key : $value');});

for-in文

List<int> list = [1, 2, 3];
// for-in
for(var value in list){
    print(value);
}

do-while文

List<int> list = [1, 2, 3];
var index = 0;
do {
    print(list[index]);
    index++;
} while(index < list.length);

覚えておくべき関数

操作するリスト
var fruits = ['banana', 'pineapple', 'watermelon'];
var numbers = [1, 3, 2, 5, 4];

map()

与えられた要素に処理を掛けたあとにその要素群に対する新しいリストを作成する。

 var mappedFruits = fruits.map((fruit) => 'I love $fruit').toList();
 print(mappedFruits); // => ['I love banana', 'I love pineapple', 'I love watermelon']

reduce() / fold()

与えられた関数を使って、要素を単一の値に圧縮する。

 var sum = numbers.reduce((curr, next) => curr + next);
 print(sum); // => 15
 const initialValue = 10;
 var sum2 = numbers.fold(initialValue, (curr, next) => curr + next);
 print(sum2); // => 25

Mix-in

以下のサイトの説明が丁寧と思われる。現時点で自分はあまり使いそうにないので割愛。

すぐにFlutterを始めたい人のためのDart入門(前編) https://itome.team/blog/2019/12/flutter-advent-calendar-day3/

名前付き引数

チョー重要。

名前付き引数は引数の名前を指定して値を渡す引数です。
名前付き引数を使うことで、次のような利点があります。

  1. 仮引数名が適切であれば読みやすいコードになる
  2. 引数の順番にこだわる必要がない

名前付き引数は引数を{}で括ります。

func({first, last});

// 呼び方
func(first: 'f'); // firstだけ指定
func(last: 'l'); // lastだけ指定
func(first: 'f', last: 'l'); // firstとlastを指定

必須の引数は @required を付ける。オプション引数は [] で囲う。

クラスのsetter/getterは以下の通り。

class Klass {
  String _name; // 外部からは取得だけ許可したい

  String get name => _name;
}
class Klass {
  String _name;

  String get name => _name;
  set name(String v) {
    _name = v;
  }
}

Listの扱い方

  • ここが参考になります。

  • よく使われ処理を列挙します

操作するリスト
var fruits = ['banana', 'pineapple', 'watermelon'];
var numbers = [1, 3, 2, 5, 4];

  • forEach()

     fruits.forEach((fruit) => print(fruit)); // => banana pineapple watermelon
    
  • map()

    var mappedFruits = fruits.map((fruit) => 'I love $fruit').toList();
    print(mappedFruits); // => ['I love banana', 'I love pineapple', 'I love watermelon']
    
  • contains()

     print(numbers.contains(6)); // => false
     print(fruits.contains('pineapple')); // => true
    
  • sort()

    print(numbers.sort((num1, num2) => num1 - num2)); // => [1, 2, 3, 4, 5]
    print(fruits.sort((a, b) => a.length.compareTo(b.length))); // => [banana, pineapple, watermelon]
    
  • reduce(), fold()

    var sum = numbers.reduce((curr, next) => curr + next);
    print(sum); // => 15
    const initialValue = 10;
    var sum2 = numbers.fold(initialValue, (curr, next) => curr + next);
    print(sum2); // => 25
    
  • every()

     List<Map<String, dynamic>> users = [
       { "name": 'John', "age": 18 },
       { "name": 'Jane', "age": 21 },
       { "name": 'Mary', "age": 23 },
     ];
     var is18AndOver = users.every((user) => user["age"] >= 18);
     print(is18AndOver); // => true
    
     var hasNamesWithJ = users.every((user) => user["name"].startsWith('J'));
     print(hasNamesWithJ); // => false
    
  • where(), firstWhere(), singleWhere()

    // See #6 for users list
     var over21s = users.where((user) => user["age"] > 21);
     print(over21s.length); // => 1
     var nameJ = users.firstWhere((user) => user["name"].startsWith(J), orElse: () => null);
     print(nameJ); // => {name: John, age: 18}
     var under18s = users.singleWhere((user) => user["age"] < 18, orElse: () => null);
     print(under18s); // => null
    
  • take(), skip()

     var fiboNumbers = [1, 2, 3, 5, 8, 13, 21];
     print(fiboNumbers.take(3).toList()); // => [1, 2, 3]
     print(fiboNumbers.skip(5).toList()); // => [13, 21]
     print(fiboNumbers.take(3).skip(2).take(1).toList()); // => [3]
    
  • List.from()

    var clonedFiboNumbers = List.from(fiboNumbers);
    print('Cloned list: $clonedFiboNumbers');
    
  • expand()

    var pairs = [[1, 2], [3, 4]];
    var flattened = pairs.expand((pair) => pair).toList();
    print('Flattened result: $flattened'); // => [1, 2, 3, 4]
    var input = [1, 2, 3];
    var duplicated = input.expand((i) => [i, i]).toList();
    print(duplicated); // => [1, 1, 2, 2, 3, 3]
    
    • Wigetを繰り返し配置するfor文の例
  final children = <Widget>[];
  for (var i = 0; i < 10; i++) {
    children.add(new ListTile());
  }
  return new ListView(
    children: children,
  );

or

  return new ListView(
    children: new List.generate(10, (index) => new ListTile()),
  );

ジェネリクス

main() {
  // ジェネリックスのインスタンス化は型を指定する
  var cache = new Cache<String>();
  cache.setByKey('key', 'test');
  print('key=${cache.getByKey('key')}');

  // メソッドのジェネリックス
  // 型の制限をするときは型のextendsを使う
  T sum<T extends num>(List<T> list, T init){
    T sum = init;
    list.forEach((value) { 
      sum += value;
    });
    return sum;
  }

  int r1 = sum<int>([1,2,3], 0);
  print(r1);
  // 型指定しない場合は左辺の型に暗黙的に型推定される
  double r2 = sum([1.1, 2.2, 3.3], 0.0);
  print(r2);
}

// 型だけが違う実装をしたクラスを実装したい場合はジェネリックスを使うと便利
// インスタンス化するときTに型を指定する
class Cache<T> {
  Map<String, T> store = <String, T>{};

  T getByKey(String key) {
    return store[key];
  }

  void setByKey(String key, T value) {
    this.store.addAll(<String, T>{key: value});
  }
}

列挙型

// 列挙型
// 列挙子は宣言された順にインデックス(0始まり)が割り振られていて、 index で参照できる。
// enumを継承できなかったり(mixinにも使えない)、enumのインスタンスを自前で生成できない(定数のみしか使えない)
// 実装を持つことが出来ない以外、Javaとほぼ同じ
enum Color { red, green, blue }

例外処理

main() {
  void errorFunc() {
    try {
      // throw Exceptionで意図的に例外を投げる
      throw Exception('例外です');
    } on Exception catch(e) {
      // 捕まえる型を指定するには on ~~ catch を使う
      // eはException型
      print(e);
      // rethrowでtry-catch-finallyブロックの外に例外を投げ直す事ができる(関数の外などでcatchする必要あり)
      rethrow;
    } finally {
      // finallyブロックは例外の有無にかかわらず実行される、省略可。
      print('finally');
    }
  }

  // 例外処理:try-catch文
  try {
    errorFunc();
  } catch (e, s) {
    // 型を指定しないcatchは、何型かわからない例外全部キャッチする
    // catchに仮引数を2つ指定すると、2つ目はStackTraceオブジェクトが入る
    print(s);
  }
}

その他、知らなかった書き方

??は左側がnullの場合、右を返す。

return '$from says $msg platform: ${device} mood: ${mood ?? 'unknown'}';    

?.という書き方もある。

main () {
    // メソッド呼び出しを`?.`とすることで、nullならそのメソッドが実行されなくなる。
    a = null;
    assert(null == a?.map((v) => v * 2)?.reduce((v1, v2) => v1 + v2));

    // nullじゃなければ普通に実行される。
    a = [1, 2, 3];
    assert(12 == a?.map((v) => v * 2)?.reduce((v1, v2) => v1 + v2));
}

??=は左辺がnullのときだけ右辺の値を代入する。上の?系含め、これらはNull-aware演算子というらしい。

String str = null;
str ??= '';

// ↓と同じ
String str = null;
if (str == null) {
  str = '';
}

isは型判定するときに使う。

var a = "value"
//Stringなのでtrueがかえります
a is String

カスケード記法。

main() {
  // カスケード記法、..で対象のインスタンスに対するメンバ関数呼び出しの操作を続けられる
  var fullString = StringBuffer()
    ..write('Use a StringBuffer for ')
    ..writeAll(['efficient', 'string', 'creation'], ' ')
    ..toString();
  print(fullString);
  // 次と等価
  // var sb = StringBuffer();
  // sb.write('Use a StringBuffer for ');
  // sb.writeAll(['efficient', 'string', 'creation'], ' ');
  // var fullString = sb.toString();
  // print(fullString);
}

ドキュメントコメント

  • 型や関数は[]で囲む
  • サマリーを1行目に書き、2行目は開ける
  • コードサンプルを書く場合は```で囲む
  • 他の言語でよくある@paramなどは使用せず、文章で説明する
/// Defines a flag.
///
/// Throws an [ArgumentError] if there is already an option named [name] or
/// there is already an option using abbreviation [abbr]. Returns the new flag.
///
/// ```
/// addFlag('sampleName', '.....');
/// ```
Flag addFlag(String name, String abbr) => ...

part

外部のパッケージを読み込むときはimportを使いますが、自分自身のパッケージ内のファイルの依存関係の解決はpartとpart ofを使う。それぞれ、

  • part
    • 私のパーツはこの子です
  • part of
    • 私はこの親のパーツです

という宣言になる。

// something.dart(親)
library something;

part "src/something_foo.dart";

void main () {
    Foo foo = new Foo();
}
// src/something_foo.dart(子)
part of something;

class Foo() {
    // something
}

Flutterのコーディングのススメ方

初歩的なところ

  • まずはアプリのベースとなるWidgetを知る

    • 以下のようなテンプレをスニペットにでも登録しとくとよいかも
    void main() { // main.dartは基本これだけを呼ぶ
      runApp(MaterialApp( // runAppにはStatelessWidget or StatefullWidgetを継承したものを渡す
        home: HomePage(),  //homeで、ルーティング情報ここにかいてく。
      ));
    }
    
    class HomePage extends StatefulWidget {
      @override
      _HomePageState createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(); //Scaffold() アプリの中身(appbar+title構造)を簡単につくれる
      }
    }
    

    タイトルとバーを作るスニペット

      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("あああああ"),
            backgroundColor: Colors.green,
          ),
        );
      }
    

    scaffold()は、バー、タイトル、中身、アクション、ボトムの典型的なアプリ構造を簡単にできる仕組みらしい。

  • StatefulWidgetとStatelessWidgetを知る

    • Statelessは静的なコンテンツを表示する際に使うWidget
    • Statefulは状態を持つ、動的な画面を作る際に使うWidget
    • どちらも内部にbuildメソッドを持ち、buildメソッドの返り値としてレンダリングしたいWidgetを記載する
    • StatefulWigetはStateクラスを継承したクラスを作成し、そのクラスがもつsetStateメソッドで状態を変化させることでbuildによる再レンダリングを呼び出し、画面描画しなおす(Rebuild)
    • このsetStateを呼びまくってbuildが走りまくるとめっちゃ重いアプリになること必須
    • setStateで状態を保持する変数は、一時的な変数として、_(アンダーバー)で始まるローカル変数とするとよい。
      • なのでBLoC等のデザインパターン?があると認識
  • レイアウトを構成するWidgetを知る

    • Wiget → child or children → Wiget 以下無限ループ、みたいなWigetの入れ後構造でレイアウトを組んでいくことになります
    • Row、Column、Padding、Continer等のレイアウトを組むWidgetの使いかたを覚えましょう
    • ListviewやGridView、Card等、簡単に見栄えの良いコンテンツを表示できるWidgetの使い方も覚えておくと便利です
    • ListTileも簡単に画像+タイトルみたいなリストを作れるので便利ですよ!
  • コンストラクタを知る(上にも書いてあるけど再掲)

    • super 基本クラスのコンストラクターを呼び出すために使用されます。

    Key Flutterでウィジェットを識別するために使用されるタイプで、ツリー内で移動されたウィジェットが以前に別の場所にあったウィジェットと同じである場合にFlutterが認識できるようにします。キーに関する良いビデオがここにあります:https://www.youtube.com/watch?v=kn0EOS-ZiIc
    - 名前付きコンストラクタとリダイレクトコンストラクタはちょっとわかりづらいが、こちらが分かりやすいと思います。ちょっと抜粋させていただくと、

    Column(
    children: <Widget>[
     MemberCard.normal(name: '鈴木 たかし', imgFileName: 'avator1.png'),
     MemberCard.disabled(name: '高橋 ゆうさく', imgFileName: 'avator2.png'),
     MemberCard.normal(name: 'Sarah Adams', imgFileName: 'avator3.jpeg'),
     MemberCard.normal(name: 'Richard Mason', imgFileName: 'avator4.jpeg'),
    ]
    )
    

    オプショナルにしたければ{}で囲む。必須なら@requiredを付けるのがオススメ。

    以下も抜粋しますが、イニシャライザ(:)につづいてthis.コンストラ名と記載するとコンストラクタの処理をリダイレクトすることができる。

  • 画面遷移の仕方を知る

    • Flutterでは、ページ間を移動する方法がいくつかあります。 ・route名をMapに指定(MaterialApp) ・routeに直接移動する(WidgetApp)
    • 例として、以下のように定義し、
    void main() {
      runApp(MaterialApp(
        home: MyAppHome(), // becomes the route named '/'
        routes: <String, WidgetBuilder> {
          '/a': (BuildContext context) => MyPage(title: 'page A'),
          '/b': (BuildContext context) => MyPage(title: 'page B'),
          '/c': (BuildContext context) => MyPage(title: 'page C'),
        },
      ));
    }
    

    以下のように呼び出すことで画面遷移が可能

    Navigator.of(context).pushNamed('/b');
    
  • スタックに画面を積むイメージです。pushで積んでpopで取り去るイメージを持って貰えればよいかと思います。

以下のようなRouteの作り方もある。

  Navigator.of(context).push(
    MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Hello')),
          body: ...,
        );
      },
    ),
  );

引数を渡すときは、以下のようにし、

  Navigator.of(context).pushNamed('/a', arguments: 'Hello');

受け取り側では以下のように値を取り出す。

  final args = ModalRoute.of(context).settings.arguments;

  print(args); // 'Hello'

複数の値を渡したいときは、以下のようにクラスを作る。

  class MyPageArguments {
    MyPageArguments({this.name, this.greeting});

    final String name;
    final String greeting;
  }

  final args = ModalRoute.of(context).settings.arguments;

  print(args); // MyPageArguments { name: .., greeting: .. }

遷移先から遷移元へ返却したい値を pop メソッドに渡すこともできます。

  Navigator.of(context).pop('Hi!');

遷移元は、 Navigator.push メソッドが返す Future から 遷移先が返した値を取り出すことができます。 Future で値を返すのは他のフレームワークと比べても珍しいですが、ギャラリーを開いて画像を取得したり 外部サービスでログインしてからアプリに戻ってきたりなどのときにも使うFlutterの頻出パターンです。

  final result = await Navigator.of(context).pushNamed('/a', arguments: 'Hello');

  print(result); // 'Hi';

Futureとasync/await

  • 非同期処理を含むクラスはFuture型とする。

  • JavaScriptと同じくasnyc/await方式。

    • 非同期処理(時間を要し、ユーザビリティに影響を与え得る処理)が必要な関数にはasync宣言
    • awaitを書くことで以降の処理を待ち合わせる(メソッド自体がasyncで非同期になっているため、シングルスレッドを妨害しない)
  • アロー演算子で書いた非同期処理は以下みたいな感じ

  const foo = async () => {
    // do something
  }
  • 即時関数として書いた場合は以下みたいな感じ
  (async () => {
      console.log(await asyncFunction());
  })();

FutureBuilder

こちらがよく纏まっています。

StreamBuilder

StreamBuilderのイメージは、コードの順序関係なく流れてきたデータをキャッチして処理を行う感じです。
そして2つの要素、streamとbuilderから構成されています。

 Widget userName() {

    return StreamBuilder<QuerySnapshot>(

        //表示したいFiresotreの保存先を指定
        // snapshotsでデータ取得
        stream: Firestore.instance
        .collection('users')
        .document(firebaseUser.uid)
        .collection("transaction")
        .snapshots(),

        //streamが更新されるたびに呼ばれる
        builder: (BuildContext context,
            AsyncSnapshot<QuerySnapshot> snapshot) {

          //データが取れていない時の処理
          if (!snapshot.hasData) return const Text('Loading...');

          return Text(snapshot.data.documents[0]['userName']);

        }
     );
  }

key

以下をあとで纏める予定!!

Flutter Widget Keyの種類と使い方について - Qiita

ちょっとした通知と簡単なアクションを促すのに便利なスナックバー - 各種 Material ウィジェットの使い方 - Flutter 入門 https://flutter.keicode.com/basics/snackbar.php

Intentの処理方法

Under Construction

StatefulWidgetのメソッド構成

クラス メソッド メソッド
Widget createState() ビルド後に呼ばれるメソッドで必須。型はState。
State initState() 最初に一度呼ばれる。Widgetツリーの初期化を実行。
didChangeDependencies() initState()呼び出し直後に呼ばれる。Widgetツリーの変更を要素に通知する。
build() didChangeDependencies()呼出し後にしか呼ばれない。複数回呼ばれる。Widgetツリーを置き換える。
didUpdateWidget(Widget oldWidget) リビルド時のinitState()的な位置づけ。
setState() 任意で呼べるメソッド。Widgetツリーを再構成して、変更を反映させる。簡単な変数の代入だけでなく非同期処理でも使える。
deactivate() StateをWidgetツリーから削除する。滅多に使わないらしい。
dispose() Stateを永続的に削除する。画面をおとすときやストリーム停止で使うらしい。

Collection if

Flutterのchildrenが書きやすいようになるためだけに生まれたような新機能です。childrenの要素に条件分岐が入るときでも宣言的な構文を崩さずにかけます。

Widget build(BuildContext context) {
  var children = [
    IconButton(icon: Icon(Icons.menu)),
    Expanded(child: title)
  ];

  if (isAndroid) {
    children.add(IconButton(icon: Icon(Icons.search)));
  }

  return Row(children: children);
}

Collection for

Collection ifと一緒に追加された機能。こちらもFlutterの影響がかなり大きいです。

Widget build(BuildContext context) {

  return Row(
    children: titles.map((title) => Text(title)).toList(),
  );
}
Widget build(BuildContext context) {
  return Row(
    children: [
      for (final title in titles) Text(title),
    ],
  );
}

カスタムフォントを使う

  • pubspec.yamlに以下を記載(fontsフォルダは作ってください)
  fonts:
     - family: MyCustomFont
       fonts:
         - asset: fonts/MyCustomFont.ttf
         - style: italic

そして、フォントをTextWidgetに割り当てる

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: Text(
          'This is a custom font text',
          style: TextStyle(fontFamily: 'MyCustomFont'),
        ),
      ),
    );
  }

覚えておきたいWidget

Text

基本的な使い方

Text(
                  'Text largeeeeeeeeeeeeeeeeeeeeeee',
                  overflow: TextOverflow.ellipsis,
                  style: new TextStyle(
                    fontSize: 13.0,
                    fontFamily: 'Roboto',
                    color: new Color(0xFF212121),
                    fontWeight: FontWeight.bold,
                  )

長くなってはみ出しそうなときは、EllipsisもしくはFadeを使うとよい

TextField

「TextEditingController」(以下ではそのインスタンスをeCtrlとしている)はテキスト入力を制御するのに利用します。
「TextField」や、「TextFormField」に対して「TextEditingController」を使って、複雑な制御を行うことができます。
「TextField」の制御を「TextEditingController」を利用して行います。

TextField(
  controller: eCtrl,
  onSubmitted: (text) {
    litems.add(text);  // Append Text to the list
    eCtrl.clear();     // Clear the Text area
    setState(() {});   // Redraw the Stateful Widget
  },
),

eCtrl.text() でフィールドに入力した内容を取得する
eCtrl.clear() でフィールドに入力した内容をクリアする

Text(
"This is a long text",
overflow: TextOverflow.ellipsis,
),

Fade

Text(
  "This is a long text",
  overflow: TextOverflow.fade,
  maxLines: 1,
  softWrap: false,
),

Container

ちなみに、正攻法かは?ですが、以下のように特定の値によってWidgetを表示/何も表示しない、というのをContainerで実装することもできるようです。

Widget foo(bool isVisible) {
  if (isVisibile) {
    return Text("foo");
  } else {
    return Container(); // Empty Container Widget
  }
}

FlatButton (ボタンのデザインはこちら参照)

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Form'),
        ),
        body: Center(
          child: ChangeForm(),
        ),
      ),
    );
  }
}

class ChangeForm extends StatefulWidget {
  @override
  _ChangeFormState createState() => _ChangeFormState();
}

class _ChangeFormState extends State<ChangeForm> {
  int _count = 0;

  void _handlePressed() {
    setState(() {
      _count++;
    });
  }

  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(50.0),
      child: Column(
        children: <Widget>[
          Text(
            "$_count",
            style: TextStyle(
              color:Colors.blueAccent,
              fontSize: 30.0,
              fontWeight: FontWeight.w500
            ),
          ),
          FlatButton(
            onPressed: _handlePressed,
            color: Colors.blue,
            child: Text(
              '更新',
              style: TextStyle(
                color:Colors.white,
                fontSize: 20.0
              ),
            ),
          )
        ],
      )
    );
  }
}

RaisedButton

RaisedButton(
  onPressed: _handlePressed,
  color: Colors.blue,
  child: const Text(
    '更新',
    style: TextStyle(
        color:Colors.white,
        fontSize: 20.0
    ),),
)

OutlineButton

OutlineButton(
  onPressed: _handlePressed,
  borderSide: BorderSide(color: Colors.blue),
  child: const Text(
    '更新',
    style: TextStyle(
        color:Colors.blue,
        fontSize: 20.0
    ),
  ),
)

IconButton

IconButton(
  iconSize: 100,
  onPressed: _handlePressed,
  color: Colors.blue,
  icon: Icon(Icons.add_circle_outline),
)

FloatingActionButton

FloatingActionButton(
  onPressed: _handlePressed,
  backgroundColor: Colors.blue,
  child: Icon(Icons.add)
)

文字も設定できる。

FloatingActionButton(
  onPressed: _handlePressed,
  backgroundColor: Colors.blue,
  child: const Text(
    '更新',
    style: TextStyle(
        color:Colors.white,
        fontSize: 20.0
    ),
  ),
)

横長にもできる。

FloatingActionButton.extended(
  onPressed: _handlePressed,
  backgroundColor: Colors.blue,
  icon: Icon(Icons.add),
  label: const Text('ボタン'),
)

PopupMenuButton

PopupMenuButton<String>(
  onSelected: _handleChange,
  itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
    const PopupMenuItem<String>(
      value: "1",
      child: Text('選択1'),
    ),
    const PopupMenuItem<String>(
      value: "2",
      child: Text('選択2'),
    ),
    const PopupMenuItem<String>(
      value: "3",
      child: Text('選択3'),
    ),
    const PopupMenuItem<String>(
      value: "4",
      child: Text('選択4'),
    ),
  ],
),

DropdownButton

class _ChangeFormState extends State<ChangeForm> {

  String _defaultValue = 'りんご';
  List<String> _list = <String>['りんご', 'オレンジ', 'みかん', 'ぶどう'];
  String _text = '';

  void _handleChange(String newValue) {
    setState(() {
      _text = newValue;
      _defaultValue = newValue;
    });
  }

  Widget build(BuildContext context) {
    return Container(
        padding: const EdgeInsets.all(50.0),
        child: Column(
          children: <Widget>[
            Text(
              "$_text",
              style: TextStyle(
                  color:Colors.blueAccent,
                  fontSize: 30.0,
                  fontWeight: FontWeight.w500
              ),
            ),
            DropdownButton<String>(
              value: _defaultValue,
              onChanged: _handleChange,
              items: _list.map<DropdownMenuItem<String>>((String value) {
                return DropdownMenuItem<String>(
                  value: value,
                  child: Text(value),
                );
              }).toList(),
            ),
          ],
        )
    );
  }
}

ListTile

アイコン付きのリストみたいなのを簡単に表示できるのでとても便利です。
ダイアログを出すときにも使えます。isThreeLineがtrueの場合、heightが高くなります。denseがtrueの場合、defaultTextStyleの設定がなくなり、テキスト間の距離が密になります。contentPaddingを設定すると、デフォルトで適用されているpaddingがなくなり、指定したpaddingが適用されます。
CheckboxListTileとRadioListTileってのもありますが自分はあまり使わなそうなので割愛。

Card(
  child: ListTile(
    leading: Icon(Icons.people),
    title: Text("ListTile with isThreeLine true"),
    trailing: Icon(Icons.more_vert),
    subtitle: Text(
        "This is subtitle. Subtitle is very long and use three lines."),
    onTap: () {},
    isThreeLine: true,
    dense: true,
    contentPadding: EdgeInsets.all(10.0),
  ),
),

ListView

ListView.builder

ListViewに動的コンテンツ(Listとか)を表示する場合に使う。

使い方としては、いくつのリストを表示するかをitemCountに設定し、実際の表示コンテンツはitemBuilderに設定する。以下、サンプル。

body: new ListView.builder
  (
    itemCount: litems.length,
    itemBuilder: (BuildContext ctxt, int index) {
     return Text(litems[index]);
    }
  )

GridView

GridViewは写真などのボックス状のレイアウトを配置しスクロールするのに便利なウィジェットです。

GridViewを構築するためには4つの方法があります。

  1. 「GridView.count」は最も一般的に使用されるグリッドレイアウトとなり、横に並べる数を固定数で指定してグリッドを表示します。
  2. 「GridView.extent」は横に並べる幅を指定値分最大限確保してグリッドを並べる表示に方法になります。
  3. 「GridView.builder」で作成すると、実際に表示されている子要素のみビルダーが呼び出されるため随時読み込みなど、多数(または無限)のリストを表示する場合に利用する作成方法です。
  4. 「GridView.custom」で作成すると、「SliverGridDelegate」を追加してカスタマイズでき、整列されていないまたは重ねた配置を実現できます。

GridView.countのサンプル実装

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var list = [
      _photoItem("pic0"),
      _photoItem("pic1"),
      _photoItem("pic2"),
      _photoItem("pic3"),
      _photoItem("pic4"),
      _photoItem("pic5"),
    ];
    return MaterialApp(
        home: Scaffold(
            appBar: AppBar(
              title: Text('GridView'),
            ),
            body: GridView.count(
                crossAxisCount: 2,
                children: list
            )
        )
    );
  }

  Widget _photoItem(String image) {
    var assetsImage = "assets/img/" + image + ".png";
    return Container(
      child: Image.asset(assetsImage, fit: BoxFit.cover,),
    );
  }
}

crossAxisCountで横に並べる件数を指定することでグリッドが並びます。

GridView.builderのサンプル実装

GridView.builderは「ListView」の時同様、表示する要素が事前にわからない場合に利用する書き方です。
itemBuilderは画面表示時に実行されるため、無限にグリッドを作成することが可能です。
ギャラリー、検索結果の表示などに利用すると良いかと思います。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var grid = ["pic0", "pic1", "pic2", "pic3", "pic4", "pic5",];
    return MaterialApp(
        home: Scaffold(
            appBar: AppBar(
              title: Text('GridView'),
            ),
            body: GridView.builder(
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                ),
                itemBuilder: (BuildContext context, int index) {
                  if (index >= grid.length) {
                    grid.addAll(["pic0", "pic1", "pic2", "pic3", "pic4", "pic5",]);
                  }
                  return _photoItem(grid[index]);
                }
            )
        )
    );
  }

  Widget _photoItem(String image) {
    var assetsImage = "assets/img/" + image + ".png";
    return Container(
      child: Image.asset(assetsImage, fit: BoxFit.cover,),
    );
  }
}

builderで利用している、SliverGridDelegateWithFixedCrossAxisCountはGridView.countと同じです。
crossAxisCountで横に並べる数を指定して、グリッドを作成します。

グリッド同士の間にスペースを作成したいときに指定するのが、mainAxisSpacingcrossAxisSpacingになります。
RowやColumnでも同じ考え方が出てきましたが、以下のようなイメージで利用します。

このプロパティは、「GridView」クラスであれば上記で説明したどれでも利用できます。

StreamBuilder

StreamBuilderのイメージは、コードの順序関係なく流れてきたデータをキャッチして処理を行う感じです。
そして2つの要素、streamとbuilderから構成されています。

StreamBuilderのサンプル実装

Widget userName() {

    return StreamBuilder<QuerySnapshot>(

        //表示したいFiresotreの保存先を指定
        stream: Firestore.instance
        .collection('users')
        .document(firebaseUser.uid)
        .collection("transaction")
        .snapshots(),

        //streamが更新されるたびに呼ばれる
        builder: (BuildContext context,
            AsyncSnapshot<QuerySnapshot> snapshot) {

          //データが取れていない時の処理
          if (!snapshot.hasData) return const Text('Loading...');

          return Text(snapshot.data.documents[0]['userName']);

        }
     );
  }

Expanded and Flexible

こちらがよくまとまっていますので一読ください。

注意したいのは、どちらもRowやColumnのなかでつかうものであることと、Widgetのサイズの比率を調整するものであることです。違いとしては、

  • Expandedは内部に配置したWidgetのサイズ(例えばTextの長さ)に関わらずに指定した比率にする
  • FlexibleはExpandedと同様にflexプロパティで比率を指定するものの、内部に配置したWidgetのサイズに合わせて柔軟に比率を変更する

ちなみに、Columnの下でListView.builder等の縦幅不明のWidgetを使う場合、Expandedで囲まないとエラーになったりする。

AlertDialog

おなじみの注意を促すダイアログ。

            var alert = new AlertDialog(
              title: new Text("グループを作成する"),
              actions: <Widget>[
                new FlatButton(onPressed: () {
                  Navigator.pop(context);
                  return Navigator.pushNamed(context, "/create_group");
                }, child: new Text("作成する")),
              ],
            );
            showDialog(
                context: context,
                builder: (BuildContext context) {
                  return alert;
                });

ちなみにNavigator.pop(context)でダイアログを取り消せる。

事前にルーティング情報を定義しておく場合は、あらかじめビルド関数を定義するので動的にパラメータを渡すことができません。パラメータを渡す場合は、遷移都度ルーティング情報を設定する方法で対応します。

Navigator.push(
    context,
    new MaterialPageRoute<Null>(
    settings: const RouteSettings(name: "/my-page-1"),
    builder: (BuildContext context) => MyPage1(/* 必要なパラメータがあればここで渡す */),
    ),
);

ダイアログの表示をsetState()でリフレッシュしたい場合も注意が必要です。

詳しくはこちらを御覧ください。

Widgetの応用 

StreamBuilderとGridViewの組み合わせ

ここ参考

StreamBuilder(
              stream: FirebaseDatabase.instance
                  .reference()
                  .child('messages')
                  .child(groupId)
                  .orderByChild('messagetype')
                  .equalTo(1)
                  .onValue,
              builder: (BuildContext context, AsyncSnapshot<Event> snapshot) {
                if (snapshot.hasData) {
                  if (snapshot.data.snapshot.value != null) {
                    Map<dynamic, dynamic> map = snapshot.data.snapshot.value;
                    List<dynamic> list = map.values.toList()
                      ..sort(
                          (a, b) => b['timestamp'].compareTo(a['timestamp']));

                    return GridView.builder(
                      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 3),
                      itemCount: list.length,
                      padding: EdgeInsets.all(2.0),
                      itemBuilder: (BuildContext context, int index) {
                        return Container(
                          child: GestureDetector(
                            onTap: () {
                              Navigator.push(
                                context,
                                MaterialPageRoute(
                                    builder: (context) => SecondScreen(
                                        imageUrl: list[index]["imageUrl"])),
                              );
                            },
                            child: CachedNetworkImage(
                              imageUrl: list[index]["imageUrl"],
                              fit: BoxFit.cover,
                            ),
                          ),
                          padding: EdgeInsets.all(2.0),
                        );
                      },
                    );
                  } else {
                    return Container(
                        child: Center(
                            child: Text(
                      'Es wurden noch keine Fotos im Chat gepostet.',
                      style: TextStyle(fontSize: 20.0, color: Colors.grey),
                      textAlign: TextAlign.center,
                    )));
                  }
                } else {
                  return CircularProgressIndicator();
                }
              })),

Contextについて

こちらこちらが非常に分かりやすく纏まっています。

BuildContextとは親Widget自身

私の理解を纏めると、BuildContextは、宣言された(build関数の中など)場所でのElement(Widgetへの参照)を返す。よって、例えばStateクラス下のbuildで受け取ったcontextは、そのbuildクラスのなかで、Scaffold.of(context)などとしても、上方向にScaffoldをたどることはできない(正確には辿ってもScaffoldがない)。

この場合、以下のようにしてScaffoldを辿れるようにするか、Builder widgetをScaffoldより下で使うことで解決できる。

// ここ重要
+  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

   @override
   Widget build(BuildContext context) {
    return Scaffold(
+     key: _scaffoldKey,
      body: Center(
        child: Text(
          '$_counter',
          style: Theme.of(context).textTheme.display1,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
-         Scaffold.of(context).showSnackBar(SnackBar(content: Text('message')));
+         _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text('message')));
            _incrementCounter();
        },
        child: Icon(Icons.add),
      ),
    );
  }

もしくは、

   @override
   Widget build(BuildContext context) {
    return Scaffold(
      body: ・・・ ,
      floatingActionButton: Builder(builder: (context) {
        return FloatingActionButton(
          onPressed: () {
            Scaffold.of(context).showSnackBar(SnackBar(content: Text('message')));
            _incrementCounter();
          },
          child: Icon(Icons.add),
        );
      }),

pubspec.yamlの記法

Under Construction

ちなみに、import 'dart:convert' のように、dart: から始まるプラグインについてはdartの基本プラグインとして最初から内包されており、pubspec.yamlに追記する必要はない。

おすすめプラグイン

http

こちら 参照。GETリクエストとかでREST APIからデータ取得するような操作をやるときには必須。

使い方は以下を参考に。

import 'package:http/http.dart' as http;
import 'dart:convert';

List<String> _data;

 void fetchPosts() async {
    const url = 'https://jsonplaceholder.typicode.com/posts';
    http.get(url)
        .then((response) {
      print("Response status: ${response.statusCode}");
      print("Response body: ${response.body}");
      setState(() {
        List list = json.decode(response.body);
        _data = list.map<String>((value) {
          return value['title'];
        }).toList();
      });
    });
  }

  @override
  void initState() {
    _data = [];
    fetchPosts();

    super.initState();
  }

ちなみに、上記の例ではjson.decodeでJSONのデコードをしているが、 json_serializableってのを使うとより簡単に型安全に記載できるらしい。そこらへんはまた追って追記します。

また、APIにGETリクエストを投げてどんなレスポンスがあるか簡単に知りたい場合は、Postmanみたいなツールを使うと私みたいな素人にも分かりやすく簡単に扱えてよいのではないでしょうか。

(注)Unhandled Exception: InternalLinkedHashMap is not a subtype of type List が発生したときはこちらを参照してください。JSONの形によりパースの仕方が異なります。

flutter_advanced_networkimage

画像を表示するライブラリ。キャッシュやプリキャッシュを使う処理がかんたんに書ける。

詳しくはこちら

Example

// using image provider
Image(
  image: AdvancedNetworkImage(
    url,
    header: header,
    useDiskCache: true,
    cacheRule: CacheRule(maxAge: const Duration(days: 7)),
  ),
  fit: BoxFit.cover,
)
// work with precacheImage
precacheImage(
  AdvancedNetworkImage(
    url,
    header: header,
    useDiskCache: true,
    cacheRule: CacheRule(maxAge: const Duration(days: 7)),
  ),
  context,
);

// or svg provider (flutter_svg)
SvgPicture(
  AdvancedNetworkSvg(url, SvgPicture.svgByteDecoder, useDiskCache: true),
)
// get the disk cache folder size
int cacheSize = await DiskCache().cacheSize();
// clean the disk cache
bool isSucceed = await DiskCache().clear();

Flutter_launcher_icons

iOS・Androidアプリアイコンを生成するとても便利なツール。
使い方は、以下のような感じ。

# pubspec.yaml(flutter_launcher_icons-development.yamlでも良い)
flutter_icons:
  image_path: "assets/images/icon.png"
  android: true
  ios: true

アプリアイコンを反映するには、image_pathを設定してから、以下コマンドを打つ必要あり。

flutter pub pub run flutter_launcher_icons:main

その他

以下の記事で素敵なライブラリをたくさん纏めてくださっています。

【Flutter】アプリ開発_初心者のアプリをプロっぽくする最強のpackageを紹介 - Qiita https://qiita.com/kazumaz/items/876e162cf429014661d8#%E3%81%95%E3%81%8F%E3%81%A3%E3%81%A8%E8%A6%8B%E6%A0%84%E3%81%88%E3%82%92%E3%82%88%E3%81%8F%E3%81%99%E3%82%8B%E7%B7%A8

Firebase

Firebase CLI

GUIからだと面倒な手順はCLIから操作できたほうが便利で早い。

コマンド 操作
firebase login ログイン
firebase projects:list プロジェクト一覧を表示
firebase init 現在のディレクトリに新しい Firebase プロジェクトを関連付けて設定する。このコマンドを実行すると、firebase.json 構成ファイルが現在のディレクトリに作成される。
firebase use CLI にアクティブな Firebase プロジェクトを設定する。
プロジェクトのエイリアスを管理する。
firebase logout ログアウト
firestore:delete アクティブ プロジェクトのデータベース内のドキュメントを削除する。CLI を使用すると、コレクション内のすべてのドキュメントを再帰的に削除できる。
firestore:indexes アクティブなプロジェクトのデプロイされたインデックスをリスト表示する。

セキュリティルール

テストモードで開発していると、Firebaseから、「データベースへのクライアント アクセスが◯日後に期限切れになります」みたいなメールをもらうことになります。その際はセキュリティルールを設定しないとアクセスできなくなります。

セキュリティルールは以下のように記載します。以下の例では、すべてのアプリユーザにデータベース全体の任意のドキュメントに対する読み書き権限を与えることになります。詳しくはこちらを御覧ください。

// Allow read/write access on all documents to any user signed in to the application
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
    }
  }
}

すべての Cloud Firestore セキュリティ ルールは、データベース内のドキュメントを識別する match ステートメントと、それらのドキュメントへのアクセスを制御する allow 式で構成されています。

ルールの設定はFirebaseコンソールまたはCLIからできます。

ルール シミュレータを使用すると、読み取り、書き込み、削除についてそれぞれ認証済みと未認証の動作をシミュレートできます。認証済みリクエストをシミュレートする場合は、さまざまなプロバイダから認証トークンを作成してプレビューすることができます。シミュレートされるリクエストは、現在デプロイされているルールセットではなく、エディタ内のルールセットと照らし合わせて実行されます。

service cloud.firestore {
  match /databases/{database}/documents {

    // Match any document in the 'cities' collection
    match /cities/{city} {
      allow read: if <condition>;
      allow write: if <condition>;
    }
  }
}

すべての match ステートメントは、コレクションではなくドキュメントを指す必要があります。match ステートメントでは match /cities/SF のように特定のドキュメントを指すことや、match /cities/{city} のようにワイルドカードを使用して、指定されたパスのすべてのドキュメントを指すことができます。

上の例では、match ステートメントで {city} ワイルドカード構文を使用しています。つまり、このルールは、cities コレクションの、/cities/SF/cities/NYC などのすべてのドキュメントに適用されます。match ステートメントの allow 式が評価されると、city 変数は SFNYC などの city のドキュメント名に解決されます。

状況によっては、readwrite をより詳細なオペレーションに分割すると便利です。たとえば、アプリで、ドキュメントの作成に対して、削除の場合とは異なる条件を適用する必要がある場合があります。または、単一のドキュメントの読み取りを許可し、大規模なクエリを拒否することが必要になる場合があります。

read ルールは getlist に分割でき、write ルールは createupdatedelete に分割できます。

service cloud.firestore {
  match /databases/{database}/documents {
    // A read rule can be divided into get and list rules
    match /cities/{city} {
      // Applies to single document read requests
      allow get: if <condition>;

      // Applies to queries and collection read requests
      allow list: if <condition>;
    }

    // A write rule can be divided into create, update, and delete rules
    match /cities/{city} {
      // Applies to writes to nonexistent documents
      allow create: if <condition>;

      // Applies to writes to existing documents
      allow update: if <condition>;

      // Applies to delete operations
      allow delete: if <condition>;
    }
  }
}

こちらも非常にわかりやすく纏まっているので参照ください。

Firebase Authentication

こちらこちらこちらが参考になる。

ユーザの作成

createUserWithEmailAndPasswordメソッドを呼び出すか、Google ログインFacebook ログインなどのフェデレーション ID プロバイダを使用して、ユーザーが初めてログインすると、Firebase プロジェクトに新しいユーザーが作成されます。

現在ログインしているユーザを取得する

firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    // User is signed in.
  } else {
    // No user is signed in.
  }
}); 

currentUser プロパティを使用しても、現在ログインしているユーザーを取得できます。ユーザーがログインしていない場合、currentUser は null です。

var user = firebase.auth().currentUser;

if (user) {
  // User is signed in.
} else {
  // No user is signed in.
}

ユーザーのプロフィールを取得する

ユーザーのプロフィール情報を取得するには、User のインスタンスのプロパティを使用します。

var user = firebase.auth().currentUser;
var name, email, photoUrl, uid, emailVerified;

if (user != null) {
  name = user.displayName;
  email = user.email;
  photoUrl = user.photoURL;
  emailVerified = user.emailVerified;
  uid = user.uid;  // The user's ID, unique to the Firebase project. Do NOT use
                   // this value to authenticate with your backend server, if
                   // you have one. Use User.getToken() instead.
}

ユーザーのプロバイダ別のプロフィール情報を取得する

ユーザーにリンクされているログイン プロバイダからプロフィール情報を取得する場合は、providerData プロパティを使用します。

var user = firebase.auth().currentUser;

if (user != null) {
  user.providerData.forEach(function (profile) {
    console.log("Sign-in provider: " + profile.providerId);
    console.log("  Provider-specific UID: " + profile.uid);
    console.log("  Name: " + profile.displayName);
    console.log("  Email: " + profile.email);
    console.log("  Photo URL: " + profile.photoURL);
  });
}

ユーザーのプロフィールを更新する

updateProfile メソッドを使用して、ユーザーの基本的なプロフィール情報(ユーザーの表示名とプロフィール写真の URL)を更新できます。

var user = firebase.auth().currentUser;

user.updateProfile({
  displayName: "Jane Q. User",
  photoURL: "https://example.com/jane-q-user/profile.jpg"
}).then(function() {
  // Update successful.
}).catch(function(error) {
  // An error happened.
});

他の処理はこちらから。

Firestore

Collection、document、dataの概念はこちらがわかりやすかったので参照してください。

分かりやすい絵を引用させていただきます。

また、登録

void setData(String collection, Map data) {
 Firestore.instance.collection(collection).document().setData(data);
}

Map型のデータ形式は以下のとおり。

Map data = {
  "field名": "登録するデータ",
};

更新

void updateData(String collection, String documentID, Map data) {
  Firestore.instance.collection(collection).document(documentID).updateData(data);
}

削除

void deleteData(String collection, String documentId) {
  Firestore.instance.collection(collection).document(documentId).delete();
}

その他、以下を知っておくと良い。

  • CollectionReference
  • Query
  • DocumentReference
  • QuerySnapshot
  • QueryDocumentSnapshot
  • DocumentSnapshot

firestoreからのデータ取得の流れ

具体的な話の前に、firestoreからデータを取得するときに、クライアント側でどういう処理をするかの全体像を確認しておきます。

  1. データへの参照を作成する(例えば、db.collection('cities')db.collection('cities').doc('TOK')
  2. 参照からスナップショットを得る(get()メソッド)
  3. スナップショットからデータを得る(data()メソッド)

ここで、1の参照の作成と、3のスナップショットからデータへの変換は同期的で、2の参照からスナップショットへの変換だけが非同期(Promiseが返ってくる)ことを覚えておくと良いでしょう。

それでは、"参照"と"スナップショット"について、具体的に以下で見ていきます。

参照(CollectionReferenceQueryDocumentReference

CollectionReferenceQueryDocumentReferenceはいずれもデータへの参照です。このうち、CollectionReferenceQueryはほとんど同じもので、いずれも複数のドキュメントへの参照です。DocumentReferenceは一つのドキュメントへの参照です。

CollectionReference

CollectionReferenceはコレクションやサブコレクションへの参照で、以下のように作成できます。

const citiesRef = db.collection('cities')

CollectionReferenceを使って、コレクションにドキュメントを追加することができます。

citiesRef.add(data)

また、条件を絞り込んだり、ドキュメントのidを指定することで、後述するQueryDocumentReferenceを得ることができます。

const capitalsRef = citiesRef.where('capital', '==', true) // Query型
const SFRef = citiesRef.doc('SF') // DocumentReference型

Query

コレクションの中で、特定の条件を満たすドキュメントだけを取り出したいことや、一定の数のドキュメントを取り出したいことがあります。Queryは、その絞り込んだ結果への参照で、例えば以下のように生成します。

// capitalプロパティがtrueなドキュメントの集合への参照
const capitalsRef = citiesRef.where('capital', '==', true) 

// ドキュメントの集合3つのみへの参照
const threeCitiesRef = citiesRef.limit(3)

// 人口で昇順にソートされたcitiesへの参照
const citiesSortedByPopulationRef = citiesRef.orderBy('population')

Queryの使い方は、CollectionReferenceとほとんど同じです(ドキュメントによるとCollectionReferenceQueryのサブクラスのようです)。そのため、例えばCollectionReferenceから絞り込んでQueryを生成したように、Query型をさらに絞り込んでQueryを生成することができます。以下の例では、citiesSortedByPopulationRefというQueryを絞り込んで、threeLargestCitiesRefという別のQueryを取得しています。

// 人口が多い順にソートしたcitiesへの参照
const citiesSortedByPopulationRef = citiesRef.orderBy('population', 'desc')

// もっとも人口が多い3つの都市への参照
const threeLargestCitiesRef = citiesSortedByPopulationRef.limit(3)

DocumentReference

ある一つのドキュメントの参照がDocumentReferenceです。ドキュメントが所属するコレクションを参照するCollectionReferencedocメソッドの引数にドキュメントのidを指定することで、以下のように生成できます。

const tokyoRef = citiesRef.doc('TOK')

スナップショット(QuerySnapshotQueryDocumentSnapshotDocumentSnapshot

ここまでは、あくまでデータへの"参照"を取得する話でした。firestoreに保存されているデータの内容を取得するには、"参照"をもとに、実際にデータを持っている"スナップショット"を得る必要があります。"スナップショット"について、以下で説明します。

DocumentSnapshot

DocumentSnapshotは、DocumentReferenceから得られるスナップショットで、単一のドキュメントのデータを持っています。

ここでもう一度、参照からスナップショットを得るための方法について確認しておきます。スナップショットを得るには参照に生えたget()メソッドを呼べば良いです。get()の返り値の型はスナップショットを返すPromiseです。例えば、DocumentReferenceからDocumentSnapshotを得るには以下のようにします。

const tokyoRef = citiesRef.doc('TOK')
tokyoRef.get().then(docSnapshot => { 
  // docSnapshotはidが'TOK'であるドキュメントのデータをもつDocumentSnapshot
  // ここでdocSnapshotを使ってなんかやる
})

もちろん、async/awaitを使っても良いです。

(async () => {
  const tokyoRef = citiesRef.doc('TOK')
  const docSnapshot = await tokyoRef.get()
  // ここでquerySnapshotを使ってなんかやる
})()

さて、実際にfirestoreに保存されているデータを取得したいときは、上記の方法で生成したDocumentSnapshotdata()メソッドを呼びます。

const tokyoRef = citiesRef.doc('TOK')
tokyoRef.get().then(docSnapshot => {
  console.log(docSnapshot.data())
  // { capital: true,
  //   country: 'Japan',
  //   name: 'Tokyo',
  //   population: 9000000,
  //   state: null }
})

また、特定のフィールドの値だけが欲しい時はdata()の代わりにget()メソッドを呼びます。

const tokyoRef = citiesRef.doc('TOK')
tokyoRef.get().then(docSnapshot => {
  console.log(docSnapshot.get('country'))
  // Japan
})

QuerySnapshot

QuerySnapshotは、CollectionReferenceQueryから得られるスナップショットです。DocumentSnapshotが単一のドキュメントのデータを持っていたのに対して、QuerySnapshotは複数のドキュメントのデータを持つスナップショットです。QueryだけでなくCollectionReferenceからもQuerySnapshotが得られることに注意してください。

QuerySnapshotは、以下のように使います。CollectionReferenceであるcitiesRefからスナップショットを得ている例です。

citiesRef.get().then(querySnapshot => {
  // ここでquerySnapshotを使ってなんかやる
})

Queryからも全く同じようにQuerySnapshotが得られます。以下の例では、querySnapshotcapitaltrueである都市のみのデータを持っています。

const capitalsRef = citiesRef.where('capital', '==', true) // capitalsRefはQuery型
capitalsRef.get().then(querySnapshot => {
  // ここでquerySnapshotを使ってなんかやる
})

QueryDocumentSnapshot

複数のドキュメントのデータを持つQuerySnapshotから実際にデータを取得する際には、一つ一つのドキュメントのデータを持つQueryDocumentSnapshotに対して操作を行います。コレクションから一つ一つドキュメントを取り出して、何らかの操作を行うイメージです。具体的には、主に以下の2つの方法が取られます。

// 1. forEach
citiesRef.get().then(querySnapshot => {
  querySnapshot.forEach(queryDocSnapshot => {
    // ここでqueryDocSnapshotを使ってなんかやる
  })
})

// 2. docs
citiesRef.get().then(querySnapshot => {
  const queryDocsSnapshot = querySnapshot.docs
  // ここでqueryDocsSnapshotを使ってなんかやる
})

1のqueryDocSnapshotQueryDocumentSnapshot、2のqueryDocsSnapshotQueryDocumentSnapshotの配列になっています。QueryDocumentSnapshotはほとんどDocumentSnapshotと同じように使えます。例えば、コレクションを構成するドキュメントのデータの配列が欲しいときには、data()メソッドを使って以下のようにすれば良いです。

citiesRef.get().then(querySnapshot => {
  const cities = querySnapshot.docs.map(doc => doc.data())
})

また、forEachを使った例で、capitalである都市について出力したい場合は、例えば以下のようになるでしょう。

citiesRef.get().then(querySnapshot => {
  querySnapshot.forEach(citySnapshot => {
    const city = citySnapshot.data()
    if (city.capital) {
      console.log(`${city.name} is capital!`)
    }
  })
})
// Beijing is capital!
// Washington, D.C. is capital!
// Tokyo is capital!

QueryDocumentSnapshotDocumentSnapshotの違いについては明示的に意識しなくても良いと思いますが、例えば Firestore - Difference between DocumentSnapshot and QueryDocumentSnapshot を読むとわかると思います。

取得したデータの絞り込み、ソートについては以下の通り。

  • where:特定のデータ指定
  • orderBy:昇順、降順
Stream getStreamSnapshots(String collection) {
  return Firestore.instance
      .collection(collection)
      .where("title", isEqualTo: "test")
      .orderBy('createdAt', descending: true)
      .snapshots();
}

トランザクション処理もできるらしい。ここ参照。

CLI経由で大量のデータをFirestoreにぶっこむ

Typescriptで大量のデータを突っ込む方法は下記参照。上表のとおり、まだfirestoreのCLIはあまり無い模様。

こちら参照

CLI経由でデータを削除する

firebase firestore:delete [options] <<path>>

Collectionすべて削除する場合は、

firebase firestore:delete --all-collections

具体的には、自分のプロジェクトでは以下のようにして'books'というcollectionのなかのdocumentをすべて削除できた。

firebase firestore:delete books/ -r --project flutter-app-798c0

Cloud Functions for Firebase

何ができるかは本家リファレンス参照。関数はJavaScriptまたはTypeScriptで書ける。

Cloud Functions for Firebase を使用すると、Firebase 機能や HTTPS リクエストによってトリガーされたイベントに応じて、バックエンド コードを自動的に実行できます。コードは Google のクラウドに保存され、マネージド環境で実行されます。独自のサーバーを管理およびスケーリングする必要はありません。

何を基に発火するかは、以下のトリガーがある。

Admin SDK を Cloud Functions とともに使用して、Firebase のさまざまな機能を統合したり、独自の webhook を作成してサードパーティのサービスと統合したりすることができます。Cloud Functions はボイラープレート コードを最小限に抑え、ファンクション内で Firebase と Google Cloud を使いやすくします。

マネージドなので、関数を書くことにだけ集中すればよく、インフラのメンテはすべてGoogleがやってくれる。
また、Flutterクライアントアプリ側でTokenやServerキーなどを内包することはセキュリティ上のリスクがあるため、それらを含むロジックをよりセキュアなCloud Functionsに実装するという選択肢がある。

Firebase CLIによるCloud Functionsのローカルエミュレート

開始するには、

firebase emulators:start

特定のエミュレータだけ起動する場合は、

firebase emulators:start --only functions

エミュレータの起動後にテストスイートやテスト スクリプトを実行する場合は、

firebase emulators:exec "./my-test.sh"

エミュレータは関数からのログを実行中のターミナル ウィンドウにストリーミングします。関数内の console.log()console.info()console.error()console.warn() のステートメントからの出力がすべて表示されます。

Firebase Cloud Messaging

(要確認)たぶん無課金だと本機能は使えない。大まかな手順的には、

  1. 通知許可の実装
  2. 通知受信時の挙動の実装

といったところでしょうか。実際に通知を送る実装は、CloudFunctionsにでも実装し、上述したようにHTTPリクエストやFirestoreの特定のデータ更新等をトリガにPushを送る実装をすると考えておけばよいと思います。なぜCloudFunctionsに置くか、という点については、ServerKeyやToken等の秘匿情報の隠蔽、共通ロジックの追い出し等、幾つかの理由があるかと思います。こちらがよく纏まっています。

1、2ともにアプリ起動時等の初期段階で実行させたい処理であるため、initState()等に実装するのがよいのではと思います。以下に実装例を記します。

    // ここで通知許可の設定を行う
    _firebaseMessaging.requestNotificationPermissions(
        const IosNotificationSettings(sound: true, badge: true, alert: true));
    _firebaseMessaging.onIosSettingsRegistered
        .listen((IosNotificationSettings settings) {
      print("Settings registered: $settings");
    });
    // ここで通知受信時の挙動を設定しています。
    _firebaseMessaging.configure(
        // アプリがフォアグラウンドにある際に呼ばれる
      onMessage: (Map<String, dynamic> message) async {
        print("onMessage: $message");
        _buildDialog(context, "onMessage");
      },
        // 通知を押してアプリが起動する際に呼ばれる
      onLaunch: (Map<String, dynamic> message) async {
        print("onLaunch: $message");
        _buildDialog(context, "onLaunch");
      },
        // 通知を押してアプリが再開する際に呼ばれる
      onResume: (Map<String, dynamic> message) async {
        print("onResume: $message");
        _buildDialog(context, "onResume");
      },
    );

受信側がトピックをサブスクライブ(購読)して、送信側がトピックに対してPushするPub/Sub型もある。

void _onChanged1() {
    _firebaseMessaging.subscribeToTopic("/topics/gsacademy");
    _buildDialog(context, '通知の受信を開始します');
  }

以下は、クライアント側、サーバ側(CloudFunctionsにNode.jsで実装)のデータ授受の例。

▼クライアント側

    final HttpsCallable callable = CloudFunctions.instance
        .getHttpsCallable(functionName: 'sendMessage')
          ..timeout = const Duration(seconds: 30);

         final HttpsCallableResult result = await callable.call(
                <String, dynamic>{
                  'registerUser': book.data['registerUser'],
                  'displayName': displayName,
                  'bookTitle': book.data['title'],
                },
              );

▼サーバ側

const functions = require("firebase-functions");
const admin = require("firebase-admin");

admin.initializeApp();

const db = admin.firestore();
const fcm = admin.messaging();

// sendMessageがクライアント側から呼び出す際の関数名になる
exports.sendMessage = functions.https.onCall(async (data, context) => {
  const registerUserId = data.registerUser;
  const registerUserTokenRef = await db
    .collection("users")
    .doc(registerUserId)
    .get();
  const registerUserToken = registerUserTokenRef.data()["token"];

  const sender = data.displayName;
  const bookTitle = data.bookTitle;

  console.log(
    "registeredUserId: " +
      registerUserId +
      " registerUserToken: " +
      registerUserToken +
      " sender: " +
      sender +
      " bookTitle: " +
      bookTitle
  );

  const payload = {
    notification: {
      title: "テスト",
      body: `${sender}${bookTitle} を…`,
      // icon: "your-icon-url",
      // click_action: "FLUTTER_NOTIFICATION_CLICK", // required only for onResume or onLaunch callbacks
    },
  };

  return fcm.sendToDevice(registerUserToken, payload);
});

CloudFunctionsへのデプロイは以下のコマンドで。

firebase deploy --only functions

Push通知メッセージを開いたあとにタスクを実行する方法はこちらを参照ください。

(たぶん)中・上級者向け

Stateful WidgetのStateライフサイクル

こちらがよく纏まっています。

Stateのライフサイクルは以下の10ステップに分かれています。

  1. createState
  2. mounted is true
  3. initState
  4. didChangeDependencies
  5. build
  6. didUpdateWidget
  7. setState
  8. deactivate
  9. dispose
  10. mounted is false

ここで絶対に覚えて置く必要があるのは、1, 3, 5, 7でしょうか。これが分かっていないと画面のレンダリングに掛かる処理をどこに書けば良いのか?となるかと思います。

initStateはレンダリング時に1回だけ呼ばれます。

また、2, 10にあるmountedについても知っておくべきかと思います。トラブルシューティングにも書きましたが、StateオブジェクトがWidgetツリーに組み込まれたタイミングで何かしたい場合に必要になるので、覚えておくとよいでしょう。

4はInheritedWidgetのBuildContext.inheritFromawidgetOfExactTypeが実行された場合に呼ばれるとのこと。InheritedWidgetは下位Widgetから上位WidgetのオブジェクトをO(1)で参照できるものですが、その際にcontextにアクセスしようとすると、initState内からではタイミング的にcontextにアクセスできない(そうです)。その際にはdidChangeWidgetを使いましょう。

6は親Widgetが変更されて再ビルドされる場合に呼び出される(そうです。使ったことないので知らない…)。initStateで初期化したデータを再び初期化する場合に用いられる。例として挙げられているのは、Stateのbuild関数がStreamや他のオブジェクトに依存してた場合に古いオブジェクトの購読を解除して、新しいオブジェクトの購読をし直す場合。これは実装してみないと分かりづらいですね。改めて実装して確認したら掲載します。

状態管理

  • 単純にWidget毎に個別に状態を持つこともできる

    • この場合は1Widget-1State。StateクラスのsetStateで管理する。
  • 複数のWidgetで状態を共有するような場合、状態管理が必要になる

    • ここでよく聞く?ReduxやBLoCが出てくる
    • 例えば以下のような場面で必要
    • ユーザー設定
    • ログイン情報
    • SNSの通知
    • ショッピングカート
    • ニュースアプリの読んだ、読んでないの状態
  • ここではプロバイダーパッケージに触れる

    • プロバイダーとはなにか

    こちら参照
    - ビュー(UI)とロジックやステート(Model)を適切に分離する
    - 具体的にはコードを以下のように分離する
    - UI・・・Stateless Widgetを中心としたViewで構成されます。
    - Model・・・BlocやViewModelと呼ばれる層で、ロジックとステート(状態)を持ちます。
    - Repository・・・Modelが外部リソースとやりとりする、その接続点としてRepositoryをインターフェースとして扱います。
    - DAOやAPI・・・外部リソースへのアクセスを行います。(本アプリではAPIは扱っていません)
    - プロバイダーのポイントはざっくり以下の4つ
    - ChangeNotifierのModelを作って、ロジックと値(ステート)を作る
    - ChangeNotifierProviderで、ModelをWidgetで使えるように内包する
    - Provider.ofを使ってModelを呼び出し、値(ステート)をViewに使う
    - Model内のnotifyListeners()で値(ステート)の変更を通知する

  • Android Studio では「Flutter Performance」ウィンドウで各Widgetがどれだけrebuildされているかがわかる。

Flutter Performanceを使うときは、通常の実行ではなく、デバッグビルドで実行すること。

Rebuild stats のWidget名の横に黄色の丸が表示されるものについては状態管理を見直したほうがよいかもしれない。これに対するリファクタリングに係る対策は2つ、

  1. 大きなビルドメソッドを複数のウィジェットに分割する

  2. 状態管理を見直す

Flutter Performanceは以下のよくある4つのパフォーマンス問題を改善するのを手伝ってくれます。

  1. 画面全体(または画面の大部分)が単一のStatefulWidgetによって構築されるため、不要なUI構築が発生します。小さいbuild()関数を使用して、UIを小さいウィジェットに分割します。
  2. オフスクリーンウィジェットが再構築されています。これは、たとえば、ListViewが画面外に広がる高い列にネストされている場合に発生する可能性があります。または、画面外に拡張するリストにRepaintBoundaryが設定されておらず、リスト全体が再描画される場合。
  3. AnimatedBuilderのbuild()関数は、アニメーション化する必要のないサブツリーを描画し、静的オブジェクトの不要な再構築を引き起こします。
  4. 不透明度ウィジェットがウィジェットツリーの不必要に高い位置に配置されています。または、不透明度アニメーションは、不透明度ウィジェットの不透明度プロパティを直接操作して作成され、ウィジェット自体とそのサブツリーが再構築されます。

Inherited Widget

FlutterのInherited Widgetクラスを継承したWidgetを作れば、State/状態の保持とウィジットツリー下流への伝播及び、State/状態の参照と更新も実現できる。

Inherited Widgetは簡単にいうと、Scaffold.of(context)だと子孫Widgetから目的Widgetまで線形に辿っていくところを、Inherited Widgetを実装していると、O(1)で一発で辿れるようになるから軽くて早いよ!というものだと認識している。

Inherited Widgetを使わない場合、ツリー上位のWidgetから下位Widgetに値を伝播する際に、いちいちコンストラクタで値をリレーしていく必要があり、めんどいうえに上位ツリーが伝播している値が変わると下位ツリーすべてにでんぱすることでRebuildが連発し、非常に非効率な実装となる。でも、以下のWidgetはRebuildされない。

  • const を指定したWidget
  • StateのフィールドにキャッシュされたWidget
  • 上位ツリーからchild引数などで渡されて使い回されるWidget

ProviderモデルはこのInherited WidgetをWrapしたもので、より簡単にキレいにIhnerited Widgetを実装することができるもの。

こちらに分かりやすい解説があります。

つまり、InheritedWidgetの変更伝播の仕組みは、上述のリビルド抑制の仕組みを何かしら使って無駄なリビルドを塞き止めた上で、さらに必要な状態の変更のみをバイパスして伝播してリビルドする、という二段構えのものなのです。

Scoped Model

Inherited Widgetを含有して拡張した外部パッケージのScoped Modelクラスを使えばStateの参照や更新とツリーの一部の効率的な再描画が実現できる。(Inherited Widgetではツリーの一部の効率的な再描画ができない)

テスト

以下、まとめる予定

リファクタリング

Widgetの切り出し

階層が深くなったときには積極的にWidgetの切り出し(追い出し?)を行うべき。
Windowsであれば、Ctrl+Shift+Aで表示されるクイックアクションから「extract widget」でできます。

Placeholder

レイアウトを組むときに、とりあえずスペースだけは確保しておきたいときに使うWidget。
あとでリファクタリング(実装?)すべし。

Logging

Android Studioの場合、print() でLogcatに表示される。

きちんとLogとして出したい場合、以下のようにする。

import 'package:logging/logging.dart';

final log = new Logger('hoge');

main() {
  Logger.root.level = Level.All;
  Logger.root.onRecord.listen((LogRecord rec) {
    print('${rec.level.name}: ${rec.time}: ${rec.message}');
  });
}

上記の例では'hoge'のところを別の文字列にすれば他のロガー
Logger.root.level`で、出力するログレベルを指定する。ログレベルは以下のとおり。

ログレベル 説明 Level.value
OFF 全てのログ無し 2000
SHOUT デバッグ時に目立つ騒がしいログ 1200
SEVERE 深刻な障害情報 1000
WARNING 問題の可能性があるログ 900
INFO 情報提供 800
CONFIG コンフィグ設定のメッセージ 700
FINE トレース用情報 500
FINER そこそこ詳細なトレース用情報 400
FINEST かなり詳細なトレース用情報 300
ALL すべてのログ 0

そして、Logger.root.onRecord.listenによって、どのようにログ出力するかを書く。

コーディングスタイルガイド

ここ見ましょう。好きずきはあるのかもしれませんが、ある程度、どの言語にも共通のガイドになっているかと思います。

アプリアイコンの作成と設定

作成

取り敢えずなんでもよければAndroid Asset Studioでも如何でしょうか。

設定

Under Construction

Android Studio

ショートカット

★は自分(と多くの人)にとって必須

キー 内容
★stless [Live Template] StatelessWidgetのテンプレートを作成してくれる
★stful [Live Template] StatefulWidgetのテンプレートを作成してくれる
★stanim [Live Template] AnimationControllerを含むテンプレートを作成してくれる
inh [Live Template] InheritedWidgetのテンプレートを作成してくれる
Ctrl + Space コード補完
★Ctrl + P メソッドのパラメータ情報の表示
Shift + F6 変数のリネーム。ファイル名変更も
★Ctrl + B 定義箇所へ移動
Alt + F7 利用箇所検索
★ALT + Enter 以下のようなときに便利(ほんの一部です)
・Widgetを削除したいとき
・Widgetを何かしら(パディングやマージンなど)でラップしたいとき
・child→childrenに変換したいとき
・Wigetを上下に動かしたいとき
・Stateless→Statefulに変換したいとき
★Ctrl + w 選択範囲を徐々に広げる ※Shiftを押しながら実行すると選択範囲を縮小できる。
Ctrl + Q Wigetのプロパティ・定義を見る
★Ctrl + Shift + I これもWidgetの定義を確認する
Ctrl + Shift + F プロジェクト内で文字検索ができる
Ctrl + F10 実行
Ctrl + Alt + S 設定画面を表示する
★Ctrl + Shift + '+' コードを折りたたむ
★Ctrl + Shift + '-' コードを展開する
Ctrl + '.' カーソル箇所のコードの折りたたみと展開のトグル
★Ctrl + fn + [ 対応する括弧に戻る
★Ctrl + fn + ] 対応する括弧に進む
★Ctrl + Y 行削除
Shift + Alt + ↑↓ 行の入れ替え
★Ctrl + / コメント/アンコメント
F11 行のブックマーク
Ctrl + Shift + V コピー履歴から貼り付け
★Shift + Alt + Insert 矩形選択モード
★Ctrl + R 置換・・・現在のドキュメントが対象
Shift + F6 変数の名前変更・・・プロジェクト全体が対象
★Ctrl + ↑↓ カーソルを維持したまま上下にスクロール
Ctrl + Shift + BackSpace 前の場所に戻る
★Shift + F10 実行
Ctrl + F10 変更を適用して実行 Instant Run
Ctrl + Shift + Z やり直し(REDO)

Plugin

Floobits: Plugin Help: IntelliJ IDEA https://floobits.com/help/plugins/intellij

トラブルシューティング

実装上のトラブル

  • 遷移先の画面からすぐに遷移元の画面に戻った際にエラーになる

これはこのページでも報告されていて、解決策も解説されてますね。遷移先画面のStateオブジェクトのmountedプロパティをチェックすればそのWidgetがTree上に存在しているかわかるそうなので、以下のように書けば良いとのこと。

    Future<void> _loadData() async {
      final HogeData data = await fetchHogeData(widget._hogeId);

      if(mounted) { // ←これを追加!!
        setState(() => _hogeData = data); 
      }
    }
    @override  Widget build(BuildContext context) {
      // 省略
    }
  }

IDEのトラブル(Android Studio)

  • 以下のエラーが出る
   ADB server didn't ACK
  * failed to start daemon *
  • タスクマネージャからadb.exeを終了させ、Android Studio再起動

    • エミュレータや実機にインストールする前に、一度、アンインストールしてからインストールしたい

以下の実行構成の該当するエントリポイントの「Before launch: Gradle task Activate tool window」→「Tasks」に「:app:uninstallDebug」と記載

  • Flutter Outlineを開いても「Nothing to show」としか表示されない

    • 左下の(Dart Anlysis)の回転している矢印マークをクリックする

参考

後で要整理

Widgetのキャッシュ

Widgetをメンバ変数として定義する。

Widgetのconst指定によるパフォーマンス向上

constを指定してWidgetを定義することでコンパイル時定数としてパフォーマンスを向上させることができる。また、呼び出し時にconstをつけて呼び出すことで再インスタンス化を防ぐことができる。

ツリー間の値の受け渡し

GlobalKeyを使うことで上位→下位の参照が可能

使い方は、

GlobalKey<参照したい下位クラス> sampleGlobalKey = GlobalKey();

次に参照したい下位クラスにsampleGlobalKeyをコンストラクタで渡す。最後に、GlobalKeyを作成したクラスにて参照したい下位クラスの値を参照する。

逆方向の参照(下位クラスが上位クラスのオブジェクトを参照したい場合)は、ancestorWidgetOfExactTypeを使う。

例えば、MyWidgetの下にAnotherWidgetがあるような構造の場合、AnotherWidgetのbuildメソッド内で、以下のように記述する。

final MyWidget myWidget = context.ancestorWidgetOfExactType(MyWidget);

StatefulWidgetではなく、Stateインスタンスにアクセスしたい場合、そのStateインスタンスをgetterで公開しておく必要がある。

ただし、GlobalKeyやancestorWidgetOfExactTypeの方法では、直線的にすべてのツリーを探索していくことになりパフォーマンスが悪い→InheritedWidget

具体的にはinheritFromWidgetOfExactTypeを使うことでO(n)コストがO(1)になる。これで下位ツリーから上位ツリーへの参照がO(1)で可能になり、かつ、上位→下位の特定Widgetへの変更通知が可能となる。

InheritedWidgetの実装時に注意する点は以下3つ。

  • 変更通知したいデータ(ステート)の決定
  • 上位ツリーのBuildContextを受け取るstaticメソッドの実装
  • 変更通知の条件定義

Inheritedクラスではコンストラクタで状態となるデータの受け渡しをしています。これは上位ツリーのステートが持つ値への参照とその変更を監視するために定義しています。

また、Inheritedクラスでは観葉的にstaticのofメソッドを実装します。上位ツリーのBuildContextを受け取り、取りたいデータをO(1)で探索するために必要です。第2引数のbool値であるobserveは、データ変更を監視する必要があるかどうかを外部から指定可能にしています。

変更が必要な場合は、context.inheritFromWidgetOfExactType(Inherited)を呼び出し、不要な場合には、context.ancestorInheritedElementForWidgetOfExactType(Inherited).widgetを呼び出します。

ScopedModel

Title: Scoped Modelにおける各クラスのやり取り
User -> View: input
View -> Scoped Model: Notify input
Scoped Model -> Model: Create a new state
Model -> Scoped Model: Notify the new state
Scoped Model -> View: Create a new UI

基本はModelが管理する状態をViewに反映すること。Scoped ModelではScopedModelとScopedModelDescendantの2つのクラスを使ってModelの状態をViewに反映する。

Title: ScopedModelライブラリにおける各クラスのやり取り
Widget -> ScopedModel: Notify the user input
ScopedModel -> Model: Create a new state
Model -> ScopedModelDescendant: Notify the new state
ScopedModelDescendant -> Widget: create a new UI

ScopedModelDescendantは状態をWidgetに変換する役割がある。

ScopedModelでは、以下の2つの役割を担う。

  1. Viewからの入力をModelに伝達する(左から右の流れ)
  2. 状態をWidgetに変換する(右から左の流れ)

Redux

比較的大規模なアプリケーションにも対応できるアーキテクチャパターン。Facebookが提唱するFluxを更に発展させたもの。思想としては、GUIアプリの開発で複雑になりがちなデータの流れを1方向にするというもの。

View -> Action:
Action -> Dispatcher: 
Dispatcher -> Store:
Store -> View:

Actionクラスはユーザの行動によって生成されるクラス。どのページを閲覧したか、どのボタンをクリックしたかなどの情報を持つ。Dispatcherクラスは受け取ったActionを状態を管理するStoreクラスに渡す。Dispatcherはアプリ内でユニークな存在として作成する。Actionを受け取ったStoreはActionの内容から新たな状態であるStateを生成する。
ViewはStoreから受け取ったStateからViewを構築する。

View -> Action:
Action -> Reducer:
Reducer -> Store:
Store -> View:

FluxではDispatcherにActionを渡していたが、ReduxではReducerに渡す。ReducerはActionの内容と現在の状態を読み取り、新しい状態を作成する役割を担う。StoreはReducerによって変化する状態を保持する。ReduxではStoreがアプリ内でユニークになる。つまり、どの画面に遷移しても一元的に同じStoreを参照する。

BLoC

Precentation ComponentはWidgetなどGUIを扱う部分のこと。Business Logic Componentが状態を管理する部分。BackendはWebAPIなどの通信先を表している。

Presentation Component -> Business Logic Component: Send event
Business Logic Component -> Backend: Request
Backend -> Business Logic Component: Result of request
Business Logic Component -> Presentation Component: Notify a new state

Presentation Componentは入力情報を持つEventオブジェクトをBusiness Logic Componentに送る。Business Logic Componentは受け取ったEventの内容を判断する。Eventを判断したら内容に応じて必要な処理を実行して、自らが持つプロパティを変更する。

アプリ内広告

アプリ内課金

アプリ内課金の定期購入(サブスクリプション)をFlutterとFirebaseで実装するときのポイント - Studyplus Engineering Blog https://tech.studyplus.co.jp/entry/2020/04/13/102204

88
89
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
88
89