Create view-model and presenter for TargetCard; reduce boilerplate

pull/699/head
Alinson S. Xavier 5 years ago
parent 4dc1df5887
commit bda3e42e2b

@ -1,68 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views;
import androidx.test.filters.*;
import androidx.test.runner.*;
import android.view.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SubtitleCardTest extends BaseViewTest
{
public static final String PATH = "habits/show/SubtitleCard/";
private SubtitleCard view;
private Habit habit;
@Before
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY));
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.subtitleCard);
view.setHabit(habit);
view.refreshData();
measureView(view, 800, 200);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
}

@ -0,0 +1,54 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.show.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class SubtitleCardTest : BaseViewTest() {
val PATH = "habits/show/SubtitleCard/"
private lateinit var view: SubtitleCard
@Before
override fun setUp() {
super.setUp()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.subtitleCard)
view.onData(ShowHabitViewModel(
question = "Did you meditate this morning?",
reminderText = "8:30 AM",
frequencyText = "3 times in 7 days",
))
measureView(view, 800f, 200f)
}
@Test
fun testRender() {
assertRenders(view, PATH + "render.png")
}
}

@ -0,0 +1,45 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities
import android.content.*
import android.util.*
import android.widget.*
abstract class DataView<T>(
context: Context,
attrs: AttributeSet,
) : LinearLayout(context, attrs), Presenter.Listener<T> {
lateinit var presenter: Presenter<T>
override fun onAttachedToWindow() {
super.onAttachedToWindow()
presenter.addListener(this)
presenter.requestData(this)
}
override fun onDetachedFromWindow() {
presenter.removeListener(this)
super.onDetachedFromWindow()
}
abstract override fun onData(data: T)
}

@ -0,0 +1,68 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities
import org.isoron.uhabits.core.commands.*
abstract class Presenter<M>(
val commandRunner: CommandRunner,
) : CommandRunner.Listener {
private val listeners = mutableListOf<Listener<M>>()
private var data: M? = null
fun onResume() {
commandRunner.addListener(this)
data = refresh()
notifyListeners()
}
abstract fun refresh(): M
fun onPause() {
commandRunner.removeListener(this)
}
fun addListener(listener: Listener<M>) {
listeners.add(listener)
}
fun removeListener(listener: Listener<M>) {
listeners.remove(listener)
}
fun requestData(listener: Listener<M>) {
if (data == null) data = refresh()
listener.onData(data!!)
}
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
data = refresh()
notifyListeners()
}
private fun notifyListeners() {
for (l in listeners) l.onData(data!!)
}
interface Listener<T> {
fun onData(data: T)
}
}

@ -23,6 +23,7 @@ import android.annotation.*
import android.content.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
@ -36,47 +37,14 @@ import javax.inject.*
class ShowHabitPresenter
@Inject constructor(
val habit: Habit,
val commandRunner: CommandRunner,
val preferences: Preferences,
commandRunner: CommandRunner,
@ActivityContext val context: Context,
) : CommandRunner.Listener {
) : Presenter<ShowHabitViewModel>(commandRunner) {
private val listeners = mutableListOf<Listener>()
private var data = ShowHabitViewModel()
private val resources = context.resources
fun onResume() {
commandRunner.addListener(this)
refresh()
notifyListeners()
}
fun onPause() {
commandRunner.removeListener(this)
}
fun addListener(listener: Listener) {
listeners.add(listener)
}
fun removeListener(listener: Listener) {
listeners.remove(listener)
}
fun requestData(listener: Listener) {
listener.onData(data)
}
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
refresh()
notifyListeners()
}
private fun notifyListeners() {
for (l in listeners) l.onData(data)
}
private fun refresh() {
override fun refresh(): ShowHabitViewModel {
val today = DateUtils.getTodayWithOffset()
val lastMonth = today.minus(30)
val lastYear = today.minus(365)
@ -92,46 +60,7 @@ class ShowHabitPresenter
val scoreLastMonth = scores.getValue(lastMonth).toFloat()
val scoreLastYear = scores.getValue(lastYear).toFloat()
val checkmarks = habit.checkmarks
val valueToday = checkmarks.todayValue / 1e3
val valueThisWeek = checkmarks.getThisWeekValue(preferences.firstWeekday) / 1e3
val valueThisMonth = checkmarks.thisMonthValue / 1e3
val valueThisQuarter = checkmarks.thisQuarterValue / 1e3
val valueThisYear = checkmarks.thisYearValue / 1e3
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
val daysInQuarter = 91
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
val targetToday = habit.getTargetValue() / habit.frequency.denominator
val targetThisWeek = targetToday * 7
val targetThisMonth = targetToday * daysInMonth
val targetThisQuarter = targetToday * daysInQuarter
val targetThisYear = targetToday * daysInYear
val targetCompleted = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targetCompleted.add(valueToday)
if (habit.frequency.denominator <= 7) targetCompleted.add(valueThisWeek)
targetCompleted.add(valueThisMonth)
targetCompleted.add(valueThisQuarter)
targetCompleted.add(valueThisYear)
val targetTotal = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targetTotal.add(targetToday)
if (habit.frequency.denominator <= 7) targetTotal.add(targetThisWeek)
targetTotal.add(targetThisMonth)
targetTotal.add(targetThisQuarter)
targetTotal.add(targetThisYear)
val targetLabels = ArrayList<String>()
if (habit.frequency.denominator <= 1) targetLabels.add(resources.getString(R.string.today))
if (habit.frequency.denominator <= 7) targetLabels.add(resources.getString(R.string.week))
targetLabels.add(resources.getString(R.string.month))
targetLabels.add(resources.getString(R.string.quarter))
targetLabels.add(resources.getString(R.string.year))
data = ShowHabitViewModel(
return ShowHabitViewModel(
title = habit.name,
description = habit.description,
question = habit.question,
@ -144,9 +73,6 @@ class ShowHabitPresenter
targetText = "${habit.targetValue.toShortString()} ${habit.unit}",
frequencyText = habit.frequency.format(),
reminderText = reminderText,
targetCompleted = targetCompleted,
targetTotal = targetTotal,
targetLabels = targetLabels,
)
}
@ -184,8 +110,4 @@ class ShowHabitPresenter
resources.getString(R.string.days),
)
}
interface Listener {
fun onData(data: ShowHabitViewModel)
}
}

