Roomによるデータベースアクセス(select)

  • 投稿者:
  • 投稿カテゴリー:その他

概要

Roomを使ってローカルDBに接続する
テーブルはScheduleのみ。data classをSchedule.ktに定義
AndroidManifest.xmlに定義したApplicationクラスからdatabaseオブジェクトを作成
画面はフラグメント2画面のみでnav_graph.xmlでnavigation設定
ファイル構成
root
┣database
┃┣schedule
┃┃┣ScheduleDao.kt // DAO SQLなどを記載
┃┃┗Schedule.kt // エンティティ情報
┃┗AppDatabase.kt // DataBase
┣viewmodels
┃┣BusScheduleViewModel.kt // 2つのフラグメントの共有ViewModel
┃┗BusScheduleViewModelFactory.kt // ViewModelFactory
┣MainActivity.kt // メイン
┣FullScheduleFragment.kt // 初期表示フラグメント
┣StopScheduleFragment.kt // 遷移先フラグメント
┣BusScheduleApplication.kt // AndroidManifest.xmlから優先起動するApplicationクラス
┗BusStopAdapter.kt // RecyclerViewのListAdapter

環境設定

プロジェクトのbuild.gradleにRoomバージョンを定義

ext {
….
room_version = ‘2.3.0’
}

appのbuild.gradleに依存関係を追記

dependencies {
….
implementation “androidx.room:room-runtime:$room_version”
kapt “androidx.room:room-compiler:$room_version”
implementation “androidx.room:room-ktx:$room_version”
}

サンプル

エンティティ

エンティティ(data class)

data classでテーブルを定義
Roomが認識できるように@Entityアノテーションをつける
Not Null制約は@NonNullを付加する
DBでのタイムスタンプはInt型を用いる

import androidx.annotation.NonNull
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

// data classでテーブルを定義
// Roomが認識できるように@Entityアノテーションをつける
// Not Null制約は@NonNullを付加する
// DBでのタイムスタンプはInt型を用いる
@Entity
data class Schedule (
    @PrimaryKey val id: Int,
    @NonNull @ColumnInfo(name = "stop_name") val stopName: String,
    @NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)

DAO(interface)

@Daoのアノテーションを付けてinterfaceオブジェクトとして定義
@Query(SQL文)と取得結果を戻す任意名称の関数をペアで記載
パラメータありのSQLは関数の引数にパラメータを定義
DBの変更を監視するにはFlow型で戻す

// DAOの定義
// @Query(SQL文)と取得結果を戻す任意名称の関数をペアで記載
// DBの変更を監視するにはFlow型で戻す
import androidx.room.Dao
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface ScheduleDao {
    // パラメータなしのSQL
    @Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
    fun getAll(): Flow<List<Schedule>>

    // パラメータありのSQLは関数の引数にパラメータを定義
    @Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
    fun getByStopName(stopName: String): Flow<List<Schedule>>
}

DataBaseのオブジェクトの定義

@Databaseアノテーションでアクセスするエンティティを指定する。
RoomDatabase型の抽象クラスでDAOへのアクセスを共通化した抽象メソッドを実装
DBアクセスは競合回避のため1つのみインスタス化する(companion object内で管理)
AndroidManifest.xmlで起動したBusScheduleApplicationからgetDatabaseがコールされてDBインスタンスが作成される。

/**
 * DataBaseのオブジェクトの定義
 * @Databaseアノテーションでアクセスするエンティティを指定する
 * RoomDatabase型の抽象クラスでDAOへのアクセスを共通化した抽象メソッドを実装
 * DBアクセスは競合回避のため1つのみインスタス化する(companion object内で管理)
 * AndroidManifest.xmlで定義したBusScheduleApplicationから
 * コールされてDBインスタンスが作成される。
 */
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.busschedule.database.schedule.Schedule
import com.example.busschedule.database.schedule.ScheduleDao

// entitiesは使用するテーブル名のクラスファイル名を全て記載
@Database(entities = arrayOf(Schedule::class), version = 1)
abstract class AppDatabase: RoomDatabase() {
    // DAOへのアクセスを共通化した抽象メソッドを作成
    // DataBaseオブジェクトで使用するDaoクラスを紐づけ
    abstract fun scheduleDao(): ScheduleDao

