「The Complete 2021 Flutter Development Bootcamp with Dart」学習メモ Part 4：Section15-18

みんな大好きFlutterの人気講座「The Complete 2021 Flutter Development Bootcamp with Dart」、前から知ってはいたのですが英語への苦手意識のため放置、どうやらかなり良い講座のようでこのたび通しでやってみることにしました。全部で30時間近くあるらしいので気長に進めていきます。学習の要点メモをまとめていきます。

https://www.udemy.com/course/flutter-bootcamp-with-dart/

Section1-9の記事はこちら：
https://qiita.com/igakeso/items/b6e602449cfb2d586f36

Section10-12の記事はこちら：
https://qiita.com/igakeso/items/00a5dcca6b40c0a3e644

Section13-14の記事はこちら：
https://qiita.com/igakeso/items/f8edac7d1f19c9cbc7f7

・セクション15：名前付きルート定義、static、Animation、Mixin、Firebase環境構築、Firebase Authentication、FirebaseのCRUD処理、Stream、StreamBuilder、ListView、TextEditingController、Authルール設定等
みんな大好きFirebase、FirebaseのAuthとCRUDが出来れば作りたいアプリの基礎が作れるようになる、

参考リンク：
https://dart.dev/guides/language/language-tour#keywords
https://firebase.google.com
https://firebase.google.com/docs
https://firebase.flutter.dev
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407895/3ab55a0b-3c90-fea8-c637-0c2ff1069aec.png' width=200/>

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(FlashChat());
}

class FlashChat extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: WelcomeScreen.id,
      routes: {
        WelcomeScreen.id: (context) => WelcomeScreen(),
        RegistrationScreen.id: (context) => RegistrationScreen(),
        LoginScreen.id: (context) => LoginScreen(),
        ChatScreen.id: (context) => ChatScreen(),
      },
    );
  }
}

class WelcomeScreen extends StatefulWidget {
  static const String id = 'welcome_screen';
  @override
  _WelcomeScreenState createState() => _WelcomeScreenState();
}

class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProviderStateMixin{
  late AnimationController controller;
  late Animation animation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );
    controller.forward();
    controller.addListener(() {
      setState(() {});
    });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Hero(
                  tag: 'logo',
                  child: Container(
                    child: Image.asset('images/logo.png'),
                    height: 40.0,
                  ),
                ),
                AnimatedTextKit(
                  animatedTexts: [
                    TypewriterAnimatedText(
                      'ぬこしんチャット',
                      textStyle: const TextStyle(
                      color: Colors.black,
                      fontSize: 30.0,
                      fontWeight: FontWeight.w900,
                    ),),
                  ],
                ),
              ],
            ),
            RoundedButton(
              color: Colors.lightBlue,
              onPressed: () {
                Navigator.pushNamed(context, LoginScreen.id);
              },
              buttonTitle: 'Login',
            ),
            RoundedButton(
              color: Colors.blueAccent,
              onPressed: () {
                Navigator.pushNamed(context, RegistrationScreen.id);
              },
              buttonTitle: 'Register',
            ),
          ],
        ),
      ),
    );
  }
}

class RoundedButton extends StatelessWidget {
  RoundedButton({required this.buttonTitle, required this.color, required this.onPressed});
  final String buttonTitle;
  final Color color;
  final dynamic onPressed;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 16.0),
      child: Material(
        elevation: 5.0,
        color: color,
        borderRadius: BorderRadius.circular(30.0),
        child: MaterialButton(
          onPressed: onPressed,
          minWidth: 200.0,
          height: 42.0,
          child: Text(buttonTitle),
        ),
      ),
    );
  }
}

class LoginScreen extends StatefulWidget {
  static const String id = 'login_screen';
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  late String email;
  late String password;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Flexible(
              child: Hero(
                tag: 'logo',
                child: Container(
                  height: 200.0,
                  child: Image.asset('images/logo.png'),
                ),
              ),
            ),
            const SizedBox(
              height: 40.0,
            ),
            TextField(
              keyboardType: TextInputType.emailAddress,
              textAlign: TextAlign.center,
              onChanged: (value) {
                email = value;
              },
              decoration: kTextFieldDecoration.copyWith(
                hintText: 'Enter your email',
              ),
            ),
            const SizedBox(
              height: 8.0,
            ),
            TextField(
              obscureText: true,
              textAlign: TextAlign.center,
              onChanged: (value) {
                password = value;
              },
              decoration: kTextFieldDecoration.copyWith(
                hintText: 'Enter your password',
              ),
            ),
            const SizedBox(
              height: 24.0,
            ),
            RoundedButton(
              buttonTitle: 'Login',
              color: Colors.lightBlueAccent,
              onPressed: () async {
                try {
                  await FirebaseAuth.instance.signInWithEmailAndPassword(
                      email: email,
                      password: password
                  );
                } catch (e) {
                  print(e);
                };
                Navigator.pushNamed(context, ChatScreen.id);
              },
            ),
          ],
        ),
      ),
    );
  }
}

