My Note Pad

エンジニアリングや日々の雑感を書いていきます

CodelabsのAndroid Room with a ViewをKotlinでやってみた

Codelabsについて

Googleが提供している、様々なチュートリアルやハンズオンのコースです。 https://codelabs.developers.google.com/

Android Room with a View

AndroidRoom ViewModel LiveDataについて学ぶことができるコースです。

https://codelabs.developers.google.com/codelabs/android-room-with-a-view/index.html?index=..%2F..%2Findex#0

GoogleがKotlinで実装した良いサンプルとして、こちらも公開されています。 https://github.com/googlesamples/android-sunflower

motivation

Android開発の学習をする上で、LiveData について押さえておきたかったので、Room with a Viewのcodelabsを見つけました。 手を動かしながら学べるのでちょうど良いと思いましたが、実装がJavaだったので置き換えつつ進めてみることにしました。

ゴールはCodelabのアプリがKotlinコードで動作することです。 Javaの元コードを見て、Kotlinだとこんな感じかな〜という風に書き換えて実装していったものになるので、正しい書き方とは異なる可能性もあります。

Environment

各セクション毎の変更点

2. Create your app

特になし。 Kotlinでアプリを作成します。

3. Update gradle files

kotlin-kaptプラグインを有効にし、annotationProcessorの定義をkaptに変更しています。

apply plugin: 'kotlin-kapt'
・
・
・
    // Room components
    implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
//    annotationProcessor "android.arch.persistence.room:compiler:$rootProject.roomVersion"
    kapt "android.arch.persistence.room:compiler:$rootProject.roomVersion"
    androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"

    // Lifecycle components
    implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
//    annotationProcessor "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"
    kapt "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"

4. Create the entity

Entityの定義方法自体がJavaとKotlinで大きく異なり、かなり楽に記述できます。

import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey

@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") var mWord: String)

5. Create the DAO / 6. The LiveData class

  1. ではgetAllWords() の戻り値はList<Word>ですが、6.でLiveData<List<Word>>に修正する
import android.arch.lifecycle.LiveData
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
import com.example.hoge.roomwithaview.entity.Word

@Dao
interface WordDao {
    @Insert
    fun insert(word: Word)

    @Query("DELETE from word_table")
    fun deleteAll()

    @Query("SELECT * from word_table ORDER BY word ASC")
    fun getAllWords(): LiveData<List<Word>>
}

7. Add a Room database / 12. Populate the database

getDatabaseメソッドはcompanion objectで定義していますが、package-level functionで定義するのが正しいかも。 ※sunflowerサンプルアプリケーションでも実装はcompanion objectで実装されていました。

PopulateDbAsyncクラスは12. Populate the databaseでの実装になります。 ここはCallbackの書き方で少しハマりました。

import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import android.content.Context
import android.os.AsyncTask
import com.example.hoge.roomwithaview.dao.WordDao
import com.example.hoge.roomwithaview.entity.Word

@Database(entities = [Word::class], version = 1)
abstract class WordRoomDatabase: RoomDatabase() {

    abstract fun wordDao(): WordDao

    companion object {
        @Volatile
        private var instance: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            if (instance == null) {
                synchronized(WordRoomDatabase::class.java) {
                    if (instance == null) {
                        instance = Room.databaseBuilder(context.applicationContext,
                                WordRoomDatabase::class.java, "word_database")
                                .addCallback(object : RoomDatabase.Callback() {
                                    override fun onOpen(db: SupportSQLiteDatabase) {
                                        PopulateDbAsync(instance!!).execute()
                                    }
                                })
                                .build()
                    }
                }
            }
            return instance!!
        }
    }
}

class PopulateDbAsync(db: WordRoomDatabase): AsyncTask<Void, Void, Void>() {
    private val mDao: WordDao = db.wordDao()

    override fun doInBackground(vararg params: Void?): Void? {
        mDao.deleteAll()
        var word = Word("Hello")
        mDao.insert(word)
        word = Word("World")
        mDao.insert(word)
        return null
    }
}  

8. Create the Repository

import android.app.Application
import android.arch.lifecycle.LiveData
import android.os.AsyncTask
import com.example.hoge.roomwithaview.dao.WordDao
import com.example.hoge.roomwithaview.db.WordRoomDatabase
import com.example.hoge.roomwithaview.entity.Word

class WordRepository(application: Application) {
    private val mWordDao: WordDao
    private var mAllWords: LiveData<List<Word>>

    init {
        val db = WordRoomDatabase.getDatabase(application)
        mWordDao = db.wordDao()
        mAllWords = mWordDao.getAllWords()
    }

    fun getAllWords(): LiveData<List<Word>> {
        return mAllWords
    }

