Final Results
Our module’s build.gradle
file should resemble:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs.kotlin'
id 'kotlin-kapt'
}
android {
compileSdk 31
defaultConfig {
applicationId "com.commonsware.todo"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation 'com.google.android.material:material:1.4.0'
implementation "io.insert-koin:koin-android:$koin_version"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
In the end, RosterMotor
should contain:
package com.commonsware.todo.ui.roster
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
data class RosterViewState(
val items: List<ToDoModel> = listOf()
)
class RosterMotor(private val repo: ToDoRepository) : ViewModel() {
val states = repo.items()
.map { RosterViewState(it) }
.stateIn(viewModelScope, SharingStarted.Eagerly, RosterViewState())
fun save(model: ToDoModel) {
viewModelScope.launch {
repo.save(model)
}
}
}
The updated RosterListFragment
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.ToDoModel
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
class RosterListFragment : Fragment() {
private val motor: RosterMotor by viewModel()
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 {
when {
state.items.isEmpty() -> {
empty.visibility = View.VISIBLE
empty.setText(R.string.msg_empty)
}
else -> empty.visibility = View.GONE
}
}
}
}
binding?.empty?.visibility = View.GONE
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.actions_roster, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add -> {
add()
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))
}
}
Our revised SingleModelMotor
should contain:
package com.commonsware.todo.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
data class SingleModelViewState(
val item: ToDoModel? = null
)
class SingleModelMotor(
private val repo: ToDoRepository,
modelId: String?
) : ViewModel() {
val states = repo.find(modelId)
.map { SingleModelViewState(it) }
.stateIn(viewModelScope, SharingStarted.Eagerly, SingleModelViewState())
fun save(model: ToDoModel) {
viewModelScope.launch {
repo.save(model)
}
}
fun delete(model: ToDoModel) {
viewModelScope.launch {
repo.delete(model)
}
}
}
The tweaked DisplayFragment
should resemble:
package com.commonsware.todo.ui.display
import android.os.Bundle
import android.text.format.DateUtils
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.commonsware.todo.R
import com.commonsware.todo.databinding.TodoDisplayBinding
import com.commonsware.todo.ui.SingleModelMotor
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class DisplayFragment : Fragment() {
private val args: DisplayFragmentArgs by navArgs()
private var binding: TodoDisplayBinding? = null
private val motor: SingleModelMotor by viewModel { parametersOf(args.modelId) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = TodoDisplayBinding.inflate(inflater, container, false)
.apply { binding = this }
.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
motor.states.collect { state ->
state.item?.let {
binding?.apply {
completed.visibility =
if (it.isCompleted) View.VISIBLE else View.GONE
desc.text = it.description
createdOn.text = DateUtils.getRelativeDateTimeString(
requireContext(),
it.createdOn.toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0
)
notes.text = it.notes
}
}
}
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.actions_display, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.edit -> {
edit()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun edit() {
findNavController().navigate(
DisplayFragmentDirections.editModel(
args.modelId
)
)
}
}
And the current EditFragment
should look like:
package com.commonsware.todo.ui.edit
import android.os.Bundle
import android.view.*
import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.commonsware.todo.R
import com.commonsware.todo.databinding.TodoEditBinding
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.ui.SingleModelMotor
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class EditFragment : Fragment() {
private var binding: TodoEditBinding? = null
private val args: EditFragmentArgs by navArgs()
private val motor: SingleModelMotor by viewModel { parametersOf(args.modelId) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = TodoEditBinding.inflate(inflater, container, false)
.apply { binding = this }
.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
motor.states.collect { state ->
if (savedInstanceState == null) {
state.item?.let {
binding?.apply {
isCompleted.isChecked = it.isCompleted
desc.setText(it.description)
notes.setText(it.notes)
}
}
}
}
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.actions_edit, menu)
menu.findItem(R.id.delete).isVisible = args.modelId != null
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.save -> {
save()
return true
}
R.id.delete -> {
delete()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun save() {
binding?.apply {
val model = motor.states.value.item
val edited = model?.copy(
description = desc.text.toString(),
isCompleted = isCompleted.isChecked,
notes = notes.text.toString()
) ?: ToDoModel(
description = desc.text.toString(),
isCompleted = isCompleted.isChecked,
notes = notes.text.toString()
)
edited.let { motor.save(it) }
}
navToDisplay()
}
private fun delete() {
val model = motor.states.value.item
model?.let { motor.delete(it) }
navToList()
}
private fun navToDisplay() {
hideKeyboard()
findNavController().popBackStack()
}
private fun navToList() {
hideKeyboard()
findNavController().popBackStack(R.id.rosterListFragment, false)
}
private fun hideKeyboard() {
view?.let {
val imm = context?.getSystemService<InputMethodManager>()
imm?.hideSoftInputFromWindow(
it.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}
}
}
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.