🤦

Singletonと仲良くなりたい

2023/01/20に公開

Riverpod頑張らなくてもいいかも?

Riverpodを公式が推奨するようになったようです?
https://www.youtube.com/watch?v=vU9xDLdEZtU&list=PLjxrf2q8roU3wk7CDw4RfV3mEwOJbjx1k

状態管理のpackageで人気があったProviderは、更新が止まっているようで、後継として開発された、状態管理するのに便利だけど初心者がやったら挫折するRiverpodが現在は周流になっちゃったみたいですね😱

Riverpod難しいよどうしょう...

画面とロジックは切り分けないと、一つのファイルにコードをいっぱい書かないといけません!

  • StatefulWidgetだけで、アプリを作るとどうなるのか?
    • コードの可読性が悪くなる.
    • アプリが何度も再描画される.
    • アプリに負荷がかかる.
    • メンテナンスが悪くなる.

小さなアプリだったら、気にしなくてもいいと思いますが、規模の大きなアプリだとバグだらけになってしまいます。
人が書いたコードを見たときに、「あれ、このロジックってどこにあるの?」を探すのが、大変だったりします。
DevToolsを使って探したりしますが、フォルダ別にファイルを分けてくれた方が探しやすいですね。

Singletonを使ってみる

同じインスタンスを生成しないSingletonなるものがあります。
これを使うと、どこからでも変数やメソッドを呼び出せるようになります。コードの可読性を良くすることができそうですね。
こちらの記事が参考になると思います。
https://zenn.dev/pressedkonbu/articles/stateful-is-good

Firebaseに必要なpackageを用意

こちらのpackageを使用するので、Flutterアプリに追加しておいてください。
https://pub.dev/packages/firebase_core
https://pub.dev/packages/cloud_firestore
https://pub.dev/packages/firebase_auth

Firebaseと連携してみる

Singletonは少し使ったことあるだけで、全然理解してなかったので、Riverpod + Firebaseの組み合わせをSingleton + Firebaseに変更して、どうなるか試してみました。

まずは、utilsディレクトリを作成して、singleton_data.dartをその中に作成してください。

utils/singleton_data.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class SingletonData {
  // インスタンスを事前に作成.
  SingletonData.internal();

  // グローバルなアクセスポイントを与える.
  static final _singleton = SingletonData.internal();

  // FireStoreにデータを保存するメソッド.
  Future<void> addData(String _etController) async {
    final getDate = DateTime.now();
    final singletonFunction =
        await FirebaseFirestore.instance.collection('singleton').add({
      'createdAt': Timestamp.fromDate(getDate), // Timestampを作成.
      'addData': _etController // TextFieldのデータを保存.
    });
  }

  // factoryを使うコンストラクター.
  // コンストラクターの初期化の助けを借りてクラスのインスタンスを遅延させる.
  factory SingletonData() {
    return _singleton;
  }
}

uiディレクトリを作成して、その中にmyhome_page.dartを作成してください。

ui/myhome_page.dart
import 'package:flutter/material.dart';
import 'package:timelineview_app/utils/singleton_data.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final TextEditingController _etController = TextEditingController();
  // シングルトンを呼び出す.
  SingletonData singletonData = SingletonData();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Singleton'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          TextField(
            controller: _etController,
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                    child: MaterialButton(
                  onPressed: () {
                    setState(() {
                      // FireStoreにデータを追加するメソッドを使う.
                      // TextEditingControllerの値を入れる.
                      singletonData.addData(_etController.text);
                    });
                  },
                  child: const Text('保存'),
                  color: Colors.blue,
                )),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

アプリを実行するmain.dartを作成します。

main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:timelineview_app/ui/myhome_page.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

スクリーンショット

FireStoreにデータが保存できているようですね。
Riverpodいるのか疑問だな...
初心者はSingletonから使っていった方がいいかもですね💁


補足情報

setStateなしでも関数が実行できたのに、後で気づきました。
これは、便利かもしれない!
画面を必要以上に更新しなくなるので、アプリへの負荷を減少させることできそうですね。

FireStoreに値を追加するロジックを実行する箇所から、setStateを削除。