class RegistrationScreen extends StatefulWidget {
  static const String id = 'registration_screen';
  @override
  _RegistrationScreenState createState() => _RegistrationScreenState();
}

class _RegistrationScreenState extends State<RegistrationScreen> {
  late String email;
  late String password;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Flexible(
              child: Hero(
                tag: 'logo',
                child: Container(
                  height: 200.0,
                  child: Image.asset('images/logo.png'),
                ),
              ),
            ),
            const SizedBox(
              height: 40.0,
            ),
            TextField(
              keyboardType: TextInputType.emailAddress,
              textAlign: TextAlign.center,
              onChanged: (value) {
                email = value;
              },
              decoration: kTextFieldDecoration.copyWith(
                hintText: 'Enter your email',
              ),
            ),
            const SizedBox(
              height: 8.0,
            ),
            TextField(
              obscureText: true,
              textAlign: TextAlign.center,
              onChanged: (value) {
                password = value;
              },
              decoration: kTextFieldDecoration.copyWith(
                hintText: 'Enter your password',
              ),
            ),
            const SizedBox(
              height: 24.0,
            ),
            RoundedButton(
              buttonTitle: 'Register',
              color: Colors.blueAccent,
              onPressed: () async {
                try {
                  await FirebaseAuth.instance.createUserWithEmailAndPassword(
                      email: email,
                      password: password
                  );
                } catch (e) {
                  print(e);
                }
                Navigator.pushNamed(context, ChatScreen.id);
              },
            ),
          ],
        ),
      ),
    );
  }
}

late User loggedInUser;

class ChatScreen extends StatefulWidget {
  static const String id = 'chat_screen';
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  late String messageText;
  final TextEditingController messageTextController = TextEditingController();

  @override
  void initState() {
    super.initState();
    getCurrentUser();
    // getMessage();
  }

  Future<void> getCurrentUser() async {
    try {
      loggedInUser = await FirebaseAuth.instance.currentUser!;
      print(loggedInUser.email);
    } catch (e) {
      print(e);
    }
  }

  // void getMessage() async {
  //   final messages = await FirebaseFirestore.instance.collection('messages')
  //       .get();
  //   for (var message in messages.docs) {
  //     print(message['text']);
  //     print(message['sender']);
  //   }
  // }

  // void messageStream() async {
  //   final snapshots = FirebaseFirestore.instance.collection('messages').snapshots();
  //   snapshots.forEach((snapshot) {
  //     for (var doc in snapshot.docs) {
  //       print(doc.data());
  //     }
  //   });
  // }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: null,
        actions: [
          IconButton(
              icon: const Icon(Icons.logout),
              onPressed: () {
                FirebaseAuth.instance.signOut();
                Navigator.pop(context);
              }
              ),
        ],
        title: const Text('ぬこしんチャット'),
        backgroundColor: Colors.lightBlueAccent,
      ),
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            StreamBuilder<QuerySnapshot>(
              stream: FirebaseFirestore.instance.collection('messages').orderBy('createdTime', descending:true).snapshots(),
              builder: (context, snapshot) {
                List<MessageItem> messageItems = [];
                if(snapshot.hasData) {
                  final messages = snapshot.data!.docs;
                  for (var message in messages) {
                    final currentUser = loggedInUser.email;
                    final messageItem = MessageItem(
                      text: message['text'],
                      sender: message['sender'],
                      isMe: currentUser == message['sender'],
                    );
                    messageItems.add(messageItem);
                  }
                  return Expanded(
                    child: ListView(
                      reverse: true,
                      padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
                      children: messageItems,
                    ),
                  );
                } else {
                  return const Center(
                      child: CircularProgressIndicator(
                        backgroundColor: Colors.blueAccent,
                      )
                  );
                }
              }
            ),
            Container(
              decoration: kMessageContainerDecoration,
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Expanded(
                    child: TextField(
                      controller: messageTextController,
                      onChanged: (value) {
                        messageText = value;
                      },
                      decoration: kMessageTextFieldDecoration,
                    ),
                  ),
                  TextButton(
                    onPressed: () {
                      FirebaseFirestore.instance.collection('messages').add({
                        'text': messageText,
                        'sender': loggedInUser.email,
                        'createdTime': DateTime.now(),
                      });
                      messageTextController.clear();
                    },
                    child: const Icon(Icons.send),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class MessageItem extends StatelessWidget {
  MessageItem({required this.text, required this.sender, required this.isMe});
  final String text;
  final String sender;
  final bool isMe;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Column(
        crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
        children: [
          Material(
            elevation: 5.0,
            color: isMe ? Colors.lightBlue: Colors.white,
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
              child: Text(
                '$text',
                style: TextStyle(
                  color: isMe ? Colors.white : Colors.black,
                  fontSize: 20,
                ),
              ),
            ),
            borderRadius: isMe
                ? const BorderRadius.only(
                topLeft: Radius.circular(30.0),
                topRight: Radius.circular(30.0),
                bottomLeft: Radius.circular(30.0),
                )
                : const BorderRadius.only(
                topLeft: Radius.circular(30.0),
                topRight: Radius.circular(30.0),
                bottomRight: Radius.circular(30.0),
            ),
          ),
          Text(
              sender,
            style: const TextStyle(
              color: Colors.black38,
              fontSize: 12,
            ),
          ),
        ],
      ),
    );
  }
}

