From 24a6a1f4c5b368ee8af9c173b48ea3cde4cb54bd Mon Sep 17 00:00:00 2001 From: akashmeruva9 Date: Fri, 7 Jun 2024 10:20:50 +0530 Subject: [PATCH 1/8] Refactor Recent Transaction from XML to Compose --- .../mobile/ui/activities/HomeActivity.kt | 1 + .../fragments/RecentTransactionsFragment.kt | 287 ------------------ .../RecentTransactionScreen.kt | 197 ++++++++++++ .../RecentTransactionViewModel.kt | 2 +- .../RecentTransactionsFragment.kt | 54 ++++ 5 files changed, 253 insertions(+), 288 deletions(-) delete mode 100644 app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt create mode 100644 app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt rename app/src/main/java/org/mifos/mobile/{viewModels => ui/recent_transactions}/RecentTransactionViewModel.kt (97%) create mode 100644 app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt index 8c0113d89..847030a72 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt @@ -39,6 +39,7 @@ import org.mifos.mobile.ui.getThemeAttributeColor import org.mifos.mobile.ui.help.HelpActivity import org.mifos.mobile.ui.home.HomeOldFragment import org.mifos.mobile.ui.login.LoginActivity +import org.mifos.mobile.ui.recent_transactions.RecentTransactionsFragment import org.mifos.mobile.utils.Constants import org.mifos.mobile.utils.TextDrawable import org.mifos.mobile.utils.Toaster 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 deleted file mode 100644 index dae06d6cb..000000000 --- a/app/src/main/java/org/mifos/mobile/ui/fragments/RecentTransactionsFragment.kt +++ /dev/null @@ -1,287 +0,0 @@ -package org.mifos.mobile.ui.fragments - -import android.os.Bundle -import android.os.Parcelable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener -import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import org.mifos.mobile.R -import org.mifos.mobile.databinding.FragmentRecentTransactionsBinding -import org.mifos.mobile.models.Transaction -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.utils.* -import org.mifos.mobile.utils.Network.isConnected -import org.mifos.mobile.utils.ParcelableAndSerializableUtils.getCheckedArrayListFromParcelable -import org.mifos.mobile.viewModels.RecentTransactionViewModel -import javax.inject.Inject - -/** - * @author Vishwwajeet - * @since 09/08/16 - */ -@AndroidEntryPoint -class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { - - private var _binding: FragmentRecentTransactionsBinding? = null - private val binding get() = _binding!! - - @JvmField - @Inject - var recentTransactionsListAdapter: RecentTransactionListAdapter? = null - - private val recentTransactionViewModel: RecentTransactionViewModel by viewModels() - - private var sweetUIErrorHandler: SweetUIErrorHandler? = null - private var recentTransactionList: MutableList? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - recentTransactionList = ArrayList() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - _binding = FragmentRecentTransactionsBinding.inflate(inflater, container, false) - sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) - showUserInterface() - setToolbarTitle(getString(R.string.recent_transactions)) - if (savedInstanceState == null) { - recentTransactionViewModel.loadRecentTransactions(false, 0) - } - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - recentTransactionViewModel.recentTransactionUiState.collect{ - 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) - } - - RecentTransactionUiState.Initial -> {} - } - } - } - } - - binding.layoutError.btnTryAgain.setOnClickListener { - retryClicked() - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelableArrayList( - Constants.RECENT_TRANSACTIONS, - ArrayList( - recentTransactionList, - ), - ) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - if (savedInstanceState != null) { - val transactions: List = - savedInstanceState.getCheckedArrayListFromParcelable( - Transaction::class.java, - Constants.RECENT_TRANSACTIONS, - ) ?: listOf() - showRecentTransactions(transactions) - } - } - - /** - * Setting up `rvRecentTransactions` - */ - fun showUserInterface() { - val layoutManager = LinearLayoutManager(activity) - layoutManager.orientation = LinearLayoutManager.VERTICAL - binding.rvRecentTransactions.layoutManager = layoutManager - binding.rvRecentTransactions.setHasFixedSize(true) - binding.rvRecentTransactions.addItemDecoration( - DividerItemDecoration( - requireActivity(), - layoutManager.orientation, - ), - ) - recentTransactionsListAdapter?.setTransactions(recentTransactionList) - binding.rvRecentTransactions.adapter = recentTransactionsListAdapter - binding.rvRecentTransactions.addOnScrollListener( - object : EndlessRecyclerViewScrollListener(layoutManager) { - override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { - recentTransactionViewModel.loadRecentTransactions(true, totalItemsCount) - } - - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) - if (!recyclerView.canScrollVertically(1) && - newState == RecyclerView.SCROLL_STATE_IDLE - ) { - Toaster.show(binding.root, R.string.no_more_transactions_available) - } - } - } - ) - binding.swipeTransactionContainer.setColorSchemeColors( - *requireActivity() - .resources.getIntArray(R.array.swipeRefreshColors), - ) - binding.swipeTransactionContainer.setOnRefreshListener(this) - } - - /** - * Refreshes the List of [Transaction] - */ - override fun onRefresh() { - if (binding.layoutError.root.visibility == View.VISIBLE) { - resetUI() - } - recentTransactionViewModel.loadRecentTransactions(false, 0) - } - - /** - * Shows a Toast - */ - private fun showMessage(message: String?) { - (activity as BaseActivity?)?.showToast(message!!) - } - - /** - * Updates `recentTransactionsListAdapter` with `recentTransactionList` fetched from - * server - * - * @param recentTransactionList List of [Transaction] - */ - private fun showRecentTransactions(recentTransactionList: List?) { - this.recentTransactionList = recentTransactionList as MutableList? - recentTransactionsListAdapter?.setTransactions(recentTransactionList) - } - - /** - * Appends more Transactions in `recentTransactionList` - * - * @param transactions List of [Transaction] - */ - fun showLoadMoreRecentTransactions(transactions: List?) { - this.recentTransactionList?.addAll(recentTransactionList!!) - recentTransactionsListAdapter?.notifyDataSetChanged() - } - - private fun resetUI() { - sweetUIErrorHandler?.hideSweetErrorLayoutUI( - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } - - /** - * Hides `rvRecentTransactions` and shows a textview prompting no transactions - */ - private fun showEmptyTransaction() { - sweetUIErrorHandler?.showSweetEmptyUI( - getString(R.string.recent_transactions), - R.drawable.ic_error_black_24dp, - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } - - /** - * It is called whenever any error occurs while executing a request - * - * @param message Error message that tells the user about the problem. - */ - fun showErrorFetchingRecentTransactions(message: String?) { - if (!isConnected(requireActivity())) { - sweetUIErrorHandler?.showSweetNoInternetUI( - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } else { - sweetUIErrorHandler?.showSweetErrorUI( - message, - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } - } - - private fun retryClicked() { - if (isConnected(requireContext())) { - sweetUIErrorHandler?.hideSweetErrorLayoutUI( - binding.rvRecentTransactions, - binding.layoutError.root, - ) - recentTransactionViewModel.loadRecentTransactions(false, 0) - } else { - Toast.makeText( - context, - getString(R.string.internet_not_connected), - Toast.LENGTH_SHORT, - ).show() - } - } - - fun showProgress() { - showSwipeRefreshLayout(true) - } - - fun hideProgress() { - showSwipeRefreshLayout(false) - } - - private fun showSwipeRefreshLayout(show: Boolean) { - binding.swipeTransactionContainer.post { - binding.swipeTransactionContainer.isRefreshing = show - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - companion object { - fun newInstance(): RecentTransactionsFragment { - val fragment = RecentTransactionsFragment() - val args = Bundle() - fragment.arguments = args - return fragment - } - } -} diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt new file mode 100644 index 000000000..bc98bd3b6 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt @@ -0,0 +1,197 @@ +package org.mifos.mobile.ui.recent_transactions + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.mifos.mobile.MifosSelfServiceApp +import org.mifos.mobile.R +import org.mifos.mobile.core.ui.component.MFScaffold +import org.mifos.mobile.core.ui.component.MifosErrorComponent +import org.mifos.mobile.core.ui.component.MifosProgressIndicator +import org.mifos.mobile.core.ui.theme.MifosMobileTheme +import org.mifos.mobile.models.Transaction +import org.mifos.mobile.utils.CurrencyUtil +import org.mifos.mobile.utils.DateHelper +import org.mifos.mobile.utils.Network +import org.mifos.mobile.utils.RecentTransactionUiState +import org.mifos.mobile.utils.Utils + +@Composable +fun RecentTransactionScreen( + viewModel: RecentTransactionViewModel = hiltViewModel(), + navigateBack: () -> Unit ) +{ + val uiState by viewModel.recentTransactionUiState.collectAsStateWithLifecycle() + LaunchedEffect(key1 = Unit) { + viewModel.loadRecentTransactions(false, 0) + } + + RecentTransactionScreen( + uiState = uiState, + navigateBack = navigateBack, + retryConnection = { viewModel.loadRecentTransactions(false, 0) } + ) +} + + +@Composable +fun RecentTransactionScreen( + uiState: RecentTransactionUiState, + navigateBack: () -> Unit, + retryConnection: () -> Unit) { + + val context = LocalContext.current + Column(modifier = Modifier.fillMaxSize()) { + + when (uiState) { + + RecentTransactionUiState.EmptyTransaction -> { + MifosErrorComponent(isEmptyData = true) + } + + is RecentTransactionUiState.Error -> { + MifosErrorComponent( + isNetworkConnected = Network.isConnected(context), + isEmptyData = false, + isRetryEnabled = true, + onRetry = retryConnection + ) + } + + RecentTransactionUiState.Initial -> { + } + + RecentTransactionUiState.Loading -> { + MifosProgressIndicator( + modifier = Modifier + .fillMaxSize() + .padding(10.dp) + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)) + ) + } + + is RecentTransactionUiState.LoadMoreRecentTransactions -> { + + } + is RecentTransactionUiState.RecentTransactions -> { + if( uiState.transactions?.isNotEmpty() == true) { + LoadRecentTransactions(transactionList = uiState.transactions as ArrayList) + } else { + MifosErrorComponent(isEmptyData = true) + } + } + } + } + } + + +@Composable +fun LoadRecentTransactions( transactionList : ArrayList ){ + + LazyColumn { + items(items = transactionList?.toList().orEmpty()) { + RecentTransactionListItem(it) + } + } +} + + +@Composable +fun RecentTransactionListItem(transaction: Transaction?) { + Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.ic_local_atm_black_24dp), + contentDescription = stringResource(id = R.string.atm_icon), + modifier = Modifier.size(39.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = Utils.formatTransactionType(transaction?.type?.value), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Row { + Text( + text = stringResource( + id = R.string.string_and_string, + transaction?.currency?.displaySymbol ?: transaction?.currency?.code ?: "", + CurrencyUtil.formatCurrency( + MifosSelfServiceApp.context, + transaction?.amount ?: 0.0, + ) + ), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .weight(1f) + .alpha(0.7f), + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = DateHelper.getDateAsString(transaction?.submittedOnDate), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.alpha(0.7f), + color = MaterialTheme.colorScheme.onSurface + ) + } + + } + } +} + + +class RecentTransactionScreenPreviewProvider : PreviewParameterProvider { + + var tList : ArrayList = ArrayList() + override val values: Sequence + get() = sequenceOf( + RecentTransactionUiState.RecentTransactions(tList), + RecentTransactionUiState.Loading, + RecentTransactionUiState.LoadMoreRecentTransactions(tList), + RecentTransactionUiState.Error(R.string.recent_transactions), + RecentTransactionUiState.EmptyTransaction, + RecentTransactionUiState.Initial + ) +} +@Preview(showSystemUi = true, showBackground = true) +@Composable +private fun RecentTransactionScreenPreview( + @PreviewParameter(RecentTransactionScreenPreviewProvider::class) recentTransactionUiState: RecentTransactionUiState +) { + + MifosMobileTheme { + RecentTransactionScreen( + uiState = recentTransactionUiState, + navigateBack = { }) { } + } +} + diff --git a/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionViewModel.kt similarity index 97% rename from app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt rename to app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionViewModel.kt index e6cf3a80c..7b2ba5090 100644 --- a/app/src/main/java/org/mifos/mobile/viewModels/RecentTransactionViewModel.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionViewModel.kt @@ -1,4 +1,4 @@ -package org.mifos.mobile.viewModels +package org.mifos.mobile.ui.recent_transactions import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt new file mode 100644 index 000000000..4ab41ae3b --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt @@ -0,0 +1,54 @@ +package org.mifos.mobile.ui.recent_transactions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import org.mifos.mobile.R +import org.mifos.mobile.core.ui.component.mifosComposeView +import org.mifos.mobile.core.ui.theme.MifosMobileTheme +import org.mifos.mobile.ui.fragments.base.BaseFragment + +/** + * @author Vishwwajeet + * @since 09/08/16 + */ +@AndroidEntryPoint +class RecentTransactionsFragment : BaseFragment() { + + private val recentTransactionViewModel: RecentTransactionViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + setToolbarTitle(getString(R.string.recent_transactions)) + return ComposeView(requireContext()).apply { + setContent { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + MifosMobileTheme { + RecentTransactionScreen( + recentTransactionViewModel, + navigateBack = { activity?.supportFragmentManager?.popBackStack() }) + } + } + } + } + + companion object { + fun newInstance(): RecentTransactionsFragment { + val fragment = RecentTransactionsFragment() + val args = Bundle() + fragment.arguments = args + return fragment + } + } +} From 9d58aae96d1c08146297454bd87f995b346323a2 Mon Sep 17 00:00:00 2001 From: akashmeruva9 Date: Fri, 7 Jun 2024 11:11:25 +0530 Subject: [PATCH 2/8] Refactor Recent Transaction screen from XML to Compose --- .../mifos/mobile/viewModels/RecentTransactionViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt b/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt index 9353a64da..7d7caa32b 100644 --- a/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt +++ b/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt @@ -17,6 +17,7 @@ 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.ui.recent_transactions.RecentTransactionViewModel import org.mifos.mobile.util.RxSchedulersOverrideRule import org.mifos.mobile.utils.RecentTransactionUiState import org.mockito.Mock From 38f190f7a37d0787eae88a01d6e178ef350a8973 Mon Sep 17 00:00:00 2001 From: akashmeruva9 Date: Sat, 8 Jun 2024 18:05:27 +0530 Subject: [PATCH 3/8] Added Swipe Pull to Refresh Implementation --- app/build.gradle.kts | 3 + .../RecentTransactionScreen.kt | 118 ++++++++++++------ .../RecentTransactionViewModel.kt | 13 ++ .../RecentTransactionsFragment.kt | 1 - gradle/libs.versions.toml | 2 + 5 files changed, 97 insertions(+), 40 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dc09a9229..1a5545adb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -160,6 +160,9 @@ dependencies { implementation("com.github.rahul-gill.mifos-ui-library:uihouse:alpha-2.1") + //swipe to pull refresh + implementation ("androidx.compose:compose-bom:2024.05.00") + // Jetpack Compose api(libs.androidx.activity.compose) api(libs.androidx.compose.material3) diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt index bc98bd3b6..575c674cb 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt @@ -1,5 +1,6 @@ package org.mifos.mobile.ui.recent_transactions +import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -12,14 +13,22 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -48,67 +57,99 @@ fun RecentTransactionScreen( navigateBack: () -> Unit ) { val uiState by viewModel.recentTransactionUiState.collectAsStateWithLifecycle() + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + LaunchedEffect(key1 = Unit) { viewModel.loadRecentTransactions(false, 0) } - RecentTransactionScreen( + RecentTransactionScreenContent( uiState = uiState, navigateBack = navigateBack, - retryConnection = { viewModel.loadRecentTransactions(false, 0) } + retryConnection = { viewModel.loadRecentTransactions(false, 0) }, + isRefreshing = isRefreshing, + onRefresh = { viewModel.refresh() } ) } - +@OptIn( ExperimentalMaterial3Api::class) @Composable -fun RecentTransactionScreen( +fun RecentTransactionScreenContent( uiState: RecentTransactionUiState, navigateBack: () -> Unit, - retryConnection: () -> Unit) { + retryConnection: () -> Unit, + isRefreshing: Boolean, + onRefresh: () -> Unit) { val context = LocalContext.current - Column(modifier = Modifier.fillMaxSize()) { + val pullRefreshState = rememberPullToRefreshState() - when (uiState) { + Box(Modifier.nestedScroll(pullRefreshState.nestedScrollConnection)) + { + Column { - RecentTransactionUiState.EmptyTransaction -> { - MifosErrorComponent(isEmptyData = true) - } + when (uiState) { - is RecentTransactionUiState.Error -> { - MifosErrorComponent( - isNetworkConnected = Network.isConnected(context), - isEmptyData = false, - isRetryEnabled = true, - onRetry = retryConnection - ) - } + RecentTransactionUiState.EmptyTransaction -> { + MifosErrorComponent(isEmptyData = true) + } - RecentTransactionUiState.Initial -> { - } + is RecentTransactionUiState.Error -> { + MifosErrorComponent( + isNetworkConnected = Network.isConnected(context), + isEmptyData = false, + isRetryEnabled = true, + onRetry = retryConnection + ) + } - RecentTransactionUiState.Loading -> { - MifosProgressIndicator( - modifier = Modifier - .fillMaxSize() - .padding(10.dp) - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)) - ) - } + RecentTransactionUiState.Initial -> { + } - is RecentTransactionUiState.LoadMoreRecentTransactions -> { + RecentTransactionUiState.Loading -> { - } - is RecentTransactionUiState.RecentTransactions -> { - if( uiState.transactions?.isNotEmpty() == true) { - LoadRecentTransactions(transactionList = uiState.transactions as ArrayList) - } else { - MifosErrorComponent(isEmptyData = true) + MifosProgressIndicator( + modifier = Modifier + .fillMaxSize() + .padding(10.dp) + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)) + ) } + + is RecentTransactionUiState.LoadMoreRecentTransactions -> { + + } + + is RecentTransactionUiState.RecentTransactions -> { + + if (uiState.transactions?.isNotEmpty() == true) { + LoadRecentTransactions(transactionList = uiState.transactions as ArrayList) + } else { + MifosErrorComponent(isEmptyData = true) + } + } + } + } + + if (pullRefreshState.isRefreshing) { + LaunchedEffect(key1 = true) { + onRefresh() } } + LaunchedEffect(key1 = isRefreshing) { + if (isRefreshing) + pullRefreshState.startRefresh() + else + pullRefreshState.endRefresh() + } + + PullToRefreshContainer( + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + + ) } - } +} @Composable @@ -187,11 +228,10 @@ class RecentTransactionScreenPreviewProvider : PreviewParameterProvider(RecentTransactionUiState.Initial) val recentTransactionUiState: StateFlow = _recentTransactionUiState + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + + fun refresh() { + viewModelScope.launch { + _isRefreshing.emit(true) + loadRecentTransactions(false, 0) + _isRefreshing.emit(false) + } + } fun loadRecentTransactions(loadmore: Boolean, offset: Int) { this.loadmore = loadmore loadRecentTransactions(offset, limit) diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt index 4ab41ae3b..eaf09395a 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt @@ -33,7 +33,6 @@ class RecentTransactionsFragment : BaseFragment() { setToolbarTitle(getString(R.string.recent_transactions)) return ComposeView(requireContext()).apply { setContent { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) MifosMobileTheme { RecentTransactionScreen( recentTransactionViewModel, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a872a140..463a32fff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ appcompatVersion = "1.6.1" compileSdk = "34" constraintlayoutVersion = "2.1.4" coreKtxVersion = "1.12.0" +materialPullrefreshVersion = "1.0.0" minSdk = "24" targetSdk = "34" androidGradlePlugin = "8.2.2" @@ -68,6 +69,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest"} androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling"} androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } +androidx-material-pullrefresh = { module = "androidx.compose.material:material-pullrefresh", version.ref = "materialPullrefreshVersion" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview"} From 9327a7107b6ee529361e594268e332ca8fd0d62e Mon Sep 17 00:00:00 2001 From: akashmeruva9 Date: Sat, 8 Jun 2024 18:06:29 +0530 Subject: [PATCH 4/8] Added Pull to Refresh Implemntation --- .../mobile/ui/recent_transactions/RecentTransactionScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt index 575c674cb..eee45e730 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt @@ -86,7 +86,7 @@ fun RecentTransactionScreenContent( Box(Modifier.nestedScroll(pullRefreshState.nestedScrollConnection)) { - Column { + Column(modifier = Modifier.fillMaxSize()) { when (uiState) { From fe78605a39d093bf5f047e8c53be0f344d26d685 Mon Sep 17 00:00:00 2001 From: akashmeruva9 Date: Mon, 10 Jun 2024 05:11:30 +0530 Subject: [PATCH 5/8] Resolved the pull indicator not appearing --- .../RecentTransactionScreen.kt | 82 +++-- .../RecentTransactionViewModel.kt | 8 +- .../RecentTransactionsFragment_Old.kt | 286 ++++++++++++++++++ 3 files changed, 349 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt index eee45e730..cc102080a 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt @@ -1,8 +1,8 @@ package org.mifos.mobile.ui.recent_transactions -import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,7 +12,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -21,13 +25,9 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -38,13 +38,16 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf import org.mifos.mobile.MifosSelfServiceApp import org.mifos.mobile.R -import org.mifos.mobile.core.ui.component.MFScaffold import org.mifos.mobile.core.ui.component.MifosErrorComponent import org.mifos.mobile.core.ui.component.MifosProgressIndicator import org.mifos.mobile.core.ui.theme.MifosMobileTheme import org.mifos.mobile.models.Transaction +import org.mifos.mobile.repositories.RecentTransactionRepositoryImp import org.mifos.mobile.utils.CurrencyUtil import org.mifos.mobile.utils.DateHelper import org.mifos.mobile.utils.Network @@ -63,12 +66,13 @@ fun RecentTransactionScreen( viewModel.loadRecentTransactions(false, 0) } - RecentTransactionScreenContent( + RecentTransactionScreenContent( uiState = uiState, navigateBack = navigateBack, retryConnection = { viewModel.loadRecentTransactions(false, 0) }, isRefreshing = isRefreshing, - onRefresh = { viewModel.refresh() } + onRefresh = { viewModel.refresh() }, + viewModel= viewModel ) } @@ -79,19 +83,27 @@ fun RecentTransactionScreenContent( navigateBack: () -> Unit, retryConnection: () -> Unit, isRefreshing: Boolean, - onRefresh: () -> Unit) { + onRefresh: () -> Unit, + viewModel: RecentTransactionViewModel +) { val context = LocalContext.current val pullRefreshState = rememberPullToRefreshState() + val scrollState = rememberScrollState() + val lazyColumnState = rememberLazyListState() - Box(Modifier.nestedScroll(pullRefreshState.nestedScrollConnection)) - { - Column(modifier = Modifier.fillMaxSize()) { + Box( + Modifier + .fillMaxSize() + .nestedScroll(pullRefreshState.nestedScrollConnection)){ + Column(modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), verticalArrangement = Arrangement.Center) { when (uiState) { RecentTransactionUiState.EmptyTransaction -> { - MifosErrorComponent(isEmptyData = true) + MifosErrorComponent( isEmptyData = true) } is RecentTransactionUiState.Error -> { @@ -104,6 +116,7 @@ fun RecentTransactionScreenContent( } RecentTransactionUiState.Initial -> { + } RecentTransactionUiState.Loading -> { @@ -117,13 +130,20 @@ fun RecentTransactionScreenContent( } is RecentTransactionUiState.LoadMoreRecentTransactions -> { - + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyColumnState + ) { + items(uiState.transactions) { transaction -> + RecentTransactionListItem(transaction) + } + } } is RecentTransactionUiState.RecentTransactions -> { if (uiState.transactions?.isNotEmpty() == true) { - LoadRecentTransactions(transactionList = uiState.transactions as ArrayList) + LoadRecentTransactions(transactionList = uiState.transactions as ArrayList,lazyColumnState,uiState, viewModel) } else { MifosErrorComponent(isEmptyData = true) } @@ -146,20 +166,36 @@ fun RecentTransactionScreenContent( PullToRefreshContainer( state = pullRefreshState, modifier = Modifier.align(Alignment.TopCenter), + ) + } - ) - } } @Composable -fun LoadRecentTransactions( transactionList : ArrayList ){ - - LazyColumn { +fun LoadRecentTransactions( + transactionList: ArrayList, + lazyColumnState: LazyListState, + uiState: RecentTransactionUiState.RecentTransactions, + viewModel: RecentTransactionViewModel +){ + + LazyColumn(Modifier.fillMaxSize(), + state = lazyColumnState) { items(items = transactionList?.toList().orEmpty()) { RecentTransactionListItem(it) } + item { + val visibleItems = lazyColumnState.layoutInfo.visibleItemsInfo + val lastVisibleItemIndex = visibleItems.lastOrNull()?.index ?: 0 + val isNearBottom = lastVisibleItemIndex >= uiState.transactions.size - 5 + + if (isNearBottom) { + viewModel.loadRecentTransactions(true, uiState.transactions.size) + } + } } + } @@ -228,10 +264,12 @@ class RecentTransactionScreenPreviewProvider : PreviewParameterProvider get() = _isRefreshing.asStateFlow() fun refresh() { - viewModelScope.launch { - _isRefreshing.emit(true) - loadRecentTransactions(false, 0) - _isRefreshing.emit(false) - } + _isRefreshing.value = true + loadRecentTransactions(false, 0) } fun loadRecentTransactions(loadmore: Boolean, offset: Int) { this.loadmore = loadmore @@ -57,6 +54,7 @@ class RecentTransactionViewModel @Inject constructor(private val recentTransacti _recentTransactionUiState.value = RecentTransactionUiState.RecentTransactions(it.pageItems) } + _isRefreshing.emit(false) } } } diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt new file mode 100644 index 000000000..ac6521112 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt @@ -0,0 +1,286 @@ +package org.mifos.mobile.ui.recent_transactions + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import org.mifos.mobile.R +import org.mifos.mobile.databinding.FragmentRecentTransactionsBinding +import org.mifos.mobile.models.Transaction +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.utils.* +import org.mifos.mobile.utils.Network.isConnected +import org.mifos.mobile.utils.ParcelableAndSerializableUtils.getCheckedArrayListFromParcelable +import javax.inject.Inject + +/** + * @author Vishwwajeet + * @since 09/08/16 + */ +@AndroidEntryPoint +class RecentTransactionsFragment_Old : BaseFragment(), OnRefreshListener { + + private var _binding: FragmentRecentTransactionsBinding? = null + private val binding get() = _binding!! + + @JvmField + @Inject + var recentTransactionsListAdapter: RecentTransactionListAdapter? = null + + private val recentTransactionViewModel: RecentTransactionViewModel by viewModels() + + private var sweetUIErrorHandler: SweetUIErrorHandler? = null + private var recentTransactionList: MutableList? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + recentTransactionList = ArrayList() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentRecentTransactionsBinding.inflate(inflater, container, false) + sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) + showUserInterface() + setToolbarTitle(getString(R.string.recent_transactions)) + if (savedInstanceState == null) { + recentTransactionViewModel.loadRecentTransactions(false, 0) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + recentTransactionViewModel.recentTransactionUiState.collect{ + 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) + } + + RecentTransactionUiState.Initial -> {} + } + } + } + } + + binding.layoutError.btnTryAgain.setOnClickListener { + retryClicked() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelableArrayList( + Constants.RECENT_TRANSACTIONS, + ArrayList( + recentTransactionList, + ), + ) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (savedInstanceState != null) { + val transactions: List = + savedInstanceState.getCheckedArrayListFromParcelable( + Transaction::class.java, + Constants.RECENT_TRANSACTIONS, + ) ?: listOf() + showRecentTransactions(transactions) + } + } + + /** + * Setting up `rvRecentTransactions` + */ + fun showUserInterface() { + val layoutManager = LinearLayoutManager(activity) + layoutManager.orientation = LinearLayoutManager.VERTICAL + binding.rvRecentTransactions.layoutManager = layoutManager + binding.rvRecentTransactions.setHasFixedSize(true) + binding.rvRecentTransactions.addItemDecoration( + DividerItemDecoration( + requireActivity(), + layoutManager.orientation, + ), + ) + recentTransactionsListAdapter?.setTransactions(recentTransactionList) + binding.rvRecentTransactions.adapter = recentTransactionsListAdapter + binding.rvRecentTransactions.addOnScrollListener( + object : EndlessRecyclerViewScrollListener(layoutManager) { + override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { + recentTransactionViewModel.loadRecentTransactions(true, totalItemsCount) + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (!recyclerView.canScrollVertically(1) && + newState == RecyclerView.SCROLL_STATE_IDLE + ) { + Toaster.show(binding.root, R.string.no_more_transactions_available) + } + } + } + ) + binding.swipeTransactionContainer.setColorSchemeColors( + *requireActivity() + .resources.getIntArray(R.array.swipeRefreshColors), + ) + binding.swipeTransactionContainer.setOnRefreshListener(this) + } + + /** + * Refreshes the List of [Transaction] + */ + override fun onRefresh() { + if (binding.layoutError.root.visibility == View.VISIBLE) { + resetUI() + } + recentTransactionViewModel.loadRecentTransactions(false, 0) + } + + /** + * Shows a Toast + */ + private fun showMessage(message: String?) { + (activity as BaseActivity?)?.showToast(message!!) + } + + /** + * Updates `recentTransactionsListAdapter` with `recentTransactionList` fetched from + * server + * + * @param recentTransactionList List of [Transaction] + */ + private fun showRecentTransactions(recentTransactionList: List?) { + this.recentTransactionList = recentTransactionList as MutableList? + recentTransactionsListAdapter?.setTransactions(recentTransactionList) + } + + /** + * Appends more Transactions in `recentTransactionList` + * + * @param transactions List of [Transaction] + */ + fun showLoadMoreRecentTransactions(transactions: List?) { + this.recentTransactionList?.addAll(recentTransactionList!!) + recentTransactionsListAdapter?.notifyDataSetChanged() + } + + private fun resetUI() { + sweetUIErrorHandler?.hideSweetErrorLayoutUI( + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } + + /** + * Hides `rvRecentTransactions` and shows a textview prompting no transactions + */ + private fun showEmptyTransaction() { + sweetUIErrorHandler?.showSweetEmptyUI( + getString(R.string.recent_transactions), + R.drawable.ic_error_black_24dp, + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } + + /** + * It is called whenever any error occurs while executing a request + * + * @param message Error message that tells the user about the problem. + */ + fun showErrorFetchingRecentTransactions(message: String?) { + if (!isConnected(requireActivity())) { + sweetUIErrorHandler?.showSweetNoInternetUI( + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } else { + sweetUIErrorHandler?.showSweetErrorUI( + message, + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } + } + + private fun retryClicked() { + if (isConnected(requireContext())) { + sweetUIErrorHandler?.hideSweetErrorLayoutUI( + binding.rvRecentTransactions, + binding.layoutError.root, + ) + recentTransactionViewModel.loadRecentTransactions(false, 0) + } else { + Toast.makeText( + context, + getString(R.string.internet_not_connected), + Toast.LENGTH_SHORT, + ).show() + } + } + + fun showProgress() { + showSwipeRefreshLayout(true) + } + + fun hideProgress() { + showSwipeRefreshLayout(false) + } + + private fun showSwipeRefreshLayout(show: Boolean) { + binding.swipeTransactionContainer.post { + binding.swipeTransactionContainer.isRefreshing = show + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun newInstance(): RecentTransactionsFragment_Old { + val fragment = RecentTransactionsFragment_Old() + val args = Bundle() + fragment.arguments = args + return fragment + } + } +} \ No newline at end of file From 2126910c65b90eaf3b3f51280dd976572c81e06d Mon Sep 17 00:00:00 2001 From: akashmeruva9 Date: Mon, 10 Jun 2024 23:05:57 +0530 Subject: [PATCH 6/8] resoleved Preview Isuue and Adeed Version Catalog --- app/build.gradle.kts | 4 +--- .../ui/recent_transactions/RecentTransactionScreen.kt | 6 +++--- gradle/libs.versions.toml | 2 -- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a5545adb..46b6f13dc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -160,11 +160,9 @@ dependencies { implementation("com.github.rahul-gill.mifos-ui-library:uihouse:alpha-2.1") - //swipe to pull refresh - implementation ("androidx.compose:compose-bom:2024.05.00") - // Jetpack Compose api(libs.androidx.activity.compose) + api(platform(libs.androidx.compose.bom)) api(libs.androidx.compose.material3) api(libs.androidx.compose.foundation) api(libs.androidx.compose.foundation.layout) diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt index cc102080a..cfd9e5ff1 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt @@ -264,12 +264,12 @@ class RecentTransactionScreenPreviewProvider : PreviewParameterProvider Date: Wed, 12 Jun 2024 13:09:35 +0530 Subject: [PATCH 7/8] "Changed to empty data view and added some changes" --- .../recent_transactions/RecentTransactionScreen.kt | 8 +++++--- .../RecentTransactionsFragment.kt | 12 ++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt index cfd9e5ff1..1fd498a2e 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.getString import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf import org.mifos.mobile.MifosSelfServiceApp import org.mifos.mobile.R +import org.mifos.mobile.core.ui.component.EmptyDataView import org.mifos.mobile.core.ui.component.MifosErrorComponent import org.mifos.mobile.core.ui.component.MifosProgressIndicator import org.mifos.mobile.core.ui.theme.MifosMobileTheme @@ -103,7 +105,7 @@ fun RecentTransactionScreenContent( when (uiState) { RecentTransactionUiState.EmptyTransaction -> { - MifosErrorComponent( isEmptyData = true) + EmptyDataView( modifier = Modifier.fillMaxSize() , R.drawable.ic_error_black_24dp, R.string.no_transaction, getString(context, R.string.no_transaction) ) } is RecentTransactionUiState.Error -> { @@ -145,7 +147,7 @@ fun RecentTransactionScreenContent( if (uiState.transactions?.isNotEmpty() == true) { LoadRecentTransactions(transactionList = uiState.transactions as ArrayList,lazyColumnState,uiState, viewModel) } else { - MifosErrorComponent(isEmptyData = true) + EmptyDataView( modifier = Modifier.fillMaxSize() , R.drawable.ic_error_black_24dp, R.string.no_transaction, getString(context, R.string.no_transaction) ) } } } @@ -205,7 +207,7 @@ fun RecentTransactionListItem(transaction: Transaction?) { Image( painter = painterResource(id = R.drawable.ic_local_atm_black_24dp), contentDescription = stringResource(id = R.string.atm_icon), - modifier = Modifier.size(39.dp) + modifier = Modifier.size(40.dp) ) Spacer(modifier = Modifier.width(8.dp)) diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt index eaf09395a..2b2d09b54 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt @@ -31,14 +31,10 @@ class RecentTransactionsFragment : BaseFragment() { savedInstanceState: Bundle?, ): View { setToolbarTitle(getString(R.string.recent_transactions)) - return ComposeView(requireContext()).apply { - setContent { - MifosMobileTheme { - RecentTransactionScreen( - recentTransactionViewModel, - navigateBack = { activity?.supportFragmentManager?.popBackStack() }) - } - } + return mifosComposeView(requireContext()) { + RecentTransactionScreen( + recentTransactionViewModel, + navigateBack = { activity?.onBackPressedDispatcher?.onBackPressed() }) } } From b98a951ecac52d54c1c3cfa31a3467b0e0a552fb Mon Sep 17 00:00:00 2001 From: Avneet Singh Date: Fri, 14 Jun 2024 20:49:11 +0530 Subject: [PATCH 8/8] refactor: recent transaction screen migration to compose --- .../mobile/ui/activities/HomeActivity.kt | 6 +- .../RecentTransactionScreen.kt | 210 ++++++------- .../RecentTransactionViewModel.kt | 61 ++-- .../RecentTransactionsComposeFragment.kt | 42 +++ .../RecentTransactionsFragment.kt | 257 +++++++++++++++- .../RecentTransactionsFragment_Old.kt | 286 ------------------ .../mobile/utils/RecentTransactionUiState.kt | 10 - .../main/java/org/mifos/mobile/utils/Utils.kt | 18 +- .../RecentTransactionViewModelTest.kt | 9 +- 9 files changed, 433 insertions(+), 466 deletions(-) create mode 100644 app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsComposeFragment.kt delete mode 100644 app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt index 847030a72..ef0b7b0fe 100644 --- a/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt +++ b/app/src/main/java/org/mifos/mobile/ui/activities/HomeActivity.kt @@ -39,7 +39,7 @@ import org.mifos.mobile.ui.getThemeAttributeColor import org.mifos.mobile.ui.help.HelpActivity import org.mifos.mobile.ui.home.HomeOldFragment import org.mifos.mobile.ui.login.LoginActivity -import org.mifos.mobile.ui.recent_transactions.RecentTransactionsFragment +import org.mifos.mobile.ui.recent_transactions.RecentTransactionsComposeFragment import org.mifos.mobile.utils.Constants import org.mifos.mobile.utils.TextDrawable import org.mifos.mobile.utils.Toaster @@ -182,7 +182,7 @@ class HomeActivity : } R.id.item_recent_transactions -> replaceFragment( - RecentTransactionsFragment.newInstance(), + RecentTransactionsComposeFragment.newInstance(), true, R.id.container, ) @@ -407,7 +407,7 @@ class HomeActivity : setNavigationViewSelectedItem(R.id.item_accounts) } - is RecentTransactionsFragment -> { + is RecentTransactionsComposeFragment -> { setNavigationViewSelectedItem(R.id.item_recent_transactions) } diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt index 1fd498a2e..b93fac303 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionScreen.kt @@ -1,13 +1,12 @@ package org.mifos.mobile.ui.recent_transactions import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -19,6 +18,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshContainer import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -39,168 +39,148 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.getString import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flowOf import org.mifos.mobile.MifosSelfServiceApp import org.mifos.mobile.R import org.mifos.mobile.core.ui.component.EmptyDataView +import org.mifos.mobile.core.ui.component.MFScaffold import org.mifos.mobile.core.ui.component.MifosErrorComponent import org.mifos.mobile.core.ui.component.MifosProgressIndicator +import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay import org.mifos.mobile.core.ui.theme.MifosMobileTheme import org.mifos.mobile.models.Transaction -import org.mifos.mobile.repositories.RecentTransactionRepositoryImp +import org.mifos.mobile.models.client.Type import org.mifos.mobile.utils.CurrencyUtil import org.mifos.mobile.utils.DateHelper import org.mifos.mobile.utils.Network -import org.mifos.mobile.utils.RecentTransactionUiState import org.mifos.mobile.utils.Utils @Composable fun RecentTransactionScreen( viewModel: RecentTransactionViewModel = hiltViewModel(), - navigateBack: () -> Unit ) -{ + navigateBack: () -> Unit +) { val uiState by viewModel.recentTransactionUiState.collectAsStateWithLifecycle() val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + val isPaginating by viewModel.isPaginating.collectAsStateWithLifecycle() LaunchedEffect(key1 = Unit) { - viewModel.loadRecentTransactions(false, 0) + viewModel.loadInitialTransactions() } - RecentTransactionScreenContent( + RecentTransactionScreen( uiState = uiState, navigateBack = navigateBack, - retryConnection = { viewModel.loadRecentTransactions(false, 0) }, + onRetry = { viewModel.loadInitialTransactions() }, isRefreshing = isRefreshing, onRefresh = { viewModel.refresh() }, - viewModel= viewModel + isPaginating = isPaginating, + loadMore = { offset -> viewModel.loadPaginatedTransactions(offset) } ) } -@OptIn( ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun RecentTransactionScreenContent( +fun RecentTransactionScreen( uiState: RecentTransactionUiState, navigateBack: () -> Unit, - retryConnection: () -> Unit, + onRetry: () -> Unit, isRefreshing: Boolean, onRefresh: () -> Unit, - viewModel: RecentTransactionViewModel + isPaginating: Boolean, + loadMore: (offset: Int) -> Unit ) { - val context = LocalContext.current val pullRefreshState = rememberPullToRefreshState() - val scrollState = rememberScrollState() - val lazyColumnState = rememberLazyListState() - - Box( - Modifier - .fillMaxSize() - .nestedScroll(pullRefreshState.nestedScrollConnection)){ - Column(modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState), verticalArrangement = Arrangement.Center) { + MFScaffold( + topBarTitleResId = R.string.recent_transactions, + navigateBack = navigateBack, + scaffoldContent = { paddingValues -> + Box(modifier = Modifier.padding(paddingValues = paddingValues)) { when (uiState) { - - RecentTransactionUiState.EmptyTransaction -> { - EmptyDataView( modifier = Modifier.fillMaxSize() , R.drawable.ic_error_black_24dp, R.string.no_transaction, getString(context, R.string.no_transaction) ) - } - is RecentTransactionUiState.Error -> { MifosErrorComponent( isNetworkConnected = Network.isConnected(context), - isEmptyData = false, isRetryEnabled = true, - onRetry = retryConnection + onRetry = onRetry ) } - RecentTransactionUiState.Initial -> { - + is RecentTransactionUiState.Loading -> { + MifosProgressIndicatorOverlay() } - RecentTransactionUiState.Loading -> { - - MifosProgressIndicator( - modifier = Modifier - .fillMaxSize() - .padding(10.dp) - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)) - ) - } - - is RecentTransactionUiState.LoadMoreRecentTransactions -> { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyColumnState - ) { - items(uiState.transactions) { transaction -> - RecentTransactionListItem(transaction) - } - } - } - - is RecentTransactionUiState.RecentTransactions -> { - - if (uiState.transactions?.isNotEmpty() == true) { - LoadRecentTransactions(transactionList = uiState.transactions as ArrayList,lazyColumnState,uiState, viewModel) + is RecentTransactionUiState.Success -> { + if (uiState.transactions.isEmpty()) { + EmptyDataView( + icon = R.drawable.ic_error_black_24dp, + error = R.string.no_transaction, + modifier = Modifier.fillMaxSize() + ) } else { - EmptyDataView( modifier = Modifier.fillMaxSize() , R.drawable.ic_error_black_24dp, R.string.no_transaction, getString(context, R.string.no_transaction) ) + RecentTransactionsContent( + transactions = uiState.transactions, + isPaginating = isPaginating, + loadMore = loadMore, + canPaginate = uiState.canPaginate + ) } } } } + } + ) - if (pullRefreshState.isRefreshing) { - LaunchedEffect(key1 = true) { - onRefresh() - } - } - LaunchedEffect(key1 = isRefreshing) { - if (isRefreshing) - pullRefreshState.startRefresh() - else - pullRefreshState.endRefresh() - } - - PullToRefreshContainer( - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) + if (pullRefreshState.isRefreshing) { + LaunchedEffect(key1 = true) { + onRefresh() + } + } + LaunchedEffect(key1 = isRefreshing) { + if (isRefreshing) + pullRefreshState.startRefresh() + else + pullRefreshState.endRefresh() } + PullToRefreshContainer( + state = pullRefreshState, + ) } - @Composable -fun LoadRecentTransactions( - transactionList: ArrayList, - lazyColumnState: LazyListState, - uiState: RecentTransactionUiState.RecentTransactions, - viewModel: RecentTransactionViewModel -){ +fun RecentTransactionsContent( + transactions: List, + isPaginating: Boolean, + canPaginate: Boolean, + loadMore: (offset: Int) -> Unit +) { + val lazyColumnState = rememberLazyListState() + + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyColumnState + ) { + val visibleItems = lazyColumnState.layoutInfo.visibleItemsInfo + val lastVisibleItemIndex = visibleItems.lastOrNull()?.index ?: 0 + val isNearBottom = lastVisibleItemIndex >= transactions.size - 5 - LazyColumn(Modifier.fillMaxSize(), - state = lazyColumnState) { - items(items = transactionList?.toList().orEmpty()) { - RecentTransactionListItem(it) + if (!isPaginating && canPaginate && isNearBottom) { + loadMore(transactions.size - 1) } - item { - val visibleItems = lazyColumnState.layoutInfo.visibleItemsInfo - val lastVisibleItemIndex = visibleItems.lastOrNull()?.index ?: 0 - val isNearBottom = lastVisibleItemIndex >= uiState.transactions.size - 5 - if (isNearBottom) { - viewModel.loadRecentTransactions(true, uiState.transactions.size) - } + items(items = transactions) { transaction -> + RecentTransactionListItem(transaction) } - } + if(isPaginating) { + item { + MifosProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + } + } } - @Composable fun RecentTransactionListItem(transaction: Transaction?) { Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { @@ -218,21 +198,15 @@ fun RecentTransactionListItem(transaction: Transaction?) { style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface ) - Row { Text( text = stringResource( id = R.string.string_and_string, transaction?.currency?.displaySymbol ?: transaction?.currency?.code ?: "", - CurrencyUtil.formatCurrency( - MifosSelfServiceApp.context, - transaction?.amount ?: 0.0, - ) + CurrencyUtil.formatCurrency(MifosSelfServiceApp.context, transaction?.amount ?: 0.0,) ), style = MaterialTheme.typography.labelMedium, - modifier = Modifier - .weight(1f) - .alpha(0.7f), + modifier = Modifier.weight(1f).alpha(0.7f), color = MaterialTheme.colorScheme.onSurface ) Text( @@ -249,28 +223,28 @@ fun RecentTransactionListItem(transaction: Transaction?) { class RecentTransactionScreenPreviewProvider : PreviewParameterProvider { - - var tList : ArrayList = ArrayList() override val values: Sequence get() = sequenceOf( - RecentTransactionUiState.RecentTransactions(tList), RecentTransactionUiState.Loading, - RecentTransactionUiState.LoadMoreRecentTransactions(tList), - RecentTransactionUiState.Error(R.string.recent_transactions), - RecentTransactionUiState.EmptyTransaction, - RecentTransactionUiState.Initial + RecentTransactionUiState.Error(""), + RecentTransactionUiState.Success(listOf(), canPaginate = true) ) } -@Preview(showSystemUi = true, showBackground = true) + +@Preview(showSystemUi = true) @Composable private fun RecentTransactionScreenPreview( @PreviewParameter(RecentTransactionScreenPreviewProvider::class) recentTransactionUiState: RecentTransactionUiState ) { - val viewmodel : RecentTransactionViewModel= hiltViewModel() MifosMobileTheme { - RecentTransactionScreenContent( + RecentTransactionScreen( uiState = recentTransactionUiState, - navigateBack = {}, {}, false, {}, viewmodel + navigateBack = {}, + onRetry = {}, + isRefreshing = false, + onRefresh = {}, + isPaginating = false, + loadMore = {} ) } } diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionViewModel.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionViewModel.kt index 9e3b2bf29..7e4eb646b 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionViewModel.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionViewModel.kt @@ -4,15 +4,15 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch import org.mifos.mobile.R +import org.mifos.mobile.models.Transaction +import org.mifos.mobile.models.client.Type import org.mifos.mobile.repositories.RecentTransactionRepository -import org.mifos.mobile.utils.RecentTransactionUiState import javax.inject.Inject @HiltViewModel @@ -20,43 +20,52 @@ class RecentTransactionViewModel @Inject constructor(private val recentTransacti ViewModel() { private val limit = 50 - private var loadmore = false + private var transactions: MutableList = mutableListOf() - private val _recentTransactionUiState = - MutableStateFlow(RecentTransactionUiState.Initial) + private val _recentTransactionUiState = MutableStateFlow(RecentTransactionUiState.Loading) val recentTransactionUiState: StateFlow = _recentTransactionUiState private val _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + private val _isPaginating = MutableStateFlow(false) + val isPaginating: StateFlow get() = _isPaginating.asStateFlow() + fun refresh() { _isRefreshing.value = true - loadRecentTransactions(false, 0) + loadInitialTransactions() + } + + fun loadPaginatedTransactions(offset: Int) { + _isPaginating.value = true + transactions.clear() + loadRecentTransactions(offset) } - fun loadRecentTransactions(loadmore: Boolean, offset: Int) { - this.loadmore = loadmore - loadRecentTransactions(offset, limit) + + fun loadInitialTransactions() { + _recentTransactionUiState.value = RecentTransactionUiState.Loading + loadRecentTransactions(0) } - private fun loadRecentTransactions(offset: Int, limit: Int) { + private fun loadRecentTransactions(offset: Int) { viewModelScope.launch { - _recentTransactionUiState.value = RecentTransactionUiState.Loading - recentTransactionRepositoryImp.recentTransactions(offset, limit).catch { - _recentTransactionUiState.value = - RecentTransactionUiState.Error(R.string.recent_transactions) - }.collect { - if (it.totalFilteredRecords == 0) { - _recentTransactionUiState.value = RecentTransactionUiState.EmptyTransaction - } else if (loadmore && it.pageItems.isNotEmpty()) { - _recentTransactionUiState.value = - RecentTransactionUiState.LoadMoreRecentTransactions(it.pageItems) - } else if (it.pageItems.isNotEmpty()) { - _recentTransactionUiState.value = - RecentTransactionUiState.RecentTransactions(it.pageItems) + recentTransactionRepositoryImp.recentTransactions(offset, limit) + .catch { + _recentTransactionUiState.value = RecentTransactionUiState.Error(it.message) + } + .collect { + transactions.plus(it.pageItems) + _recentTransactionUiState.value = RecentTransactionUiState.Success(transactions = transactions, canPaginate = it.pageItems.isNotEmpty()) + _isPaginating.emit(false) + _isRefreshing.emit(false) } - _isRefreshing.emit(false) - } } } -} \ No newline at end of file +} + +sealed class RecentTransactionUiState { + data object Loading : RecentTransactionUiState() + data class Error(val message: String?) : RecentTransactionUiState() + data class Success(val transactions: List, val canPaginate: Boolean) : RecentTransactionUiState() +} diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsComposeFragment.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsComposeFragment.kt new file mode 100644 index 000000000..531c1d1c6 --- /dev/null +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsComposeFragment.kt @@ -0,0 +1,42 @@ +package org.mifos.mobile.ui.recent_transactions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import org.mifos.mobile.R +import org.mifos.mobile.core.ui.component.mifosComposeView +import org.mifos.mobile.ui.activities.base.BaseActivity +import org.mifos.mobile.ui.fragments.base.BaseFragment + +/** + * @author Vishwwajeet + * @since 09/08/16 + */ +@AndroidEntryPoint +class RecentTransactionsComposeFragment : BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + (activity as? BaseActivity)?.hideToolbar() + return mifosComposeView(requireContext()) { + RecentTransactionScreen( + navigateBack = { activity?.onBackPressedDispatcher?.onBackPressed() } + ) + } + } + + companion object { + fun newInstance(): RecentTransactionsComposeFragment { + val fragment = RecentTransactionsComposeFragment() + val args = Bundle() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt index 2b2d09b54..30ac957cf 100644 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt +++ b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment.kt @@ -1,28 +1,54 @@ package org.mifos.mobile.ui.recent_transactions import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy +import android.widget.Toast import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.mifos.mobile.R -import org.mifos.mobile.core.ui.component.mifosComposeView -import org.mifos.mobile.core.ui.theme.MifosMobileTheme +import org.mifos.mobile.databinding.FragmentRecentTransactionsBinding +import org.mifos.mobile.models.Transaction +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.utils.* +import org.mifos.mobile.utils.Network.isConnected +import org.mifos.mobile.utils.ParcelableAndSerializableUtils.getCheckedArrayListFromParcelable +import javax.inject.Inject +/* /** * @author Vishwwajeet * @since 09/08/16 */ @AndroidEntryPoint -class RecentTransactionsFragment : BaseFragment() { +class RecentTransactionsFragment : BaseFragment(), OnRefreshListener { + + private var _binding: FragmentRecentTransactionsBinding? = null + private val binding get() = _binding!! + + @JvmField + @Inject + var recentTransactionsListAdapter: RecentTransactionListAdapter? = null private val recentTransactionViewModel: RecentTransactionViewModel by viewModels() + + private var sweetUIErrorHandler: SweetUIErrorHandler? = null + private var recentTransactionList: MutableList? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + recentTransactionList = ArrayList() } override fun onCreateView( @@ -30,12 +56,223 @@ class RecentTransactionsFragment : BaseFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View { + _binding = FragmentRecentTransactionsBinding.inflate(inflater, container, false) + sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) + showUserInterface() setToolbarTitle(getString(R.string.recent_transactions)) - return mifosComposeView(requireContext()) { - RecentTransactionScreen( - recentTransactionViewModel, - navigateBack = { activity?.onBackPressedDispatcher?.onBackPressed() }) + if (savedInstanceState == null) { + recentTransactionViewModel.loadRecentTransactions(false, 0) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + recentTransactionViewModel.recentTransactionUiState.collect{ + 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) + } + + RecentTransactionUiState.Initial -> {} + } + } + } + } + + binding.layoutError.btnTryAgain.setOnClickListener { + retryClicked() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelableArrayList( + Constants.RECENT_TRANSACTIONS, + ArrayList( + recentTransactionList, + ), + ) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (savedInstanceState != null) { + val transactions: List = + savedInstanceState.getCheckedArrayListFromParcelable( + Transaction::class.java, + Constants.RECENT_TRANSACTIONS, + ) ?: listOf() + showRecentTransactions(transactions) + } + } + + /** + * Setting up `rvRecentTransactions` + */ + fun showUserInterface() { + val layoutManager = LinearLayoutManager(activity) + layoutManager.orientation = LinearLayoutManager.VERTICAL + binding.rvRecentTransactions.layoutManager = layoutManager + binding.rvRecentTransactions.setHasFixedSize(true) + binding.rvRecentTransactions.addItemDecoration( + DividerItemDecoration( + requireActivity(), + layoutManager.orientation, + ), + ) + recentTransactionsListAdapter?.setTransactions(recentTransactionList) + binding.rvRecentTransactions.adapter = recentTransactionsListAdapter + binding.rvRecentTransactions.addOnScrollListener( + object : EndlessRecyclerViewScrollListener(layoutManager) { + override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { + recentTransactionViewModel.loadRecentTransactions(true, totalItemsCount) + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (!recyclerView.canScrollVertically(1) && + newState == RecyclerView.SCROLL_STATE_IDLE + ) { + Toaster.show(binding.root, R.string.no_more_transactions_available) + } + } + } + ) + binding.swipeTransactionContainer.setColorSchemeColors( + *requireActivity() + .resources.getIntArray(R.array.swipeRefreshColors), + ) + binding.swipeTransactionContainer.setOnRefreshListener(this) + } + + /** + * Refreshes the List of [Transaction] + */ + override fun onRefresh() { + if (binding.layoutError.root.visibility == View.VISIBLE) { + resetUI() } + recentTransactionViewModel.loadRecentTransactions(false, 0) + } + + /** + * Shows a Toast + */ + private fun showMessage(message: String?) { + (activity as BaseActivity?)?.showToast(message!!) + } + + /** + * Updates `recentTransactionsListAdapter` with `recentTransactionList` fetched from + * server + * + * @param recentTransactionList List of [Transaction] + */ + private fun showRecentTransactions(recentTransactionList: List?) { + this.recentTransactionList = recentTransactionList as MutableList? + recentTransactionsListAdapter?.setTransactions(recentTransactionList) + } + + /** + * Appends more Transactions in `recentTransactionList` + * + * @param transactions List of [Transaction] + */ + fun showLoadMoreRecentTransactions(transactions: List?) { + this.recentTransactionList?.addAll(recentTransactionList!!) + recentTransactionsListAdapter?.notifyDataSetChanged() + } + + private fun resetUI() { + sweetUIErrorHandler?.hideSweetErrorLayoutUI( + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } + + /** + * Hides `rvRecentTransactions` and shows a textview prompting no transactions + */ + private fun showEmptyTransaction() { + sweetUIErrorHandler?.showSweetEmptyUI( + getString(R.string.recent_transactions), + R.drawable.ic_error_black_24dp, + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } + + /** + * It is called whenever any error occurs while executing a request + * + * @param message Error message that tells the user about the problem. + */ + fun showErrorFetchingRecentTransactions(message: String?) { + if (!isConnected(requireActivity())) { + sweetUIErrorHandler?.showSweetNoInternetUI( + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } else { + sweetUIErrorHandler?.showSweetErrorUI( + message, + binding.rvRecentTransactions, + binding.layoutError.root, + ) + } + } + + private fun retryClicked() { + if (isConnected(requireContext())) { + sweetUIErrorHandler?.hideSweetErrorLayoutUI( + binding.rvRecentTransactions, + binding.layoutError.root, + ) + recentTransactionViewModel.loadRecentTransactions(false, 0) + } else { + Toast.makeText( + context, + getString(R.string.internet_not_connected), + Toast.LENGTH_SHORT, + ).show() + } + } + + fun showProgress() { + showSwipeRefreshLayout(true) + } + + fun hideProgress() { + showSwipeRefreshLayout(false) + } + + private fun showSwipeRefreshLayout(show: Boolean) { + binding.swipeTransactionContainer.post { + binding.swipeTransactionContainer.isRefreshing = show + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null } companion object { @@ -47,3 +284,5 @@ class RecentTransactionsFragment : BaseFragment() { } } } + + */ \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt b/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt deleted file mode 100644 index ac6521112..000000000 --- a/app/src/main/java/org/mifos/mobile/ui/recent_transactions/RecentTransactionsFragment_Old.kt +++ /dev/null @@ -1,286 +0,0 @@ -package org.mifos.mobile.ui.recent_transactions - -import android.os.Bundle -import android.os.Parcelable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener -import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import org.mifos.mobile.R -import org.mifos.mobile.databinding.FragmentRecentTransactionsBinding -import org.mifos.mobile.models.Transaction -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.utils.* -import org.mifos.mobile.utils.Network.isConnected -import org.mifos.mobile.utils.ParcelableAndSerializableUtils.getCheckedArrayListFromParcelable -import javax.inject.Inject - -/** - * @author Vishwwajeet - * @since 09/08/16 - */ -@AndroidEntryPoint -class RecentTransactionsFragment_Old : BaseFragment(), OnRefreshListener { - - private var _binding: FragmentRecentTransactionsBinding? = null - private val binding get() = _binding!! - - @JvmField - @Inject - var recentTransactionsListAdapter: RecentTransactionListAdapter? = null - - private val recentTransactionViewModel: RecentTransactionViewModel by viewModels() - - private var sweetUIErrorHandler: SweetUIErrorHandler? = null - private var recentTransactionList: MutableList? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - recentTransactionList = ArrayList() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - _binding = FragmentRecentTransactionsBinding.inflate(inflater, container, false) - sweetUIErrorHandler = SweetUIErrorHandler(activity, binding.root) - showUserInterface() - setToolbarTitle(getString(R.string.recent_transactions)) - if (savedInstanceState == null) { - recentTransactionViewModel.loadRecentTransactions(false, 0) - } - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - recentTransactionViewModel.recentTransactionUiState.collect{ - 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) - } - - RecentTransactionUiState.Initial -> {} - } - } - } - } - - binding.layoutError.btnTryAgain.setOnClickListener { - retryClicked() - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelableArrayList( - Constants.RECENT_TRANSACTIONS, - ArrayList( - recentTransactionList, - ), - ) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - if (savedInstanceState != null) { - val transactions: List = - savedInstanceState.getCheckedArrayListFromParcelable( - Transaction::class.java, - Constants.RECENT_TRANSACTIONS, - ) ?: listOf() - showRecentTransactions(transactions) - } - } - - /** - * Setting up `rvRecentTransactions` - */ - fun showUserInterface() { - val layoutManager = LinearLayoutManager(activity) - layoutManager.orientation = LinearLayoutManager.VERTICAL - binding.rvRecentTransactions.layoutManager = layoutManager - binding.rvRecentTransactions.setHasFixedSize(true) - binding.rvRecentTransactions.addItemDecoration( - DividerItemDecoration( - requireActivity(), - layoutManager.orientation, - ), - ) - recentTransactionsListAdapter?.setTransactions(recentTransactionList) - binding.rvRecentTransactions.adapter = recentTransactionsListAdapter - binding.rvRecentTransactions.addOnScrollListener( - object : EndlessRecyclerViewScrollListener(layoutManager) { - override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { - recentTransactionViewModel.loadRecentTransactions(true, totalItemsCount) - } - - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) - if (!recyclerView.canScrollVertically(1) && - newState == RecyclerView.SCROLL_STATE_IDLE - ) { - Toaster.show(binding.root, R.string.no_more_transactions_available) - } - } - } - ) - binding.swipeTransactionContainer.setColorSchemeColors( - *requireActivity() - .resources.getIntArray(R.array.swipeRefreshColors), - ) - binding.swipeTransactionContainer.setOnRefreshListener(this) - } - - /** - * Refreshes the List of [Transaction] - */ - override fun onRefresh() { - if (binding.layoutError.root.visibility == View.VISIBLE) { - resetUI() - } - recentTransactionViewModel.loadRecentTransactions(false, 0) - } - - /** - * Shows a Toast - */ - private fun showMessage(message: String?) { - (activity as BaseActivity?)?.showToast(message!!) - } - - /** - * Updates `recentTransactionsListAdapter` with `recentTransactionList` fetched from - * server - * - * @param recentTransactionList List of [Transaction] - */ - private fun showRecentTransactions(recentTransactionList: List?) { - this.recentTransactionList = recentTransactionList as MutableList? - recentTransactionsListAdapter?.setTransactions(recentTransactionList) - } - - /** - * Appends more Transactions in `recentTransactionList` - * - * @param transactions List of [Transaction] - */ - fun showLoadMoreRecentTransactions(transactions: List?) { - this.recentTransactionList?.addAll(recentTransactionList!!) - recentTransactionsListAdapter?.notifyDataSetChanged() - } - - private fun resetUI() { - sweetUIErrorHandler?.hideSweetErrorLayoutUI( - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } - - /** - * Hides `rvRecentTransactions` and shows a textview prompting no transactions - */ - private fun showEmptyTransaction() { - sweetUIErrorHandler?.showSweetEmptyUI( - getString(R.string.recent_transactions), - R.drawable.ic_error_black_24dp, - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } - - /** - * It is called whenever any error occurs while executing a request - * - * @param message Error message that tells the user about the problem. - */ - fun showErrorFetchingRecentTransactions(message: String?) { - if (!isConnected(requireActivity())) { - sweetUIErrorHandler?.showSweetNoInternetUI( - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } else { - sweetUIErrorHandler?.showSweetErrorUI( - message, - binding.rvRecentTransactions, - binding.layoutError.root, - ) - } - } - - private fun retryClicked() { - if (isConnected(requireContext())) { - sweetUIErrorHandler?.hideSweetErrorLayoutUI( - binding.rvRecentTransactions, - binding.layoutError.root, - ) - recentTransactionViewModel.loadRecentTransactions(false, 0) - } else { - Toast.makeText( - context, - getString(R.string.internet_not_connected), - Toast.LENGTH_SHORT, - ).show() - } - } - - fun showProgress() { - showSwipeRefreshLayout(true) - } - - fun hideProgress() { - showSwipeRefreshLayout(false) - } - - private fun showSwipeRefreshLayout(show: Boolean) { - binding.swipeTransactionContainer.post { - binding.swipeTransactionContainer.isRefreshing = show - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - companion object { - fun newInstance(): RecentTransactionsFragment_Old { - val fragment = RecentTransactionsFragment_Old() - val args = Bundle() - fragment.arguments = args - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt b/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt index 5fa15e671..ef256f710 100644 --- a/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt +++ b/app/src/main/java/org/mifos/mobile/utils/RecentTransactionUiState.kt @@ -1,13 +1,3 @@ package org.mifos.mobile.utils import org.mifos.mobile.models.Transaction - -sealed class RecentTransactionUiState { - object Initial : 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/utils/Utils.kt b/app/src/main/java/org/mifos/mobile/utils/Utils.kt index 1ff37259b..6c76e5bc3 100644 --- a/app/src/main/java/org/mifos/mobile/utils/Utils.kt +++ b/app/src/main/java/org/mifos/mobile/utils/Utils.kt @@ -76,14 +76,16 @@ object Utils { @JvmStatic fun formatTransactionType(type: String?): String { val builder = StringBuilder() - for (str in type?.lowercase(Locale.ROOT)?.split("_".toRegex())?.toTypedArray()!!) { - builder.append( - str[0].toString().uppercase(Locale.ROOT) + str.substring( - 1, - str.length, - ) + " ", - ) - } + try { + for (str in type?.lowercase(Locale.ROOT)?.split("_".toRegex())?.toTypedArray()!!) { + builder.append( + str[0].toString().uppercase(Locale.ROOT) + str.substring( + 1, + str.length, + ) + " ", + ) + } + } catch (_: Exception) {} return builder.toString() } } diff --git a/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt b/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt index 7d7caa32b..cecd058d6 100644 --- a/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt +++ b/app/src/test/java/org/mifos/mobile/viewModels/RecentTransactionViewModelTest.kt @@ -2,13 +2,11 @@ package org.mifos.mobile.viewModels import CoroutineTestRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer import app.cash.turbine.test import junit.framework.TestCase.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import okhttp3.ResponseBody import org.junit.* import org.junit.runner.RunWith import org.mifos.mobile.R @@ -24,7 +22,6 @@ import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.MockitoAnnotations import org.mockito.junit.MockitoJUnitRunner -import retrofit2.Response @RunWith(MockitoJUnitRunner::class) class RecentTransactionViewModelTest { @@ -80,7 +77,7 @@ class RecentTransactionViewModelTest { `when`(recentTransactionRepositoryImp.recentTransactions(offset, limit)) .thenReturn(flowOf(transactions)) viewModel.recentTransactionUiState.test { - viewModel.loadRecentTransactions(loadmore = false, offset) + viewModel.loadRecentTransactions(loadMore = false, offset) assertEquals(RecentTransactionUiState.Initial, awaitItem()) assertEquals(RecentTransactionUiState.Loading, awaitItem()) assertEquals(transactions.pageItems.let { RecentTransactionUiState.RecentTransactions(it) }, awaitItem()) @@ -108,7 +105,7 @@ class RecentTransactionViewModelTest { `when`(recentTransactionRepositoryImp.recentTransactions(offset, limit)) .thenReturn(flowOf(transactions)) viewModel.recentTransactionUiState.test { - viewModel.loadRecentTransactions(loadmore = false, offset) + viewModel.loadRecentTransactions(loadMore = false, offset) assertEquals(RecentTransactionUiState.Initial, awaitItem()) assertEquals(RecentTransactionUiState.Loading, awaitItem()) assertEquals(RecentTransactionUiState.EmptyTransaction, awaitItem()) @@ -138,7 +135,7 @@ class RecentTransactionViewModelTest { .thenReturn(flowOf(transactions)) viewModel.recentTransactionUiState.test { - viewModel.loadRecentTransactions(loadmore = false, offset) + viewModel.loadRecentTransactions(loadMore = false, offset) assertEquals(RecentTransactionUiState.Initial, awaitItem()) assertEquals(RecentTransactionUiState.Loading, awaitItem()) assertEquals(transactions.pageItems.let { RecentTransactionUiState.RecentTransactions(it) }, awaitItem())