    // インスタンスが一つだけとなるようにcompanion objectで管理する
    companion object {
        // @Volatileは複数のスレッドからアクセスされる場合のメモリ管理を考慮
        @Volatile
        private var INSTANCE: AppDatabase? = null

        // DBインスタンスを取得するメソッド
        // AndroidManifest.xmlで起動したBusScheduleApplicationからコールされる
        // インスタンスが存在しない場合は
        // 新規にRoomのdatabaseBuilderでインスタンス化
        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context,
                    AppDatabase::class.java,
                    "app_database")
                    .createFromAsset("database/bus_schedule.db")
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

アプリケーションクラス

AndroidManifest.xmlのapplicationタグでandroid:name=Applicationクラス名とする事で指定クラスのインスタンス化を一番先に行う事ができる。
これを利用してアプリケーションクラスの派生クラスからDatabaseオブジェクトを先にインスタンス化する。

/**
 * AndroidManifest.xmlのapplicationタグで
 * android:name=Applicationクラス名とする事で
 * 指定クラスのインスタンス化を一番先に行う
 */

import android.app.Application
import com.example.busschedule.database.AppDatabase

class BusScheduleApplication : Application() {
    // BusScheduleApplication.ktで定義した
    // AppDatabaseクラスのgetDatabaseでDBをインスタンス化する
    // getDatabaseはAppDatabaseで定義されたcompanion object
    val database: AppDatabase by lazy {
        AppDatabase.getDatabase(this)
    }
}

マニフェスト

android:nameはアプリに実装するApplicationサブクラス。
アプリプロセスが開始すると、どのアプリコンポーネントよりも前にこのクラスがインスタンス化される。
Applicationクラスの派生クラスBusScheduleApplicationを起動

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.busschedule">

    <!-- android:nameはアプリに実装するApplicationサブクラス。
    アプリプロセスが開始すると、どのアプリコンポーネントよりも前に
    このクラスがインスタンス化される -->
    <application
        android:name="com.example.busschedule.BusScheduleApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BusScheduler">
        <activity android:name="com.example.busschedule.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

ナビゲーション

Fragment2つでFullScheduleFragmentが起動画面。
画面遷移はFullScheduleFragment→StopScheduleFragmentはstopNameがある。
StopScheduleFragmentはstopNameの引数をもつフラグメント。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/fullScheduleFragment">

    <fragment
        android:id="@+id/fullScheduleFragment"
        android:name="com.example.busschedule.FullScheduleFragment"
        android:label="@string/app_name"
        tools:layout="@layout/full_schedule_fragment">
        <action
            android:id="@+id/action_fullScheduleFragment_to_stopScheduleFragment"
            app:destination="@id/stopScheduleFragment" />
    </fragment>
    <fragment
        android:id="@+id/stopScheduleFragment"
        android:name="com.example.busschedule.StopScheduleFragment"
        android:label="{stopName}"
        tools:layout="@layout/stop_schedule_fragment">
        <argument
            android:name="stopName"
            app:argType="string" />
    </fragment>
</navigation>

レイアウトxml

アクティビティは1つのみでactivity_main.xmlにフラグメントの定義のみ
full_schedule_fragment.xmlとstop_schedule_fragment.xmlは同じレイアウトのRecyclerViewを持つフラグメント
bus_stop_item.xmlはRecyclerViewのレイアウト

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph"/>

</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<!--
RecyclerViewを持つフラグメント
-->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FullScheduleFragment"
    android:orientation="vertical">

    <TextView
        android:id="@+id/bus_stop_header"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/bus_stop_header"
        android:textSize="16sp"
        android:gravity="center_horizontal"
        android:padding="8dp"
        app:layout_constraintWidth_percent="0.5"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="@id/arrival_time_header"
        app:layout_constraintEnd_toStartOf="parent"/>