const kSendButtonTextStyle = TextStyle(
  color: Colors.lightBlueAccent,
  fontWeight: FontWeight.bold,
  fontSize: 18.0,
);

const kMessageTextFieldDecoration = InputDecoration(
  contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
  hintText: 'message',
  border: InputBorder.none,
);

const kMessageContainerDecoration = BoxDecoration(
  border: Border(
    top: BorderSide(color: Colors.lightBlueAccent, width: 2.0),
  ),
);

const kTextFieldDecoration =  InputDecoration(
  hintText: 'hintText',
  contentPadding:
  EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
  border: OutlineInputBorder(
    borderRadius: BorderRadius.all(Radius.circular(32.0)),
  ),
  enabledBorder: OutlineInputBorder(
    borderSide:
    BorderSide(color: Colors.lightBlueAccent, width: 1.0),
    borderRadius: BorderRadius.all(Radius.circular(32.0)),
  ),
  focusedBorder: OutlineInputBorder(
    borderSide:
    BorderSide(color: Colors.lightBlueAccent, width: 2.0),
    borderRadius: BorderRadius.all(Radius.circular(32.0)),
  ),
);

```

・セクション16：state management、StatelessWidget/StatefulWidgetのメモリ消費の違い、Lifting state up、Provider、ChangeNotifier、Consumer、Inherited Widget等
状態管理の方法としてProviderモデルを紹介、途中までLifting state upモデルでアプリを作りつつ、後半はProviderモデルへリファクタリングしていく、状態管理はこれじゃなくてはいけないみたいな指定はないが、最近はProviderモデルが推奨らしい
参考リンク：
https://pub.dev/packages/provider
https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407895/a6793dd9-3297-b2a7-fd40-ad4a44dedb0c.png' width=200/>

```
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:collection';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => TaskData(),
      child: MaterialApp(
        home: TaskScreen(),
      ),
    );
  }
}

class TaskData extends ChangeNotifier{
  final List<Task> _taskLists =[
    Task(name: 'Flutter Bootcampを進める', isDone: false),
    Task(name: 'ご飯を食べる', isDone: false),
    Task(name: 'サウナに行く', isDone: false),
  ];

  UnmodifiableListView<Task> get tasks{
    return UnmodifiableListView(_taskLists);
  }

  int get TaskCount{
    return _taskLists.length;
  }

  void addTask(String newTaskTitle) {
    final task = Task(name: newTaskTitle);
    _taskLists.add(task);
    notifyListeners();
  }

  void updateTask(Task task) {
    task.toggleDone();
    notifyListeners();
  }

  void deleteTask(Task task) {
    _taskLists.remove(task);
    notifyListeners();
  }
}

class Task {
  String name;
  bool isDone;
  Task({required this.name, this.isDone = false});

  void toggleDone() {
    isDone = !isDone;
  }
}

