Final Results

Our new actions_roster menu resource should contain:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:android="http://schemas.android.com/apk/res/android">

  <item
    android:id="@+id/add"
    android:icon="@drawable/ic_add"
    android:title="@string/menu_add"
    app:showAsAction="ifRoom|withText" />
</menu>

The nav_graph resource should resemble:

<?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" />
    <action
      android:id="@+id/createModel"
      app:destination="@id/editFragment" >
      <argument
        android:name="modelId"
        android:defaultValue="@null" />
    </action>
  </fragment>
  <fragment
    android:id="@+id/displayFragment"
    android:name="com.commonsware.todo.DisplayFragment"
    android:label="@string/app_name" >
    <argument
      android:name="modelId"
      app:argType="string" />
    <action
      android:id="@+id/editModel"
      app:destination="@id/editFragment" />
  </fragment>
  <fragment
    android:id="@+id/editFragment"
    android:name="com.commonsware.todo.EditFragment"
    android:label="@string/app_name" >
    <argument
      android:name="modelId"
      app:argType="string"
      app:nullable="true" />
  </fragment>
</navigation>

SingleModelMotor should look like:

package com.commonsware.todo

import androidx.lifecycle.ViewModel

class SingleModelMotor(
  private val repo: ToDoRepository,
  private val modelId: String?
) : ViewModel() {
  fun getModel() = repo.find(modelId)

  fun save(model: ToDoModel) {
    repo.save(model)
  }

  fun delete(model: ToDoModel) {
    repo.delete(model)
  }
}

ToDoRepository should resemble:

package com.commonsware.todo

class ToDoRepository {
  var items = emptyList<ToDoModel>()

  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 }

  fun delete(model: ToDoModel) {
    items = items.filter { it.id != model.id }
  }
}

RosterListFragment should look like:

package com.commonsware.todo

import android.os.Bundle
import android.view.*
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 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
        )
      )
    }

    adapter.submitList(motor.getItems())
    binding?.empty?.visibility =
      if (adapter.itemCount == 0) View.VISIBLE else 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))
  }
}

The actions_edit menu resource should now resemble:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:android="http://schemas.android.com/apk/res/android">

  <item
    android:id="@+id/save"
    android:icon="@drawable/ic_save"
    android:title="@string/menu_save"
    app:showAsAction="ifRoom|withText" />
  <item
    android:id="@+id/delete"
    android:icon="@drawable/ic_delete"
    android:title="@string/menu_delete"
    app:showAsAction="ifRoom|withText" />
</menu>

SingleModelMotor should look like:

package com.commonsware.todo

import androidx.lifecycle.ViewModel

class SingleModelMotor(
  private val repo: ToDoRepository,
  private val modelId: String?
) : ViewModel() {
  fun getModel() = repo.find(modelId)

  fun save(model: ToDoModel) {
    repo.save(model)
  }

  fun delete(model: ToDoModel) {
    repo.delete(model)
  }
}

Finally, EditFragment overall should look like:

package com.commonsware.todo

import android.os.Bundle
import android.view.*
import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.commonsware.todo.databinding.TodoEditBinding
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?) {
    motor.getModel()?.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.getModel()
      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.getModel()

    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.