Commit bc3588a0 authored by Joey's avatar Joey

app: refactor

* Move Holders out of adapter classes
* Move ViewPager out of MainActivity
* Cleanup
Signed-off-by: Joey's avatarJoey <bevilacquajoey@gmail.com>
parent 2616e7e6
......@@ -15,6 +15,7 @@ import it.diab.core.data.AppDatabase
import it.diab.core.data.entities.TimeFrame
import it.diab.core.data.repositories.GlucoseRepository
import it.diab.core.util.extensions.glucose
import it.diab.viewmodels.overview.OverviewViewModel
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
......
......@@ -12,14 +12,13 @@ import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityOptionsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.tabs.TabLayout
import it.diab.adapters.FragmentsPagerAdapter
import it.diab.core.util.Activities
import it.diab.core.util.event.EventObserver
import it.diab.core.util.intentTo
......@@ -29,16 +28,8 @@ import it.diab.fragments.OverviewFragment
import it.diab.util.ShortcutUtils
class MainActivity : AppCompatActivity() {
private lateinit var coordinator: CoordinatorLayout
private lateinit var tabLayout: TabLayout
private lateinit var viewPager: ViewPager
private lateinit var adapter: ViewPagerAdapter
private lateinit var fab: FloatingActionButton
private lateinit var overviewFragment: OverviewFragment
private lateinit var glucoseFragment: GlucoseListFragment
private lateinit var insulinFragment: InsulinFragment
private val fragmentsLifeCycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(
fm: FragmentManager,
......@@ -62,19 +53,18 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstance)
setContentView(R.layout.activity_main)
overviewFragment = OverviewFragment()
glucoseFragment = GlucoseListFragment()
insulinFragment = InsulinFragment()
coordinator = findViewById(R.id.coordinator)
tabLayout = findViewById(R.id.tabs)
viewPager = findViewById(R.id.viewpager)
val tabLayout = findViewById<TabLayout>(R.id.tabs)
val viewPager = findViewById<ViewPager>(R.id.viewpager)
fab = findViewById(R.id.fab)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentsLifeCycleCallback, false)
adapter = ViewPagerAdapter(supportFragmentManager)
viewPager.adapter = adapter
viewPager.adapter = FragmentsPagerAdapter(
supportFragmentManager,
resources,
OverviewFragment(),
GlucoseListFragment(),
InsulinFragment()
)
tabLayout.setupWithViewPager(viewPager)
fab.setOnClickListener { onGlucoseClick(-1) }
......@@ -102,12 +92,4 @@ class MainActivity : AppCompatActivity() {
ShortcutUtils.setupShortcuts(this)
}
}
inner class ViewPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manager) {
private val fragments = arrayOf(overviewFragment, glucoseFragment, insulinFragment)
override fun getCount() = fragments.size
override fun getItem(position: Int) = fragments[position]
override fun getPageTitle(position: Int): String = getString(fragments[position].getTitle())
}
}
/*
* 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.adapters
import android.content.res.Resources
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import it.diab.fragments.BaseFragment
class FragmentsPagerAdapter(
manager: FragmentManager,
private val resources: Resources,
vararg fragment: BaseFragment
) : FragmentPagerAdapter(manager) {
private val fragments: Array<out BaseFragment> = fragment
override fun getCount() = fragments.size
override fun getItem(position: Int) = fragments[position]
override fun getPageTitle(position: Int): String = fragments[position].getTitle(resources)
}
\ No newline at end of file
......@@ -11,26 +11,23 @@ package it.diab.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import it.diab.R
import it.diab.core.data.entities.Glucose
import it.diab.core.util.PreferencesUtil
import it.diab.core.util.event.Event
import it.diab.core.util.extensions.setPrecomputedText
import it.diab.holders.GlucoseHolder
import it.diab.holders.GlucoseHolderCallbacks
import it.diab.util.UIUtils
import it.diab.util.extensions.diff
import it.diab.viewmodels.glucose.GlucoseListViewModel
import kotlinx.coroutines.CoroutineScope
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
......@@ -38,14 +35,14 @@ import java.util.Locale
class GlucoseListAdapter(
val context: Context,
private val viewModel: GlucoseListViewModel
) : PagedListAdapter<Glucose, GlucoseListAdapter.GlucoseHolder>(CALLBACK) {
) : PagedListAdapter<Glucose, GlucoseHolder>(CALLBACK), GlucoseHolderCallbacks {
private val _openGlucose = MutableLiveData<Event<Long>>()
val openGlucose: LiveData<Event<Long>> = _openGlucose
// Store the these for better performance
private val lowIndicator by lazy { getIndicator(R.color.glucose_indicator_low) }
private val highIndicator by lazy { getIndicator(R.color.glucose_indicator_high) }
private val lowIndicator by lazy { buildIndicator(R.color.glucose_indicator_low) }
private val highIndicator by lazy { buildIndicator(R.color.glucose_indicator_high) }
private val highThreshold by lazy { PreferencesUtil.getGlucoseHighThreshold(context) }
private val lowThreshold by lazy { PreferencesUtil.getGlucoseLowThreshold(context) }
......@@ -57,20 +54,42 @@ class GlucoseListAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GlucoseHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_glucose, parent, false)
LayoutInflater.from(parent.context).inflate(R.layout.item_glucose, parent, false),
this
)
override fun onBindViewHolder(holder: GlucoseHolder, position: Int) {
val item = getItem(position)
if (item == null) {
holder.clear()
holder.onLoading()
} else {
holder.onBind(item)
}
}
private fun shouldInsertHeader(position: Int): Boolean {
override fun fetchHeaderText(date: Date, onFetch: (String, CoroutineScope) -> Unit) {
viewModel.setHeader(date, dateFormat, onFetch)
}
override fun fetchHourText(date: Date, onFetch: (String, CoroutineScope) -> Unit) {
val text = hourFormat.format(date)
onFetch(text, viewModel.viewModelScope)
}
override fun getIndicator(value: Int) = when {
value < lowThreshold -> lowIndicator
value > highThreshold -> highIndicator
else -> null
}
override fun getInsulinName(uid: Long) =
viewModel.getInsulin(uid).name
override fun onClick(uid: Long) {
_openGlucose.value = Event(uid)
}
override fun shouldInsertHeader(position: Int): Boolean {
if (position == 0) {
return true
}
......@@ -83,103 +102,13 @@ class GlucoseListAdapter(
return b.diff(Date()) != 0 && a.diff(b) > 0
}
private fun getIndicator(@ColorRes colorId: Int): Drawable? {
private fun buildIndicator(@ColorRes colorId: Int): Drawable? {
val resources = context.resources
val color = ContextCompat.getColor(context, colorId)
val size = resources.getDimensionPixelSize(R.dimen.item_glucose_indicator)
return UIUtils.createRoundDrawable(resources, size, color)
}
inner class GlucoseHolder(view: View) : RecyclerView.ViewHolder(view) {
private val icon = view.findViewById<ImageView>(R.id.item_glucose_timezone)
private val title = view.findViewById<TextView>(R.id.item_glucose_value)
private val summary = view.findViewById<TextView>(R.id.item_glucose_insulin)
private val indicator = view.findViewById<ImageView>(R.id.item_glucose_status)
private val header = view.findViewById<ConstraintLayout>(R.id.item_glucose_header)
private val headerTitle = view.findViewById<TextView>(R.id.item_glucose_header_title)
fun onBind(glucose: Glucose) {
itemView.visibility = View.VISIBLE
bindHeader(glucose)
bindValue(glucose)
icon.setImageResource(glucose.timeFrame.icon)
itemView.setOnClickListener { _openGlucose.value = Event(glucose.uid) }
bindInsulins(glucose)
}
fun clear() {
itemView.visibility = View.INVISIBLE
}
private fun bindHeader(glucose: Glucose) {
val shouldShowHeader = shouldInsertHeader(adapterPosition)
header.visibility = if (shouldShowHeader) View.VISIBLE else View.GONE
if (shouldShowHeader) {
viewModel.setHeader(glucose.date, dateFormat) { date ->
headerTitle.setPrecomputedText(date, viewModel.viewModelScope)
}
}
}
private fun bindValue(glucose: Glucose) {
title.setPrecomputedText(
"%1\$d (%2\$s)".format(glucose.value, hourFormat.format(glucose.date)),
viewModel.viewModelScope
)
// High / low indicator
val indicatorDrawable = when {
glucose.value > highThreshold -> highIndicator
glucose.value < lowThreshold -> lowIndicator
else -> null
}
if (indicatorDrawable == null) {
indicator.visibility = View.GONE
} else {
indicator.setImageDrawable(indicatorDrawable)
indicator.visibility = View.VISIBLE
}
}
private fun bindInsulins(glucose: Glucose) {
val builder = StringBuilder()
val insulinId = glucose.insulinId0
val basalId = glucose.insulinId1
if (insulinId >= 0) {
builder.append(glucose.insulinValue0)
.append(" ")
.append(viewModel.getInsulin(insulinId).name)
if (basalId >= 0) {
builder.append(", ")
}
}
if (basalId >= 0) {
builder.append(glucose.insulinValue1)
.append(" ")
.append(viewModel.getInsulin(basalId).name)
}
if (builder.isEmpty()) {
summary.visibility = View.GONE
return
}
summary.setPrecomputedText(builder.toString(), viewModel.viewModelScope)
summary.visibility = View.VISIBLE
}
}
companion object {
private val CALLBACK = object : DiffUtil.ItemCallback<Glucose>() {
override fun areContentsTheSame(oldItem: Glucose, newItem: Glucose) =
......
......@@ -9,20 +9,18 @@
package it.diab.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import it.diab.R
import it.diab.core.data.entities.Insulin
import it.diab.core.util.event.Event
import it.diab.holders.InsulinHolderCallbacks
import it.diab.holders.InsulinHolder
class InsulinAdapter : PagedListAdapter<Insulin, InsulinAdapter.InsulinHolder>(CALLBACK) {
class InsulinAdapter : PagedListAdapter<Insulin, InsulinHolder>(CALLBACK), InsulinHolderCallbacks {
private val _editInsulin = MutableLiveData<Event<Long>>()
internal val editInsulin: LiveData<Event<Long>> = _editInsulin
......@@ -30,7 +28,8 @@ class InsulinAdapter : PagedListAdapter<Insulin, InsulinAdapter.InsulinHolder>(C
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InsulinHolder {
return InsulinHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_insulin, parent, false)
.inflate(R.layout.item_insulin, parent, false),
this
)
}
......@@ -45,34 +44,14 @@ class InsulinAdapter : PagedListAdapter<Insulin, InsulinAdapter.InsulinHolder>(C
val item = getItem(position)
if (item == null) {
holder.clear()
holder.onLoading()
} else {
holder.onBind(item)
}
}
inner class InsulinHolder(view: View) : RecyclerView.ViewHolder(view) {
private val title: TextView = view.findViewById(R.id.item_insulin_name)
private val icon: ImageView = view.findViewById(R.id.item_insulin_icon)
fun onBind(insulin: Insulin) {
title.text = insulin.name
icon.setImageResource(insulin.timeFrame.icon)
itemView.setOnClickListener { _editInsulin.value = Event(insulin.uid) }
}
fun onBind() {
val res = itemView.resources
title.text = res.getString(R.string.insulin_add_item)
icon.setImageResource(R.drawable.ic_add)
itemView.setOnClickListener { _editInsulin.value = Event(-1) }
}
fun clear() {
itemView.visibility = View.GONE
}
override fun onClick(uid: Long) {
_editInsulin.value = Event(uid)
}
companion object {
......
......@@ -8,10 +8,11 @@
*/
package it.diab.fragments
import androidx.annotation.StringRes
import android.content.res.Resources
import androidx.fragment.app.Fragment
abstract class MainFragment : Fragment() {
@StringRes
abstract fun getTitle(): Int
abstract class BaseFragment : Fragment() {
protected abstract val titleRes: Int
fun getTitle(resources: Resources): String = resources.getString(titleRes)
}
\ No newline at end of file
......@@ -29,7 +29,9 @@ import it.diab.core.util.event.EventObserver
import it.diab.viewmodels.glucose.GlucoseListViewModel
import it.diab.viewmodels.glucose.GlucoseListViewModelFactory
class GlucoseListFragment : MainFragment() {
class GlucoseListFragment : BaseFragment() {
override val titleRes = R.string.fragment_glucose
private lateinit var recyclerView: RecyclerView
private lateinit var viewModel: GlucoseListViewModel
......@@ -84,8 +86,6 @@ class GlucoseListFragment : MainFragment() {
setViewModelStrings()
}
override fun getTitle() = R.string.fragment_glucose
private fun update(data: PagedList<Glucose>?) {
adapter.submitList(data)
}
......
......@@ -26,7 +26,9 @@ import it.diab.core.util.intentTo
import it.diab.viewmodels.insulin.InsulinViewModel
import it.diab.viewmodels.insulin.InsulinViewModelFactory
class InsulinFragment : MainFragment() {
class InsulinFragment : BaseFragment() {
override val titleRes = R.string.fragment_insulin
private lateinit var recyclerView: RecyclerView
private lateinit var viewModel: InsulinViewModel
......@@ -56,8 +58,6 @@ class InsulinFragment : MainFragment() {
return view
}
override fun getTitle() = R.string.fragment_insulin
private fun onItemClick(uid: Long) {
val intent = intentTo(Activities.Insulin.Editor).apply {
putExtra(Activities.Insulin.Editor.EXTRA_UID, uid)
......
......@@ -37,12 +37,14 @@ import it.diab.core.util.SystemUtil
import it.diab.core.util.intentTo
import it.diab.ui.graph.OverviewGraphView
import it.diab.util.extensions.isToday
import it.diab.viewmodels.glucose.OverviewViewModel
import it.diab.viewmodels.glucose.OverviewViewModelFactory
import it.diab.viewmodels.overview.OverviewViewModel
import it.diab.viewmodels.overview.OverviewViewModelFactory
import java.text.SimpleDateFormat
import java.util.Locale
class OverviewFragment : MainFragment() {
class OverviewFragment : BaseFragment() {
override val titleRes = R.string.fragment_overview
private lateinit var lastValueView: TextView
private lateinit var lastDescView: TextView
private lateinit var chart: OverviewGraphView
......@@ -91,8 +93,6 @@ class OverviewFragment : MainFragment() {
setupMenu()
}
override fun getTitle() = R.string.fragment_overview
private fun updateChart(data: List<Glucose>?) {
if (data == null || data.isEmpty()) {
return
......
/*
* 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.holders
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import it.diab.R
import it.diab.core.data.entities.Glucose
import it.diab.core.util.extensions.setPrecomputedText
class GlucoseHolder(
view: View,
private val callbacks: GlucoseHolderCallbacks
) : RecyclerView.ViewHolder(view) {
private val headerView = view.findViewById<View>(R.id.item_glucose_header)
private val headerTitleView = view.findViewById<TextView>(R.id.item_glucose_header_title)
private val iconView = view.findViewById<ImageView>(R.id.item_glucose_timezone)
private val titleView = view.findViewById<TextView>(R.id.item_glucose_value)
private val summaryView = view.findViewById<TextView>(R.id.item_glucose_insulin)
private val indicatorView = view.findViewById<ImageView>(R.id.item_glucose_status)
fun onBind(glucose: Glucose) {
itemView.visibility = View.VISIBLE
bindHeader(glucose)
bindValue(glucose)
bindInsulin(glucose)
iconView.setImageResource(glucose.timeFrame.icon)
itemView.setOnClickListener { callbacks.onClick(glucose.uid) }
}
fun onLoading() {
itemView.visibility = View.INVISIBLE
}
private fun bindHeader(glucose: Glucose) {
val shouldShowHeader = callbacks.shouldInsertHeader(adapterPosition)
headerView.visibility = if (shouldShowHeader) View.VISIBLE else View.GONE
if (shouldShowHeader) {
callbacks.fetchHeaderText(glucose.date, headerTitleView::setPrecomputedText)
}
}
private fun bindValue(glucose: Glucose) {
val title = "${glucose.value} (%1\$s)"
callbacks.fetchHourText(glucose.date) { text, scope ->
titleView.setPrecomputedText(title.format(text), scope)
}
val indicatorDrawable = callbacks.getIndicator(glucose.value)
if (indicatorDrawable == null) {
indicatorView.visibility = View.GONE
} else {
indicatorView.visibility = View.VISIBLE
indicatorView.setImageDrawable(indicatorDrawable)
}
}
private fun bindInsulin(glucose: Glucose) {
val builder = StringBuilder()
val insulinId = glucose.insulinId0
val basalId = glucose.insulinId1
builder.apply {
if (insulinId >= 0) {
append(glucose.insulinValue0)
append(" ")
append(callbacks.getInsulinName(insulinId))
}
if (basalId >= 0) {
if (insulinId >= 0) {
append(", ")
}
append(glucose.insulinValue1)
append(" ")
append(callbacks.getInsulinName(basalId))
}
}
if (builder.isEmpty()) {
summaryView.visibility = View.GONE
} else {
summaryView.visibility = View.VISIBLE
summaryView.text = builder.toString()
}
}
}
\ 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.holders
import android.graphics.drawable.Drawable
import kotlinx.coroutines.CoroutineScope
import java.util.Date
interface GlucoseHolderCallbacks {
/**
* Fetch a String that for the header
*
* @param date date to be put in the string
* @param onFetch callback for fetch completion
*/
fun fetchHeaderText(
date: Date,
onFetch: (String, CoroutineScope) -> Unit
)
/**
* Fetch a String that represents hours of a given [Date]
*
* @param date date to be put in the string
* @param onFetch callback for fetch completion
*/
fun fetchHourText(
date: Date,
onFetch: (String, CoroutineScope) -> Unit
)
/**
* Get the indicator drawable for
* a given glucose value
*/
fun getIndicator(value: Int): Drawable?
/**
* Get the name of an insulin
*
* @param uid uid of the insulin
*/
fun getInsulinName(uid: Long): String
/**
* OnClick event callback
*/
fun onClick(uid: Long)
/**
* Whether the header should be shown
*
* @param position position of the glucose in the list
*/
fun shouldInsertHeader(position: Int): Boolean
}
/*
* 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.holders
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import it.diab.R
import it.diab.core.data.entities.Insulin
class InsulinHolder(
view: View,
private val callbacks: InsulinHolderCallbacks
) : RecyclerView.ViewHolder(view) {
private val titleView = view.findViewById<TextView>(R.id.item_insulin_name)
private val iconView = view.findViewById<ImageView>(R.id.item_insulin_icon)
fun onBind(insulin: Insulin) {
titleView.text = insulin.name
iconView.setImageResource(insulin.timeFrame.icon)
itemView.setOnClickListener { callbacks.onClick(insulin.uid) }
}
fun onBind() {
val res = itemView.resources
titleView.text = res.getString(R.string.insulin_add_item)
iconView.setImageResource(R.drawable.ic_add)
itemView.setOnClickListener { callbacks.onClick(-1) }
}
fun onLoading() {
itemView.visibility = View.GONE
}
}
\ 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.holders
interface InsulinHolderCallbacks {
/**
* On item click callback
*
* @param uid insulin uid
*/
fun onClick(uid: Long)
}
\ No newline at end of file