class TaskScreen extends StatelessWidget {
  String newTaskTitle = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.lightBlueAccent,
      body: Column(
        children: [
          Padding(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const CircleAvatar(
                  child: Icon(
                    Icons.list,
                    size: 30.0,
                    color: Colors.lightBlueAccent,
                  ),
                  radius: 30.0,
                  backgroundColor: Colors.white,
                ),
                const SizedBox(
                  height: 10.0,
                ),
                const Text(
                    'ぬこしんTODO',
                    style: TextStyle(
                        color: Colors.white,
                        fontSize: 40.0,
                        fontWeight: FontWeight.bold
                ),
                ),
                Text(
                  '${Provider.of<TaskData>(context).TaskCount} Tasks',
                  style: const TextStyle(
                      color: Colors.white,
                      fontSize: 18.0,
                  ),
                ),
              ],
            ),
            padding: const EdgeInsets.only(top: 60, left: 20, right: 20, bottom: 20),
          ),
          Expanded(
            child: Container(
              child: Padding(
                padding: const EdgeInsets.only(left: 20, right: 20),
                child: Consumer<TaskData>(
                  builder: (context, TaskData, child) {
                    return ListView.builder(
                      itemCount: TaskData.TaskCount,
                      itemBuilder: (context, index) {
                        return ListTile(
                          title: Text(
                            TaskData.tasks[index].name,
                            style: TextStyle(
                              color: TaskData.tasks[index].isDone ? Colors.lightBlueAccent : null,
                            ),
                          ),
                          trailing: Checkbox(
                            value: TaskData.tasks[index].isDone,
                            onChanged: (checkboxState) {
                              TaskData.updateTask(TaskData.tasks[index]);
                            },
                            activeColor: Colors.lightBlueAccent,
                          ),
                          onLongPress: () {
                            TaskData.deleteTask(TaskData.tasks[index]);
                          },
                        );
                      },
                    );
                  },
                ),
              ),
              decoration: const BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(20.0),
                    topRight: Radius.circular(20.0),
                  )
              ),
            ),
          )
        ],
      ),
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.lightBlueAccent,
        child: const Icon(
            Icons.add
        ),
        onPressed: () {
          showModalBottomSheet(
              context: context, 
              builder: (context) {
                return SingleChildScrollView(
                    child: Container(
                      child: Padding(
                        padding: const EdgeInsets.all(20.0),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: [
                            const Text(
                              'Add Task',
                              textAlign: TextAlign.center,
                              style: TextStyle(
                                fontSize: 30.0,
                                color: Colors.lightBlueAccent,
                              ),
                            ),
                            TextField(
                              autofocus: true,
                              textAlign: TextAlign.center,
                              onChanged: (value) {
                                newTaskTitle = value;
                              },
                            ),
                            const SizedBox(
                              height: 20.0,
                            ),
                            TextButton(
                              child: const Text(
                                'Add',
                                style: TextStyle(
                                  fontSize: 30.0,
                                  color: Colors.white,
                                ),
                              ),
                              onPressed: () {
                                Provider.of<TaskData>(context, listen: false).addTask(newTaskTitle);
                                Navigator.pop(context);
                              },
                              style: TextButton.styleFrom(
                                backgroundColor: Colors.lightBlueAccent,
                              ),
                            ),
                          ],
                        ),
                      ),
                      decoration: const BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.only(
                            topLeft: Radius.circular(20.0),
                            topRight: Radius.circular(20.0),
                          )
                      ),
                    ),
                );
              }
          );
        },
      ),
    );
  }
}
```

・セクション16-205：Provider
セクション16の途中で登場したのProviderによる状態管理の解説
参考リンク：
https://pub.dev/packages/provider

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Data>(
      builder: (context) => Data(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: MyText(),
          ),
          body: Level1(),
        ),
      ),
    );
  }
}

class Level1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Level2(),
    );
  }
}

class Level2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        MyTextField(),
        Level3(),
      ],
    );
  }
}

class Level3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(Provider.of<Data>(context).data);
  }
}

class MyText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(Provider.of<Data>(context, listen: false).data);
  }
}

class MyTextField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextField(
      onChanged: (newText) {
        Provider.of<Data>(context).changeString(newText);
      },
    );
  }
}

class Data extends ChangeNotifier {
  String data = 'Some data';

  void changeString(String newString) {
    data = newString;
    notifyListeners();
  }
}
```
セクション17、18は演習なし
・セクション17：今後の学び方等
・セクション18：フリートーク
簡単なものも含め全部で15個くらいのアプリを作りつつ、Flutterの基本を網羅的に学ぶことが出来ました。特に、オブジェクト志向プログラミング、Widgetのカスタム、APIの扱い、Firebaseとの連携、Providerモデルあたりが重要ポイント、30時間は長かったですが、終わってみればあっという間でした。良い教材です。