〈kotlin〉コンストラクタ引数のあるViewModelの作り方

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

Android Room とビュー - Kotlin  |  Android デベロッパー  |  Android Developers
このcodelabでコンストラクタ引数としてrepositoryを持つViewModelが出てきた。

引数なしViewModel

引数を何も待たない場合は以下のように定義していた。

class SampleViewModel : ViewModel(){
...
}
class SampleActivity : AppCompatActivity(){
    private val viewModel : SampleViewModel  by viewModels() 
    ...
}

引数ありViewModel

今回のようにrepositoryなどのコンストラクタ引数を持たせる場合、
渡した引数との依存関係を取得するためにはViewModelProvider.Factoryを使用して定義する必要がある。
ViewModelProvider.FactoryはViewModelをインスタンス化するためのインターフェース。

class SampleViewModelFactory(private val repository: SampleRepository) : ViewModelProvider.Factory{
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if(modelClass.isAssignableFrom(SampleViewModel::class.java)){
            @Suppress("UNCHECKED_CAST")
            return SampleViewModel(repository) as T
        }
        throw (IllegalArgumentException("Unknown ViewModel Class"))
    }
}
class SampleActivity : AppCompatActivity(){
    private val viewModel : SampleViewModel  by viewModels{
        SampleViewModelFactory(repository)
        //repositoryはApplicationクラスなどで定義しておく
    } 
    ...
}

〈kotlin〉Room③ データベースインスタンス

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

データベースSQLiteを簡単に扱うことができるRoomの実装をまとめる。

前回の記事の続き。
<kotlin> Room - ゆるプログラミング日記

「データベースのインスタンスは高コストで通常アプリに1つで良いためシングルトンにする」を解決したい。

データベースクラスの作成

RoomDatabaseを拡張する抽象クラスとDAOインスタンスを返す抽象メソッドを定義。
データベースが存在しない場合はデータベースを作成し、
一度作成した後は既存のデータベースを返すメソッドをコンパニオンオブジェクトで定義する。

@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {

   abstract fun itemDao(): ItemDao

   companion object {
       @Volatile
       private var INSTANCE: ItemRoomDatabase? = null

       fun getDatabase(context: Context): ItemRoomDatabase {
           return INSTANCE ?: synchronized(this) {
               val instance = Room.databaseBuilder(
                   context.applicationContext,
                   ItemRoomDatabase::class.java,
                   "item_database"
               )
                   .fallbackToDestructiveMigration()
                   .build()
               INSTANCE = instance
               return instance
           }
       }
   }
}
  • exportSchema

データベースのスキーマをフォルダーにエクスポートするかどうか。
trueの場合エクスポートが行われ、スキーマのバージョン履歴を残せる。
デフォルトではtrueで、履歴を残さないデータベースはfalseを設定する。

  • @Volatile

アノテーションをつけた変数はキャッシュに保存されなくなり、
書き込み読み込みが全てメインメモリとの間で行われる。
これにより値が常に最新になり、全ての実行スレッドで同じになる。

  • INSTANCE変数

データベース作成時に、データベースに対する参照を保持する。

  • getDatabase()メソッド

INSTANCE変数を返すか、
INSTANCE変数がnull(一度も作成されてない)の場合は初期化処理を行い、
作成したデータベースを返す。

  • synchronized

このコードブロックには一度に 1 つのスレッドしか入ることができず、
データベースの初期化が一度しか行われないようにする。
(これがないと複数のスレッドが競合状態になってデータベース インスタンスを同時に要求した時、
結果的に 1 つではなく 2 つのデータベースが作成される可能性がある。)

  • fallbackToDestructiveMigration()

スキーマが変更されたとき古いスキーマから新しいスキーマへの移行方法を定義した移行パスが必要になるが、
移行パスが見つからなかったらエラーが発生してしまう。
このオプションをつけると、
移行パスが見つからなければ古いデータは破棄してデータベースを再作成する。

データベースのインスタンス

Applicationクラス内でインスタンスを作成する。

class MyApplication : Application(){
   val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}