    <TextView
        android:id="@+id/arrival_time_header"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/arrival_time_header"
        android:textSize="16sp"
        android:gravity="center_horizontal"
        android:padding="8dp"
        app:layout_constraintWidth_percent="0.5"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toEndOf="@id/bus_stop_header"
        app:layout_constraintEnd_toEndOf="parent"/>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:layout_constraintTop_toBottomOf="@id/bus_stop_header"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<!--
RecyclerViewを持つフラグメント
-->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".StopScheduleFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<!--
RecyclerViewのアイテムのレイアウト
-->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/selectableItemBackground"
    android:paddingLeft="@dimen/bus_stop_item_horizontal_padding"
    android:paddingRight="@dimen/bus_stop_item_horizontal_padding"
    android:paddingTop="@dimen/bus_stop_item_vertical_padding"
    android:paddingBottom="@dimen/bus_stop_item_vertical_padding">

    <TextView
        android:id="@+id/stop_name_text_view"
        android:textSize="20sp"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="2" />

    <TextView
        android:id="@+id/arrival_time_text_view"
        android:textSize="20sp"
        android:textStyle="bold"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="end" />

</LinearLayout>

アクティビティ

フラグメントの表示のみ
ナビゲーションコントローラーによる画面遷移

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
import com.example.busschedule.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // ナビゲーションコントローラー
    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // ナビゲーションの設定
        // activity_main.xmlに定義したFragmentContainerViewを取得
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        // ナビゲーションコントローラーをセット
        navController = navHostFragment.navController
        // アプリバーを表示
        setupActionBarWithNavController(navController)
    }

    // 上へボタンを有効
    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp() || super.onSupportNavigateUp()
    }
}

ViewModel

2つのフラグメントの共有ViewModel
複数の画面からDAOへアクセスせずViewModel経由した形で統一する。
DAOオブジェクトを引数にしてインスタンス化。
これによりViewModelによるライフサイクルの管理が利用できる。
ただし、ViewModelのインスタンス化はそれぞれのフラグメントからではなくViewModelオブジェクトをインスタンス化するファクトリークラスを用いる。

import androidx.lifecycle.ViewModel
import com.example.busschedule.database.schedule.Schedule
import com.example.busschedule.database.schedule.ScheduleDao
import kotlinx.coroutines.flow.Flow

/**
 * ViewModel
 * 複数の画面からDAOへアクセスせずViewModel経由した形で統一する。
 * これによりViewModelによるライフサイクルの管理が利用できる。
 * ただし、ViewModelのインスタンス化はそれぞれのフラグメントからではなく
 * ViewModelオブジェクトをインスタンス化するファクトリークラスを用いる。
 */
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
    // DAOのSQLと関数の紐づけ
    fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()
    fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}

ViewModelファクトリー

ViewModelのメモリ管理を行うファクトリークラス
個々のFragmentからViewModelを直接インスタンス化しない
ViewModel同様にDAOオブジェクトを引数にしてインスタンス化

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.busschedule.database.schedule.ScheduleDao


// ViewModelのメモリ管理を行うファクトリークラス
// 個々のFragmentからViewModelを直接インスタンス化しない 
class BusScheduleViewModelFactory(
    private val scheduleDao: ScheduleDao
) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return BusScheduleViewModel(scheduleDao) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

RecyclerViewのアダプタ

RecyclerViewのListAdapterクラス
2つのフラグメントで利用する2つのRecyclerViewで共通のAdapter

書式は
class クラス名 : ListAdapter<A,B>(C: DiffUtils.ItemCallback)

ListAdapterクラス内には、前述のViewHolderとDiffUtil.ItemCallbackオブジェクトの他に
onCreateViewHolderとonBindViewHolderを実装する。
onCreateViewHolder→ViewHolderをリターン
onBindViewHolder→画面スクロール時に現在のposのデータをViewHolderに渡す

引数の部分は画面遷移の関数を受取る(Schedule型を受取りUnit(void)を返す)
画面遷移メソッドは呼出し元のフラグメント側で設定している

import android.annotation.SuppressLint
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.example.busschedule.database.schedule.Schedule
import com.example.busschedule.databinding.BusStopItemBinding
import java.text.SimpleDateFormat
import java.util.Date

