ネイティブあまり経験ないけどFlutterがそれなりに使えるまで

ネイティブは年単位での経験が有るわけではなく、フロントエンドとバックエンドをメインで経験しています。そんな中でFlutterをそれなりに使えるようになるまで残したメモをまとめています。

ホットリロードとDartのおかげで、
個人的には「これ、ネイティブ・・？」と錯覚するほどフロントエンドっぽさを感じました。

※なにか変な部分や間違いが有りましたらご指摘いただけると幸いです！

# 環境構築
### エディタは何を使えばいいんだろう？
個人的にはVSCodeがなれていたのでそちらで開発していましたが、やりたいなと思ったことは今のところできています。

### 環境構築する
公式のget-started通りにやればokです(丸投げだけど本当に思っていたよりかなり楽に構築できます)。
途中なにかが不足している場合などでも、`flutter doctor`コマンドで環境構築完了に何が足りないかわかるのがとてもありがたい。

参考: https://flutter.dev/docs/get-started/install

# ビルドする
ネイティブ経験がそれほどなかったので、「え、ビルド、大変そう・・」と思ったのですがかんたんでした。

シミュレータかmacにつなげた実機か、どのデバイスにビルドするか選択してビルドします。
実機の場合は少し設定が必要があるかと思うので「ios実機ビルドできない」の項目を下記参照ください。

### コマンドの場合
```
$ flutter devices
---
2 connected devices:
hogePhone         • XXXXXXXXXXXXXXXXXxx            • ios • iOS 13.4.1
iPhone 11 Pro Max • XXXXXXXXXXXXXXXXXxx            • ios •

$ flutter run -d {device_id}
```

### VSCodeでビルドする場合

![スクリーンショット 2020-07-21 11.36.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56194/3b269e01-0bd5-1ef6-91f0-048e221bd727.png)

vscodeの画面右下にビルド対象が表示されているので対象にしたい端末を設定します。

# よく使ったコマンド

```
$ flutter create {app_name}

# pubspec.yamlを更新したときパッケージを取得(基本エディタで保存したときに自動で取得される)
$ flutter pub get

# ビルド
$ flutter run
$ flutter run -d {device_id} # デバイスを指定してビルド

# deviceIdを列挙
$ flutter devices
-----------------
2 connected devices:
hogePhone             • 00008030-001838320CF2802E            • ios • iOS 13.4.1
iPhone 11 Pro Max • 9A5B8A02-0CE1-4955-9450-B787F6BAD274 • ios •
```

# packageの追加

pubspec.yamlに追加したいpackageを追記して保存 -> 自動でinstallが始まる

```
dependencies:
  flutter:
    sdk: flutter
  http: ^0.12.1 # ←
```

# HotReloadとHotRestart

### HotReload
エディタでビルドを実行した場合は保存した際にホットリロードされますが、
ターミナルの方で flutter runでビルドした場合、
コマンドで`r`をいちいち入力しないとホットリロードされません。(ほんとに？！)

```
$ flutter run
....
Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
```

### HotRestart
コンパイルエラーやアプリが落ちた場合やパッケージ追加時・フォントの変更など、ホットリロードしても変更が反映されないケース(下記参考に詳細)があります。なので、更新されないなぁという時はホットリスタートを行う必要があります。(それでも変更されないケースがあるのでその時はアプリ再起動)

下記はVSCodeのデバッグUI。緑の矢印がhot-restart
![vscode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56194/4ad930cf-9d05-d87c-a19e-decfe1ea8147.png)


参考: https://flutter.dev/docs/development/tools/hot-reload#special-cases![uploading-0]()

# 自動フォーマット

VSCodeの場合settings.jsonに下記追加
フォーマットはされるけど変なフォーマットされるなぁと言う時は、大体`,`が無い。
自分は一行にまとめたいケースなどではあえて`,`を省略したりしていました。

```
"editor.formatOnSave": true,
```

# 状態管理

自分は一番これに時間を費やしてしまいました。
setState, BLoC, Provider, Reduxなど、状態管理のやりかたが色いろあるようで、ちょこちょこ変わっているそうです。
僕はめんどくさがりなので、今後メインになりそうな手法をとりあえず選びたい・・と思っているものの

“What toppings should I get on my pizza?”
参考: https://blog.codemagic.io/flutter-tutorial-pros-and-cons-of-state-management-approaches/

みたいなもんだと書いてあるとおりどれがいいっていうのはまだ明確にはなっていないそう。。　


というのを横目で見つつgoogleとしては、特にこだわりが無ければProviderを推すということなのでProviderを使ってみることにしました。

### Providerで状態管理

テキストで実装の流れを書くと下記のような感じ

1. rootにしたいwidgetにChangeNotifierProvider でwidgetをwrapする
2. Providerには状態と各methodをもたせる
3. rootに設定したtree内でProviderを参照できる

```dart:main.dart

import 'package:provider/provider.dart';
import './providers/counter.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext ctx) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (ctx) => Counter()),
        // 複数設置できる
      ],
      ...
```

```dart:provider/counter.dart
import 'package:provider/provider.dart';

class Counter with ChangeNotifier {
  int count = 0;

  void add() {
    count++;
    notifyListeners();　 // これでproviderに更新を伝える
  }
}
```

```dart:CounterScreen.dart
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of<Counter>(context); // CounterのProviderを参照
    return Column(
          children: <Widget>[
            Center(child: Text(counter.count)), // Providerに持たせている状態を参照
            FlatButton(
             onPressed: () => counter.add(), // Providerが持っているmethodを呼び出す
             child: Text('Plus One'),
            ),
         ],
      }),
    );
  }
}
```

# REST-APIを使う

まずモデルを作成する

```dart
class AlbumModel {
  final int userId;
  final int id;
  final String title;

  AlbumModel({this.userId, this.id, this.title});

  factory AlbumModel.fromJson(Map<String, dynamic> json) {
    return AlbumModel(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}
```

そのModelを使ってapiを叩いていく

### GET

```dart
Future<AlbumModel> fetchAlbum() async {
  String url = 'https://jsonplaceholder.typicode.com/albums/2';
  
  final response = await http.get(url);
  if(response.statusCode == 200) {
    return AlbumModel.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to load album');
  }
}
```

### POST

```dart
Future<AlbumModel> createAlbum(String title) async {
  String url = 'https://jsonplaceholder.typicode.com/albums';
  final http.Response response = await http.post(
    url,
    headers: <String, String> {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    })
  );

  if (response.statusCode == 201) {
    return AlbumModel.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to load album.');
  }
}
```