🏸

sealed class で例外処理を作ってみた

2023/12/28に公開

対象者

  • Dart3のシールドクラスに興味ある人.
  • 何が作れるか探求したい人.

やること/やらないこと

  • 思いつきで作ったみたので、とりあえず認証機能を実装してみた.
  • sealed classをすごく追求はしなかった.

プロジェクトの説明

Dart3から追加されたsealed classクラスを使って何か作ってみたかった。認証機能の例外処理が発生したときに、エラーによって異なるSnackBarを出す処理を考えてみた。普段書いてるコードよりも長くなってしまって、いるのかな〜という感じでしたが...

sealed classってなんなのか?、Kotlinでもあるんですけど、enumのような使い方ができるクラスです。私は、エラー処理をするswith文を書いた関数に取り入れてみました。

これがシールドクラス
// インスタン化できないシールドクラスを定義します。
sealed class FirebaseAuthSealed {}
// 継承したクラスを定義します。
class EmailInvalid extends FirebaseAuthSealed {
  EmailInvalid(this.message);
  final String message;
}

class AccountDisabled extends FirebaseAuthSealed {
  AccountDisabled(this.message);
  final String message;
}

class AccountNotFound extends FirebaseAuthSealed {
  AccountNotFound(this.message);
  final String message;
}

class PasswordsDoNotMatch extends FirebaseAuthSealed {
  PasswordsDoNotMatch(this.message);
  final String message;
}

class DefaultError extends FirebaseAuthSealed {
  DefaultError(this.message);
  final String message;
}

// FirebaseAuthSealedは、FirebaseAuthの例外を列挙型で表現したものです。
FirebaseAuthSealed mapErrorCodeToEnum(String code) {
  switch (code) {
    case 'invalid-email':
      return EmailInvalid('メールアドレスが無効です。');
    case 'user-disabled':
      return AccountDisabled('このアカウントは無効になっています。');
    case 'user-not-found':
      return AccountNotFound('アカウントが見つかりません。');
    case 'wrong-password':
      return PasswordsDoNotMatch('パスワードが一致しません。');
    default:
      return DefaultError('エラーが発生しました。');
  }
}

// FirebaseAuthSealedの例外を、ユーザーに見せるメッセージに変換します。
String describe(FirebaseAuthSealed exception) {
  switch (exception.runtimeType) {
    case EmailInvalid:
      return (exception as EmailInvalid).message;
    case AccountDisabled:
      return (exception as AccountDisabled).message;
    case AccountNotFound:
      return (exception as AccountNotFound).message;
    case PasswordsDoNotMatch:
      return (exception as PasswordsDoNotMatch).message;
    case DefaultError:
    default:
      return (exception as DefaultError).message;
  }
}

今回は、ログインだけしか使っていないが、認証に必要なメソッドを定義したクラスを作ってみました。メールアドレスが一致していなければ、メールアドレスのエラーを出し、パスワードが一致していなければパスワードのエラーを出します。

認証のクラス
import 'package:auth_seald/auth_sealed.dart';
import 'package:firebase_auth/firebase_auth.dart';

// メールアドレスとパスワードでの認証を行うサービスで、sealedクラスで例外を表現します。
class AuthService {
  final auth = FirebaseAuth.instance;

  Future<UserCredential> signUp(String email, String password) async {
    try {
      final result = await auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      return result;
    } on FirebaseAuthException catch (e) {
      throw describe(mapErrorCodeToEnum(e.code));
    } catch (e) {
      throw 'エラーが発生しました。';
    }
  }

  Future<UserCredential> signIn(String email, String password) async {
    try {
      final result = await auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      return result;
    } on FirebaseAuthException catch (e) {
      throw describe(mapErrorCodeToEnum(e.code));
    } catch (e) {
      throw 'エラーが発生しました。';
    }
  }

  // パスワードのリセット
  Future<void> sendPasswordResetEmail(String email) async {
    try {
      await auth.sendPasswordResetEmail(email: email);
    } on FirebaseAuthException catch (e) {
      throw describe(mapErrorCodeToEnum(e.code));
    } catch (e) {
      throw 'エラーが発生しました。';
    }
  }
}

こちらがアプリのログインとログイン後のデモをするページです。

main.dart
import 'package:auth_seald/auth_service.dart';
import 'package:auth_seald/firebase_options.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SignInPage(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    final _emailController = TextEditingController();
    final _passwordController = TextEditingController();
    return Scaffold(
      appBar: AppBar(
        title: const Text('ログイン'),
      ),
      body: Center(
        child: Column(
          children: [
            const SizedBox(height: 16),
            const Text('メールアドレス'),
            const SizedBox(height: 8),
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                hintText: 'メールアドレスを入力してください',
              ),
            ),
            const SizedBox(height: 16),
            const Text('パスワード'),
            const SizedBox(height: 8),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                hintText: 'パスワードを入力してください',
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () async {
                try {
                  await AuthService().signIn(
                    _emailController.text,
                    _passwordController.text,
                  );
                  if (context.mounted) {
                    Navigator.of(context).pushReplacement(
                      MaterialPageRoute(
                        builder: (context) => const HomePage(),
                      ),
                    );
                  }
                } catch (e) {
                  if(context.mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text(e.toString()),
                    ),
                  );
                  }
                }
              },
              child: const Text('ログイン'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ホーム'),
      ),
      body: Center(
        child: Column(
          children: [
            const SizedBox(height: 16),
            const Text('ホーム'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () async {
                await FirebaseAuth.instance.signOut();
                if (context.mounted) {
                  Navigator.of(context).pushReplacement(
                    MaterialPageRoute(
                      builder: (context) => const SignInPage(),
                    ),
                  );
                }
              },
              child: const Text('ログアウト'),
            ),
          ],
        ),
      ),
    );
  }
}

こちらがデモです。入力に成功するとログインできます。



感想

今日は、sealed classについて探求して認証機能で使えないか試してみました。他にも便利な使い道ないか探求中です。Dart3ってあまり情報ないので、色々試してみたいです。

Discussion