Commit 30ae4fdb authored by Joey's avatar Joey

insulin: use event-based architecture components

Change-Id: I116985fea1992a085079a5dc32a19b22b65b222e
Signed-off-by: Joey's avatarJoey <bevilacquajoey@gmail.com>
parent a61066f9
......@@ -13,8 +13,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import it.diab.data.AppDatabase
import it.diab.data.entities.TimeFrame
import it.diab.data.repositories.InsulinRepository
import it.diab.data.extensions.insulin
import it.diab.data.repositories.InsulinRepository
import it.diab.insulin.components.status.EditableOutStatus
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
......@@ -47,9 +48,10 @@ class EditorViewModelTest {
repository.insert(insulin)
val result = viewModel.runSetInsulin(insulin.uid)
assertEquals(insulin.uid, result.uid)
assertEquals(insulin, result)
viewModel.setInsulin(insulin.uid) { result ->
assertEquals(insulin.uid, result.uid)
assertEquals(insulin, result)
}
}
@Test
......@@ -61,7 +63,7 @@ class EditorViewModelTest {
repository.insert(insulin)
viewModel.insulin = insulin
viewModel.runSetInsulin(insulin.uid)
viewModel.runDelete()
assertNotEquals(insulin.uid, repository.getById(insulin.uid).uid)
......@@ -69,20 +71,18 @@ class EditorViewModelTest {
@Test
fun save() = runBlocking {
viewModel.runSetInsulin(-1)
val testUid = 12L
assertNotEquals(testUid, repository.getById(testUid).uid)
val origSize = repository.getInsulins().size
viewModel.insulin.apply {
uid = testUid
name = "BarFoo"
timeFrame = TimeFrame.LATE_MORNING
isBasal = true
}
viewModel.runSave()
assertEquals(testUid, repository.getById(testUid).uid)
viewModel.runSetInsulin(-1)
viewModel.runSave(
EditableOutStatus(
name = "BarFoo",
timeFrameIndex = TimeFrame.LATE_MORNING.ordinal,
hasHalfUnits = true,
isBasal = false
)
)
assertEquals(origSize + 1, repository.getInsulins().size)
}
}
\ No newline at end of file
......@@ -10,26 +10,21 @@ package it.diab.insulin.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import it.diab.core.util.event.Event
import it.diab.core.arch.EventBusFactory
import it.diab.data.entities.Insulin
import it.diab.insulin.R
import it.diab.insulin.components.status.ListItemStatus
import it.diab.insulin.holders.InsulinHolder
import it.diab.insulin.holders.InsulinHolderCallbacks
class InsulinAdapter : PagedListAdapter<Insulin, InsulinHolder>(CALLBACK), InsulinHolderCallbacks {
private val _editInsulin = MutableLiveData<Event<Long>>()
internal val editInsulin: LiveData<Event<Long>> = _editInsulin
class InsulinAdapter(private val bus: EventBusFactory) : PagedListAdapter<Insulin, InsulinHolder>(CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InsulinHolder {
return InsulinHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_list_insulin, parent, false),
this
bus
)
}
......@@ -46,14 +41,16 @@ class InsulinAdapter : PagedListAdapter<Insulin, InsulinHolder>(CALLBACK), Insul
if (item == null) {
holder.onLoading()
} else {
holder.onBind(item)
holder.onBind(
ListItemStatus(
item.uid,
item.name,
item.timeFrame.icon
)
)
}
}
override fun onClick(uid: Long) {
_editInsulin.value = Event(uid)
}
companion object {
private val CALLBACK = object : DiffUtil.ItemCallback<Insulin>() {
override fun areContentsTheSame(oldItem: Insulin, newItem: Insulin) =
......
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.components
import android.view.View
import it.diab.core.arch.EventBusFactory
import it.diab.core.arch.UiComponent
import it.diab.insulin.components.views.EditableView
import it.diab.insulin.events.EditEvent
import kotlinx.coroutines.CoroutineScope
class EditableComponent(
container: View,
scope: CoroutineScope,
bus: EventBusFactory
) : UiComponent {
private val uiView = EditableView(container)
init {
bus.subscribe(EditEvent::class, scope) {
when (it) {
is EditEvent.IntentEdit -> uiView.setStatus(it.status)
is EditEvent.IntentRequestSave -> {
bus.emit(EditEvent::class, EditEvent.IntentSave(uiView.getStatus()))
}
}
}
}
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.components
import android.view.View
import it.diab.core.arch.EventBusFactory
import it.diab.core.arch.UiComponent
import it.diab.insulin.components.views.EditorActionsView
class EditorActionsComponent(
container: View,
bus: EventBusFactory
) : UiComponent {
init {
EditorActionsView(container, bus)
}
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.components
import android.view.View
import it.diab.core.arch.EventBusFactory
import it.diab.core.arch.UiComponent
import it.diab.insulin.components.status.ListStatus
import it.diab.insulin.components.views.ListView
import it.diab.insulin.events.ListEvent
import kotlinx.coroutines.CoroutineScope
class ListComponent(
container: View,
scope: CoroutineScope,
bus: EventBusFactory
) : UiComponent {
init {
val view = ListView(container, bus)
bus.subscribe(ListEvent::class, scope) {
if (it is ListEvent.UpdateEvent) {
view.setStatus(ListStatus(it.pagedList))
}
}
}
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
@file:JvmName("EditableStatus")
package it.diab.insulin.components.status
import it.diab.core.arch.ViewStatus
data class EditableInStatus(
val isEdit: Boolean,
val name: String,
val timeFrameIndex: Int,
val timeFrameOptions: List<Int>,
val hasHalfUnits: Boolean,
val isBasal: Boolean
) : ViewStatus
data class EditableOutStatus(
val name: String = "",
val timeFrameIndex: Int = 1,
val hasHalfUnits: Boolean = false,
val isBasal: Boolean = false
) : ViewStatus
\ No newline at end of file
......@@ -6,14 +6,10 @@
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.holders
package it.diab.insulin.components.status
interface InsulinHolderCallbacks {
import it.diab.core.arch.ViewStatus
/**
* On item click callback
*
* @param uid insulin uid
*/
fun onClick(uid: Long)
}
\ No newline at end of file
data class EditorActionsStatus(
val isEdit: Boolean = false
) : ViewStatus
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
@file:JvmName("ListStatus")
package it.diab.insulin.components.status
import androidx.annotation.DrawableRes
import androidx.paging.PagedList
import it.diab.core.arch.ViewStatus
import it.diab.data.entities.Insulin
data class ListStatus(
val pagedList: PagedList<Insulin>
) : ViewStatus
data class ListItemStatus(
val uid: Long,
val name: String,
@DrawableRes val icon: Int
) : ViewStatus
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.components.views
import android.view.View
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.appcompat.widget.AppCompatEditText
import androidx.appcompat.widget.AppCompatSpinner
import androidx.appcompat.widget.SwitchCompat
import com.google.android.material.button.MaterialButton
import it.diab.core.arch.UiView
import it.diab.insulin.R
import it.diab.insulin.components.status.EditableInStatus
import it.diab.insulin.components.status.EditableOutStatus
class EditableView(container: View) : UiView<EditableInStatus, EditableOutStatus>(container) {
private val title: TextView =
container.findViewById(R.id.insulin_edit_dialog_title)
private val name: AppCompatEditText =
container.findViewById(R.id.insulin_edit_name)
private val timeFrame: AppCompatSpinner =
container.findViewById(R.id.insulin_edit_time)
private val hasHalfUnits: SwitchCompat =
container.findViewById(R.id.insulin_edit_half_units)
private val isBasal: SwitchCompat =
container.findViewById(R.id.insulin_edit_basal)
private val delete: MaterialButton =
container.findViewById(R.id.insulin_edit_delete)
override fun setStatus(status: EditableInStatus) {
title.setText(if (status.isEdit) R.string.insulin_editor_edit else R.string.insulin_editor_add)
name.setText(status.name)
timeFrame.adapter = ArrayAdapter(
timeFrame.context,
androidx.appcompat.R.layout.support_simple_spinner_dropdown_item,
status.timeFrameOptions.map(timeFrame.context::getString)
)
timeFrame.setSelection(status.timeFrameIndex)
hasHalfUnits.isChecked = status.hasHalfUnits
isBasal.isChecked = status.isBasal
delete.visibility = if (status.isEdit) View.VISIBLE else View.GONE
}
override fun getStatus() = EditableOutStatus(
name.text.toString(),
timeFrame.selectedItemPosition,
hasHalfUnits.isChecked,
isBasal.isChecked
)
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.components.views
import android.view.View
import com.google.android.material.button.MaterialButton
import it.diab.core.arch.EventBusFactory
import it.diab.core.arch.UiView
import it.diab.core.arch.ViewStatus
import it.diab.insulin.R
import it.diab.insulin.events.EditEvent
class EditorActionsView(
container: View,
bus: EventBusFactory
) : UiView<ViewStatus, ViewStatus>(container) {
private val save: MaterialButton =
container.findViewById(R.id.insulin_edit_save)
private val delete: MaterialButton =
container.findViewById(R.id.insulin_edit_delete)
init {
save.setOnClickListener {
bus.emit(EditEvent::class, EditEvent.IntentRequestSave)
}
delete.setOnClickListener {
bus.emit(EditEvent::class, EditEvent.IntentRequestDelete)
}
}
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.components.views
import android.view.View
import it.diab.core.arch.EventBusFactory
import it.diab.core.arch.UiView
import it.diab.core.arch.ViewStatus
import it.diab.insulin.R
import it.diab.insulin.adapters.InsulinAdapter
import it.diab.insulin.components.status.ListStatus
import it.diab.ui.widgets.RecyclerViewExt
class ListView(
container: View,
bus: EventBusFactory
) : UiView<ListStatus, ViewStatus>(container) {
private val list: RecyclerViewExt =
container.findViewById(R.id.insulin_list)
private val adapter = InsulinAdapter(bus)
init {
list.adapter = adapter
}
override fun setStatus(status: ListStatus) {
adapter.submitList(status.pagedList)
}
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.events
import it.diab.core.arch.ComponentEvent
import it.diab.insulin.components.status.EditableInStatus
import it.diab.insulin.components.status.EditableOutStatus
sealed class EditEvent : ComponentEvent {
class IntentEdit(val status: EditableInStatus) : EditEvent()
class IntentSave(val status: EditableOutStatus) : EditEvent()
object IntentRequestSave : EditEvent()
object IntentRequestDelete : EditEvent()
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.insulin.events
import androidx.paging.PagedList
import it.diab.core.arch.ComponentEvent
import it.diab.data.entities.Insulin
sealed class ListEvent : ComponentEvent {
class UpdateEvent(val pagedList: PagedList<Insulin>) : ListEvent()
class ClickEvent(val uid: Long) : ListEvent()
}
\ No newline at end of file
......@@ -12,36 +12,28 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.appcompat.widget.AppCompatEditText
import androidx.appcompat.widget.AppCompatSpinner
import androidx.appcompat.widget.SwitchCompat
import androidx.lifecycle.ViewModelProviders
import com.google.android.material.button.MaterialButton
import it.diab.core.util.Activities
import it.diab.core.util.extensions.bus
import it.diab.data.entities.Insulin
import it.diab.data.entities.TimeFrame
import it.diab.data.repositories.InsulinRepository
import it.diab.insulin.R
import it.diab.insulin.components.EditableComponent
import it.diab.insulin.components.EditorActionsComponent
import it.diab.insulin.components.status.EditableInStatus
import it.diab.insulin.events.EditEvent
import it.diab.insulin.viewmodels.EditorViewModel
import it.diab.insulin.viewmodels.EditorViewModelFactory
import it.diab.ui.util.UIUtils
import it.diab.ui.widgets.BottomSheetDialogFragmentExt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class EditorFragment : BottomSheetDialogFragmentExt() {
private lateinit var titleView: TextView
private lateinit var editText: AppCompatEditText
private lateinit var spinner: AppCompatSpinner
private lateinit var basalSwitch: SwitchCompat
private lateinit var halfUnitsSwitch: SwitchCompat
private lateinit var saveButton: MaterialButton
private lateinit var deleteButton: MaterialButton
private lateinit var viewModel: EditorViewModel
private var editMode = false
private val uiScope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -55,77 +47,42 @@ class EditorFragment : BottomSheetDialogFragmentExt() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.fragment_insulin_edit, container, false)
titleView = view.findViewById(R.id.insulin_edit_dialog_title)
editText = view.findViewById(R.id.insulin_edit_name)
spinner = view.findViewById(R.id.insulin_edit_time)
basalSwitch = view.findViewById(R.id.insulin_edit_basal)
halfUnitsSwitch = view.findViewById(R.id.insulin_edit_half_units)
saveButton = view.findViewById(R.id.insulin_edit_save)
deleteButton = view.findViewById(R.id.insulin_edit_delete)
UIUtils.setWhiteNavBarIfNeeded(requireContext(), dialog)
return view
}
): View = inflater.inflate(R.layout.fragment_insulin_edit, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
EditableComponent(view, uiScope, bus)
EditorActionsComponent(view, bus)
val uid = arguments?.getLong(Activities.Insulin.EXTRA_EDITOR_UID, -1L) ?: -1L
editMode = uid >= 0
viewModel.setInsulin(uid, this::setup)
}
private fun setup(insulin: Insulin) {
titleView.text = getString(if (editMode) R.string.insulin_editor_edit else R.string.insulin_editor_add)
editText.setText(insulin.name)
basalSwitch.isChecked = insulin.isBasal
halfUnitsSwitch.isChecked = insulin.hasHalfUnits
setupSpinner(insulin)
saveButton.setOnClickListener { saveInsulin() }
deleteButton.setOnClickListener { deleteInsulin() }
if (!editMode) {
deleteButton.visibility = View.GONE
bus.subscribe(EditEvent::class, uiScope) {
when (it) {
is EditEvent.IntentSave -> {
viewModel.save(it.status)
dismiss()
}
is EditEvent.IntentRequestDelete -> {
viewModel.delete()
dismiss()
}
}
}
}
private fun setupSpinner(insulin: Insulin) {
val context = context ?: return
val timeFrames = TimeFrame.values()
spinner.apply {
adapter = ArrayAdapter(
context,
androidx.appcompat.R.layout.support_simple_spinner_dropdown_item,
timeFrames.map { timeFrame -> getString(timeFrame.string) }
bus.emit(
EditEvent::class, EditEvent.IntentEdit(
EditableInStatus(
insulin.uid > 0L,
insulin.name,
insulin.timeFrame.toInt() + 1,
TimeFrame.values().map(TimeFrame::string),
insulin.hasHalfUnits,
insulin.isBasal
)
)
setSelection(insulin.timeFrame.toInt() + 1)
}
}
private fun saveInsulin() {
viewModel.insulin.apply {
name = editText.text.toString()
isBasal = basalSwitch.isChecked
hasHalfUnits = halfUnitsSwitch.isChecked
setTimeFrame(spinner.selectedItemPosition - 1)
}
viewModel.save()
dismiss()
}
private fun deleteInsulin() {
viewModel.delete()
dismiss()
)
}
}
\ No newline at end of file
......@@ -15,21 +15,25 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.RecyclerView
import it.diab.core.util.Activities
import it.diab.core.util.event.EventObserver
import it.diab.core.util.extensions.bus
import it.diab.data.repositories.InsulinRepository
import it.diab.insulin.R
import it.diab.insulin.adapters.InsulinAdapter
import it.diab.insulin.components.ListComponent
import it.diab.insulin.events.ListEvent
import it.diab.insulin.viewmodels.ListViewModel
import it.diab.insulin.viewmodels.ListViewModelFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class ListFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var list: ListComponent
private lateinit var viewModel: ListViewModel
private val uiScope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstance: Bundle?) {
super.onCreate(savedInstance)
......@@ -43,16 +47,22 @@ class ListFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_insulin_list, container, false)
recyclerView = view.findViewById(R.id.insulin_list)
): View = inflater.inflate(R.layout.fragment_insulin_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
list = ListComponent(view, uiScope, bus)
val adapter = InsulinAdapter()
recyclerView.adapter = adapter
viewModel.list.observe(viewLifecycleOwner, Observer(adapter::submitList))
adapter.editInsulin.observe(viewLifecycleOwner, EventObserver(this::onItemClick))
bus.subscribe(ListEvent::class, uiScope) {
if (it is ListEvent.ClickEvent) {
onItemClick(it.uid)
}
}
return view