🔥

Firestoreの非同期処理をMVVMモデルで実装を分離する

2021/10/18に公開

Firestoreを利用したデータベースへのデータの設定と取得は非同期処理となります。
Androidにおいて、ActivityやFragmentにFirestoreの処理を実装をしてしまうと、画面更新処理とデータ更新処理が合わさってしまい処理が複雑になりがちです。
そこで、MVVMモデル化する事により画面処理とデータ処理を分離する事で、クリーンな実装になることを紹介したいと思います。

紹介する実装の画面イメージ

ActivityにFragmentがあり、タイトルとサブタイトルの2つのEditTextの内容をFirestoreへ設定と取得を行います。

変更前の内容

まず、MVVMモデル化する前のFirestoreの処理がFragmentにまとまってしまっている実装を紹介します。

変更前クラス図

ItemDetailFragmentにて、画面処理とBrandクラスのカスタムオブジェクトを利用してFirestoreへのデータ処理を行います。

変更前実装

ItemDetailFragment内に画面処理とデータ処理が含まれているので、ぱっと見で分かりづらくなっています。

public class ItemDetailFragment extends Fragment {
    private String name = "銘柄名";
    private EditText mTitle;
    private EditText mSubTitle;
    
    private FirebaseFirestore mDb;
    private CollectionReference mCollectionRef;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_item_detail, container, false);
	mTitle = view.findViewById(R.id.detail_title);
	mSubTitle = view.findViewById(R.id.detail_subtitle);

	mDb = FirebaseFirestore.getInstance();
	mCollectionRef = mDb.collection("BrandList");
        mCollectionRef.whereEqualTo("title", name).get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        if (task.isSuccessful()) {
                            if (task.getResult().getDocuments().isEmpty()) return;
                            Map<String, Object> brandData = (Map<String, Object>) task.getResult().getDocuments().get(0).getData();
                            String title = (String) brandData.get("title");
                            String subTitle = (String) brandData.get("subTitle");
			    mTitle.setText(title);
			    mSubTitle.setText(subTitle);
                        } else {
                            Log.d("FIREBASE", task.getException().toString());
                        }
                    }
                });

	return view;
    }

    /**
     * Toobarのチェックボタンを押下時に呼ばれる
     */
    public void confirmEditData() {
        String title = mTitle.getText().toString();
        String subTitle = mSubTitle.getText().toString();
	Brand brand = new Brand(title, subTitle);

        mCollectionRef.whereEqualTo("title", brand.getTitle()).get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        if (task.isSuccessful()) {
                            if (task.getResult().getDocuments().isEmpty()) {
                                mCollectionRef.add(brand);
                            } else {
                                String id = task.getResult().getDocuments().get(0).getId();
                                mCollectionRef.document(id).set(brand);
                            }
                        } else {
                            Log.d("FIREBASE", task.getException().toString());
                        }
                    }
        });
    }

変更後の内容

MVVMモデルのアーキテクチャを適用して、画面処理とデータ処理を分けてクリーンな実装に変えます。

変更後クラス図

ItemDetailFragmentでは、画面更新処理とBrandクラスのオブジェクトを設定し、DataRepositoryクラスでFirestoreへのデータ操作を行います。

変更後実装

ItemDetailFragmentでは画面処理、ItemDetailViewModelでバインディングして、DataRepositoryでデータ処理の実装が分離されたことにより、クリーンな実装となりました。

public class ItemDetailFragment extends Fragment {
    private String name = "銘柄名";
    private EditText mTitle;
    private EditText mSubTitle;
    private ItemDetailViewModel mItemDetailViewModel

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_item_detail, container, false);
	mTitle = view.findViewById(R.id.detail_title);
	mSubTitle = view.findViewById(R.id.detail_subtitle);
	
	mItemDetailViewModel = new ViewModelProvider(this).get(ItemDetailViewModel.class);
        mItemDetailViewModel.getBrand(name).observe(getViewLifecycleOwner(), brand -> {
            mTitle.setText(brand.getTitle());
	    mSubTitle.setText(brand.getSubTitle());
        });
	
	return view;
    }

    /**
     * Toobarのチェックボタンを押下時に呼ばれる
     */
    public void confirmEditData() {
        String title = mTitle.getText().toString();
        String subTitle = mSubTitle.getText().toString();
	Brand brand = new Brand(title, subTitle);
	
        mItemDetailViewModel.updateBrand(brand);
    }
public class ItemDetailViewModel extends AndroidViewModel {
    private PonshuRepository mRepository;

    public ItemDetailViewModel(@NonNull Application application) {
        super(application);
        mRepository = PonshuRepository.getInstance();
    }

    public LiveData<Brand> getBrand(String name) {
        return mRepository.getBrand(name);
    }

    public void updateBrand(Brand brand) {
        mRepository.updateBrand(brand);
    }
}
public class PonshuRepository {
    private static PonshuRepository mInstance;

    private FirebaseFirestore mDb;
    private CollectionReference mCollectionRef;

    private MutableLiveData<Brand> mBrand;

    public static PonshuRepository getInstance() {
        if (mInstance == null) {
            mInstance = new PonshuRepository();
        }
        return mInstance;
    }

    private PonshuRepository() {
        mDb = FirebaseFirestore.getInstance();
        mCollectionRef = mDb.collection("BrandList");
    }

    public LiveData<Brand> getBrand(String name) {
        if (mBrand == null) {
            mBrand = new MutableLiveData<>();
        }
        mCollectionRef.whereEqualTo("title", name).get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        if (task.isSuccessful()) {
                            if (task.getResult().getDocuments().isEmpty()) return;
                            Map<String, Object> brandData = (Map<String, Object>) task.getResult().getDocuments().get(0).getData();
                            String title = (String) brandData.get("title");
                            String subTitle = (String) brandData.get("subTitle");
                            Brand brand = new Brand(title, subTitle);
                            mBrand.setValue(brand);
                        } else {
                            Log.d("FIREBASE", task.getException().toString());
                        }
                    }
                });

        return mBrand;
    }

    public void updateBrand(Brand brand) {
        mCollectionRef.whereEqualTo("title", brand.getTitle()).get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        if (task.isSuccessful()) {
                            if (task.getResult().getDocuments().isEmpty()) {
                                mCollectionRef.add(brand);
                            } else {
                                String id = task.getResult().getDocuments().get(0).getId();
                                mCollectionRef.document(id).set(brand);
                            }
                        } else {
                            Log.d("FIREBASE", task.getException().toString());
                        }
                    }
        });
    }

さいごに

変更後の処理の方が合計ステップ数としては増えていますが、画面に配置するレイアウトやデータが増えたりすると、どんどん実装が増えていき複雑化していきます。
なので、このようにMVVMモデルのアーキテクチャを適用すると実装が増えていってもクリーンな実装が保てるようになると思います。
閲覧ありがとうございました!

さいごに、自分が個人開発で公開しているアプリの紹介です。
https://play.google.com/store/apps/details?id=com.highcom.passwordmemo&hl=ja

Discussion