Expanded(
                    child: MaterialButton(
                  onPressed: () {
                    singletonData.addData(_etController.text);
                  },
                  child: const Text('保存'),
                  color: Colors.blue,
                )),

実行してみましたが、データが保存されています!


ログインもやってみた!

エラーメッセージを一応出せるコードを関数化して、Singletonで使ってます。
YouTube動画を撮ってみたので、動作は確認できるようにしました。
動画見せないと、エラーメッセージが出てくるところがわかりにくいですからね。
https://youtu.be/EKgdyh_CII4

使用したコード

ログイン後の状態を維持する機能もつけているので、初心者の方には、ありがたい情報だと思います。
ぜひ、参考にしてみてください。

utils/singleton_data.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:timelineview_app/pages/user_login.dart';

class SingletonData {
  // インスタンスを事前に作成.
  SingletonData.internal();

  // グローバルなアクセスポイントを与える.
  static final _singleton = SingletonData.internal();

  // FireStoreにデータを保存するメソッド.
  Future<void> addData(String _etController) async {
    final getDate = DateTime.now();
    final singletonFunction =
        await FirebaseFirestore.instance.collection('singleton').add({
      'createdAt': Timestamp.fromDate(getDate), // Timestampを作成.
      'addData': _etController // TextFieldのデータを保存.
    });
  }

  // ログアウトするメソッド.
  Future<void> signOut(BuildContext context) async {
    final _auth = FirebaseAuth.instance;
    await _auth.signOut();
    if (_auth.currentUser == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('ログアウトしました'),
        ),
      );
      print('ログアウトしました!');
    }
    Navigator.pushReplacement(
        context, MaterialPageRoute(builder: (context) => UserLogin()));
  }

  // ログインをするメソッド.
  Future<void> loginUser(
      String email, String, password, BuildContext context) async {
    try {
      final _auth = FirebaseAuth.instance;
      final newUser = await _auth.signInWithEmailAndPassword(
          email: email, password: password);
      if (newUser != null) {
        Navigator.pushReplacement(
            context, MaterialPageRoute(builder: (context) => MainContent()));
      }
    } on FirebaseAuthException catch (e) {
      if (e.code == 'invalid-email') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(''),
          ),
        );
        print('メールアドレスのフォーマットが正しくありません');
      } else if (e.code == 'user-disabled') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('現在指定したメールアドレスは使用できません'),
          ),
        );
        print('現在指定したメールアドレスは使用できません');
      } else if (e.code == 'user-not-found') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('指定したメールアドレスは登録されていません'),
          ),
        );
        print('指定したメールアドレスは登録されていません');
      } else if (e.code == 'wrong-password') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('パスワードが間違っています'),
          ),
        );
        print('パスワードが間違っています');
      }
    }
  }

  // ユーザー作成をするメソッド.
  Future<void> createUser(
      String email, String, password, BuildContext context) async {
    try {
      final _auth = FirebaseAuth.instance;
      final newUser = await _auth.createUserWithEmailAndPassword(
          email: email, password: password);
      if (newUser != null) {
        Navigator.pushReplacement(
            context, MaterialPageRoute(builder: (context) => MainContent()));
      }
    } on FirebaseAuthException catch (e) {
      if (e.code == 'email-already-in-use') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('指定したメールアドレスは登録済みです'),
          ),
        );
        print('指定したメールアドレスは登録済みです');
      } else if (e.code == 'invalid-email') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('メールアドレスのフォーマットが正しくありません'),
          ),
        );
        print('メールアドレスのフォーマットが正しくありません');
      } else if (e.code == 'operation-not-allowed') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('指定したメールアドレス・パスワードは現在使用できません'),
          ),
        );
        print('指定したメールアドレス・パスワードは現在使用できません');
      } else if (e.code == 'weak-password') {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('パスワードは6文字以上にしてください'),
          ),
        );
        print('パスワードは6文字以上にしてください');
      }
    }
  }

  // factoryを使うコンストラクター.
  // コンストラクターの初期化の助けを借りてクラスのインスタンスを遅延させる.
  factory SingletonData() {
    return _singleton;
  }
}

UIのファイル

pagesディレクトリを作成して、その中に、sign_up.dartとuser_login.dartを作成してください。
以前書いたコードを使いまわしたのですがコードが減って、スッキリした気がします。
でもまだ改良が必要かなと思います😅

