駆け出しエンジニアぱかぱかの成長記録

引くほど忘れっぽい新卒2年目駆け出しSEぱかぱかの備忘録です。

【Android】RecyclerView + ListAdapter + DataBinding + LiveData + ViewModelで行った店リスト実装?

こんにちは!ぱかぱかです!
新年早々の大地震、被災された方へお見舞い申し上げます。
一昨日からついつい地震のニュースばかり見てしまい心が落ち着かず…
でも私からできることはほとんどありませんので、微力ながら寄付をさせていただきました。

donation.yahoo.co.jp

昔作ったけど全く使っていなかったyahooアカウントに久しぶりにログイン。
クレジットだけで簡単に募金できました。
よろしければ上記リンクご活用ください。

こんな状況の中ですが、日常は回り続けますね。
今日は箱根駅伝を横目に見ながらPCをカタカタしていました。
現在開発を進めている飲食店記録アプリ「グルメノート」で、LiveDataを使った実装をしてみたのでまとめていきたいと思います。

色々と苦戦してあれこれ調べながら実装したのですが、なんか間違っているような気もするのでご指摘あればよろしくお願いいたします!

前回の記事

radish-se.hatenablog.com

ViewbindingとDataBindingを使ってデータをRecyclerViewに表示できるようになった!
ただし表示するデータはコード上にベタ打ち…

今回のゴール

DBに登録されている情報をRoomを使って取得し、それをLiveDataとして管理。
取得完了後にRecyclerViewが更新されるようにする。

DBから登録したデータを取得する

1ヶ月以上前になってしまいましたが、以下の記事で行った店をDBに登録できるようにしました。
radish-se.hatenablog.com

以下のように行った店登録画面で情報を入力して登録ボタンを押すと

軽くディスっている…

2つのテーブルにデータが挿入されます。

restaurantsテーブル
visited_recordsテーブル

今度はこのDBからViewに表示するデータを取得したいと思います。

DAOクラスにデータ取得メソッド追加

RestaurantEntityとVisitedRecordsEntityに対して操作を行うRestaurantVisitedRecordsEntityDAOを作ります。
そして以下のように検索メソッドselectRestaurantWithVisitedRecordとデータクラスVisitedRestaurantDataを作成します。
Roomではこんな感じでクエリを書いて実行できるんですね。
クエリ内でrestaurants.restaurant_name AS restaurantNameのように書くと、データクラスのプロパティに対応してデータが格納されるようです…ほほう…

@Dao
interface RestaurantVisitedRecordsEntityDAO {

    @Query(
        """
            SELECT
            restaurants.restaurant_name AS restaurantName,
            visited_records.evaluation AS restaurantEvaluation,
            restaurants.place AS restaurantPlace,
            restaurants.genre AS restaurantGenre,
            restaurants.created_at AS restaurantRegistrationDate,
            visited_records.visited_note AS restaurantMemo
            FROM restaurants
            INNER JOIN visited_records ON visited_records.restaurant_id = restaurants.restaurant_id;
        """
    )
    suspend fun selectRestaurantWithVisitedRecord(): MutableList<VisitedRestaurantData>

    data class VisitedRestaurantData(
        val restaurantName: String,
        val restaurantEvaluation: Int,
        val restaurantPlace: String,
        val restaurantGenre: String,
        val restaurantRegistrationDate: String,
        val restaurantMemo: String
    )
}

今度はそれをRestaurantVisitedRecordsRepositoryから呼び出してあげます。

class RestaurantVisitedRecordsRepository(application: Application) {
    private val _db: AppDatabase

    init {
        _db = AppDatabase.getDatabase(application)
    }

    ...

    suspend fun selectRestaurantWithVisitedRecord(): MutableList<RestaurantVisitedRecordsEntityDAO.VisitedRestaurantData> {
        val restaurantVisitedRecordsEntityDAO = _db.createRestaurantVisitedRecordsEntityDAO()
        return restaurantVisitedRecordsEntityDAO.selectRestaurantWithVisitedRecord()
    }
}

これでDBからデータを取得する処理の準備は完了です。

次にこれをViewModelから呼び出しLiveDataで管理してあげます。

LiveData

LiveDataはデータのライフサイクル(作成・更新・削除など)を監視できるデータクラスです。
データのライフサイクルを検知して、UIに反映することができます。
公式でもLiveDataはViewModelで管理するよう記載があるので、ViewModelで持っておきましょう。

