Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Week8 storage #9

Open
wants to merge 10 commits into
base: week6-navigation-component
Choose a base branch
from
13 changes: 13 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: "kotlin-kapt"
apply plugin: "androidx.navigation.safeargs.kotlin"

android {
Expand Down Expand Up @@ -51,4 +52,16 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'


/*** Room Dependencies Start ***/
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"

// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
/*** Room Dependencies End ***/
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_add_todo.*
import tw.andyang.kotlinandroidworkshop.database.AppDatabase
import tw.andyang.kotlinandroidworkshop.mvvm.AnyViewModelFactory
import tw.andyang.kotlinandroidworkshop.repository.TodoItemRepository

class AddTodoFragment : Fragment() {

Expand Down Expand Up @@ -40,7 +43,12 @@ class AddTodoFragment : Fragment() {
editTodo.setText(args.memo)
editTodo.setSelection(args.memo.length)

val todoViewModel = ViewModelProvider(requireActivity()).get(TodoViewModel::class.java)
val todoItemDb = AppDatabase.getInstance(requireActivity().applicationContext)
val todoItemRepo = TodoItemRepository(todoItemDb)
val viewModelFactory = AnyViewModelFactory {
TodoViewModel(todoItemRepo)
}
val todoViewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(TodoViewModel::class.java)

buttonAdd.setOnClickListener {
if (editTodo.text.isNullOrEmpty()) {
Expand All @@ -49,7 +57,8 @@ class AddTodoFragment : Fragment() {
// clear error
editTodo.error = null
// post data to view model
todoViewModel.onNewTodo.postValue(editTodo.text.toString())
val title = editTodo.text.toString()
todoViewModel.createNewTodo(title)
// hide soft keyboard when item added
view.clearFocus()
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tw.andyang.kotlinandroidworkshop

interface OnTodoChangeListener {
fun onChange(todo: Todo.Item)
}
6 changes: 5 additions & 1 deletion app/src/main/java/tw/andyang/kotlinandroidworkshop/Todo.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package tw.andyang.kotlinandroidworkshop

import java.util.*

sealed class Todo(val viewType: Int) {
data class Title(val text: String) : Todo(TYPE_TITLE)
data class Item(
val id: Int,
val memo: String,
val checked: Boolean
val checked: Boolean,
val createdAt: Date
) : Todo(TYPE_ITEM)

companion object {
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/java/tw/andyang/kotlinandroidworkshop/TodoAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_todo.view.*
import java.text.SimpleDateFormat

class TodoAdapter : ListAdapter<Todo, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<Todo>() {
Expand All @@ -21,14 +22,16 @@ class TodoAdapter : ListAdapter<Todo, RecyclerView.ViewHolder>(
}
) {

var onTodoChangeListener: OnTodoChangeListener? = null

override fun getItemViewType(position: Int): Int {
return getItem(position).viewType
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
Todo.TYPE_TITLE -> TodoTitleViewHolder(parent)
else -> TodoViewHolder(parent)
else -> TodoViewHolder(parent, onTodoChangeListener)
}
}

Expand All @@ -40,15 +43,21 @@ class TodoAdapter : ListAdapter<Todo, RecyclerView.ViewHolder>(
}
}

class TodoViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
class TodoViewHolder(parent: ViewGroup, private val onTodoChangeListener: OnTodoChangeListener?) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_todo, parent, false)
) {

private val checkbox: AppCompatCheckBox = itemView.checkbox
private val date = itemView.create_at

fun bind(todo: Todo.Item) {
checkbox.text = todo.memo
checkbox.isChecked = todo.checked
checkbox.setOnClickListener { view ->
onTodoChangeListener?.onChange(Todo.Item(todo.id, todo.memo, !todo.checked, todo.createdAt))
}

date.text = SimpleDateFormat.getDateTimeInstance().format(todo.createdAt)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_todo_list.*
import tw.andyang.kotlinandroidworkshop.database.AppDatabase
import tw.andyang.kotlinandroidworkshop.mvvm.AnyViewModelFactory
import tw.andyang.kotlinandroidworkshop.repository.TodoItemRepository

class TodoListFragment : Fragment() {

Expand All @@ -25,7 +28,20 @@ class TodoListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val adapter = TodoAdapter()
val todoItemDb = AppDatabase.getInstance(requireActivity().applicationContext)
val todoItemRepo = TodoItemRepository(todoItemDb)
val viewModelFactory = AnyViewModelFactory {
TodoViewModel(todoItemRepo)
}
val todoViewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(TodoViewModel::class.java)

val adapter = TodoAdapter().apply {
onTodoChangeListener = object : OnTodoChangeListener {
override fun onChange(todo: Todo.Item) {
todoViewModel.updateTodo(todo)
}
}
}
recyclerView.adapter = adapter
recyclerView.layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
Expand All @@ -36,8 +52,6 @@ class TodoListFragment : Fragment() {
)
)

val todoViewModel = ViewModelProvider(requireActivity()).get(TodoViewModel::class.java)

todoViewModel.todoLiveData.observe(viewLifecycleOwner, Observer { todos: List<Todo> ->
adapter.submitList(todos)
})
Expand Down
53 changes: 43 additions & 10 deletions app/src/main/java/tw/andyang/kotlinandroidworkshop/TodoViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
package tw.andyang.kotlinandroidworkshop

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.*
import kotlinx.coroutines.launch
import tw.andyang.kotlinandroidworkshop.database.TodoItem
import tw.andyang.kotlinandroidworkshop.repository.TodoItemRepository
import java.util.*

class TodoViewModel : ViewModel() {
class TodoViewModel(private val repository: TodoItemRepository) : ViewModel() {

val onNewTodo = MutableLiveData<String>()
private val title = Todo.Title("This is a title")

val todoLiveData: LiveData<List<Todo>> = MediatorLiveData<List<Todo>>().apply {
addSource(onNewTodo) { text ->
val todo = Todo.Item(text, false)
this.value = this.value!! + listOf(todo)
val source = repository.getTodoItems().map {
it.map { todoItem ->
Todo.Item(
todoItem.id,
todoItem.title,
todoItem.done,
todoItem.createdAt
)
}
}
addSource(source) {
this.value = mutableListOf(title) + it
}
value = mutableListOf(title)
}

fun createNewTodo(title: String) {
val todoItem = TodoItem(
title = title,
done = false,
createdAt = Date()
)
viewModelScope.launch {
repository.insertTodoItem(todoItem)
}
}

fun updateTodo(todo: Todo.Item) {
val todoItem = TodoItem(
title = todo.memo,
done = todo.checked,
createdAt = todo.createdAt
).apply { id = todo.id }

viewModelScope.launch {
repository.updateTodoItem(todoItem)
}
value = mutableListOf(Todo.Title("This is a title"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package tw.andyang.kotlinandroidworkshop.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

@Database(
version = 1,
entities = [
TodoItem::class
],
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
companion object {
private const val DATABASE_NAME = "todo_list_db"

// For Singleton instantiation
@Volatile private var instance: AppDatabase? = null

fun getInstance(context: Context): AppDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}

private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME).build()
}
}

abstract fun todoItemDao(): TodoItemDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package tw.andyang.kotlinandroidworkshop.database

import androidx.room.TypeConverter
import java.util.*

class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}

@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package tw.andyang.kotlinandroidworkshop.database

import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update

interface GenericDao<T> {

/**
* Insert an object in the database.
*
* @param obj the object to be inserted.
*/
@Insert
suspend fun insert(obj: T)

/**
* Insert an array of objects in the database.
*
* @param obj the objects to be inserted.
*/
@Insert
suspend fun insert(vararg obj: T)

/**
* Update an object from the database.
*
* @param obj the object to be updated
*/
@Update
suspend fun update(obj: T)

/**
* Delete an object from the database
*
* @param obj the object to be deleted
*/
@Delete
suspend fun delete(obj: T)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tw.andyang.kotlinandroidworkshop.database

import androidx.room.*
import java.util.*

@Entity(
tableName = TodoItem.TABLE_NAME
)
data class TodoItem (
@ColumnInfo(name = COLUMN_TITLE) var title: String,
@ColumnInfo(name = COLUMN_DONE) var done: Boolean,
@ColumnInfo(name = COLUMN_CREATED_AT) var createdAt: Date
) {
companion object {
const val TABLE_NAME = "todo_items"

const val COLUMN_ID = "id"
const val COLUMN_TITLE = "title"
const val COLUMN_DONE = "done"
const val COLUMN_CREATED_AT = "created_at"
}

// 必須為 var 才會有 setter
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = COLUMN_ID) var id: Int = 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tw.andyang.kotlinandroidworkshop.database

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Query

@Dao
interface TodoItemDao: GenericDao<TodoItem> {

@Query("SELECT * FROM ${TodoItem.TABLE_NAME} ORDER BY ${TodoItem.COLUMN_CREATED_AT} DESC")
fun findAll(): LiveData<List<TodoItem>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tw.andyang.kotlinandroidworkshop.mvvm

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class AnyViewModelFactory<T : ViewModel?>(val creator: () -> T) : ViewModelProvider.Factory {

override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return creator() as T
}

}
Loading