lazy デリゲートを使用して、アプリの起動時ではなく最初にアクセスされたときに
上記で作成したgetDatabase()が呼び出されてデータベースインスタンスが作成される。
Applicationクラスは、AndroidManifest.xmlのapplicationタグ内に、
android:name=".MyApplication"」として含めておく必要がある。
データベースが必要なときはこのクラス内から取得する。


参考
Room を使用してデータを永続化する  |  Android Developers
Room データベースを移行する  |  Android デベロッパー  |  Android Developers
Database  |  Android Developers

〈kotlin〉Room② 非同期DAO

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

データベースSQLiteを簡単に扱うことができるRoomの実装をまとめる。

前回の記事の続き。
<kotlin> Room - ゆるプログラミング日記

「クエリが UI をブロックしないように、Room はメインスレッドでのデータベース アクセスを許可していない」を解決したい。
そのためにDAOクエリを非同期にする必要がある。

1. suspend関数にする

DAO内のメソッドにsuspendキーワードをつけることで、コルーチンで使用するメソッドにする。
挿入、更新、削除などの書き込みや、その時点のデータベースの読み取りなど
1回で終了するワンショットクエリに使用する。

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Update
    suspend fun updateUsers(vararg users: User)

    @Delete
    suspend fun deleteUsers(vararg users: User)

    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun loadUserById(id: Int): User

    @Query("SELECT * from user WHERE region IN (:regions)")
    suspend fun loadUsersByRegion(regions: List<String>): List<User>
}

2. Flowを使用する

メソッドの戻り値をFlowにすることで、クエリは自動的にバックグラウンドで実行されるようになる。
(Flowはデータの変更を監視できるコルーチンのメソッド)
基になるデータベース テーブルが変更されるたびにデータベースからデータを読み取り、
その変更を反映するために新しい値を出力するオブザーバブルクエリで使用する。

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>

    @Query("SELECT * from user ")
    fun loadUsers() : Flow<List<User>>
}

ViewModel内でLiveDataに変換して取得することもできる。

val allUsers : LiveData<List<User>> = UserDao.loadUsers().asLiveData()


DAO内のメソッドには

  • 1回処理を行うだけならsuspend
  • データの変更を監視し続けるなら戻り値Flow

で良さそう。



参考非同期 DAO クエリを作成する  |  Android デベロッパー  |  Android Developers

〈kotlin〉Room

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

データベースSQLiteを簡単に扱うことができるRoomの実装をまとめる。

Roomの構成

Roomは以下3つの主要コンポーネントで構成されている。

データエンティティ データベースのテーブル
データアクセスオブジェクト(DAO) データベースを操作するメソッド
データベースクラス データベースを保持する

この3つを順に実装していく。

build.gradle

//Room
implementation "androidx.room:room-runtime:2.4.1"
//アノテーション
kapt "androidx.room:room-compiler:2.4.1"

データエンティティ

@Entity アノテーションを付けたクラスとして定義する。
ここで定義したものがデータベースのテーブル内のカラムになる。

@Entity
data class User(
    @PrimaryKey val id: Int,
    val firstName: String?,
    val lastName: String?
)

エンティティにはデータベーステーブルの各行を一意に識別するために主キーを含める必要がある。
主キーは@PrimaryKeyのアノテーションをつけて定義する。
autoGenerateをtrueにすることでキーを自動生成することができる。

@PrimaryKey(autoGenerate = true) val id: Int

カラム名はデフォルトでフィールド名が使われるが、
@ColumnInfoアノテーションに記載することで自分で設定できる。
SQLiteカラム名は大文字小文字の区別はされない。)

@ColumnInfo(name = "first_name") val firstName: String?

テーブル名はデフォルトでクラス名が使われるが、
@Entity アノテーションに記載することで自分で設定できる。

@Entity(tableName = "users")

データアクセスオブジェクト(DAO)

DAOはインターフェイスとして定義する。
コンパイル時に、Room が定義した DAO の実装を自動的に生成してくれる。
DAO内にはデータベースを操作する(挿入、更新、削除、クエリ)メソッドを定義する。

@Dao
interface UserDao {
    //以下3つはコンビニエンスメソッド
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)

    @Update
    fun updateUsers(vararg users: User)

    //以下はクエリメソッド
    @Query("SELECT * FROM user")
    fun getAll(): List<User>
}
コンビニエンスメソッド

