iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 21
0

前一篇Day20 ROOM -1 (番外)

接著新建一個recyclerview來做資料呈現

先去build新增libs
build.gradle (Module: app)

 dependencies{
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta03'
implementation 'com.google.android.material:material:1.0.0'
}

更新字串

styles.xml

<style name="word_title">
   <item name="android:layout_width">match_parent</item>
   <item name="android:layout_height">26dp</item>
   <item name="android:textSize">24sp</item>
   <item name="android:textStyle">bold</item>
   <item name="android:layout_marginBottom">6dp</item>
   <item name="android:paddingLeft">8dp</item>
</style>

在res/layout/
底下新增recycleview用的的xml

recycleview_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical" android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <TextView
       android:id="@+id/textView"
       style="@style/word_title"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="@android:color/holo_orange_light" />
</LinearLayout>

主頁的xml也作調整 把recyclerview加進去

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   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:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".main.MainActivity">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recyclerview"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:background="@android:color/darker_gray"
       tools:layout_editor_absoluteX="33dp"
       tools:layout_editor_absoluteY="233dp"
       tools:listitem="@layout/recyclerview_item" />

   <com.google.android.material.floatingactionbutton.FloatingActionButton
       android:id="@+id/fab"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="bottom|end"
       android:layout_marginEnd="8dp"
       android:layout_marginBottom="8dp"
       android:src="@drawable/ic_add_black_24dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

其中
@drawable/ic_add_black_24dp
圖標可以匯入任意圖片 或是用內建工具新增

內建工具路徑如下
File -> New -> Vector Asset

接著新增一個adapter 用來綁定recyclerview的item

WordListAdapter.kt

class WordListAdapter internal constructor(
   context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
   private val inflater: LayoutInflater = LayoutInflater.from(context)
   private var words = emptyList<WordEntity>()
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
       return WordViewHolder(
           itemView = inflater.inflate(
               R.layout.recyclerview_item,
               parent,
               false
           )
       )
   }

   override fun getItemCount() = words.size

   override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
       val current = words[position]
       holder.wordItemView.text = current.word
   }

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

   internal fun setWords(words: List<WordEntity>) {
       this.words = words
       notifyDataSetChanged()
   }
}

在MainActivity初始化

MainActivity.kt

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

接著是充填database
目前從WordRoomDatabase拿到的東西是沒有任何資料的
所以要在裡面添加打開數據庫的callback
由於onOpen的行為會在 IO 線程上運行 所以需要帶入CoroutineScope啟用coroutine

更新WordRoomDatabase類的getDatabase 多傳入CoroutineScope

WordRoomDatabase.kt

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

更新WordViewModel的init

WordViewModel.kt

val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()

在WordRoomDatabase中複寫RoomDatabase.Callback
以下是在WordRoomDatabase中複寫後的代碼:

WordRoomDatabase.kt

public abstract class WordRoomDatabase : RoomDatabase() {

...

//callback
private class WordDatabaseCallback(
   private val scope: CoroutineScope
) : RoomDatabase.Callback() {

   override fun onOpen(db: SupportSQLiteDatabase) {
       super.onOpen(db)
       INSTANCE?.let { database ->
           scope.launch {
               var wordDao = database.wordDao()

               // 刪除所有
               wordDao.deleteAll()

               // 新增範例字串
               var word = WordEntity("Hello")
               wordDao.insert(word)
               word = WordEntity("World!")
               wordDao.insert(word)

               word = WordEntity("Custom!")
               wordDao.insert(word)
           }
       }
   }
}
}

更新後的WordRoomDatabase 如下

WordRoomDatabase.kt

//database class 需使用抽象類並且繼承RoomDatabase
@Database(entities = arrayOf(WordEntity::class), version = 1)
abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   //callback
   private class WordDatabaseCallback(
       private val scope: CoroutineScope
   ) : RoomDatabase.Callback() {

       override fun onOpen(db: SupportSQLiteDatabase) {
           super.onOpen(db)
           INSTANCE?.let { database ->
               scope.launch {
                   var wordDao = database.wordDao()

                   // 刪除所有
                   wordDao.deleteAll()

                   // 新增範例字串
                   var word = WordEntity("Hello")
                   wordDao.insert(word)
                   word = WordEntity("World!")
                   wordDao.insert(word)

                   word = WordEntity("Custom!")
                   wordDao.insert(word)
               }
           }
       }
   }

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

       fun getDatabase(
           context: Context,
           scope: CoroutineScope
       ): WordRoomDatabase {
           // 如果INSTANCE已經存在 則回傳 否則創建database後回傳
           return INSTANCE ?: synchronized(this) {
               val instance = Room.databaseBuilder(
                   context.applicationContext,
                   WordRoomDatabase::class.java,
                   "word_database"
               )
                   .addCallback(WordDatabaseCallback(scope))
                   .build()
               INSTANCE = instance
               return instance
           }
       }
   }
}

還記得在Day3 coroutines時有看到viewModelScope吧
這其實是要減少coroutine的模板代碼用的
一些異步的function 用coroutine的形式撰寫的話會在前面加susped
而要該類型的function
需要在viewModelScope.launch內才能運行

接著在Mainactivity串接

Mainactivity.kt

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
...      

       wordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)
       wordViewModel.allWords.observe(this, Observer { words ->
           //更新
           words?.let { wordAdapter.setWords(it) }
       })
   }
}

observer請選擇import androidx.lifecycle
https://ithelp.ithome.com.tw/upload/images/20191006/201202790Ljak96x3p.png

另外如果owner找不到的話 檢查
build.gradle (Module: app)

 dependencies{
implementation 'androidx.appcompat:appcompat:1.1.0'
}

確保版本在1.1.0以上

然後我們可以新增一個按鈕來新增文字

代碼如下

val word = WordEntity("your input")
wordViewModel.insertWord(word)

隨便找個地方新增個button運行click行為
此例子是跳轉到一個新的頁面做新增後返回

MainActivity範例

MainActivity.kt

class MainActivity : AppCompatActivity() {
   private lateinit var wordViewModel: WordViewModel
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

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

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

       wordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)
       wordViewModel.allWords.observe(this, Observer { words ->
           //更新
           words?.let { wordAdapter.setWords(it) }
       })
   }

   companion object {
       const val newWordActivityRequestCode = 1
   }

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

       if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
           data?.let {
               val word = WordEntity(it.getStringExtra(NewWordActivity.EXTRA_REPLY))
               wordViewModel.insertWord(word)
           }!!
       } else {
           Toast.makeText(
               applicationContext,
               R.string.empty_not_saved,
               Toast.LENGTH_LONG
           ).show()
       }
   }
}

到此基本功能就已完成

solution
https://github.com/mars1120/jetpackMvvmDemo/tree/room

明天會來撰寫tests


上一篇
Day20 ROOM -1 (番外)
下一篇
Day22 ROOM -3 tests (番外)
系列文
Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言