Open21

ゼロから始める Flutter

izuchyizuchy

Flutter SDK をインストール

SDK を好きなところにダウンロードする
https://flutter.dev/docs/get-started/install/macos#get-sdk

export コマンドで環境変数を設定する

ターミナルを起動、ダウンロードした SDK のパスにあわせて環境変数を設定する

cd [インストールしたディレクトリ]
export PATH="$PATH:`pwd`/flutter/bin"
izuchyizuchy

flutter doctor を実行

flutter doctor
  • flutter doctor を実行すると環境を自動的に確認してくれる
  • ターミナルにレポート結果が表示される
  • 不足しているソフトや必要なタスクが表示される

e.g.

izuchyizuchy

iOS の環境セットアップ

  • Xcode の最新版を入手する(AppStore など)
  • ターミナルを起動してダウンロードした Xcode コマンドラインツールを利用できるように以下のコマンドを実行する
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
  • Xcode 起動するか、以下のコマンドでライセンスの同意を行う
sudo xcodebuild -license
  • iOS シミュレーターを起動したい場合は以下のコマンドで起動できる
open -a Simulator
izuchyizuchy

Flutter アプリをまずは作ってみる

  • 好きなディレクトリで以下のコマンドを実行
flutter create flutter_example
(中略)
All done!
In order to run your application, type:

  $ cd flutter_example
  $ flutter run

Your application code is in flutter_example/lib/main.dart.
  • 作成されたディレクトリに移動して以下のコマンドを実行
cd flutter_example
flutter run
  • 転送したいデバイスを選択する
  • iOS の証明書を選択する
  • ビルドが実行される
  • 実機でデモアプリが起動する
izuchyizuchy

"iproxy"は、開発元を検証できないため開けません と表示される場合

  • Macの「システム環境設定」から「セキュリティーとプライバシー」を開く
  • 一般の最下部に許可ボタンが表示されているので許可する
  • これを許可しないと Hot reloadHot restart の機能を利用することができない
izuchyizuchy

flutter create で生成されるサンプルコードはこういう感じ

lib/main.dart

import 'package:flutter/material.dart'; // マテリアル UI がまとめられたパッケージ

void main() { // アプリケーションのエントリーポイント
  runApp(const MyApp()); // アプリを実行するための runApp 関数を MyApp を引き渡して実行
}
class MyApp extends StatelessWidget { // ウィジェットを返すクラス Stateless = 状態をもたないウィジェット
  const MyApp({Key? key}) : super(key: key);  // コストラクタ