class BusStopAdapter(
    private val onItemClicked: (Schedule) -> Unit
    // 引数の部分は画面遷移を表す(Schedule型を受取りUnit(void)を返す)
    // 画面遷移メソッドは、呼出し元のフラグメント側で設定している
) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {

    // 2つの要素を比較するObject 
    // ListAdapterのコンストラクタにて必要
    companion object {
        private val DiffCallback = object : DiffUtil.ItemCallback<Schedule>() {
            // idで比較
            override fun areItemsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
                return oldItem.id == newItem.id
            }
            // エンティティ項目全体で比較
            override fun areContentsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
                return oldItem == newItem
            }
        }
    }

    // ビューホルダの定義 型はRecyclerView.ViewHolder(binding.root)
    // 引数はレイアウトファイル(bus_stop_item.xml)のバインディングデータ
    // ListAdapterのコンストラクタにて必要
    class BusStopViewHolder(
        private var binding: BusStopItemBinding
        ): RecyclerView.ViewHolder(binding.root) {
        // 後述のonBindViewHolderよりコールされる
        // 該当posのデータを受取りビュー側のバインドデータにセットする
        // @SuppressLintはクラスまたはメソッドに対するlintチェックのみを無効にする
        @SuppressLint("SimpleDateFormat")
        fun bind(schedule: Schedule) {
            binding.stopNameTextView.text = schedule.stopName
            binding.arrivalTimeTextView.text = SimpleDateFormat(
                "h:mm a").format(Date(schedule.arrivalTime.toLong() * 1000)
            )
        }
    }

    // 抽象メソッド1
    // 前述したビューホルダ(BusStopViewHolder)をCreateして戻す
    override fun onCreateViewHolder(
        parent: ViewGroup, viewType: Int
    ): BusStopViewHolder {
        // bindingデータをインフレートしてBusStopViewHolderをインスタンス化
        val viewHolder = BusStopViewHolder(
            BusStopItemBinding.inflate(
                LayoutInflater.from( parent.context),
                parent,
                false
            )
        )
        // リスナーの登録
        // 現在のposのデータをonItemClickedに渡す
        viewHolder.itemView.setOnClickListener {
            val position = viewHolder.adapterPosition
            // 呼出し元で設定した画面遷移を実行する
            onItemClicked(getItem(position))
        }
        return viewHolder
    }

    // 抽象メソッド2
    // 画面スクロール時に現在のposのデータを受渡する
    override fun onBindViewHolder(holder: BusStopViewHolder, position: Int) {
        // getItem(pos)で現在のposのデータを返す(ListAdapterの機能)
        holder.bind(getItem(position))
    }
}

フラグメント

同じレイアウトのRecyclerViewを持つフラグメント2つ
ViewModelはライフサイクルを考慮しファクトリクラスを利用してインスタンス化。
ViewModelの引数のDAOは、AndroidManifestで定義したBusScheduleApplicationからデータベースインスタンスを生成し、そこで作られたDAOオブジェクト。
ViewModelは共有ビューモデル
FullScheduleFragmentは、RecyclerViewのアダプタ設定時に画面遷移を定義した関数を引数として渡す。一方でStopScheduleFragmentは、空の関数を渡す。
StopScheduleFragmentは画面起動時の引数あり

/**
 * ViewModelはライフサイクルを考慮しファクトリクラスを利用してインスタンス化
 * ViewModelの引数のDAOは、
 * AndroidManifestで定義したBusScheduleApplicationから
 * データベースインスタンスを生成し、そこで作られたDAOオブジェクト
 * ViewModelは共有ビューモデル
 * RecyclerViewのアダプタ設定時に画面遷移を定義した関数を引数として渡す
 *
 */
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.coroutineScope
import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.busschedule.databinding.FullScheduleFragmentBinding
import com.example.busschedule.viewmodels.BusScheduleViewModel
import com.example.busschedule.viewmodels.BusScheduleViewModelFactory
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

// 起動時の画面
class FullScheduleFragment: Fragment() {

    private var _binding: FullScheduleFragmentBinding? = null

    private val binding get() = _binding!!

    private lateinit var recyclerView: RecyclerView