LiveData の概要  |  Android デベロッパー  |  Android Developers

注: 以下の理由により、UI を更新する LiveData オブジェクトは、アクティビティやフラグメントとは異なり、ViewModel オブジェクトに保存してください。
・アクティビティやフラグメントの肥大化を避けるため。現在、これらの UI コントローラでは、データの表示は行っていますが、データの状態の保持は行っていません。
・LiveData インスタンスを特定のアクティビティやフラグメントのインスタンスから切り離して、LiveData オブジェクトが設定の変更後にも存在できるようにするため。

ViewModelの実装

前回まではViewModelにデータをベタ打ちしていたのですが、DBから取得するように修正していきます。
行った店のリストであるvisitedRestaurantListをMutableLiveDataで保持するようにします。
取得したデータはVisitedRestaurantListItemに詰め直してMutableLiveDataに格納した後、postValue()を実行します。

postValue()を呼び出すことでObserverがトリガーされてUIが更新されるとのこと。
後で動きを見てみましょう。

class VisitedRestaurantViewModel(application: Application) : AndroidViewModel(application) {
    lateinit var visitedRestaurantListAdapter: VisitedRestaurantListAdapter
    private var _restaurantVisitedRecordsRepository =
        RestaurantVisitedRecordsRepository(this.getApplication())
    
    // LiveDataで保持
    val visitedRestaurantList = MutableLiveData<List<VisitedRestaurantListItem>>()

    @RequiresApi(Build.VERSION_CODES.O)
    suspend fun fetchData() {
        viewModelScope.launch {
            // DBからデータを取得
            _restaurantVisitedRecordsRepository =
                RestaurantVisitedRecordsRepository(getApplication())
            val visitedRestaurantDataList =
                _restaurantVisitedRecordsRepository.selectRestaurantWithVisitedRecord()
            val tmpVisitedRestaurantList = mutableListOf<VisitedRestaurantListItem>()

            // 取得したデータをVisitedRestaurantListItemに格納
            visitedRestaurantDataList.forEach { data ->
                val item = VisitedRestaurantListItem(
                    data.restaurantName,
                    Evaluation.getEvaluationByNumber(data.restaurantEvaluation),
                    data.restaurantPlace,
                    data.restaurantGenre,
                    data.restaurantRegistrationDate,
                    data.restaurantMemo,
                    ContextCompat.getDrawable(getApplication(), R.drawable.no_image),
                    true
                )
                tmpVisitedRestaurantList.add(item)
            }

            // postValue実行
            visitedRestaurantList.postValue(tmpVisitedRestaurantList)
        }
    }
}

Fragmentの実装

ViewModelのvisitedRestaurantListに変更があったときに検知できるようobserve()で監視します。
変更を検知したらsubmitList()でリストを更新します。

class VisitedRestaurantFragment: Fragment()  {
    lateinit var binding: FragmentVisitedRestaurantBinding
    private val visitedRestaurantViewModel: VisitedRestaurantViewModel by viewModels()

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentVisitedRestaurantBinding.inflate(inflater, container, false)
        binding.recyclerView.layoutManager = LinearLayoutManager(context)
        val adapter = VisitedRestaurantListAdapter(visitedRestaurantViewModel)
        visitedRestaurantViewModel.visitedRestaurantListAdapter = adapter
        binding.recyclerView.adapter = adapter

        lifecycleScope.launch {
            visitedRestaurantViewModel.fetchData()
        }

        // observe()でライフサイクルの通知を検知
        visitedRestaurantViewModel.visitedRestaurantList.observe(viewLifecycleOwner){
            adapter.submitList(visitedRestaurantViewModel.visitedRestaurantList.value)
        }

        return binding.root
    }
}

いざ実践

observeメソッドにブレークを貼って実行してみます。
行った店タブを押したとき、ViewModelでDBからのデータ取得が非同期で走ります。
まだデータ取得が完了していない間はリストが空なのでまっさらな画面が表示されています。

その後、DBからのデータ取得が完了するとpostValue()が呼び出されるためFragmentのobserve()が動きsubmitList()が実行されます。
すると以下のようにリストの中身が表示されました。

これでようやくDBに基づいた情報が表示されるようになりました。
長くなってしまいましたが今日はこの辺で!