  @override // アノテーション(注釈)
  Widget build(BuildContext context) { // 画面を描画するためのメソッド
    return MaterialApp( // Flutter アプリケーション全体を管理するためのもの
      title: 'Flutter Demo',
      theme: ThemeData( // テーマ
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page') , // MyHomePage という別のウィジェット
    );
  }
}
class MyHomePage extends StatefulWidget { // 状態を持つウィジェット
  const MyHomePage({Key? key, required this.title}) : super(key: key);  // コストラクタ
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState(); // MyHomePageState と呼ばれる State を作成
}
// MyHomePageState の定義
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() { //カウンターの加算処理
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) { // State が変わるたびにこの build メソッドが実行される
    return Scaffold( // 基盤となるウィジェット
      appBar: AppBar( // トップバー
        title: Text(widget.title), // MyHomePage オブジェクトの title を AppBar に設定
      ),
      body: Center( // 本体部分、中央寄せのウィジェット
        child: Column( // children のリストを受け取って縦に並べる、横の場合は Row ウィジェットを使う
          mainAxisAlignment: MainAxisAlignment.center, 
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton( // フローティングボタン
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

末尾のカンマは大事
https://twitter.com/_mono/status/1134465942023335937

izuchyizuchy

Flutter のレンダリングに関わる Widget、Element、RenderObject

https://engineer.recruit-lifestyle.co.jp/techblog/2019-12-24-flutter-rendering/

ツリーは 3種類、この3つのツリーが組み合わせって UI ができている

  • Widget ツリー // 開発者が記述するもの
  • Element ツリー // Widget と RenderObject の仲介役
  • RenderObject ツリー // レイアウト、描画を行う

Widget

  • UI の構成要素と子 Widget を保持している immutable なオブジェクト

Widget は 4種類

  • StatelessWidget
  • StatefulWidget
  • InheritedWidget
  • RenderObjectWidget

Element

  • state をもつ mutable なオブジェクト
  • Widget は createElement メソッドで Element を生成する
  • Widget が Widget ツリーに挿入されると Element が生成される

Widget と Element の関係

  • StatelessWidget => ComponentElement
  • StatefulWidget => ComponentElement
  • InheritedWidget => ComponentElement
  • RenderObjectWidget => RenderObjectElement

RenderObject

  • UI のレイアウトと描画を行う mutable なオブジェクト
  • Element ツリーが構築されるときに、Element が RenderObjectElement の場合に RenderObject のツリーが作られる
izuchyizuchy

状態管理に関する記事

  • 各種状態管理のまとめ的記事

https://swiftfe0.hatenablog.com/entry/2020/06/24/113145

  • Provider と Riverpod をカウンターサンプルで比べる

https://qiita.com/piyonakajima/items/cdef34bea3291c57e6d8

  • Provider + ChangeNotifier

https://tesshus-blog.netlify.app/flutter-change-notifier/

  • StateNotifier + freezed + Riverpod

https://medium.com/flutter-jp/state-1daa7fd66b94

  • StateNotifier + freezed + Riverpod + Flutter Hooks

https://www.ariseanalytics.com/activities/report/20210625/

izuchyizuchy

通信〜コンポーネントのレンダリングの流れ

https://github.com/diegoveloper/flutter-samples/blob/master/lib/fetch_data/main_fetch_data.dart

  • fetchData で json 取得
  • setState でローディングアイコンの切り替え
  • ListTile で UITableViewCell のようなテーブル内のセルを定義
  • CircularProgressIndicator でローディングを表示
  • Image.networkでネットワーク越しの画像表示が可能
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_samples/fetch_data/photo.dart';
import 'package:http/http.dart' as http;

class MainFetchData extends StatefulWidget {
  
  _MainFetchDataState createState() => _MainFetchDataState();
}

class _MainFetchDataState extends State<MainFetchData> {
  List<Photo> list = [];
  var isLoading = false;

  _fetchData() async {
    setState(() {
      isLoading = true;
    });
    final response = await http
        .get(Uri.parse("https://jsonplaceholder.typicode.com/photos"));
    if (response.statusCode == 200) {
      list = (json.decode(response.body) as List)
          .map((data) => new Photo.fromJson(data))
          .toList();
      setState(() {
        isLoading = false;
      });
    } else {
      throw Exception('Failed to load photos');
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Fetch Data JSON"),
        ),
        bottomNavigationBar: Padding(
          padding: const EdgeInsets.all(8.0),
          child: ElevatedButton(
            child: Text("Fetch Data"),
            onPressed: _fetchData,
          ),
        ),
        body: isLoading
            ? Center(
                child: CircularProgressIndicator(),
              )
            : ListView.builder(
                itemCount: list.length,
                itemBuilder: (BuildContext context, int index) {
                  return ListTile(
                    contentPadding: EdgeInsets.all(10.0),
                    title: Text(list[index].title!),
                    trailing: Image.network(
                      list[index].thumbnailUrl!,
                      fit: BoxFit.cover,
                      height: 40.0,
                      width: 40.0,
                    ),
                  );
                }));
  }
}
izuchyizuchy
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.

https://qiita.com/iseruuuuu/items/8bce7046bb260d515038

izuchyizuchy

JSON シリアライズ

手動によるシリアライズ

  • dart:convert に内蔵されている jsonDecode() を使用する
  • JSON 文字列 => jsonDecode() 関数 => Map<String,dynamic> で必要な値を取得できる
Map<String, dynamic> user = jsonDecode(jsonString);

ただし、プロジェクトが大きくなり管理が大変
存在しない JSON フィールドにアクセスすると実行時にエラーになる

コード生成による自動シリアライズ

  • json_serializable、built_value によりモデルクラスから定型のコードを出力する

ただし、初期設定が必要になる

json_serializable による自動シリアライズ

  • pubspec.yaml に以下を追加
dependencies:
  json_annotation: <latest_version>

dev_dependencies:
  build_runner: <latest_version>
  json_serializable: <latest_version>
  • flutter pub get を実行
  • お作法に則り、モデルクラスを作る
  • flutter pub run build_runner build or flutter pub run build_runner watch(監視付きビルド) を実行する