@ -23,6 +23,7 @@ import android.view.*
import org.isoron.androidbase.activities.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
@ -35,7 +36,8 @@ class ShowHabitRootView
@ActivityContext context: Context,
private val habit: Habit,
private val presenter: ShowHabitPresenter,
) : BaseRootView(context), ShowHabitPresenter.Listener {
targetCardPresenter: TargetCardPresenter,
) : BaseRootView(context), Presenter.Listener<ShowHabitViewModel> {
private var controller: Controller = object : Controller {}
private var binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
@ -47,7 +49,7 @@ class ShowHabitRootView
binding.overviewCard.presenter = presenter
binding.notesCard.presenter = presenter
binding.subtitleCard.presenter = presenter
binding.targetCard.presenter = presenter
binding.targetCard.presenter = targetCardPresenter
binding.scoreCard.habit = habit
binding.historyCard.habit = habit

@ -34,7 +34,4 @@ data class ShowHabitViewModel(
val targetText: String = "",
val frequencyText: String = "",
val reminderText: String = "",
val targetCompleted: List<Double> = listOf(),
val targetTotal: List<Double> = listOf(),
val targetLabels: List<String> = listOf(),
)

@ -1,30 +1,34 @@
/*
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.databinding.*
class NotesCard(
context: Context,
attrs: AttributeSet
) : LinearLayout(context, attrs), ShowHabitPresenter.Listener {
class NotesCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitViewModel>(context, attrs) {
private val binding = ShowHabitNotesBinding.inflate(LayoutInflater.from(context), this)
lateinit var presenter: ShowHabitPresenter
override fun onAttachedToWindow() {
super.onAttachedToWindow()
presenter.addListener(this)
presenter.requestData(this)
}
override fun onDetachedFromWindow() {
presenter.removeListener(this)
super.onDetachedFromWindow()
}
override fun onData(data: ShowHabitViewModel) {
if (data.description.isEmpty()) {

@ -21,48 +21,16 @@ package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
class OverviewCard : LinearLayout, ShowHabitPresenter.Listener {
class OverviewCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitViewModel>(context, attrs) {
private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this)
lateinit var presenter: ShowHabitPresenter
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}
private fun init() {
if (isInEditMode) initEditMode()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
presenter.addListener(this)
presenter.requestData(this)
}
override fun onDetachedFromWindow() {
presenter.removeListener(this)
super.onDetachedFromWindow()
}
private fun initEditMode() {
onData(ShowHabitViewModel(
scoreToday = 0.6f,
scoreMonthDiff = 0.42f,
scoreYearDiff = 0.75f,
))
}
private fun formatPercentageDiff(percentageDiff: Float): String {
return String.format("%s%.0f%%", if (percentageDiff >= 0) "+" else "\u2212",

@ -21,44 +21,21 @@ package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
class SubtitleCard(
context: Context,
attrs: AttributeSet,
) : LinearLayout(context, attrs), ShowHabitPresenter.Listener {
class SubtitleCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitViewModel>(context, attrs) {
private val binding = ShowHabitSubtitleBinding.inflate(LayoutInflater.from(context), this)
lateinit var presenter: ShowHabitPresenter
init {
val fontAwesome = InterfaceUtils.getFontAwesome(context)
binding.targetIcon.typeface = fontAwesome
binding.frequencyIcon.typeface = fontAwesome
binding.reminderIcon.typeface = fontAwesome
if (isInEditMode) onData(ShowHabitViewModel(
isNumerical = false,
frequencyText = "Every day",
question = "How many steps did you walk today?",
color = PaletteColor(1),
))
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
presenter.addListener(this)
presenter.requestData(this)
}
override fun onDetachedFromWindow() {
presenter.removeListener(this)
super.onDetachedFromWindow()
}
override fun onData(data: ShowHabitViewModel) {

@ -21,36 +21,95 @@ package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import java.util.*
import javax.inject.*
class TargetCard(
context: Context,
attrs: AttributeSet,
) : LinearLayout(context, attrs), ShowHabitPresenter.Listener {
data class TargetCardViewModel(
val color: PaletteColor,
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val labels: List<String> = listOf(),
)
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
lateinit var presenter: ShowHabitPresenter
override fun onAttachedToWindow() {
super.onAttachedToWindow()
presenter.addListener(this)
presenter.requestData(this)
}
class TargetCardView(context: Context, attrs: AttributeSet) : DataView<TargetCardViewModel>(context, attrs) {
override fun onDetachedFromWindow() {
presenter.removeListener(this)
super.onDetachedFromWindow()
}
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
override fun onData(data: ShowHabitViewModel) {
override fun onData(data: TargetCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.targetChart.setValues(data.targetCompleted)
binding.targetChart.setTargets(data.targetTotal)
binding.targetChart.setLabels(data.targetLabels)
binding.targetChart.setValues(data.values)
binding.targetChart.setTargets(data.targets)
binding.targetChart.setLabels(data.labels)
binding.title.setTextColor(androidColor)
binding.targetChart.setColor(androidColor)
}
}
}
@ActivityScope
class TargetCardPresenter
@Inject constructor(
val habit: Habit,
val preferences: Preferences,
@ActivityContext val context: Context,
commandRunner: CommandRunner,
) : Presenter<TargetCardViewModel>(commandRunner) {
val resources = context.resources
override fun refresh(): TargetCardViewModel {
val checkmarks = habit.checkmarks
val valueToday = checkmarks.todayValue / 1e3
val valueThisWeek = checkmarks.getThisWeekValue(preferences.firstWeekday) / 1e3
val valueThisMonth = checkmarks.thisMonthValue / 1e3
val valueThisQuarter = checkmarks.thisQuarterValue / 1e3
val valueThisYear = checkmarks.thisYearValue / 1e3
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
val daysInQuarter = 91
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
val targetToday = habit.getTargetValue() / habit.frequency.denominator
val targetThisWeek = targetToday * 7
val targetThisMonth = targetToday * daysInMonth
val targetThisQuarter = targetToday * daysInQuarter
val targetThisYear = targetToday * daysInYear
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek)
values.add(valueThisMonth)
values.add(valueThisQuarter)
values.add(valueThisYear)
val targets = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targets.add(targetToday)
if (habit.frequency.denominator <= 7) targets.add(targetThisWeek)
targets.add(targetThisMonth)
targets.add(targetThisQuarter)
targets.add(targetThisYear)
val labels = ArrayList<String>()
if (habit.frequency.denominator <= 1) labels.add(resources.getString(R.string.today))
if (habit.frequency.denominator <= 7) labels.add(resources.getString(R.string.week))
labels.add(resources.getString(R.string.month))
labels.add(resources.getString(R.string.quarter))
labels.add(resources.getString(R.string.year))
return TargetCardViewModel(
color = habit.color,
values = values,
labels = labels,
targets = targets,
)
}
}

@ -28,6 +28,7 @@ import android.widget.*;
import androidx.annotation.NonNull;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.intents.*;
@ -49,6 +50,9 @@ public abstract class BaseWidget
@NonNull
private final Context context;
@NonNull
protected final CommandRunner commandRunner;
@NonNull
private WidgetDimensions dimensions;
@ -62,6 +66,7 @@ public abstract class BaseWidget
widgetPrefs = app.getComponent().getWidgetPreferences();
prefs = app.getComponent().getPreferences();
commandRunner = app.getComponent().getCommandRunner();
pendingIntentFactory = app.getComponent().getPendingIntentFactory();
dimensions = new WidgetDimensions(getDefaultWidth(), getDefaultHeight(),
getDefaultWidth(), getDefaultHeight());

@ -26,6 +26,7 @@ import android.view.ViewGroup.LayoutParams.*
import org.isoron.uhabits.activities.common.views.*
import org.isoron.uhabits.activities.habits.show.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.widgets.views.*
class TargetWidget(
@ -38,15 +39,16 @@ class TargetWidget(
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) {
// val widgetView = view as GraphWidgetView
// widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
// if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
// val chart = (widgetView.dataView as TargetChart)
// with(TargetCard.RefreshTask(context, habit, prefs.firstWeekday, chart, null)) {
// onPreExecute()
// doInBackground()
// onPostExecute()
// }
val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
val chart = (widgetView.dataView as TargetChart)
val presenter = TargetCardPresenter(habit, prefs, context, commandRunner)
val data = presenter.refresh()
chart.setColor(data.color.toThemedAndroidColor(context))
chart.setTargets(data.targets)
chart.setLabels(data.labels)
chart.setValues(data.values)
}
override fun buildView(): View {

@ -57,7 +57,7 @@
style="@style/Card"
android:paddingTop="12dp"/>
<org.isoron.uhabits.activities.habits.show.views.TargetCard
<org.isoron.uhabits.activities.habits.show.views.TargetCardView
android:id="@+id/targetCard"
style="@style/Card"
android:paddingTop="12dp"/>

Loading…
Cancel
Save