Final Results
We changed a lot of stuff in this tutorial!
The nav_graph
navigation resource should contain:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph.xml"
app:startDestination="@id/rosterListFragment">
<fragment
android:id="@+id/rosterListFragment"
android:name="com.commonsware.todo.RosterListFragment"
android:label="@string/app_name">
<action
android:id="@+id/displayModel"
app:destination="@id/displayFragment" />
</fragment>
<fragment
android:id="@+id/displayFragment"
android:name="com.commonsware.todo.DisplayFragment"
android:label="@string/app_name" >
<argument
android:name="modelId"
app:argType="string" />
</fragment>
</navigation>
RosterRowHolder
should look like:
package com.commonsware.todo
import androidx.recyclerview.widget.RecyclerView
import com.commonsware.todo.databinding.TodoRowBinding
class RosterRowHolder(
private val binding: TodoRowBinding,
val onCheckboxToggle: (ToDoModel) -> Unit,
val onRowClick: (ToDoModel) -> Unit
) :
RecyclerView.ViewHolder(binding.root) {
fun bind(model: ToDoModel) {
binding.apply {
root.setOnClickListener { onRowClick(model) }
isCompleted.isChecked = model.isCompleted
isCompleted.setOnCheckedChangeListener { _, _ -> onCheckboxToggle(model) }
desc.text = model.description
}
}
}
RosterAdapter
should resemble:
package com.commonsware.todo
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.commonsware.todo.databinding.TodoRowBinding
class RosterAdapter(
private val inflater: LayoutInflater,
private val onCheckboxToggle: (ToDoModel) -> Unit,
private val onRowClick: (ToDoModel) -> Unit
) :
ListAdapter<ToDoModel, RosterRowHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
RosterRowHolder(
TodoRowBinding.inflate(inflater, parent, false),
onCheckboxToggle,
onRowClick
)
override fun onBindViewHolder(holder: RosterRowHolder, position: Int) {
holder.bind(getItem(position))
}
}
private object DiffCallback : DiffUtil.ItemCallback<ToDoModel>() {
override fun areItemsTheSame(oldItem: ToDoModel, newItem: ToDoModel) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ToDoModel, newItem: ToDoModel) =
oldItem.isCompleted == newItem.isCompleted &&
oldItem.description == newItem.description
}
RosterListFragment
should look like:
package com.commonsware.todo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.commonsware.todo.databinding.TodoRosterBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
import androidx.navigation.fragment.findNavController
class RosterListFragment : Fragment() {
private val motor: RosterMotor by viewModel()
private var binding: TodoRosterBinding? = null
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
)
)
}
adapter.submitList(motor.items)
binding?.empty?.visibility = View.GONE
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
private fun display(model: ToDoModel) {
findNavController()
.navigate(RosterListFragmentDirections.displayModel(model.id))
}
}
MainActivity
should contain:
package com.commonsware.todo
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import com.commonsware.todo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportFragmentManager.findFragmentById(R.id.nav_host)?.findNavController()?.let { nav ->
appBarConfiguration = AppBarConfiguration(nav.graph)
setupActionBarWithNavController(nav, appBarConfiguration)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.actions, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.about -> {
startActivity(Intent(this, AboutActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onSupportNavigateUp() =
navigateUp(findNavController(R.id.nav_host), appBarConfiguration)
}
The todo_display
layout resource should contain:
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/completed"
android:layout_width="@dimen/checked_icon_size"
android:layout_height="@dimen/checked_icon_size"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/is_completed"
app:tint="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_check_circle" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAppearance="?attr/textAppearanceHeadline1"
app:layout_constraintEnd_toStartOf="@+id/completed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/labelCreated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/created_on"
android:textAppearance="?attr/textAppearanceHeadline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/desc" />
<TextView
android:id="@+id/createdOn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAppearance="?attr/textAppearanceHeadline2"
app:layout_constraintEnd_toStartOf="@+id/completed"
app:layout_constraintStart_toEndOf="@+id/labelCreated"
app:layout_constraintTop_toBottomOf="@+id/desc" />
<TextView
android:id="@+id/notes"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:textAppearance="?attr/textAppearanceBody1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createdOn" />
</androidx.constraintlayout.widget.ConstraintLayout>
The styles
resource should resemble:
<resources>
<!-- Base application theme. -->
<style name="Theme.ToDo" parent="Theme.AppCompat.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="textAppearanceHeadline1">@style/HeadlineOneAppearance</item>
<item name="textAppearanceHeadline2">@style/HeadlineTwoAppearance</item>
<item name="textAppearanceBody1">@style/BodyAppearance</item>
</style>
<style name="HeadlineOneAppearance" parent="@style/TextAppearance.AppCompat.Large">
<item name="android:textStyle">bold</item>
</style>
<style name="HeadlineTwoAppearance" parent="@style/TextAppearance.AppCompat.Medium">
</style>
<style name="BodyAppearance" parent="@style/TextAppearance.AppCompat.Medium">
</style>
</resources>
SingleModelMotor
should contain:
package com.commonsware.todo
import androidx.lifecycle.ViewModel
class SingleModelMotor(
private val repo: ToDoRepository,
private val modelId: String
) : ViewModel() {
fun getModel() = repo.find(modelId)
}
ToDoRepository
should contain:
package com.commonsware.todo
class ToDoRepository {
var items = listOf(
ToDoModel(
description = "Buy a copy of _Exploring Android_",
isCompleted = true,
notes = "See https://wares.commonsware.com"
),
ToDoModel(
description = "Complete all of the tutorials"
),
ToDoModel(
description = "Write an app for somebody in my community",
notes = "Talk to some people at non-profit organizations to see what they need!"
)
)
fun save(model: ToDoModel) {
items = if (items.any { it.id == model.id }) {
items.map { if (it.id == model.id) model else it }
} else {
items + model
}
}
fun find(modelId: String) = items.find { it.id == modelId }
}
ToDoApp
should contain:
package com.commonsware.todo
import android.app.Application
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.dsl.module
class ToDoApp : Application() {
private val koinModule = module {
single { ToDoRepository() }
viewModel { RosterMotor(get()) }
viewModel { (modelId: String) -> SingleModelMotor(get(), modelId) }
}
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
modules(koinModule)
}
}
}
Finally, DisplayFragment
should now resemble:
package com.commonsware.todo
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import com.commonsware.todo.databinding.TodoDisplayBinding
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 onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = TodoDisplayBinding.inflate(inflater, container, false)
.apply { binding = this }
.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
motor.getModel()?.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()
}
}
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.