🚢

FlutterでDDDぽいやつを設計する

2023/11/21に公開

レイヤーごとにファイルを分ける

レイヤーとは、層、階層、層にする、層をなす、などの意味を持つ英単語。 何かの構造や設計などが階層状になっているとき、それを構成する一つ一つの階層のことをレイヤーという。

図にするとこんな感じでしょうか?

過去にこんな記事を書いた
今回は、新しい技術を使うというより、目的を達成することをやっていきます。
https://zenn.dev/joo_hashi/articles/d1dc66483f4ebe

では解説しましょう

📦Entities

Entitiesとは、ただの入れ物です。Flutterではモデルクラスですかね。ここにデータを保持します。RDBのテーブルのカラムと同じ名前、データ型にする。Firestoreなら、コレクションのフィールドですね。
今回は、Freezedは使ってないです💦

エンティティ
// エンティティは、Firestoreのデータをそのまま格納する
class UserModel {
  final String id;
  final String name;
  final String email;
  final String password;

  UserModel({
    required this.id,
    required this.name,
    required this.email,
    required this.password,
  });

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'],
      name: json['name'],
      email: json['email'],
      password: json['password'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'password': password,
    };
  }
}

🎁Repository

レポジトリは、ロジックをもたないメソッドを定義したクラスです。ロジックは継承した具象クラスで使います。

抽象クラスとは、オブジェクト指向プログラミングにおいて用いられる特殊なクラスの宣言であり、その内部には実装を持たないという機能の分からない、名前の通り抽象的なクラスです。 抽象クラスは、その特徴としてクラス自身はオブジェクトの実体、インスタンスを生成できません。

具象クラスとは、実際に動作するプログラムが記述された具象メソッドが集まったクラスです。具象クラスは、直接オブジェクトの生成も行えます。

Repository
import 'package:flutter_app/domain/entities/user_model.dart';

// リポジトリはロジックは書かずに、データソースの実装を呼び出すだけ
abstract class UserRepository {
  Stream<List<UserModel>> userStream();
  Future<void> setUser(UserModel user);
}

💽DataSource

データソースは、抽象クラスがロジックを持たないクラスであるのに、対して具象クラスなので、持っています。このクラスには、APIやFirebaseとのやりとりをするロジックを書きます。

DataSource
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_app/domain/entities/user_model.dart';
import 'package:flutter_app/domain/repository/user_repository.dart';

// データソースは、データベースやAPIなどのデータの入出力を行う
class UserDataSource implements UserRepository {
  final db = FirebaseFirestore.instance;

  
  Future<void> setUser(UserModel user) async {
    try {
      await db.collection('users').add(user.toJson());
    } catch (e) {
      throw e.toString();
    }
  }

  
  Stream<List<UserModel>> userStream() {
    try {
      return db.collection('users').snapshots().map((snapshot) {
        return snapshot.docs.map((doc) => UserModel.fromJson(doc.data())).toList();
      });
    } catch (e) {
      throw e.toString();
    }
  }
}

ViewModel

ModelとViewの間にある、データを状態として持っているクラスですね。ロジック自体は書きません。エラーが出た時の処理をView側に通知するのに使います。

タイトル


import 'package:flutter_app/data/user_data_source.dart';
import 'package:flutter_app/domain/entities/user_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ViewModelは、ロジックは実行せずに、データソースの実装を呼び出すだけ
class UserNotifier extends StateNotifier<List<UserModel>> {
  final UserDataSource _userDataSource;

  UserNotifier(this._userDataSource) : super([]);

  Future<void> setUser(UserModel user) async {
    try {
      await _userDataSource.setUser(user);
      // [...state, user]は、stateのリストにuserを追加した新しいリストを返す
      state = [...state, user];
    } catch (e) {
      throw e.toString();
    }
  }

  Stream<List<UserModel>> loadUsers() {
    try {
      return _userDataSource.userStream();
    } catch (e) {
      throw e.toString();
    }
  }
}

final userNotifierProvider = StateNotifierProvider<UserNotifier, List<UserModel>>((ref) {
  return UserNotifier(UserDataSource());
});

📺View

Viewは、アプリのUIを構築している場所です。今回は実装が良いというわけではないので、エラー処理はできておりません。

UI側
import 'package:flutter/material.dart';
import 'package:flutter_app/application/view_model/user_view_model.dart';
import 'package:flutter_app/domain/entities/user_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Viewは、ViewModelから、データを受け取って、画面に描画する
class UserView extends ConsumerWidget {
  const UserView({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {

    ref.listen(userNotifierProvider, (prev, next) {
      // nullだったら、スナックバーを出す
      if (prev!.isEmpty && next.isNotEmpty) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('nullだったら、スナックバーを出す'),
          ),
        );
      }
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('User List'),
      ),
      // loadUsers()を呼び出す
      body: StreamBuilder<List<UserModel>>(
        stream: ref.watch(userNotifierProvider.notifier).loadUsers(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return const Center(
              child: Text('エラーが発生しました'),
            );
          }

          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          final users = snapshot.data!;

          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];
              return ListTile(
                title: Text(user.name),
                subtitle: Text(user.email),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(userNotifierProvider.notifier).setUser(UserModel(
                id: 'test',
                name: 'test',
                email: 'test@email',
                password: 'test',
              ));
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

実行してみる

View側で、操作をして、モデルにデータを保持してFirestoreに追加して、Model -> ViewModel ->View側に、逆向きで取得したデータを渡しして表示するのをやってみます。

main関数のあるページ
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/application/view/user_view.dart';
import 'package:flutter_app/firebase_options.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const ProviderScope(child: 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 UserView(),
    );
  }
}

今回は、ハードコーディングですが、データを追加して、View側に表示するのをやってみました。

最後に

今回は、よく使うriverpodのProviderをあまり使わずに、レイヤー毎にモデルクラスやロジックを分けて、実際に使ってみました。
不要なコードもありますが、よくわからない横文字の勉強になりました。riverpodのプロバイダーをつかわなくても要件を満たすことはできそうだとなと思いました。

Discussion