非エンジニアがFlutterでアプリ開発をしてつまづいた箇所一覧

こんにちは！ はじめまして。佐々木と申します。
普段は[富士通クラウドテクノロジーズ](https://fjct.fujitsu.com/)という会社で企画職をやっています。
業務では、ほとんどコードを書くことはなく、どちらかというと、バナーや動画を作ったりすることが多いです。
が！ 開発者の方々に向けたサービスを提供している立場なわけで、当然技術職でなくても技術のことを最低限わかっていることが望ましいわけです。
というわけで、アドベントカレンダーという機会をいかして技術のおさらいをしつつ、皆さんに読んでいただける記事を書けるよう頑張ります。

# 1. はじめに
弊社では、様々なサービスを提供していますが、僕が主に携わっているのは[ニフクラ mobile backend](https://mbaas.nifcloud.com)というスマホアプリ向けのクラウドサービスです。
ニフクラ mobile backendは、開発環境／言語(Swift / Kotlin / Unity等)にあわせたSDKを導入することで使用するのですが、公式が提供しているSDK以外にも、コミュニティSDKという非公式のSDKがあります。
今回は、その中でもFlutter SDKを使って、Flutter上でニフクラ mobile backendの機能を使ってみようと思います。

なお、自分の過去のQiita投稿をさかのぼってみたところ、どうやらかなり前にFlutterを書いたことがあったようなのですが、完全に存在を忘れいていたので、ズブの初心者同然の状態です。

# 2. つくったもの

アプリ内でメモを閲覧／登録／削除することができるというものです。
<img width="300px" alt="一覧画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280892/4fcfa777-a8cb-c892-7143-db88e5d60744.png">
<img width="300px" alt="詳細画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280892/0ad76a1d-e963-a4be-2464-d03961d7ee88.png">
<img width="300px" alt="登録画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280892/99a9f743-508a-8b10-4040-4eccc35dd23f.png">

非常に単純なアプリではあるのですが、画面遷移／ニフクラ mobile backendとのつなぎ込みなど、苦戦した箇所がたくさんあります……。

# 3. コード

コードは、main.dartにすべてを詰め込んだ非常に単純なものとなります。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:ncmb/ncmb.dart';

void main() {
  runApp(MyApp());
  NCMB ncmb = NCMB('228ed...22532', '98ccb...ea25a');
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    initialRoute: '/',
    routes: {
      '/': (context) => Home(),
      '/detail': (context) => Detail(),
      '/add': (context) => Add(),
    },
  );
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  List _data = [];

  @override
  void initState() {
    super.initState();
    print('initState');
    _get_memo_list();
  }

  _get_memo_list() async{
    var query = NCMBQuery('Item');
    var data =  await query.fetchAll();
    setState(() {
      _data = data;
    });
  }

  @override
  Widget build(BuildContext context) {
    _get_memo_list();
    var data = _data.toList();
    return Scaffold(
      appBar: AppBar(
        title: const Text('NCMB Memo App'),
      ),
      body: ListView.builder(
        itemCount: _data.length,
        itemBuilder: (context, index) => Card(
          child: ListTile(
            leading: Icon(Icons.note_alt),
            title: Text(data[index].get('title')),
            subtitle:  Text(data[index].get('description')),
            onTap: () => Navigator.pushNamed(context, '/detail', arguments: {'title': data[index].get('title'), 'description': data[index].get('description'), 'objectId': data[index].get('objectId')}),
          ),
          elevation: 5,
        )
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.pushNamed(context, '/add'),
        child: Icon(Icons.add),
      ),
    );
  }
}

_delete(String objId, BuildContext context){
  NCMBObject item = NCMBObject('Item');
  item.set('objectId', objId);
  item.delete();
  Navigator.pop(context);
}

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 遷移前の画面から引数を引っ張ってくる
    final arg = ModalRoute.of(context)!.settings.arguments as Map;
    return Scaffold(
      appBar: AppBar(
          title: Text(arg['title'] as String)
      ),
      body: Center(
        child: Center(
          child: Column(
            children: [
              Text(arg['description'] as String),
              ElevatedButton(
                onPressed: () => _delete(arg['objectId'] as String, context),
                child: Text('削除')
              ),
            ],
          ),
        ),
      ),
    );
  }
}

_add(BuildContext context, String title, String description) async{
  NCMBObject item = NCMBObject('Item')
      ..set('title', title)
      ..set('description', description);
  await item.save();
  Navigator.pop(context);
}

class Add extends StatefulWidget {
  @override
  _AddState createState() => _AddState();
}