シンプルな挿入、更新、削除はアノテーションをつけるだけで実装できる。
@Insertに指定したonConflictは、
データ挿入や更新時に重複が発生した場合の挙動を指定する。
デフォルトではABORT(トランザクションを中止)になっている。

クエリメソッド

複雑な処理やデータをクエリする場合、
@Queryアノテーションでクエリを記述して定義する。

データベース

@Databaseアノテーションをつけ、RoomDatabase を拡張する抽象クラスとして定義する。
@Databaseアノテーションにはデータベースに関連する全てのエンティティの配列と、
データベースのバージョンを渡す。
クラス内にはデータベースに関連するDAOを、
引数なしでDAO クラスのインスタンスを返す抽象メソッドとして定義する。

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

使用方法

データベースのインスタンスは以下のように作成する。

val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

データベースの作成後、データベースの抽象メソッドでDAOのインスタンスを取得し、
これを使用することでデータベース操作ができる。

val userDao = db.userDao()
val users: List<User> = userDao.getAll()

注意点

  • クエリが UI をブロックしないように、Room はメインスレッドでのデータベース アクセスを許可していない。
  • データベースのインスタンスは高コストで通常アプリに1つで良いためシングルトンにする。

このあたりは次のブログで。

続き↓
mtnmr.hatenablog.com
mtnmr.hatenablog.com

参考
Room を使用してローカル データベースにデータを保存する  |  Android デベロッパー  |  Android Developers
Room エンティティを使用してデータを定義する  |  Android デベロッパー  |  Android Developers
Room DAO を使用してデータにアクセスする  |  Android デベロッパー  |  Android Developers

〈kotlin〉BindingAdapter(DataBinding)

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

DataBindingで渡された値にオリジナルの処理を追加することができる、
BindingAdapterについてまとめる。

DataBindingはこれ。
〈kotlin〉Android studio実践メモ2:MVVM(DataBinding) - ゆるプログラミング日記

BindingAdapterの定義

ObjectとしてBindingAdapterを用意する(オブジェクト名は何でも良い)。
そこに@BindingAdapterのアノテーションをつけた関数を定義し、
DataBindingで得られた値を使った追加処理を記載する。

@BindingAdapter("imageUrl")
@JvmStatic
//Javaからも呼び出せるようにするためのアノテーション
fun loadImage(view: ImageView, url: String) {
    Picasso.get().load(url).into(view)
}

アノテーションの引数にはオリジナルの属性を定義する。
ここに書いた属性名をxmlファイル内に後ほど埋め込む。
関数の第一引数は属性に関連付けられているビューの型を指定、
第二引数はバインディング式で受け取る値を指定する。

複数の属性を受け取るアダプターを設定することもできる。

@BindingAdapter("imageUrl", "error")
@JvmStatic
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

バインディング

xml側には、アダプターに定義した属性名を使ってバインディング式を記述する。

<ImageView 
    ...
    app:imageUrl="@{model.imageUrl}" 
    app:error="@{@drawable/venueError}"
                       //アプリ内のリソースを参照
 />

この場合imageUrl属性にString型、error属性にDrawable型の値が得られたとき、
BindingAdapterとして定義したloadImage()が呼び出される。
属性のどちらかが設定されるだけで呼び出したい場合には、
adapterのrequireAllフラグをfalseにしておく。

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)


参考
バインディング アダプター  |  Android デベロッパー  |  Android Developers

〈kotlin〉ListAdapter

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

以前RecyclerViewの使い方について記録した。
〈kotlin〉Android studio使い方メモ10:RecyclerView - ゆるプログラミング日記
ここではRecuclerView.Adapterを継承してアダプターを作成したが、
ListAdapterを使うのが便利そうだったのでまとめてみる。

ListAdapterとは

RecuclerView.AdapterにAsyncListDifferがラップされたクラス。
AsyncListDifferは、バックグラウンドスレッドで DiffUtil を使って
元のリストと与えられたリストの差を計算するためのヘルパー。
差分が存在する場合に、変更箇所のみを更新してくれる。

実装

サンプルコードは以下のcodelabの一部変更。
Room によるデータの読み取りと更新  |  Android Developers

