【Flutter】Flutter × Firebase Firestore で単語帳アプリを作成

## 完成イメージ

[githubはこちら](https://github.com/Tetsukick/TANGO)

<img width="400" alt="完成イメージ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261934/c91ca63a-141f-0127-628a-cc339cbe6485.gif">

## 作業手順
1. firebaseをアプリに追加
2. firestoreを設定
3. ソースコード書き書き（Firestoreデータの読み取り）

## firebaseをflutterアプリに設定
こちらに関しては公式のほうが丁寧に説明されておりますので以下のGoogle公式サイトを参照してください。
[Flutter アプリに Firebase を追加する](https://firebase.google.com/docs/flutter/setup?hl=ja)

## firestoreを設定

### Firestore プラグインの追加

```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^0.3.0
  cloud_firestore: ^0.9.5
```

### パッケージのimport

```dart:main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
```

### トップ画面の読み取り

要点はStreamBuilderを使用して、'category'という名前のテーブルを取得するところです。

```dart
Firestore.instance.collection('category').snapshots()
```

また、アイテムをタップ時にタップされたアイテムの情報を画面遷移の際に以下のように渡しています。

```dart
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => TangoListDetailScreen(categoryName: categolies[index]["name"], categoryID: categolies[index].documentID,)),
                        );
                      }
```

Firestoreのデータはこのような感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261934/391502e1-b011-0bba-3f17-24a904dfd835.png)

```dart:main.dart
@override
  Widget build(BuildContext context) {
    return new Container(
        decoration: new BoxDecoration(
            color: Colors.white
        ),
        child: StreamBuilder<QuerySnapshot>(
          stream: Firestore.instance.collection('category').snapshots(),
          builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
            if (snapshot.hasError)
              return new Text('Error: ${snapshot.error}');
            switch (snapshot.connectionState) {
              case ConnectionState.waiting: return new Text('Loading...');
              default:
                List<DocumentSnapshot> categolies = snapshot.data.documents;
                return ListView.builder(
                  padding: EdgeInsets.fromLTRB(30, 10, 30, 10),
                  itemCount: categolies.length,
                  itemBuilder: (BuildContext context, int index) {

                    return GestureDetector(
                      child: Container(
                        margin: EdgeInsets.only(bottom: 20.0),
                        height: 97,
                        decoration: new BoxDecoration(
                          boxShadow: [BoxShadow(
                              color: Color(0x29000000),
                              offset: Offset(0,3),
                              blurRadius: 6,
                              spreadRadius: 0
                          ) ],
                        ),
                        child: Card(
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.all(Radius.circular(10)),
                          ),
                          child: Container(
                              decoration: new BoxDecoration(
                                color: Color(0xffffffff),
                                border: Border.all(
                                    color: Color(0xff007aff),
                                    width: 0.5
                                ),
                              ),
                              child: Center(
                                child: Text(
                                    categolies[index]["name"],
                                    style: TextStyle(
                                      fontSize: 25,
                                    )
                                ),
                              )
                          ),
                        ),
                      ),
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => TangoListDetailScreen(categoryName: categolies[index]["name"], categoryID: categolies[index].documentID,)),
                        );
                      }
                    );
                  },
                );
            }
          },
        ),
    );
  }
```

### 単語一覧画面の設定

単語のDBはこのような構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261934/edfc5dfa-1170-1b47-d29e-872c84b16f2c.png)

```dart
import 'package:flutter/material.dart';
import 'TangoScreen.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class TangoListDetailScreen extends StatelessWidget {
  TangoListDetailScreen({this.categoryName, this.categoryID});
  final String categoryName;
  final String categoryID;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TangoListDetailWidget(categoryName: categoryName, categoryID: categoryID,),
    );
  }
}

class Header extends StatelessWidget with PreferredSizeWidget{
  Header({this.title});
  final String title;

  @override
  Size get preferredSize => Size.fromHeight(kToolbarHeight);

  @override
  Widget build(BuildContext context) {
    return AppBar(
      iconTheme: IconThemeData(
        color: Colors.black,
      ),
      title: Text(
        title ?? '',
        style: TextStyle(
          color: Colors.black,
          fontSize: 20,
        ),
      ),
      backgroundColor: Colors.grey[200],
      centerTitle: true,
      elevation: 0.0,
    );
  }
}

class TangoListDetailWidget extends StatefulWidget {
  TangoListDetailWidget({this.categoryName, this.categoryID});
  final String categoryName;
  final String categoryID;

  @override
  State<StatefulWidget> createState() {
    return ListState(categoryName: categoryName, categoryID: categoryID);
  }
}

class ListState extends State<TangoListDetailWidget> {
  ListState({this.categoryName, this.categoryID});
  final String categoryName;
  final String categoryID;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: Header(title: categoryName),
      body: StreamBuilder<QuerySnapshot>(
        stream: Firestore.instance.collection('words').where('categories', arrayContains: categoryID).snapshots(),
        builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (snapshot.hasError)
            return new Text('Error: ${snapshot.error}');
          switch (snapshot.connectionState) {
            case ConnectionState.waiting: return new Text('Loading...');
            default:
              List<DocumentSnapshot> words = snapshot.data.documents;
              return ListView.builder(
                itemBuilder: (BuildContext context, int index) {

                  if (index == 0) {
                    return Container(
                      width: 375,
                      height: 110,
                      decoration: new BoxDecoration(
                          color: Color(0xff098a8c),
                          border: Border.all(
                              color: Color(0xff098a8c),
                              width: 1
                          )
                      ),
                      child: Container(
                        margin: EdgeInsets.only(right: 10, bottom: 10),
                        child: Row(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            verticalDirection: VerticalDirection.up,
                            textDirection: TextDirection.rtl,
                            children: [
                              Expanded(
                                child: Text(categoryName,
                                  style: TextStyle(
                                    fontFamily: 'SFProDisplay',
                                    color: Color(0xffffffff),
                                    fontSize: 30,
                                    fontWeight: FontWeight.w400,
                                    fontStyle: FontStyle.normal,
                                    letterSpacing: 0.0075,
                                  ),
                                  textAlign: TextAlign.right,
                                  overflow: TextOverflow.ellipsis,
                                ),
                              ),
                            ]
                        ),
                      ),
                    );
                  }

                  return Container(
                      decoration: BoxDecoration(
                        border: Border(
                          bottom: BorderSide(color: Colors.black38),
                        ),
                      ),
                      child: ListTile(
                        trailing: Image.asset('assets/right_detail_grey.png'),
                        title: Text(
                          words[index - 1]["indonesia"],
                          overflow: TextOverflow.ellipsis,
                        ),
                        onTap: () {
                          Navigator.push(
                            context,
                            MaterialPageRoute(builder: (context) => TangoScreen(categoryName: categoryName, categoryID: categoryID, currentIndex: index - 1,)),
                          );
                        },
                      ));},
                itemCount: words.length + 1,
              );
          }
        },
      ),
    );
  }
}
```

