Refactored TransactionsFragment, by moving all the transactions filtering logic to the corresponding ViewModel and taking advantage of Kotlin coroutines to excecute the filtering process in the background.

This commit is contained in:
Severiano Jaramillo 2019-04-26 16:05:58 -05:00
parent 0144938052
commit 1eb9b64b45
2 changed files with 133 additions and 100 deletions

View file

@ -28,9 +28,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_transactions.* import kotlinx.android.synthetic.main.fragment_transactions.*
import java.io.File import java.io.File
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
/** /**
* Shows the list of transactions as well as options to filter and export those transactions * Shows the list of transactions as well as options to filter and export those transactions
@ -44,15 +42,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100 private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100
} }
private lateinit var mTransactionsViewModel: TransactionsViewModel private lateinit var mViewModel: TransactionsViewModel
private lateinit var transfersDetailsAdapter: TransfersDetailsAdapter
private val transfersDetails = ArrayList<TransferDetail>()
private val filteredTransfersDetails = ArrayList<TransferDetail>()
/** Variables used to filter the transaction items */
private var mFilterOptions = FilterOptions()
private var mDisposables = CompositeDisposable() private var mDisposables = CompositeDisposable()
@ -70,36 +60,29 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
val userId = PreferenceManager.getDefaultSharedPreferences(context) val userId = PreferenceManager.getDefaultSharedPreferences(context)
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: "" .getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
transfersDetailsAdapter = TransfersDetailsAdapter(context!!) val transfersDetailsAdapter = TransfersDetailsAdapter(context!!)
rvTransactions.adapter = transfersDetailsAdapter rvTransactions.adapter = transfersDetailsAdapter
rvTransactions.layoutManager = LinearLayoutManager(context) rvTransactions.layoutManager = LinearLayoutManager(context)
// Configure TransactionsViewModel to fetch the transaction history // Configure TransactionsViewModel to fetch the transaction history
mTransactionsViewModel = ViewModelProviders.of(this).get(TransactionsViewModel::class.java) mViewModel = ViewModelProviders.of(this).get(TransactionsViewModel::class.java)
mTransactionsViewModel.getAll(userId).observe(this, Observer<List<TransferDetail>> { transfersDetails -> mViewModel.getFilteredTransactions(userId).observe(this,
this.transfersDetails.clear() Observer<List<TransferDetail>> { transactions ->
this.transfersDetails.addAll(transfersDetails) if (transactions.isEmpty()) {
applyFilterOptions(false)
if (transfersDetails.isEmpty()) {
rvTransactions.visibility = View.GONE rvTransactions.visibility = View.GONE
tvEmpty.visibility = View.VISIBLE tvEmpty.visibility = View.VISIBLE
} else { } else {
rvTransactions.visibility = View.VISIBLE rvTransactions.visibility = View.VISIBLE
tvEmpty.visibility = View.GONE tvEmpty.visibility = View.GONE
transfersDetailsAdapter.replaceAll(transactions)
} }
}) })
// Set custom touch listener to handle bounce/stretch effect // Set custom touch listener to handle bounce/stretch effect
val bounceTouchListener = BounceTouchListener(rvTransactions) val bounceTouchListener = BounceTouchListener(rvTransactions)
rvTransactions.setOnTouchListener(bounceTouchListener) rvTransactions.setOnTouchListener(bounceTouchListener)
// Initialize filter options
val calendar = Calendar.getInstance()
mFilterOptions.endDate = calendar.timeInMillis
calendar.add(Calendar.MONTH, -2)
mFilterOptions.startDate = calendar.timeInMillis
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -115,8 +98,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
.map { it.queryText.toString().toLowerCase() } .map { it.queryText.toString().toLowerCase() }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
mFilterOptions.query = it mViewModel.setFilterQuery(it)
applyFilterOptions()
} }
) )
@ -129,7 +111,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
R.id.menu_filter -> { R.id.menu_filter -> {
val filterOptionsDialog = FilterOptionsDialog() val filterOptionsDialog = FilterOptionsDialog()
val args = Bundle() val args = Bundle()
args.putParcelable(FilterOptionsDialog.KEY_FILTER_OPTIONS, mFilterOptions) args.putParcelable(FilterOptionsDialog.KEY_FILTER_OPTIONS, mViewModel.getFilterOptions())
filterOptionsDialog.arguments = args filterOptionsDialog.arguments = args
filterOptionsDialog.show(childFragmentManager, "filter-options-tag") filterOptionsDialog.show(childFragmentManager, "filter-options-tag")
true true
@ -154,68 +136,11 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
return size.x return size.x
} }
/**
* Filters the TransferDetail list given the user selected filter options.
* TODO move this to a background thread
*/
private fun applyFilterOptions(scrollToTop: Boolean = true) {
// Clean the filtered list
filteredTransfersDetails.clear()
// Make sure the filter dates use the same format as the transactions' dates
val startDate = mFilterOptions.startDate / 1000
val endDate = mFilterOptions.endDate / 1000
for (transferDetail in transfersDetails) {
// Filter by transfer direction
if (transferDetail.direction) { // Transfer sent
if (mFilterOptions.transactionsDirection == 1)
// Looking for received transfers only
continue
} else { // Transfer received
if (mFilterOptions.transactionsDirection == 2)
// Looking for sent transactions only
continue
}
// Filter by date range
if (!mFilterOptions.dateRangeAll && (transferDetail.date < startDate ||
transferDetail.date > endDate))
continue
// Filter by asset
if (!mFilterOptions.assetAll && transferDetail.assetSymbol != mFilterOptions.asset)
continue
// Filter by equivalent value
if (!mFilterOptions.equivalentValueAll && ((transferDetail.fiatAmount ?: -1 ) < mFilterOptions.fromEquivalentValue
|| (transferDetail.fiatAmount ?: -1) > mFilterOptions.toEquivalentValue))
continue
// Filter transactions sent to agorise
if (mFilterOptions.agoriseFees && transferDetail.to.equals("agorise"))
continue
// Filter by search query
val text = (transferDetail.from ?: "").toLowerCase() + (transferDetail.to ?: "").toLowerCase()
if (text.contains(mFilterOptions.query, ignoreCase = true)) {
filteredTransfersDetails.add(transferDetail)
}
}
// Replaces the list of TransferDetail items with the new filtered list
transfersDetailsAdapter.replaceAll(filteredTransfersDetails)
if (scrollToTop)
rvTransactions.scrollToPosition(0)
}
/** /**
* Gets called when the user selects some filter options in the [FilterOptionsDialog] and wants to apply them. * Gets called when the user selects some filter options in the [FilterOptionsDialog] and wants to apply them.
*/ */
override fun onFilterOptionsSelected(filterOptions: FilterOptions) { override fun onFilterOptionsSelected(filterOptions: FilterOptions) {
mFilterOptions = filterOptions mViewModel.applyFilterOptions(filterOptions)
applyFilterOptions(true)
} }
/** Verifies that the storage permission has been granted before attempting to generate the export options */ /** Verifies that the storage permission has been granted before attempting to generate the export options */
@ -267,12 +192,13 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
return return
} }
mViewModel.getFilteredTransactionsOnce()?.let { filteredTransactions ->
if (exportPDF) if (exportPDF)
activity?.let { PDFGeneratorTask(it).execute(filteredTransfersDetails) } activity?.let { PDFGeneratorTask(it).execute(filteredTransactions) }
if (exportCSV) if (exportCSV)
activity?.let { CSVGenerationTask(it).execute(filteredTransfersDetails) } activity?.let { CSVGenerationTask(it).execute(filteredTransactions) }
}
} }
override fun onDestroy() { override fun onDestroy() {

View file

@ -1,15 +1,122 @@
package cy.agorise.bitsybitshareswallet.viewmodels package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.*
import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.models.FilterOptions
import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
class TransactionsViewModel(application: Application) : AndroidViewModel(application) { class TransactionsViewModel(application: Application) : AndroidViewModel(application) {
private var mRepository = TransferDetailRepository(application) private var mRepository = TransferDetailRepository(application)
internal fun getAll(userId: String): LiveData<List<TransferDetail>> { /**
return mRepository.getAll(userId) * [FilterOptions] used to filter the list of [TransferDetail] taken from the database
*/
private var mFilterOptions = FilterOptions()
private lateinit var transactions : LiveData<List<TransferDetail>>
/**
* This [MediatorLiveData] is used to combine two sources of information into one, keeping the
* client of this [ViewModel] receiving only one stream of data (a list of filtered [TransferDetail])
*/
private val filteredTransactions = MediatorLiveData<List<TransferDetail>>()
init {
// Initialize the start and end dates for the FilterOptions
val calendar = Calendar.getInstance()
mFilterOptions.endDate = calendar.timeInMillis
calendar.add(Calendar.MONTH, -2)
mFilterOptions.startDate = calendar.timeInMillis
}
internal fun getFilteredTransactions(userId: String): LiveData<List<TransferDetail>> {
transactions = mRepository.getAll(userId)
filteredTransactions.addSource(transactions) { transactions ->
viewModelScope.launch {
filteredTransactions.value = filter(transactions, mFilterOptions)
}
}
return filteredTransactions
}
internal fun getFilterOptions(): FilterOptions {
return mFilterOptions
}
internal fun applyFilterOptions(filterOptions: FilterOptions) = transactions.value?.let { transactions ->
viewModelScope.launch {
filteredTransactions.value = filter(transactions, filterOptions)
}
}.also { mFilterOptions = filterOptions }
internal fun setFilterQuery(query: String) = transactions.value?.let { transactions ->
mFilterOptions.query = query
viewModelScope.launch {
filteredTransactions.value = filter(transactions, mFilterOptions)
}
}
internal fun getFilteredTransactionsOnce() = filteredTransactions.value
/**
* Filters the given list of [TransferDetail] given the [FilterOptions] and returns a filtered list
* of [TransferDetail], doing all the work in a background thread using kotlin coroutines
*/
private suspend fun filter(transactions: List<TransferDetail>, filterOptions: FilterOptions) : List<TransferDetail> {
return withContext(Dispatchers.Default) {
// Create a list to store the filtered transactions
val filteredTransactions = ArrayList<TransferDetail>()
// Make sure the filter dates use the same format as the transactions' dates
val startDate = filterOptions.startDate / 1000
val endDate = filterOptions.endDate / 1000
for (transaction in transactions) {
// Filter by transfer direction
if (transaction.direction) { // Transfer sent
if (filterOptions.transactionsDirection == 1)
// Looking for received transfers only
continue
} else { // Transfer received
if (filterOptions.transactionsDirection == 2)
// Looking for sent transactions only
continue
}
// Filter by date range
if (!filterOptions.dateRangeAll && (transaction.date < startDate ||
transaction.date > endDate))
continue
// Filter by asset
if (!filterOptions.assetAll && transaction.assetSymbol != filterOptions.asset)
continue
// Filter by equivalent value
if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount ?: -1 ) < filterOptions.fromEquivalentValue
|| (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue))
continue
// Filter transactions sent to agorise
if (filterOptions.agoriseFees && transaction.to.equals("agorise"))
continue
// Filter by search query
val text = (transaction.from ?: "").toLowerCase() + (transaction.to ?: "").toLowerCase()
if (text.contains(filterOptions.query, ignoreCase = true)) {
filteredTransactions.add(transaction)
}
}
filteredTransactions
}
} }
} }