ログインとログイン後の画面のファイル

pages/user_login.dart
import 'package:flutter/material.dart';
import 'package:timelineview_app/pages/sign_up.dart';
import 'package:timelineview_app/utils/singleton_data.dart';

class UserLogin extends StatefulWidget {
  const UserLogin({Key? key}) : super(key: key);

  
  _UserLogin createState() => _UserLogin();
}

class _UserLogin extends State<UserLogin> {
  SingletonData singletonData = SingletonData();

  String email = '';
  String password = '';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ログイン'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                email = value;
              },
              decoration: const InputDecoration(
                hintText: 'メールアドレスを入力',
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                password = value;
              },
              obscureText: true,
              decoration: const InputDecoration(
                hintText: 'パスワードを入力',
              ),
            ),
          ),
          ElevatedButton(
            child: const Text('ログイン'),
            onPressed: () async {
              singletonData.loginUser(email, String, password, context);
            },
          ),
          TextButton(
              onPressed: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => Register()));
              },
              child: Text('新規登録はこちらから'))
        ],
      ),
    );
  }
}

class MainContent extends StatefulWidget {
  const MainContent({Key? key}) : super(key: key);

  
  _MainContentState createState() => _MainContentState();
}

class _MainContentState extends State<MainContent> {
  SingletonData singletonData = SingletonData();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('成功'),
        actions: [
          IconButton(
            //ステップ2
            onPressed: () async {
              singletonData.signOut(context);
            },
            icon: Icon(Icons.close),
          ),
        ],
      ),
      body: Center(
        child: Text('ログイン成功!'),
      ),
    );
  }
}

ユーザーを新規登録するファイル

pages/sign_up.dar
import 'package:flutter/material.dart';
import 'package:timelineview_app/utils/singleton_data.dart';

class Register extends StatefulWidget {
  const Register({Key? key}) : super(key: key);

  
  _RegisterState createState() => _RegisterState();
}

class _RegisterState extends State<Register> {
  SingletonData singletonData = SingletonData();

  String email = '';
  String password = '';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('新規登録'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                email = value;
              },
              decoration: const InputDecoration(
                hintText: 'メールアドレスを入力',
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                password = value;
              },
              obscureText: true,
              decoration: const InputDecoration(
                hintText: 'パスワードを入力',
              ),
            ),
          ),
          ElevatedButton(
            child: const Text('新規登録'),
            //ステップ2
            onPressed: () async {
              singletonData.createUser(email, String, password, context);
            },
          )
        ],
      ),
    );
  }
}

アプリを実行するコード

main.dartのコードをこちらに修正します。

main.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:timelineview_app/pages/user_login.dart';

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

class App extends StatelessWidget {
  
  Widget build(BuildContext context) => MaterialApp(
        title: 'Flutter app',
        home: StreamBuilder<User?>(
          stream: FirebaseAuth.instance.authStateChanges(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              // スプラッシュ画面などに書き換えても良い
              return const SizedBox();
            }
            if (snapshot.hasData) {
              // User が null でなない、つまりサインイン済みのホーム画面へ
              return MainContent();
            }
            // User が null である、つまり未サインインのサインイン画面へ
            return UserLogin();
          },
        ),
      );
}

最後に

Flutter勉強してる方に、最近流行ってるRiverpodやgo_router使わなくてもアプリを作ることができるようにする方法を技術記事に書いてみました。
初心者は、まずは StatefulWidgetとStatelessWidgetでもアプリを作れることをやってみて、それからRiverpod勉強してほしいなと思います。
ライフサイクルがわからないと、Riverpod自体も理解できないので😱
Validationの部分や特定の機能を作るのが難しい時は、無理せずにStatefulWidgetでもいいかもしれません。

私、Flutterを専門に学べるコミュニティのFlutter大学さんに所属しております。
一人で勉強するのに限界感じて入りました🫠
現在は、Flutter別荘というシェアハウスで、講師のこんぶさんから、企画、設計、開発について学んでいます。
ご興味ある方は、入会してみてください!
会員数は現在316人います。結構増えましたね。
皆様もよきFlutterライフを送ってください。
https://flutteruniv.com/

Flutter大学

Discussion