Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar View for Expenses #14

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -37,6 +37,9 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize|stateHidden"/>

<activity android:name=".expensehistory.presentation.ExpenseHistoryActivity"
android:screenOrientation="portrait"/>

<activity android:name=".settings.presentation.SettingsActivity"
android:screenOrientation="portrait"/>

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.nominalista.expenses.expensehistory.presentation

import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nominalista.expenses.Application
import com.nominalista.expenses.data.model.Expense
import com.nominalista.expenses.data.model.Tag
import com.nominalista.expenses.data.store.DataStore
import com.nominalista.expenses.home.domain.FilterExpensesUseCase
import com.nominalista.expenses.home.domain.SortExpensesUseCase
import com.nominalista.expenses.home.presentation.ExpenseItemModel
import com.nominalista.expenses.util.extensions.plusAssign
import com.nominalista.expenses.util.reactive.DataEvent
import com.nominalista.expenses.util.reactive.Variable
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers.mainThread
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers.computation
import io.reactivex.schedulers.Schedulers.io
import org.threeten.bp.LocalDate

class ExpenseFragmentModel(
application: Application,
private val dataStore: DataStore
) : AndroidViewModel(application) {

val expenseItemModels = Variable(emptyList<ExpenseItemModel>())
val isLoading = Variable(false)
val showExpenseDetail = DataEvent<Expense>()

var expenses = emptyList<Expense>()
var tags = emptyList<Tag>()

private val disposables = CompositeDisposable()

// Lifecycle start

init {
observeExpenses()
updateExpenseItemModels(LocalDate.now())
}


private fun observeExpenses() {
disposables += dataStore.observeExpenses()
.map { SortExpensesUseCase().invoke(it) }
.subscribeOn(io())
.observeOn(mainThread())
.subscribe { expenses = it; updateExpenseItemModels(LocalDate.now()) }
}

private fun updateExpenseItemModels(date: LocalDate) {
disposables += Observable.just(expenses)
.map { FilterExpensesUseCase().invoke(it, date) }
.map { createExpenseSection(it) }
.subscribeOn(computation())
.observeOn(mainThread())
.subscribe { expenseItemModels.value = it }
}

private fun createExpenseSection(expenses: List<Expense>): List<ExpenseItemModel> {
return expenses.map { expense -> createExpenseItemModel(expense) }
}

private fun createExpenseItemModel(expense: Expense): ExpenseItemModel {
val itemModel = ExpenseItemModel(expense)
itemModel.click = { showExpenseDetail.next(expense) }
return itemModel
}

// Lifecycle end

override fun onCleared() {
super.onCleared()
disposables.clear()
}

// Public

fun showExpensesForTheDay(date: LocalDate) {
disposables += Observable.just(expenses)
.map { FilterExpensesUseCase().invoke(it, date) }
.map { createExpenseSection(it) }
.subscribeOn(computation())
.observeOn(mainThread())
.subscribe { expenseItemModels.value = it }
}

@Suppress("UNCHECKED_CAST")
class Factory(private val application: Application) : ViewModelProvider.NewInstanceFactory() {

override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ExpenseFragmentModel(
application,
application.defaultDataStore
) as T
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.nominalista.expenses.expensehistory.presentation

import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.nominalista.expenses.R
import com.nominalista.expenses.common.presentation.BaseActivity

class ExpenseHistoryActivity : BaseActivity() {

override var animationKind = ANIMATION_SLIDE_FROM_RIGHT

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_expense_history)
setSupportActionBar(findViewById(R.id.toolbar))
}

companion object {

fun start(context: Context) {
val intent = Intent(context, ExpenseHistoryActivity::class.java)
context.startActivity(intent)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.nominalista.expenses.expensehistory.presentation

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.nominalista.expenses.R
import com.nominalista.expenses.home.presentation.ExpenseItemHolder
import com.nominalista.expenses.home.presentation.ExpenseItemModel
import com.nominalista.expenses.home.presentation.HomeItemHolder
import com.nominalista.expenses.home.presentation.HomeItemModel

class ExpenseHistoryAdapter : ListAdapter<HomeItemModel, HomeItemHolder>(DiffCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpenseItemHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(viewType, parent, false)
return when (viewType) {
EXPENSE_ITEM_TYPE -> ExpenseItemHolder(itemView)
else -> throw IllegalArgumentException()
}
}

override fun onBindViewHolder(holder: HomeItemHolder, position: Int) {
val itemModel = getItem(position)
when {
(holder is ExpenseItemHolder && itemModel is ExpenseItemModel) -> holder.bind(itemModel)
}
}

override fun onViewRecycled(holder: HomeItemHolder) {
super.onViewRecycled(holder)
when (holder) {
is ExpenseItemHolder -> holder.recycle()
}
}

override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is ExpenseItemModel -> EXPENSE_ITEM_TYPE
else -> super.getItemViewType(position)
}
}

private class DiffCallback : DiffUtil.ItemCallback<HomeItemModel>() {

override fun areItemsTheSame(
oldItem: HomeItemModel,
newItem: HomeItemModel
): Boolean {
return when {
(oldItem is ExpenseItemModel && newItem is ExpenseItemModel) ->
oldItem.expense.id == newItem.expense.id
else -> false
}
}

override fun areContentsTheSame(
oldItem: HomeItemModel,
newItem: HomeItemModel
): Boolean {
return when {
(oldItem is ExpenseItemModel && newItem is ExpenseItemModel) ->
oldItem.expense == newItem.expense
else -> false
}
}
}

companion object {

private const val EXPENSE_ITEM_TYPE = R.layout.item_expense
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.nominalista.expenses.expensehistory.presentation

import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.CalendarView
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.nominalista.expenses.R
import com.nominalista.expenses.data.model.Expense
import com.nominalista.expenses.expensedetail.presentation.ExpenseDetailActivity
import com.nominalista.expenses.util.extensions.application
import com.nominalista.expenses.util.extensions.plusAssign
import io.reactivex.disposables.CompositeDisposable
import org.threeten.bp.LocalDate

class ExpenseHistoryFragment : Fragment(), CalendarView.OnDateChangeListener {

private val compositeDisposable = CompositeDisposable()
private lateinit var expenseRecyclerView: RecyclerView
private lateinit var historyCalenderView: CalendarView
private lateinit var progressBar: ProgressBar

private lateinit var adapter: ExpenseHistoryAdapter
private lateinit var layoutManager: LinearLayoutManager
private lateinit var model: ExpenseFragmentModel

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_expense_history, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindWidgets(view)
setupActionBar()
setupCalendarView()
setupRecyclerView()
setupViewModel()
bindModel()
}

private fun bindWidgets(view: View) {
expenseRecyclerView = view.findViewById(R.id.rvExpenseHistory)
historyCalenderView = view.findViewById(R.id.cvExpenseHistory)
progressBar = view.findViewById(R.id.progress_bar)
}

private fun setupViewModel() {
val factory = ExpenseFragmentModel.Factory(requireContext().application)
model = ViewModelProviders.of(this, factory).get(ExpenseFragmentModel::class.java)
}

private fun setupCalendarView() {
historyCalenderView.setOnDateChangeListener(this)
}

private fun setupRecyclerView() {
adapter = ExpenseHistoryAdapter()
layoutManager = LinearLayoutManager(context)
expenseRecyclerView.adapter = adapter
expenseRecyclerView.layoutManager = layoutManager
}

private fun bindModel() {
compositeDisposable += model.expenseItemModels
.subscribe { adapter.submitList(it) }
compositeDisposable += model.isLoading
.subscribe { configureProgressBar(it) }
compositeDisposable += model.showExpenseDetail
.subscribe { showExpenseDetail(it) }
}

private fun setupActionBar() {
val actionBar = (requireActivity() as AppCompatActivity).supportActionBar ?: return
actionBar.setTitle(R.string.expense_history_title)
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setHomeAsUpIndicator(R.drawable.ic_arrow_back_24dp)
setHasOptionsMenu(true)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> backSelected()
else -> super.onOptionsItemSelected(item)
}
}

private fun backSelected(): Boolean {
requireActivity().onBackPressed()
return true
}

override fun onDestroyView() {
super.onDestroyView()
unbindFromModel()
}

private fun unbindFromModel() {
compositeDisposable.clear()
}

private fun configureProgressBar(isVisible: Boolean) {
progressBar.isVisible = isVisible
}

private fun showExpenseDetail(expense: Expense) {
ExpenseDetailActivity.start(requireContext(), expense)
}

override fun onSelectedDayChange(view: CalendarView, year: Int, month: Int, dayOfMonth: Int) {
Toast.makeText(activity, "Selected date is $dayOfMonth", Toast.LENGTH_SHORT).show()
model.showExpensesForTheDay(LocalDate.of(year, month + 1, dayOfMonth))
}
}
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package com.nominalista.expenses.home.domain
import com.nominalista.expenses.data.model.Expense
import com.nominalista.expenses.home.presentation.DateRange
import com.nominalista.expenses.home.presentation.TagFilter
import org.threeten.bp.LocalDate

class FilterExpensesUseCase {

@@ -16,4 +17,13 @@ class FilterExpensesUseCase {
tagFilter?.let { expense.tags.containsAll(it.tags) } ?: true
}
}

operator fun invoke(
expenses: List<Expense>,
date: LocalDate
): List<Expense> {
return expenses.filter { expense ->
expense.date == date
}
}
}
Loading