見出し画像

Kyatのアーキテクチャについて試行錯誤中

とりあえず今回は下記のようなアーキテクチャにしようと思う。

Blocというか、MVVM的なアーキテクチャに近い感じの実装になる。

ProviderとRepositoryはInterfaceを使ってそれぞれをモック化し、Blocを初期化するときにそれぞれをインスタンスを作るように実装する。

本当はそれぞれDaggerのようなDIコンテナを使ってInjectしたいが、FlutterのDIコンテナはまだいろいろ制約があるようなので一旦断念。

制約があるが、使いたいって方は下記のプラグインを使うといいと思う。

素晴らしい記事も公開されていた。


ソースコードを一部公開する

一部ソースコードも公開します。まだリファクタ途中なので全部の処理をつくれているわけではないが参考なればすごく嬉しい。


- home_bloc.dart

import 'dart:async';

import 'package:group_snap_app/app/model/User.dart';
import 'package:group_snap_app/app/repository/friends_repository.dart';
import 'package:group_snap_app/app/repository/message_repository.dart';
import 'package:group_snap_app/app/repository/user_repository.dart';
import 'package:meta/meta.dart';

class HomeBloc {
  final UserRepositoryInterface userRepository;
  final FriendsRepositoryInterface friendsRepository;
  final MessageRepositoryInterface messageRepository;

  HomeBloc({
    @required this.userRepository,
    @required this.friendsRepository,
    @required this.messageRepository,
  }) {
    _getUserInfo();
    _getUserId();
    _getRequestUserCount();
    _getUnReadMessageNum();
  }

  StreamController<User> _userController = StreamController<User>();

  StreamSink<User> get _userSink => _userController.sink;

  Stream<User> get userStream => _userController.stream;

  StreamController<String> _userIdController = StreamController<String>();

  StreamSink<String> get _userIdSink => _userIdController.sink;

  Stream<String> get userIdStream => _userIdController.stream;

  StreamController<int> _friendsController = StreamController<int>();

  StreamSink<int> get _friendsSink => _friendsController.sink;

  Stream<int> get friendsStream => _friendsController.stream;

  StreamController<int> _messageController = StreamController<int>();

  StreamSink<int> get _messageSink => _messageController.sink;

  Stream<int> get messageStream => _messageController.stream;

  void dispose() {
    _userController.close();
    _userIdController.close();
    _friendsController.close();
    _messageController.close();
  }

  _getUserInfo() async {
    _userSink.add(await userRepository.getUser());
  }

  _getUserId() async {
    _userIdSink.add(await userRepository.getUserId());
  }

  _getRequestUserCount() async {
    String uid = await userRepository.getUserId();
    _friendsSink.add(await friendsRepository.getRequestUserCount(uid));
  }

  _getUnReadMessageNum() async {
    String uid = await userRepository.getUserId();
    _messageSink.add(await messageRepository.getNonReadCount(uid));
  }
}

Blocでの制約はViewへのデータの更新は Stream経由で更新する ということにした。いかにもViewModelっぽいw データのインプットは Sink経由で更新しています。

- user_repository.dart

import 'package:group_snap_app/app/model/User.dart';
import 'package:group_snap_app/app/provider/user_provider.dart';

abstract class UserRepositoryInterface {
  Future<User> getUser();
  Future<String> getUserId();
}

class UserRepository implements UserRepositoryInterface {
  final UserProviderInterface userProvider;

  UserRepository({this.userProvider});

  @override
  Future<User> getUser() async {
    String uid = await userProvider.getUid();
    return userProvider.getUser(uid);
  }

  @override
  Future<String> getUserId() async {
    return await userProvider.getUid();
  }
}

抽象的な UserRepositoryInterface を用意し、それを継承して中の処理をかいている。こうすることによって、Repositoryをモック化してViewModelのテストがかけるからこうした。ViewModelのテストの書き方は下記をご覧ください。

今ここで悩んでいるのはinterfaceという名前はなんかおかしいw

ここも将来的に変えるだろうけど一旦はこれでいこうかなと思う。


- user_provider.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:group_snap_app/app/model/User.dart';
import 'package:shared_preferences/shared_preferences.dart';

abstract class UserProviderInterface {
  Future<String> getUid();

  Future<User> getUser(String uid);

  Future<List<String>> getUsersGroupIdList(String uid);

  Future<int> getUsersMessageCount(String groupId, String uid);
}

class UserProvider implements UserProviderInterface {
  @override
  Future<String> getUid() async {
    SharedPreferences preferences = await SharedPreferences.getInstance();
    String uid = preferences.getString("uid");
    return uid;
  }

  @override
  Future<User> getUser(String uid) async {
    var db = Firestore.instance;
    DocumentSnapshot snapshot = await db.collection("user").document(uid).get();
    return User.from(snapshot.data, uid);
  }

  @override
  Future<List<String>> getUsersGroupIdList(String uid) async {
    var db = Firestore.instance;
    QuerySnapshot snapshot = await db
        .collection("user")
        .document(uid)
        .collection("group")
        .getDocuments();

    List<DocumentSnapshot> groupIdListDoc = snapshot.documents;
    List<String> groupIdList;

    for (var doc in groupIdListDoc) {
      groupIdList.add(doc.documentID);
    }

    return groupIdList;
  }

  @override
  Future<int> getUsersMessageCount(String groupId, String uid) async {
    var db = Firestore.instance;
    QuerySnapshot snapshot = await db
        .collection("user")
        .document(uid)
        .collection("group")
        .document(groupId)
        .collection("message")
        .getDocuments();

    return snapshot.documents.length;
  }
}

providerクラスでは主に、Firestoreとのやりとりや、ローカルDBとのやりとりなどの処理を書いていく層にした。

ここも UserProviderInterface を使って抽象的なクラスを設定して実装している。正直ここの命名もなんとかしたいw


これから

Kyatの次のリリースではAndroid版もリリースも控えています。また、使いづらいと感じている箇所も何点かあるので、そこも直しつつ少しずつ上で紹介したアーキテクチャに書き換えていくつもり。

Twitterを使った「〜〜〜ついて話す人募集!」みたいな機能を作りたいなーと構想中だけどリファクタ終わってからかな。


投げ銭はいりません。それより無料でできる拡散をしてください!! 感想をツイートしていただけることが一番嬉しいです!!