From b0b93689cd238d2bc101b54995c7e7d46014fbdc Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Fri, 7 Jul 2023 01:38:30 +0530 Subject: [PATCH 1/2] feat: recent transaction fragment to mvvm --- .../injection/module/RepositoryModule.kt | 7 ++ .../RecentTransactionRepository.kt | 14 ++++ .../RecentTransactionRepositoryImp.kt | 15 ++++ .../fragments/RecentTransactionsFragment.kt | 72 ++++++++++++------- .../mobile/utils/RecentTransactionUiState.kt | 11 +++ .../viewModels/RecentTransactionViewModel.kt | 68 ++++++++++++++++++ 6 files changed, 160 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt create mode 100644 app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt create mode 100644 app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt create mode 100644 app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt diff --git a/app/src/main/java/org/mifos/mobile/injection/module/RepositoryModule.kt b/app/src/main/java/org/mifos/mobile/injection/module/RepositoryModule.kt index 2063047cc..89d331320 100644 --- a/app/src/main/java/org/mifos/mobile/injection/module/RepositoryModule.kt +++ b/app/src/main/java/org/mifos/mobile/injection/module/RepositoryModule.kt @@ -5,6 +5,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.mifos.mobile.api.DataManager +import org.mifos.mobile.repositories.RecentTransactionRepository +import org.mifos.mobile.repositories.RecentTransactionRepositoryImp import org.mifos.mobile.repositories.UserAuthRepository import org.mifos.mobile.repositories.UserAuthRepositoryImp @@ -16,4 +18,9 @@ class RepositoryModule { fun providesUserAuthRepository(dataManager: DataManager): UserAuthRepository { return UserAuthRepositoryImp(dataManager) } + + @Provides + fun providesRecentTransactionRepository(dataManager: DataManager): RecentTransactionRepository { + return RecentTransactionRepositoryImp(dataManager) + } } \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt new file mode 100644 index 000000000..ea184ec98 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepository.kt @@ -0,0 +1,14 @@ +package org.mifos.mobile.repositories + + +import io.reactivex.Observable +import org.mifos.mobile.models.Page +import org.mifos.mobile.models.Transaction + +interface RecentTransactionRepository { + + fun recentTransactions( + offset: Int?, + limit: Int? + ): Observable?>? +} \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt new file mode 100644 index 000000000..da822044a --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImp.kt @@ -0,0 +1,15 @@ +package org.mifos.mobile.repositories + +import io.reactivex.Observable +import org.mifos.mobile.api.DataManager +import org.mifos.mobile.models.Page +import org.mifos.mobile.models.Transaction +import javax.inject.Inject + +class RecentTransactionRepositoryImp @Inject constructor(private val dataManager: DataManager) : + RecentTransactionRepository { + + override fun recentTransactions(offset: Int?, limit: Int?): Observable?>? { + return limit?.let { offset?.let { it1 -> dataManager.getRecentTransactions(it1, it) } } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt index 33fb7d214..fa64eacc9 100644 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener @@ -14,16 +15,12 @@ import dagger.hilt.android.AndroidEntryPoint import org.mifos.mobile.R import org.mifos.mobile.databinding.FragmentRecentTransactionsBinding import org.mifos.mobile.models.Transaction -import org.mifos.mobile.presenters.RecentTransactionsPresenter import org.mifos.mobile.ui.activities.base.BaseActivity import org.mifos.mobile.ui.adapters.RecentTransactionListAdapter import org.mifos.mobile.ui.fragments.base.BaseFragment -import org.mifos.mobile.ui.views.RecentTransactionsView -import org.mifos.mobile.utils.Constants -import org.mifos.mobile.utils.DividerItemDecoration -import org.mifos.mobile.utils.EndlessRecyclerViewScrollListener +import org.mifos.mobile.utils.* import org.mifos.mobile.utils.Network.isConnected -import org.mifos.mobile.utils.Toaster +import org.mifos.mobile.viewModels.RecentTransactionViewModel import javax.inject.Inject /** @@ -31,18 +28,17 @@ import javax.inject.Inject * @since 09/08/16 */ @AndroidEntryPoint -class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRefreshListener { +class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { private var _binding: FragmentRecentTransactionsBinding? = null private val binding get() = _binding!! - @JvmField - @Inject - var recentTransactionsPresenter: RecentTransactionsPresenter? = null - @JvmField @Inject var recentTransactionsListAdapter: RecentTransactionListAdapter? = null + + private lateinit var recentTransactionViewModel: RecentTransactionViewModel + private var sweetUIErrorHandler: SweetUIErrorHandler? = null private var recentTransactionList: MutableList? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -56,18 +52,41 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef savedInstanceState: Bundle?, ): View { _binding = FragmentRecentTransactionsBinding.inflate(inflater, container, false) - recentTransactionsPresenter?.attachView(this) + recentTransactionViewModel = ViewModelProvider(this)[RecentTransactionViewModel::class.java] sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) showUserInterface() setToolbarTitle(getString(R.string.recent_transactions)) if (savedInstanceState == null) { - recentTransactionsPresenter?.loadRecentTransactions(false, 0) + recentTransactionViewModel.loadRecentTransactions(false, 0) } return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + recentTransactionViewModel.recentTransactionUiState.observe(viewLifecycleOwner) { + when (it) { + is RecentTransactionUiState.Loading -> showProgress() + is RecentTransactionUiState.RecentTransactions -> { + hideProgress() + showRecentTransactions(it.transactions) + } + is RecentTransactionUiState.Error -> { + hideProgress() + showMessage(getString(it.message)) + } + is RecentTransactionUiState.EmptyTransaction -> { + hideProgress() + showEmptyTransaction() + } + is RecentTransactionUiState.LoadMoreRecentTransactions -> { + hideProgress() + showLoadMoreRecentTransactions(it.transactions) + } + } + } + binding.layoutError.btnTryAgain.setOnClickListener { retryClicked() } @@ -95,7 +114,7 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef /** * Setting up `rvRecentTransactions` */ - override fun showUserInterface() { + fun showUserInterface() { val layoutManager = LinearLayoutManager(activity) layoutManager.orientation = LinearLayoutManager.VERTICAL binding.rvRecentTransactions.layoutManager = layoutManager @@ -111,7 +130,7 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef binding.rvRecentTransactions.addOnScrollListener( object : EndlessRecyclerViewScrollListener(layoutManager) { override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { - recentTransactionsPresenter?.loadRecentTransactions(true, totalItemsCount) + recentTransactionViewModel.loadRecentTransactions(true, totalItemsCount) } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { @@ -138,13 +157,13 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef if (binding.layoutError.root.visibility == View.VISIBLE) { resetUI() } - recentTransactionsPresenter?.loadRecentTransactions(false, 0) + recentTransactionViewModel.loadRecentTransactions(false, 0) } /** * Shows a Toast */ - override fun showMessage(message: String?) { + fun showMessage(message: String?) { (activity as BaseActivity?)?.showToast(message!!) } @@ -154,7 +173,7 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef * * @param recentTransactionList List of [Transaction] */ - override fun showRecentTransactions(recentTransactionList: List?) { + fun showRecentTransactions(recentTransactionList: List?) { this.recentTransactionList = recentTransactionList as MutableList? recentTransactionsListAdapter?.setTransactions(recentTransactionList) } @@ -164,12 +183,12 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef * * @param transactions List of [Transaction] */ - override fun showLoadMoreRecentTransactions(transactions: List?) { + fun showLoadMoreRecentTransactions(transactions: List?) { this.recentTransactionList?.addAll(recentTransactionList!!) recentTransactionsListAdapter?.notifyDataSetChanged() } - override fun resetUI() { + fun resetUI() { sweetUIErrorHandler?.hideSweetErrorLayoutUI( binding.rvRecentTransactions, binding.layoutError.root, @@ -179,7 +198,7 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef /** * Hides `rvRecentTransactions` and shows a textview prompting no transactions */ - override fun showEmptyTransaction() { + fun showEmptyTransaction() { sweetUIErrorHandler?.showSweetEmptyUI( getString(R.string.recent_transactions), R.drawable.ic_error_black_24dp, @@ -193,7 +212,7 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef * * @param message Error message that tells the user about the problem. */ - override fun showErrorFetchingRecentTransactions(message: String?) { + fun showErrorFetchingRecentTransactions(message: String?) { if (!isConnected(requireActivity())) { sweetUIErrorHandler?.showSweetNoInternetUI( binding.rvRecentTransactions, @@ -214,7 +233,7 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef binding.rvRecentTransactions, binding.layoutError.root, ) - recentTransactionsPresenter?.loadRecentTransactions(false, 0) + recentTransactionViewModel.loadRecentTransactions(false, 0) } else { Toast.makeText( context, @@ -224,15 +243,15 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef } } - override fun showProgress() { + fun showProgress() { showSwipeRefreshLayout(true) } - override fun hideProgress() { + fun hideProgress() { showSwipeRefreshLayout(false) } - override fun showSwipeRefreshLayout(show: Boolean) { + fun showSwipeRefreshLayout(show: Boolean) { binding.swipeTransactionContainer.post { binding.swipeTransactionContainer.isRefreshing = show } @@ -240,7 +259,6 @@ class RecentTransactionsFragment : BaseFragment(), RecentTransactionsView, OnRef override fun onDestroyView() { super.onDestroyView() - recentTransactionsPresenter?.detachView() _binding = null } diff --git a/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt b/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt new file mode 100644 index 000000000..2838e9512 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt @@ -0,0 +1,11 @@ +package org.mifos.mobile.utils + +import org.mifos.mobile.models.Transaction + +sealed class RecentTransactionUiState { + object Loading : RecentTransactionUiState() + object EmptyTransaction : RecentTransactionUiState() + data class Error(val message: Int) : RecentTransactionUiState() + data class RecentTransactions(val transactions: List) : RecentTransactionUiState() + data class LoadMoreRecentTransactions(val transactions: List) : RecentTransactionUiState() +} diff --git a/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt b/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt new file mode 100644 index 000000000..b4b2e1868 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt @@ -0,0 +1,68 @@ +package org.mifos.mobile.viewModels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.observers.DisposableObserver +import io.reactivex.schedulers.Schedulers +import org.mifos.mobile.R +import org.mifos.mobile.models.Page +import org.mifos.mobile.models.Transaction +import org.mifos.mobile.repositories.RecentTransactionRepository +import org.mifos.mobile.utils.RecentTransactionUiState +import javax.inject.Inject + +@HiltViewModel +class RecentTransactionViewModel @Inject constructor(private val recentTransactionRepositoryImp: RecentTransactionRepository) : + ViewModel() { + + private val compositeDisposables: CompositeDisposable = CompositeDisposable() + private val limit = 50 + private var loadmore = false + + private val _recentTransactionUiState = MutableLiveData() + val recentTransactionUiState: LiveData = _recentTransactionUiState + + fun loadRecentTransactions(loadmore: Boolean, offset: Int) { + this.loadmore = loadmore + loadRecentTransactions(offset, limit) + } + + private fun loadRecentTransactions(offset: Int, limit: Int) { + _recentTransactionUiState.value = RecentTransactionUiState.Loading + recentTransactionRepositoryImp.recentTransactions(offset, limit) + ?.observeOn( AndroidSchedulers.mainThread()) + ?.subscribeOn(Schedulers.io()) + ?.subscribeWith(object : DisposableObserver?>() { + override fun onNext(transactions: Page) { + if (transactions.totalFilteredRecords == 0) { + _recentTransactionUiState.value = RecentTransactionUiState.EmptyTransaction + } else if (loadmore && transactions.pageItems.isNotEmpty()) { + _recentTransactionUiState.value = RecentTransactionUiState.LoadMoreRecentTransactions(transactions.pageItems) + } else if (transactions.pageItems.isNotEmpty()) { + _recentTransactionUiState.value = RecentTransactionUiState.RecentTransactions(transactions.pageItems) + } + } + + override fun onError(e: Throwable) { + _recentTransactionUiState.value = RecentTransactionUiState.Error(R.string.recent_transactions) + } + + override fun onComplete() {} + }).let { + if (it != null) { + compositeDisposables.add( + it, + ) + } + } + } + + override fun onCleared() { + super.onCleared() + compositeDisposables.clear() + } +} \ No newline at end of file From e3ed04d9237bfc815d449cf90a59a41c35415526 Mon Sep 17 00:00:00 2001 From: Pratyush Singh Date: Mon, 10 Jul 2023 22:26:47 +0530 Subject: [PATCH 2/2] feat: Unit tests for viewmodel and repository --- .../RecentTransactionRepositoryImpTest.kt | 61 +++++++ .../RecentTransactionViewModelTest.kt | 163 ++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 app/src/test/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImpTest.kt create mode 100644 app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt diff --git a/app/src/test/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImpTest.kt b/app/src/test/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImpTest.kt new file mode 100644 index 000000000..a90f8075c --- /dev/null +++ b/app/src/test/java/org/mifos/mobile/repositories/RecentTransactionRepositoryImpTest.kt @@ -0,0 +1,61 @@ +package org.mifos.mobile.repositories + +import io.reactivex.Observable +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mifos.mobile.api.DataManager +import org.mifos.mobile.models.Page +import org.mifos.mobile.models.Transaction +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + + +@RunWith(MockitoJUnitRunner::class) +class RecentTransactionRepositoryImpTest { + + @Mock + lateinit var dataManager: DataManager + + private lateinit var recentTransactionRepositoryImp: RecentTransactionRepositoryImp + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + recentTransactionRepositoryImp = RecentTransactionRepositoryImp(dataManager) + } + + @Test + fun recentTransaction_successful_response_from_dataManger() { + val success: Observable?> = + Observable.just(Mockito.mock(Page()::class.java)) + val offset = 0 + val limit = 50 + + Mockito.`when`(dataManager.getRecentTransactions(offset, limit)).thenReturn(success) + + val result = recentTransactionRepositoryImp.recentTransactions(offset, limit) + + Mockito.verify(dataManager).getRecentTransactions(offset, limit) + Assert.assertEquals(result, success) + } + + @Test + fun recentTransaction_unsuccessful_response_from_dataManger() { + val error: Observable?> = + Observable.error(Throwable("Recent Transaction Failed")) + val offset = 0 + val limit = 50 + + Mockito.`when`(dataManager.getRecentTransactions(offset, limit)).thenReturn(error) + + val result = recentTransactionRepositoryImp.recentTransactions(offset, limit) + + Mockito.verify(dataManager).getRecentTransactions(offset, limit) + Assert.assertEquals(result, error) + } + +} \ No newline at end of file diff --git a/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt b/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt new file mode 100644 index 000000000..aab9329ff --- /dev/null +++ b/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt @@ -0,0 +1,163 @@ +package org.mifos.mobile.viewModels + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import io.reactivex.Observable +import org.junit.* +import org.junit.runner.RunWith +import org.mifos.mobile.models.Page +import org.mifos.mobile.models.Transaction +import org.mifos.mobile.models.client.Currency +import org.mifos.mobile.models.client.Type +import org.mifos.mobile.repositories.RecentTransactionRepository +import org.mifos.mobile.util.RxSchedulersOverrideRule +import org.mifos.mobile.utils.RecentTransactionUiState +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner +import org.mifos.mobile.R + +@RunWith(MockitoJUnitRunner::class) +class RecentTransactionViewModelTest { + + @JvmField + @Rule + val mOverrideSchedulersRule = RxSchedulersOverrideRule() + + @get:Rule + val rule = InstantTaskExecutorRule() + + @Mock + lateinit var recentTransactionRepositoryImp: RecentTransactionRepository + + @Mock + lateinit var recentTransactionUiStateObserver: Observer + + @Mock + lateinit var type: Type + + @Mock + lateinit var currency: Currency + + lateinit var viewModel: RecentTransactionViewModel + + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + viewModel = RecentTransactionViewModel(recentTransactionRepositoryImp) + viewModel.recentTransactionUiState.observeForever(recentTransactionUiStateObserver) + } + + @Test + fun loadRecentTransaction_success_with_no_empty_transactions() { + val offset = 0 + val limit = 50 + + val transaction = Transaction( + id = 1L, + officeId = 2L, + officeName = "Office", + type = type, + date = listOf(2023, 7, 8), + currency = currency, + amount = 10.0, + submittedOnDate = listOf(2023, 7, 9), + reversed = false + ) + val transactions: Page = + Page(totalFilteredRecords = 1, pageItems = listOf(transaction)) + `when`(recentTransactionRepositoryImp.recentTransactions(offset, limit)) + .thenReturn(Observable.just(transactions)) + + viewModel.loadRecentTransactions(loadmore = false, offset) + + verify(recentTransactionUiStateObserver).onChanged(RecentTransactionUiState.Loading) + Assert.assertEquals( + transactions.pageItems.let { RecentTransactionUiState.RecentTransactions(it) }, + viewModel.recentTransactionUiState.value + ) + } + + @Test + fun loadRecentTransaction_success_with_empty_transactions() { + val offset = 0 + val limit = 50 + + val transaction = Transaction( + id = 1L, + officeId = 2L, + officeName = "Office", + type = type, + date = listOf(2023, 7, 8), + currency = currency, + amount = 10.0, + submittedOnDate = listOf(2023, 7, 9), + reversed = false + ) + val transactions: Page = + Page(totalFilteredRecords = 0, pageItems = listOf(transaction)) + `when`(recentTransactionRepositoryImp.recentTransactions(offset, limit)) + .thenReturn(Observable.just(transactions)) + + viewModel.loadRecentTransactions(loadmore = false, offset) + + verify(recentTransactionUiStateObserver).onChanged(RecentTransactionUiState.Loading) + Assert.assertEquals( + RecentTransactionUiState.EmptyTransaction, + viewModel.recentTransactionUiState.value + ) + } + + @Test + fun loadRecentTransaction_success_with_load_more_transactions() { + val offset = 0 + val limit = 50 + + val transaction = Transaction( + id = 1L, + officeId = 2L, + officeName = "Office", + type = type, + date = listOf(2023, 7, 8), + currency = currency, + amount = 10.0, + submittedOnDate = listOf(2023, 7, 9), + reversed = false + ) + val transactions: Page = + Page(totalFilteredRecords = 1, pageItems = listOf(transaction)) + `when`(recentTransactionRepositoryImp.recentTransactions(offset, limit)) + .thenReturn(Observable.just(transactions)) + + viewModel.loadRecentTransactions(loadmore = true, offset) + + verify(recentTransactionUiStateObserver).onChanged(RecentTransactionUiState.Loading) + Assert.assertEquals( + transactions.pageItems.let { RecentTransactionUiState.LoadMoreRecentTransactions(it) }, + viewModel.recentTransactionUiState.value + ) + + } + + @Test + fun loadRecentTransaction_unsuccessful() { + val error = Throwable("Recent Transaction error") + `when`(recentTransactionRepositoryImp.recentTransactions(anyInt(), anyInt())).thenReturn( + Observable.error(error) + ) + viewModel.loadRecentTransactions(false, 0) + + Assert.assertEquals( + RecentTransactionUiState.Error(R.string.recent_transactions), + viewModel.recentTransactionUiState.value + ) + } + + @After + fun tearDown() { + viewModel.recentTransactionUiState.removeObserver(recentTransactionUiStateObserver) + } + +} \ No newline at end of file