接著新建一個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
另外如果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