    fun insert(word: Word) {
        InsertAsyncTask(mWordDao).execute(word)
    }
}

class InsertAsyncTask(wordDao: WordDao): AsyncTask<Word, Void, Void>() {
    private val mAsyncTaskDao: WordDao = wordDao
    override fun doInBackground(vararg params: Word?): Void? {
        mAsyncTaskDao.insert(params[0]!!)
        return null
    }
}

9. Create the ViewModel

import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData
import com.example.hoge.roomwithaview.entity.Word
import com.example.hoge.roomwithaview.repository.WordRepository

class WordViewModel(application: Application) : AndroidViewModel(application) {
    private val mRepository: WordRepository = WordRepository(application)
    var mAllWords: LiveData<List<Word>>

    init {
        mAllWords = mRepository.getAllWords()
    }

    fun insert(word: Word) {
        mRepository.insert(word)
    }
}

10. Add XML layout

ここはcodelabの通りに実装します。

layout/activity_main.xml の変更部分で + ボタンが、ぱっとは見つからなかったので、@drawable/ic_android_black_24dp で代用

11. Add a RecyclerView

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.example.hoge.roomwithaview.R
import com.example.hoge.roomwithaview.entity.Word

class WordListAdapter(context: Context): RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {

    class WordViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        val wordItemView: TextView = itemView.findViewById(R.id.textView)
    }

    private val mInflater: LayoutInflater = LayoutInflater.from(context)
    private var mWords: List<Word>? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        val itemView = mInflater.inflate(R.layout.recyclerview_item, parent, false)
        return WordViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        if (mWords != null) {
            val current = mWords!![position]
            holder.wordItemView.text = current.mWord
        } else {
            holder.wordItemView.text = "No Word"
        }
    }

    fun setWords(words: List<Word>) {
        mWords = words
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int {
        return if (mWords != null) {
            mWords!!.size
        } else {
            0
        }
    }
}

13. Add NewWordActivity

レイアウトファイルについては、codelabの通りに実装します。

import android.app.Activity
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.text.TextUtils
import android.widget.Button
import android.widget.EditText

class NewWordActivity : AppCompatActivity() {

    companion object {
        const val EXTRA_REPLY: String = "com.example.hoge.roomwithaview.REPLY"
    }

    private var mEditWordView: EditText? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_word)
        mEditWordView = findViewById(R.id.edit_word)
        val wordText = mEditWordView!!.text

        val button = findViewById<Button>(R.id.button_save)
        button.setOnClickListener {
            val replyIntent = Intent()
            if (TextUtils.isEmpty(wordText)) {
                setResult(Activity.RESULT_CANCELED, replyIntent)
            } else {
                val word = wordText.toString()
                replyIntent.putExtra(EXTRA_REPLY, word)
                setResult(Activity.RESULT_OK, replyIntent)
            }
            finish()
        }
    }
}

14. Connect with the data

import android.app.Activity
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import com.example.hoge.roomwithaview.adapter.WordListAdapter
import com.example.hoge.roomwithaview.entity.Word
import com.example.hoge.roomwithaview.model.WordViewModel
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    companion object {
        private const val NEW_WORD_ACTIVITY_REQUEST_CODE = 1
    }

    private var mWordViewModel: WordViewModel? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
        val adapter = WordListAdapter(this)
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        mWordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)
        mWordViewModel!!.mAllWords.observe(this, Observer {
            adapter.setWords(it!!)
        })

        fab.setOnClickListener {
            val intent = Intent(this, NewWordActivity::class.java)
            startActivityForResult(intent, NEW_WORD_ACTIVITY_REQUEST_CODE)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == NEW_WORD_ACTIVITY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            val word = Word(data!!.getStringExtra(NewWordActivity.EXTRA_REPLY))
            mWordViewModel!!.insert(word)

            mWordViewModel!!.mAllWords.value!!.forEach {
                Log.i("hoge", it.mWord)
            }
        } else {
            Toast.makeText(
                    applicationContext,
                    R.string.empty_not_saved,
                    Toast.LENGTH_LONG).show()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }
}

以上で一通り手順に沿っての実装が完了しました。 早速動作確認してみます。

トップページが表示され、PopulateDbAsyncクラスのdoInBackgroundメソッドによって、HelloWorld という文字が表示されています。

f:id:yuki10k:20181014193133p:plain

Droid君のアイコンをタップすると次の画面に遷移し、文字を入力できます。

f:id:yuki10k:20181014193222p:plain

testと入力し、SAVEボタンをタップするとTOPページに戻り、testの項目が追加されています。

f:id:yuki10k:20181014193319p:plain

一通りの動作確認ができました。 引き続きAndroid開発に携われるように色々学んでいこうと思います。