Flutter初心者に必要な最低限の知識

## もくじ

1. はじめに
2. 作成したコードの紹介
3. 基本構成
4. 動的に表示を変更
5. 画面遷移
6. 抑えておくべき記法
7. さいごに


### はじめに

Javaの経験はありますが、flutterおよびdartは初心者の私がアプリ作成までに学んだ、最低限これだけは抑えておくべき内容を厳選してまとめておきます。
「初心者だけど、とにかく早くアプリが作成したい」「flutter始めたいけど調べるのが面倒くさい」なんて人にはおすすめできる内容になっていると思っています。
この記事が誰かのお役に立てれば幸いです。

[flutterとは](https://qiita.com/nskydiving/items/4fcf0dbad923d50601f9)


### 作成したコードの紹介

![sample1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527168/63f68de3-ab35-5250-b148-cf1bc538beb3.jpeg)

![sample2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527168/ce4bf1a7-ad36-cd32-5d1f-ab659402e948.jpeg)


作成してみたflutterアプリのmainファイルを最下部に記載しました。
動的な表示変更、画面遷移、ファイル読み込みを行っています。


### 基本構成

動かすために必要なクラスがあります。
下記の3つのクラスです。

#### メイン関数

```
void main()
```

ひとつめはmain関数。
プログラムが実行されると始めに実行される部分になります。

#### アプリケーションクラス

```
class MyApp extends StatelessWidget
```

2つ目はアプリケーションを定義するクラス。
このクラスでhome画面を指定したりします。

#### 静的な画面クラス

```
class MenuPage extends StatelessWidget
```

3つ目は画面を定義するクラス。
上記は静的な画面の場合になります。

#### 動的な画面クラス

```
class ScenarioPageLapper extends StatefulWidget

class _ScenarioPage extends State<ScenarioPageLapper>
```

動的な画面を使用する場合は上記2つのクラスが必要です。


### 動的に表示を変更

動的に表示内容を変更する場合は下記のように2箇所記述します。

```
  setState(() {
    this.text = "changed";
  });
```

1箇所目はsetStateメソッドです。
このメソッドを呼び出すと表示内容が変更されます。

```
  child: Text('$text'),
```

2箇所目は表示を変える部分です。
上記のように記載することで、動的に表示が変わります。


### 画面遷移

画面遷移させるには下記のように記載します。

```
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => ScenarioPageLapper(title: "タイトル")),
);
```

上記はサブ画面を呼び出す例。
現在開いているページの上に新しいページを重ねるイメージです。

```
Navigator.pop(context);
```

上記はサブ画面を閉じる例。
主に戻る際の処理として利用します。
前述した通り、Navigator.pushを行うとページが重なっていくため、Navigator.pushのみで画面遷移を行うと何重にもページが重なってしまいます。


### 抑えておくべき記法

Java経験者の私が抑えておくと良いと思うdartの記法をまとめてみました。

#### セッターの省略

```
Sample(this.name)
```

```
Sample(String name) {
  this.name = name;
}
```

上2つのコードは結果は同じです。
上記のように記述することでセッターが省略できます。

#### 記述の省略

```
State<StatefulWidget> createState() => _ScenarioPage(title);
```

```
State<StatefulWidget> createState() {
  return _ScenarioPage(title);
}
```

上2つのコードは結果は同じです。
上記のように記述することができます。

#### 初期化子

```
ScenarioPageLapper({Key key, this.title}) : super(key: key);
```

初期化子。
上記のように記述することで、コンストラクタの先頭でsuper(key: key)が実行されます。

#### 非同期の処理

ファイル読み込み処理などは非同期の処理を利用する方法が一般的なようです。
下記のような感じになります。
Flutterでは非同期処理は頻出のようです。(Androidアプリでもそうだった気もしますが)

```
var result = getFileText();
result.then((content) => this.sc = new Scenario(content));

Future<String> getFileText() {
  return rootBundle.loadString(SC_FILE_PATH + this.scName + SC_FILE_EXT);
}
```

#### ホットリロード機能

便利なホットリロード機能というものがあります。
デバッグしながらコードを修正してリアルタイムに反映させることができます。
Terminalで"flutter run"のコマンドを実行するのではなく、VSCodeであればF5キーのデバッグ機能で実行します。


### さいごに

flutterを手っ取り早く開発したい人向けに、最低限のポイントを厳選して記載してみました。
この記事がお役に立てれば幸いです。


### コード

```
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'util.dart';
import 'scenario.dart';
import 'const.dart';

/// ==================================================
/// メイン処理
/// ==================================================
void main() {
  runApp(MyApp());
}

/// ==================================================
/// アプリケーション定義
/// ==================================================
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: TITLE,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MenuPage(),
    );
  }
}

/// ==================================================
/// メニュー画面
/// ==================================================
class MenuPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            // 画像エリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの60%
              height: MediaQuery.of(context).size.height * (60 / 100),
              // ボーダー
              decoration: BoxDecoration(
                border: Border.all(color: Colors.black),
              ),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

            ),

            // ボタンエリア1
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの15%
              height: MediaQuery.of(context).size.height * (15 / 100),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

              child: Row(
                // 等間隔に並べる
                mainAxisAlignment: MainAxisAlignment.spaceAround,

                // ウィジット郡
                children: <Widget>[
                  // Episode1
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_1),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => ScenarioPageLapper(title: SC_FILE_NAME_1)),
                      );
                    },
                  ),
                  
                  // Episode2
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_2),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),
                  
                  // Episode3
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_3),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),
                ]
              )
            ),

            // ボタンエリア2
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの15%
              height: MediaQuery.of(context).size.height * (15 / 100),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

              child: Row(
                // 等間隔に並べる
                mainAxisAlignment: MainAxisAlignment.spaceAround,

                // ウィジット郡
                children: <Widget>[
                  // Episode4
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_4),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),
                  
                  // Episode5
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_5),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),
                  
                  // Episode6
                  RaisedButton(
                    child: const Text(TEXT_SCENARIO_6),
                    color: HexColor(BGCOLOR_SCENARIO),
                    textColor: Colors.black,
                    onPressed: () {
                      // シナリオ画面に遷移
                    },
                  ),
                ]
              )
            ),
          ]
        )
      )
    );
  }
}

/// ==================================================
/// シナリオ画面ラッパー
/// ==================================================
class ScenarioPageLapper extends StatefulWidget {
  /// タイトル
  final String title;

  /// =========================
  /// コンストラクタ
  /// =========================
  ScenarioPageLapper({Key key, this.title}) : super(key: key);
  
  @override
  State<StatefulWidget> createState() => _ScenarioPage(title);
}

/// ==================================================
/// シナリオ画面
/// ==================================================
class _ScenarioPage extends State<ScenarioPageLapper> {
  /// シナリオ名
  String scName;

  /// シナリオ
  Scenario sc;
  /// 描画データ
  ViewData viewData;
  
  /// 画像URL
  String imgUrl;
  /// テキスト
  String text;

  /// =========================
  /// コンストラクタ
  /// =========================
  _ScenarioPage(String scName) {
    // シナリオ名を設定
    this.scName = scName;
    // 非同期でファイルからテキストを取得してシナリオを初期化
    var result = getFileText();
    result.then((content) => this.sc = new Scenario(content));
    // 初期表示画像
    this.imgUrl = IMG_ROOT_PATH + INIT_IMG_NAME;
    // 初期表示テキスト
    this.text = INIT_TEXT;
  }

  /// =========================
  /// 描画更新
  /// =========================
  void updateView() {
    setState(() {
      switch (this.viewData.dataType.toString()) {
        case SC_TYPE_IMG:
          this.imgUrl = this.viewData.value;
          break;
        case SC_TYPE_FADEIN:
          this.imgUrl = this.viewData.value;
          break;
        case SC_TYPE_CUTIN:
          this.imgUrl = this.viewData.value;
          break;
        default:
          this.text = this.viewData.value;

          // 音声が指定されていれば音声再生
          if (this.viewData.audio != "") {
            // this.player.play(this.viewData.audio);
          }
      }
    });
  }

  /// =========================
  /// ★★★非同期処理★★★
  /// ファイルからテキストを取得
  /// =========================
  Future<String> getFileText() {
    return rootBundle.loadString(SC_FILE_PATH + this.scName + SC_FILE_EXT);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            // 画像エリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの60%
              height: MediaQuery.of(context).size.height * (70 / 100),
              // ボーダー
              decoration: BoxDecoration(
                border: Border.all(color: Colors.black),
              ),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),
              // 画像 動的に変更される
              child: Image.asset('$imgUrl'),

            ),

            // テキストエリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの20%
              height: MediaQuery.of(context).size.height * (15 / 100),
              // デコレーション
              decoration: BoxDecoration(
                // ボーダー
                border: Border.all(color: Colors.black),
                // カラー
                color: HexColor(BGCOLOR_TEXT_EREA),
              ),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),
              // テキストエリア 動的に変更される
              child: Text('$text'),

            ),

            // ボタンエリア
            Container(
              // 最大横幅の90%
              width: MediaQuery.of(context).size.width * (90 / 100),
              // 最大高さの10%
              height: MediaQuery.of(context).size.height * (10 / 100),
              // パディング
              padding: const EdgeInsets.all(5.0),
              // マージン
              margin: EdgeInsets.all(5.0),

              child: Row(
                // 等間隔に並べる
                mainAxisAlignment: MainAxisAlignment.spaceAround,

                // ウィジット郡
                children: <Widget>[
                  // 戻るボタン
                  RaisedButton(
                    child: const Text(TEXT_PREV_BTN),
                    color: HexColor(BGCOLOR_PREV_BTN),
                    shape: const StadiumBorder(),
                    onPressed: () {
                      // 前の描画データを取得
                      this.viewData = this.sc.getPrevViewData();
                      this.updateView();
                    },
                  ),
                  
                  // メニューへ戻るボタン
                  RaisedButton(
                    child: const Text(TEXT_MENU_BTN),
                    color: HexColor(BGCOLOR_MENU_BTN),
                    textColor: Colors.black,
                    onPressed: () {
                      // メニュー画面を表示
                      Navigator.pop(context);
                    },
                  ),
                  
                  // 進むボタン
                  RaisedButton(
                    child: const Text(TEXT_NEXT_BTN),
                    color: HexColor(BGCOLOR_NEXT_BTN),
                    shape: const StadiumBorder(),
                    onPressed: () {
                      // 次の描画データを取得
                      print(this.sc);
                      this.viewData = this.sc.getNextViewData();
                      this.updateView();
                    },
                  ),
                ]
              )
            ),
          ]
        )
      )
    );
  }
}
```