Skip to content
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