CodelabsのAndroid Room with a ViewをKotlinでやってみた
Codelabsについて
Googleが提供している、様々なチュートリアルやハンズオンのコースです。 https://codelabs.developers.google.com/
Android Room with a View
AndroidのRoom
ViewModel
LiveData
について学ぶことができるコースです。
GoogleがKotlinで実装した良いサンプルとして、こちらも公開されています。 https://github.com/googlesamples/android-sunflower
motivation
Android開発の学習をする上で、LiveData
について押さえておきたかったので、Room with a Viewのcodelabsを見つけました。
手を動かしながら学べるのでちょうど良いと思いましたが、実装がJavaだったので置き換えつつ進めてみることにしました。
ゴールはCodelabのアプリがKotlinコードで動作することです。 Javaの元コードを見て、Kotlinだとこんな感じかな〜という風に書き換えて実装していったものになるので、正しい書き方とは異なる可能性もあります。
Environment
- Android Studio 3.2
各セクション毎の変更点
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
- では
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
メソッドによって、HelloとWorld という文字が表示されています。
Droid君のアイコンをタップすると次の画面に遷移し、文字を入力できます。
testと入力し、SAVEボタンをタップするとTOPページに戻り、testの項目が追加されています。
一通りの動作確認ができました。 引き続きAndroid開発に携われるように色々学んでいこうと思います。