    // 共有ビューモデルでのデータ委譲(by activityViewModels())
    // ※単一のフラグメントと紐づくviewModelの場合はviewModels()を
    // デリゲートしていたが、共有ビューモデルの場合はactivityViewModels()
    private val viewModel: BusScheduleViewModel by activityViewModels {
        // 引数のDAOは、AndroidManifestで定義したBusScheduleApplicationから
        // データベースインスタンスを生成し、そこで作られたDAOオブジェクト
        BusScheduleViewModelFactory(
            (activity?.application as BusScheduleApplication).database.scheduleDao()
        )
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FullScheduleFragmentBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // レイアウトxmlよりrecycler_viewを取得
        recyclerView = binding.recyclerView
        // レイアウトマネージャーを設定(1列レイアウト)
        recyclerView.layoutManager = LinearLayoutManager(requireContext())

        // BusStopAdapterからリサイクラービューのAdapterを作成
        // BusStopAdapterの定義ではit(Schedule)を引数にしてUnit(戻りなし)があるが、
        // これはAdapter側のリスナーに登録する画面遷移関数を表す。
        // 関数の内容は、遷移先画面のパラメータをactionにセットし画面遷移する関数
        val busStopAdapter = BusStopAdapter({
            // 引数を取得し遷移情報に保存
            val action = FullScheduleFragmentDirections.actionFullScheduleFragmentToStopScheduleFragment(
                stopName = it.stopName
            )
            //画面遷移情報を設定
            view.findNavController().navigate(action)
        })
        recyclerView.adapter = busStopAdapter

        // ViewModelからデータを取得しアダプタにsubmitする
        lifecycle.coroutineScope.launch {
            viewModel.fullSchedule().collect(){
                busStopAdapter.submitList(it)
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
/**
 * ViewModelはライフサイクルを考慮しファクトリクラスを利用してインスタンス化
 * ViewModelの引数のDAOは、
 * AndroidManifestで定義したBusScheduleApplicationから
 * データベースインスタンスを生成し、そこで作られたDAOオブジェクト
 * ViewModelは共有ビューモデル
 * RecyclerViewのアダプタ設定時の画面遷移を定義した関数は空(なにもしない)
 * 画面起動時の引数あり
 *
 */

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.coroutineScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.busschedule.databinding.StopScheduleFragmentBinding
import com.example.busschedule.viewmodels.BusScheduleViewModel
import com.example.busschedule.viewmodels.BusScheduleViewModelFactory
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class StopScheduleFragment: Fragment() {

    companion object {
        var STOP_NAME = "stopName"
    }

    private var _binding: StopScheduleFragmentBinding? = null

    private val binding get() = _binding!!

    private lateinit var recyclerView: RecyclerView

    private lateinit var stopName: String

    // 共有ビューモデルでのデータ委譲(by activityViewModels())
    // ※単一のフラグメントと紐づくviewModelの場合はviewModels()を
    // デリゲートしていたが、共有ビューモデルの場合はactivityViewModels()
    // ビューモデルのメモリ管理としてViewModelProvider.Factoryを用いる
    private val viewModel: BusScheduleViewModel by activityViewModels {
        BusScheduleViewModelFactory(
            (activity?.application as BusScheduleApplication).database.scheduleDao()
        )
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 画面の引数の取得
        arguments?.let {
            stopName = it.getString(STOP_NAME).toString()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = StopScheduleFragmentBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // レイアウトxmlよりrecycler_viewを取得
        recyclerView = binding.recyclerView
        // レイアウトマネージャーを設定(1列レイアウト)
        recyclerView.layoutManager = LinearLayoutManager(requireContext())

        // BusStopAdapterからリサイクラービューのAdapterを作成
        // アダプタ作成時の画面遷移関数が指定されていないので画面遷移は発生しない
        val busStopAdapter = BusStopAdapter({})
        recyclerView.adapter = busStopAdapter

        // ViewModelからデータを取得しアダプタにsubmitする
        lifecycle.coroutineScope.launch {
            viewModel.scheduleForStopName(stopName).collect() {
                busStopAdapter.submitList(it)
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

トレーニング > KOTLIN を用いた ANDROID の基本 > データの永続化 > SQL、ROOM、フローの概要 > Roomとフローの概要 > 3. Room の依存関係を追加する