### 単語画面の設定

```dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class TangoScreen extends StatelessWidget {
  TangoScreen({this.categoryName, this.categoryID, this.currentIndex});
  final String categoryName;
  final String categoryID;
  final int currentIndex;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TangoWidget(categoryName: categoryName, categoryID: categoryID, currentIndex: currentIndex),
    );
  }
}

class Header extends StatelessWidget with PreferredSizeWidget{
  @override
  Size get preferredSize => Size.fromHeight(kToolbarHeight);

  @override
  Widget build(BuildContext context) {
    return AppBar(
      iconTheme: IconThemeData(
        color: Colors.black,
      ),
      actions: [
        Padding(
          padding: const EdgeInsets.only(right: 16.0),
          child: Row(
            children: [
              GestureDetector(
                child: Container(
                  margin: EdgeInsets.only(left: 5.0, right: 5.0),
                  child: Image.asset('assets/checkmark_lightgrey.png'),
                ),
                onTap: () {

                },
              ),
            ],
          )
        ),
      ],
      backgroundColor: Colors.white,
      centerTitle: true,
      elevation: 0.0,
    );
  }
}

class TangoWidget extends StatefulWidget {
  TangoWidget({this.categoryName, this.categoryID, this.currentIndex});
  final String categoryName;
  final String categoryID;
  final int currentIndex;

  @override
  State<StatefulWidget> createState() {
    return TangoState(categoryName: categoryName, categoryID: categoryID, currentIndex: currentIndex);
  }
}

class TangoState extends State<TangoWidget> {
  TangoState({this.categoryName, this.categoryID, this.currentIndex});
  final String categoryName;
  final String categoryID;
  int currentIndex = 0;
  bool isFront = true;

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: Firestore.instance.collection('words').where('categories', arrayContains: categoryID).snapshots(),
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
        if (snapshot.hasError)
          return new Text('Error: ${snapshot.error}');
        switch (snapshot.connectionState) {
          case ConnectionState.waiting: return new Text('Loading...');
          default:
            List<DocumentSnapshot> words = snapshot.data.documents;
            return Scaffold(
              appBar: Header(),
              body: Container(
                alignment: Alignment.center,
                child: Text(
                  isFront ? words[currentIndex]["indonesia"] : words[currentIndex]["japanese"],
                  style: TextStyle(
                    fontFamily: 'SFProDisplay',
                    color: Colors.black,
                    fontSize: 40,
                    fontWeight: FontWeight.w400,
                    fontStyle: FontStyle.normal,
                    letterSpacing: 0.0075,
                  ),
                  overflow: TextOverflow.visible,
                ),
              ),
              bottomNavigationBar: BottomAppBar(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    GestureDetector(
                      child: Container(
                        margin: EdgeInsets.all(15),
                        child: Image.asset('assets/left_arrow_big.png'),
                      ),
                      onTap: currentIndex == 0 ? null : () {
                        setState(() {
                          isFront = true;
                          currentIndex--;
                        });
                      },
                    ),
                    GestureDetector(
                      child: Container(
                        margin: EdgeInsets.all(15),
                        child: Image.asset('assets/return.png'),
                      ),
                      onTap: () {
                        setState(() {
                          isFront = !isFront;
                        });
                      },
                    ),
                    GestureDetector(
                      child: Container(
                        margin: EdgeInsets.all(15),
                        child: Image.asset('assets/right_arrow_big.png'),
                      ),
                      onTap: currentIndex + 1 >= words.length ? null : () {
                        setState(() {
                          isFront = true;
                          currentIndex++;
                        });
                      },
                    ),
                  ],
                ),
              ),
            );
        }
      },
    );
  }
}
```

## 参考記事
- [FlutterでFirestoreと連携してみた](https://qiita.com/teracy55/items/bd29c876a747f2ba8cfc)