class _AddState extends State<Add> {
  var _title = '';
  var _description = '';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text('メモを追加')
      ),
      body: Center(
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                hintText: 'タイトル',
              ),
              onChanged: (text) {
                  _title = text;
              },
            ),
            TextField(
              decoration: InputDecoration(
                hintText: '内容',
              ),
              onChanged: (text) {
                  _description = text;
              },
            ),
            ElevatedButton(
              onPressed: () => _add(context, _title, _description),
              child: Text('追加')
            ),
          ],
        )
      ),
    );
  }
}
```

構成としては、名前付きルートの定義をしているMyAppクラス、メモ一覧画面を定義しているHomeクラス、メモの詳細画面を定義しているDetailクラス、メモをの追加画面を定義しているAddクラスなどのクラスがあり、削除／追加／表示などそれぞれの機能ごとの関数を定義しているという感じです。

```dart
NCMB ncmb = NCMB('228ed...22532', '98ccb...ea25a');
```

ちなみに、上記のNCMB()の引数には、ニフクラ mobile backend上のAPIキー(アプリケーションキー／クライアントキー)というものを入力しています(一部マスクしています)。
ニフクラ mobile backend上でアプリを作成した際に払い出されるキーで、アプリごとに異なります。

# 4. 詰まったところ

おそらくですが、詰まりそうなところは一通り詰まったんじゃないかなと思います。
ひとつずつ振り返っていきます。

## 名前付きルートでの画面遷移時に値を渡す方法がわからない

Flutterの学習書を見たまま、名前付きルートという方法で画面遷移を組んだのですが、遷移先に値を渡す方法がわかりませんでした。
なので、まずは公式のドキュメントを見ると「名前付きルートでの遷移は推奨しない。他の方法での構築を強くおすすめする」とあちこちに書いてあり、いまいち遷移方法がわかりませんでした。
結局Qiitaの海を泳ぎ、なんとか見つけることができました。

pushNamedで画面遷移をする際、以下のような形で値を渡し、
```dart
Navigator.pushNamed(
    context, '遷移先',
    arguments: {
        'key1': 'value1',
        'key2': 'value2',
        'key3': 'value3',
    }
)
```
遷移先で以下のような形式で受け取ると問題なく利用できるとわかりました。
```dart
final arg = ModalRoute.of(context)!.settings.arguments as Map;
```
※Map形式でデータを渡す際のコードです

## Stateが設定できない

色々と調べているとsetStateというコードが多く出てきますが、僕の環境では何度書いてもうまく設定ができませんでした。
これは、僕がStatelessWidgetにしていたことが原因でした。ここに書くのも恥ずかしいような理由で恐縮ですが、本当に右も左も分からない状態なので許してください……。
StatefulWidgetにした上で、Stateの部分に画面表示などを書くと無事エラーがなく実行できるようになりました。

## クラス内に書いた関数が画面遷移時に勝手に実行される

これは解決できたかといわれると微妙なのですが、クラス内(Widget build()外)に書いた関数が、画面遷移時に勝手に実行されて困りました。
具体的には、_delete()関数です。
そのため、詳細画面遷移時に詳細画面に遷移せずにメモデータを削除してしまうという状況になり、どうやったら画面遷移時にクラス内に定義されている関数を実行せずにすむのか、いろいろと調べてみたのですが、結局わからず、最終的にクラス外に関数を記載するという方法で解決しました。これは解決と呼べるのか……？

## 非同期処理がわからない

このアプリは、ニフクラ mobile backendとの通信を行うため、非同期処理を行う必要があるわけですが、async / awaitといった非同期の仕組みを一切知らず、四苦八苦しながら書いていきました。
FutureクラスをListにキャストしようとして怒られたり、Futureクラスのlengthを知ろうとして苦しめられたり、今思うとかなり素っ頓狂なことをしていたように思います。
お恥ずかしながら一番苦しめられたのがここで、丸2日くらいはここで悩んでいたと思います。

# 5. おわりに

だいぶやっつけ感もあるかなと思いますが、なんとか無事完成させることができました……！！
エンジニアむけのサービスを提供している身の上なので、やっぱり普段から技術に触れておくのは大事ですね……こんなにかかると思わなかった……。
Flutterは難しかったですが、それでもマテリアルデザインに準拠したアプリは作りやすいと思うので、今後アプリ開発をする際は積極的に採用したいなあと思いました。
引き続き精進します！

## 参考　
- ニフクラ mobile backend Dart SDK
  https://github.com/NCMBMania/ncmb-dart
- ニフクラ mobile backend コミュニティSDK一覧
  https://blog.mbaas.nifcloud.com/entry/2020/08/14/140309
- ニフクラ mobile backend Flutter SDK導入方法
  https://blog.mbaas.nifcloud.com/entry/2020/01/14/103404