Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[2656] - refactor: Recent transactions from XML to compose #2602

Merged
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ dependencies {

// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ 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.notification.NotificationFragment
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
Expand Down Expand Up @@ -182,7 +183,7 @@ class HomeActivity :
}

R.id.item_recent_transactions -> replaceFragment(
RecentTransactionsFragment.newInstance(),
RecentTransactionsComposeFragment.newInstance(),
true,
R.id.container,
)
Expand Down Expand Up @@ -407,7 +408,7 @@ class HomeActivity :
setNavigationViewSelectedItem(R.id.item_accounts)
}

is RecentTransactionsFragment -> {
is RecentTransactionsComposeFragment -> {
setNavigationViewSelectedItem(R.id.item_recent_transactions)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package org.mifos.mobile.ui.recent_transactions

import androidx.compose.foundation.Image
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
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.Scaffold
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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
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 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.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.Utils

@Composable
fun RecentTransactionScreen(
viewModel: RecentTransactionViewModel = hiltViewModel(),
navigateBack: () -> Unit
) {
val uiState by viewModel.recentTransactionUiState.collectAsStateWithLifecycle()
val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
val isPaginating by viewModel.isPaginating.collectAsStateWithLifecycle()

LaunchedEffect(key1 = Unit) {
viewModel.loadInitialTransactions()
}

AvneetSingh2001 marked this conversation as resolved.
Show resolved Hide resolved
RecentTransactionScreen(
AvneetSingh2001 marked this conversation as resolved.
Show resolved Hide resolved
uiState = uiState,
navigateBack = navigateBack,
onRetry = { viewModel.loadInitialTransactions() },
isRefreshing = isRefreshing,
onRefresh = { viewModel.refresh() },
isPaginating = isPaginating,
loadMore = { offset -> viewModel.loadPaginatedTransactions(offset) }
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecentTransactionScreen(
uiState: RecentTransactionUiState,
navigateBack: () -> Unit,
onRetry: () -> Unit,
isRefreshing: Boolean,
onRefresh: () -> Unit,
isPaginating: Boolean,
loadMore: (offset: Int) -> Unit
) {
val context = LocalContext.current
val pullRefreshState = rememberPullToRefreshState()
AvneetSingh2001 marked this conversation as resolved.
Show resolved Hide resolved

MFScaffold(
topBarTitleResId = R.string.recent_transactions,
navigateBack = navigateBack,
scaffoldContent = { paddingValues ->
Box(modifier = Modifier.padding(paddingValues = paddingValues)) {
when (uiState) {
is RecentTransactionUiState.Error -> {
MifosErrorComponent(
isNetworkConnected = Network.isConnected(context),
isRetryEnabled = true,
onRetry = onRetry
)
}

is RecentTransactionUiState.Loading -> {
MifosProgressIndicatorOverlay()
}

is RecentTransactionUiState.Success -> {
if (uiState.transactions.isEmpty()) {
EmptyDataView(
icon = R.drawable.ic_error_black_24dp,
error = R.string.no_transaction,
modifier = Modifier.fillMaxSize()
)
} else {
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,
)
}

@Composable
fun RecentTransactionsContent(
transactions: List<Transaction>,
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

if (!isPaginating && canPaginate && isNearBottom) {
loadMore(transactions.size - 1)
}

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) {
Image(
painter = painterResource(id = R.drawable.ic_local_atm_black_24dp),
contentDescription = stringResource(id = R.string.atm_icon),
modifier = Modifier.size(40.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<RecentTransactionUiState> {
override val values: Sequence<RecentTransactionUiState>
get() = sequenceOf(
RecentTransactionUiState.Loading,
RecentTransactionUiState.Error(""),
RecentTransactionUiState.Success(listOf(), canPaginate = true)
)
}

@Preview(showSystemUi = true)
@Composable
private fun RecentTransactionScreenPreview(
@PreviewParameter(RecentTransactionScreenPreviewProvider::class) recentTransactionUiState: RecentTransactionUiState
) {
MifosMobileTheme {
AvneetSingh2001 marked this conversation as resolved.
Show resolved Hide resolved
RecentTransactionScreen(
uiState = recentTransactionUiState,
navigateBack = {},
onRetry = {},
isRefreshing = false,
onRefresh = {},
isPaginating = false,
loadMore = {}
)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.mifos.mobile.ui.recent_transactions

AvneetSingh2001 marked this conversation as resolved.
Show resolved Hide resolved
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
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 javax.inject.Inject

@HiltViewModel
class RecentTransactionViewModel @Inject constructor(private val recentTransactionRepositoryImp: RecentTransactionRepository) :
ViewModel() {

private val limit = 50
private var transactions: MutableList<Transaction> = mutableListOf()

private val _recentTransactionUiState = MutableStateFlow<RecentTransactionUiState>(RecentTransactionUiState.Loading)
val recentTransactionUiState: StateFlow<RecentTransactionUiState> = _recentTransactionUiState

private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean> get() = _isRefreshing.asStateFlow()

private val _isPaginating = MutableStateFlow(false)
val isPaginating: StateFlow<Boolean> get() = _isPaginating.asStateFlow()

fun refresh() {
_isRefreshing.value = true
loadInitialTransactions()
}

fun loadPaginatedTransactions(offset: Int) {
_isPaginating.value = true
transactions.clear()
loadRecentTransactions(offset)
}

fun loadInitialTransactions() {
_recentTransactionUiState.value = RecentTransactionUiState.Loading
loadRecentTransactions(0)
}

private fun loadRecentTransactions(offset: Int) {
viewModelScope.launch {
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)
}
}
}

}

sealed class RecentTransactionUiState {
data object Loading : RecentTransactionUiState()
data class Error(val message: String?) : RecentTransactionUiState()
data class Success(val transactions: List<Transaction>, val canPaginate: Boolean) : RecentTransactionUiState()
}
Loading
Loading