ListAdapterを継承したクラスを作成する。
ListAdapterにはViewHolderクラスとDiffUtil.ItemCallbackのインスタンスを渡す必要があり、
どちらもクラス内で定義する。

onCreateViewHolder()メソッドとonBindViewHolder()メソッドをオーバーライドする。

class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
   ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {
//クリックイベントはfragmentでadapterを定義する時にラムダで渡す

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val view = ItemListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ItemViewHolder(view)
   }

   override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
       val current = getItem(position)
       holder.itemView.setOnClickListener {
           onItemClicked(current)
       }
       holder.bind(current)
   }

   //ViewHolder、ビューに値をセットするbind()を定義する
   class ItemViewHolder(private var binding: ItemListItemBinding) :
       RecyclerView.ViewHolder(binding.root) {

       fun bind(item: Item) {
             binding.apply {
                itemName.text = item.itemName
            }
       }
   }

   //DiffUtil.ItemCallbackのインスタンス
   companion object {
       private val DiffCallback = object : DiffUtil.ItemCallback<Item>() {
           //2つのアイテムが全く同じものか
           override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
               return oldItem === newItem
           }

           //2つのアイテムの内容が同じものか
           override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
               return oldItem.itemName == newItem.itemName
           }
       }

ListAdapterを使う

adapterを使用するfragment内で定義する。
リストを更新するためにはsubmitList()で新しくセットするリストを渡す。

class ItemListFragment : Fragment() {
    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val adapter = ItemListAdapter{
            //引数onItemClickedをラムダで渡す、
            val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
            this.findNavController().navigate(action)
        }

        binding.recyclerView.adapter = adapter

        viewModel.allItems.observe(this.viewLifecycleOwner){ items ->
            items.let {
                adapter.submitList(it)
            }
        }

ListAdapter  |  Android Developers

〈kotlin〉スピナー

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

タップすると選択可能な値の入ったプルダウンメニューが表示され、
その中から1つの値を素早く選択することができる「スピナー」を実装する。

レイアウト

xmlファイルにSpinnerタグを追加する。

<Spinner
    android:id="@+id/planets_spinner"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

デフォルトではプルダウンで選択肢が表示されるようになっているが、
android:spinnermode="dialog"を記述するとダイアログで選択肢が表示されるように設定できる。

選択肢の作成

選択肢とする値をStrings.xmlにstring-arrayとして定義する。
string-arrayタグの中のitemタグに値を1つずつ記述する。
(ここで作成した文字列配列は後ほどAdapterに渡す。)

<resources>
    <string-array name="planets_array">
        <item>Mercury</item>
        <item>Venus</item>
        <item>Earth</item>
        <item>Mars</item>
        <item>Jupiter</item>
        <item>Saturn</item>
        <item>Uranus</item>
        <item>Neptune</item>
    </string-array>
</resources>

Adapterの設定

選択肢の配列とレイアウトを ArrayAdapterに設定する。

val adapter = ArrayAdapter(
        this,
        android.R.layout.simple_spinner_item,
        resources.getStringArray(R.array.planets_array)
)

第2引数のsimple_spinner_item レイアウトは選択肢がどのように表示されるか、
プラットフォームによって提供されている標準レイアウト。
独自のレイアウトを作成して渡すこともできる。
第3引数には上記で作成したリストを配列型にして渡す。
もしくは下記コードのようにcreateFromResource() メソッドを使用することで、
文字列配列から 直接ArrayAdapter を作成できる。

val adapter =  ArrayAdapter.createFromResource(
        this,
        R.array.planets_array,
        android.R.layout.simple_spinner_item
)

次にsetDropDownViewResource(int) を呼び出して、
アダプターがスピナー選択リストに表示するために使用するレイアウトを指定する。
これも標準レイアウトが提供されている。
その後、spinnerのadapterに設定する。

adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spinner.adapter = adapter

クリックイベントの追加

項目が選択された場合の処理を記述するには、
AdapterView.OnItemSelectedListener インターフェースと対応するコールバックメソッドを実装する。

spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
            //選択されたときの処理
        }

        override fun onNothingSelected(p0: AdapterView<*>?) {
            //選択されなかったときの処理
        }

参考
https://developer.android.com/guide/topics/ui/controls/spinner?hl=ja