Final Results
At this point, ToDoEntity
should look like:
package com.commonsware.todo.repo
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import java.time.Instant
import java.util.*
@Entity(tableName = "todos", indices = [Index(value = ["id"])])
data class ToDoEntity(
val description: String,
@PrimaryKey
val id: String = UUID.randomUUID().toString(),
val notes: String = "",
val createdOn: Instant = Instant.now(),
val isCompleted: Boolean = false
) {
constructor(model: ToDoModel) : this(
id = model.id,
description = model.description,
isCompleted = model.isCompleted,
notes = model.notes,
createdOn = model.createdOn
)
fun toModel(): ToDoModel {
return ToDoModel(
id = id,
description = description,
isCompleted = isCompleted,
notes = notes,
createdOn = createdOn
)
}
@Dao
interface Store {
@Query("SELECT * FROM todos ORDER BY description")
fun all(): Flow<List<ToDoEntity>>
@Query("SELECT * FROM todos WHERE isCompleted = :isCompleted ORDER BY description")
fun filtered(isCompleted: Boolean): Flow<List<ToDoEntity>>
@Query("SELECT * FROM todos WHERE id = :modelId")
fun find(modelId: String?): Flow<ToDoEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(vararg entities: ToDoEntity)
@Delete
suspend fun delete(vararg entities: ToDoEntity)
}
}
ToDoRepository.kt
should resemble:
package com.commonsware.todo.repo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
enum class FilterMode { ALL, OUTSTANDING, COMPLETED }
class ToDoRepository(
private val store: ToDoEntity.Store,
private val appScope: CoroutineScope
) {
fun items(filterMode: FilterMode = FilterMode.ALL): Flow<List<ToDoModel>> =
filteredEntities(filterMode).map { all -> all.map { it.toModel() } }
private fun filteredEntities(filterMode: FilterMode) = when (filterMode) {
FilterMode.ALL -> store.all()
FilterMode.OUTSTANDING -> store.filtered(isCompleted = false)
FilterMode.COMPLETED -> store.filtered(isCompleted = true)
}
fun find(id: String?): Flow<ToDoModel?> = store.find(id).map { it?.toModel() }
suspend fun save(model: ToDoModel) {
withContext(appScope.coroutineContext) {
store.save(ToDoEntity(model))
}
}
suspend fun delete(model: ToDoModel) {
withContext(appScope.coroutineContext) {
store.delete(ToDoEntity(model))
}
}
}
RosterMotor.kt
should now be:
package com.commonsware.todo.ui.roster
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.commonsware.todo.repo.FilterMode
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
data class RosterViewState(
val items: List<ToDoModel> = listOf(),
val isLoaded: Boolean = false,
val filterMode: FilterMode = FilterMode.ALL
)
class RosterMotor(private val repo: ToDoRepository) : ViewModel() {
private val _states = MutableStateFlow(RosterViewState())
val states = _states.asStateFlow()
private var job: Job? = null
init {
load(FilterMode.ALL)
}
fun load(filterMode: FilterMode) {
job?.cancel()
job = viewModelScope.launch {
repo.items(filterMode).collect {
_states.emit(RosterViewState(it, true, filterMode))
}
}
}
fun save(model: ToDoModel) {
viewModelScope.launch {
repo.save(model)
}
}
}
The actions_roster
menu resource XML should resemble:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/filter"
android:icon="@drawable/ic_filter"
android:title="@string/menu_filter"
app:showAsAction="ifRoom|withText">
<menu>
<group
android:id="@+id/filter_group"
android:checkableBehavior="single" >
<item
android:id="@+id/all"
android:checked="true"
android:title="@string/menu_filter_all" />
<item
android:id="@+id/completed"
android:title="@string/menu_filter_completed" />
<item
android:id="@+id/outstanding"
android:title="@string/menu_filter_outstanding" />
</group>
</menu>
</item>
<item
android:id="@+id/add"
android:icon="@drawable/ic_add"
android:title="@string/menu_add"
app:showAsAction="ifRoom|withText" />
</menu>
The strings
resource XML should resemble:
<resources>
<string name="app_name">ToDo</string>
<string name="msg_empty">Click the + icon to add a todo item!</string>
<string name="msg_empty_filtered">Click the + icon to add a todo item, or change your filter to show other items</string>
<string name="menu_about">About</string>
<string name="is_completed">Item is completed</string>
<string name="created_on">Created on:</string>
<string name="menu_edit">Edit</string>
<string name="desc">Description</string>
<string name="notes">Notes</string>
<string name="menu_save">Save</string>
<string name="menu_add">Add</string>
<string name="menu_delete">Delete</string>
<string name="menu_filter">Filter</string>
<string name="menu_filter_all">All</string>
<string name="menu_filter_completed">Completed</string>
<string name="menu_filter_outstanding">Outstanding</string>
</resources>
RosterListFragment
now should look like:
package com.commonsware.todo.ui.roster
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.commonsware.todo.R
import com.commonsware.todo.databinding.TodoRosterBinding
import com.commonsware.todo.repo.FilterMode
import com.commonsware.todo.repo.ToDoModel
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
class RosterListFragment : Fragment() {
private val motor: RosterMotor by viewModel()
private val menuMap = mutableMapOf<FilterMode, MenuItem>()
private var binding: TodoRosterBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = TodoRosterBinding.inflate(inflater, container, false)
.also { binding = it }
.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = RosterAdapter(
layoutInflater,
onCheckboxToggle = { motor.save(it.copy(isCompleted = !it.isCompleted)) },
onRowClick = ::display
)
binding?.items?.apply {
setAdapter(adapter)
layoutManager = LinearLayoutManager(context)
addItemDecoration(
DividerItemDecoration(
activity,
DividerItemDecoration.VERTICAL
)
)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
motor.states.collect { state ->
adapter.submitList(state.items)
binding?.apply {
loading.visibility = if (state.isLoaded) View.GONE else View.VISIBLE
when {
state.items.isEmpty() && state.filterMode == FilterMode.ALL -> {
empty.visibility = View.VISIBLE
empty.setText(R.string.msg_empty)
}
state.items.isEmpty() -> {
empty.visibility = View.VISIBLE
empty.setText(R.string.msg_empty_filtered)
}
else -> empty.visibility = View.GONE
}
}
menuMap[state.filterMode]?.isChecked = true
}
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.actions_roster, menu)
menuMap.apply {
put(FilterMode.ALL, menu.findItem(R.id.all))
put(FilterMode.COMPLETED, menu.findItem(R.id.completed))
put(FilterMode.OUTSTANDING, menu.findItem(R.id.outstanding))
}
menuMap[motor.states.value.filterMode]?.isChecked = true
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add -> {
add()
return true
}
R.id.all -> {
item.isChecked = true
motor.load(FilterMode.ALL)
return true
}
R.id.completed -> {
item.isChecked = true
motor.load(FilterMode.COMPLETED)
return true
}
R.id.outstanding -> {
item.isChecked = true
motor.load(FilterMode.OUTSTANDING)
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun display(model: ToDoModel) {
findNavController()
.navigate(RosterListFragmentDirections.displayModel(model.id))
}
private fun add() {
findNavController().navigate(RosterListFragmentDirections.createModel(null))
}
}
And the todo_roster
menu resource XML should resemble:
<?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=".ui.MainActivity">
<ProgressBar
android:id="@+id/loading"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:gravity="center"
android:text="@string/msg_empty"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/items"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.