Skip to content

Commit

Permalink
[add] Transaction screen search closes #39
Browse files Browse the repository at this point in the history
* Move logic from TransactionListScreenViewModel to TransactionsStore
* Implement search by transactions name / merchant name using Jaro Winker Similarity. This may not be the best solution but I've left the code open to use sql "like" instead if I change my mind
  • Loading branch information
ruffCode committed Sep 12, 2021
1 parent 3d174b7 commit 26683d4
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import tech.alexib.yaba.android.ui.theme.YabaTheme
import tech.alexib.yaba.android.util.moneyFormat
import tech.alexib.yaba.model.Transaction
import tech.alexib.yaba.stubs.TransactionStubs
import kotlin.math.absoluteValue


@Composable
Expand Down Expand Up @@ -126,9 +127,12 @@ private fun TransactionItemContent(
}

CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
val textStyle =
if (transaction.amount <= 0) MaterialTheme.typography.body2.copy(color = MoneyGreen)
else MaterialTheme.typography.body2
Text(
text = "$${moneyFormat.format(transaction.amount)}",
style = MaterialTheme.typography.body2,
text = "$${moneyFormat.format(transaction.amount.absoluteValue)}",
style = textStyle,
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(0.2f)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package tech.alexib.yaba.android.ui.transactions

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
Expand All @@ -30,31 +31,43 @@ import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.benasher44.uuid.Uuid
import kotlinx.coroutines.delay
import kotlinx.datetime.LocalDate
import org.koin.androidx.compose.getViewModel
import tech.alexib.yaba.android.R
import tech.alexib.yaba.android.ui.components.LoadingScreenWithCrossFade
import tech.alexib.yaba.android.ui.components.TransactionItem
import tech.alexib.yaba.android.ui.theme.YabaTheme
import tech.alexib.yaba.android.util.format
import tech.alexib.yaba.android.util.rememberFlowWithLifecycle
import tech.alexib.yaba.data.store.TransactionsStore
import tech.alexib.yaba.model.Transaction
import tech.alexib.yaba.stubs.TransactionStubs


@Composable
fun TransactionListScreen(onBack: () -> Unit, onSelected: (Uuid) -> Unit) {
val viewModel: TransactionListScreenViewModel = getViewModel()

TransactionListScreen(viewModel, onBack, onSelected)
}

Expand All @@ -64,19 +77,34 @@ private fun TransactionListScreen(
onBack: () -> Unit,
onSelected: (Uuid) -> Unit,
) {
val state by rememberFlowWithLifecycle(flow = viewModel.state).collectAsState(
initial = emptyList()
val state by rememberFlowWithLifecycle(
flow = viewModel.state
).collectAsState(
initial = TransactionsStore.State.Empty
)

TransactionListScreen(state, onBack, onSelected)
val loading = remember { mutableStateOf(state.transactions.isEmpty()) }

LaunchedEffect(Unit) {
delay(1000)
loading.value = false
}
LoadingScreenWithCrossFade(loadingState = state.transactions.isEmpty()) {
TransactionListScreen(state, onBack, onSelected) {
viewModel.store.submit(it)
}
}
}

@Composable
private fun TransactionListScreen(
transactions: List<Transaction>,
state: TransactionsStore.State,
handleBack: () -> Unit,
onSelected: (Uuid) -> Unit
onSelected: (Uuid) -> Unit,
actioner: (TransactionsStore.Action) -> Unit
) {


Scaffold(
topBar = {
Box(
Expand All @@ -94,23 +122,75 @@ private fun TransactionListScreen(
) {
Icon(Icons.Filled.ArrowBack, stringResource(R.string.back_arrow))
}
Text(
text = stringResource(R.string.transactions),
modifier = Modifier
.align(Alignment.TopCenter)
.padding(12.dp)
)

AnimatedVisibility(
visible = state.searching,
modifier = Modifier.align(Alignment.TopCenter)
) {
val query = remember { mutableStateOf(TextFieldValue(state.query)) }
TextField(
value = query.value,
onValueChange = {
query.value = it
actioner(TransactionsStore.Action.SetQuery(it.text))
},
maxLines = 1,
singleLine = true,
modifier = Modifier
.wrapContentHeight(),
colors = TextFieldDefaults
.textFieldColors(backgroundColor = MaterialTheme.colors.surface),

leadingIcon = {
AnimatedVisibility(visible = state.query.isEmpty()) {
Icon(
imageVector = Icons.Outlined.Search,
contentDescription = "search"
)
}
},
trailingIcon = {
AnimatedVisibility(visible = state.query.isNotEmpty()) {
IconButton(onClick = {
query.value = TextFieldValue("")
actioner(
TransactionsStore.Action.SetQuery(
""
)
)
}) {
Icon(
imageVector = Icons.Outlined.Clear,
contentDescription = "Clear input"
)
}
}

}
)
}
AnimatedVisibility(
visible = !state.searching, modifier = Modifier.align(
Alignment.TopEnd
)
) {
IconButton(
onClick = { actioner(TransactionsStore.Action.SetSearching) },
) {
Icon(imageVector = Icons.Outlined.Search, contentDescription = "search")
}
}
}
},
) {
TransactionList(transactions = transactions) {
TransactionList(transactions = state.transactions) {
onSelected(it)
}
}
}

@Composable
fun TransactionList(
private fun TransactionList(
transactions: List<Transaction>,
onSelected: (Uuid) -> Unit
) {
Expand Down Expand Up @@ -167,9 +247,9 @@ private fun TransactionItemPreview() {
private fun TransactionListScreenPreview() {
YabaTheme {
TransactionListScreen(
transactions = TransactionStubs.transactionsWellsFargo1,
TransactionsStore.State(transactions = TransactionStubs.transactionsWellsFargo1),
onSelected = {},
handleBack = {}
)
) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,25 @@ package tech.alexib.yaba.android.ui.transactions

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import tech.alexib.yaba.data.repository.TransactionRepository
import org.koin.core.parameter.parametersOf
import tech.alexib.yaba.data.store.TransactionsStore
import tech.alexib.yaba.util.stateInDefault

class TransactionListScreenViewModel : ViewModel(), KoinComponent {
private val transactionRepository: TransactionRepository by inject()
val state = transactionRepository.getAll().stateInDefault(viewModelScope, emptyList())

val store: TransactionsStore by inject { parametersOf(Dispatchers.Main) }
val state = store.state.stateInDefault(viewModelScope, TransactionsStore.State.Empty)

override fun onCleared() {
store.dispose()
super.onCleared()
}

init {
store.init()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface TransactionDao {
suspend fun insert(transactions: List<TransactionDto>)
suspend fun deleteByAccountId(accountId: Uuid)
fun selectRecent(userId: Uuid): Flow<List<Transaction>>
fun selectAllLikeName(userId: Uuid, query: String? = null): Flow<List<Transaction>>
fun selectAll(userId: Uuid): Flow<List<Transaction>>
fun selectById(id: Uuid): Flow<TransactionDetail?>
fun selectAllByAccountId(accountId: Uuid): Flow<List<Transaction>>
Expand Down Expand Up @@ -82,6 +83,10 @@ interface TransactionDao {
queries.deleteById(id)
}
}

override fun selectAllLikeName(userId: Uuid, query: String?): Flow<List<Transaction>> =
queries.selectAllByName(userId, query ?: "", transactionMapper).asFlow().mapToList()
.flowOn(backgroundDispatcher)
}

val transactionMapper: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,10 @@ WHERE account_id = ?;
deleteById:
DELETE FROM TransactionEntity
WHERE id = ?;

selectAllByName:
SELECT * FROM UserTransaction
WHERE user_id = :userId
AND merchant_name LIKE ('%' || :query || '%')
OR name LIKE ('%' || :query || '%')
ORDER BY date DESC;
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import tech.alexib.yaba.model.Transaction

class ObserveTransactions(
private val transactionsRepository: TransactionRepository
) : SubjectInteractor<Unit, List<Transaction>>() {
) : SubjectInteractor<ObserveTransactions.Params, List<Transaction>>() {

override fun createObservable(params: Unit): Flow<List<Transaction>> {
override fun createObservable(params: Params): Flow<List<Transaction>> {
return transactionsRepository.getAll()
}

data class Params(val query: String?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import tech.alexib.yaba.model.TransactionDetail

interface TransactionRepository {
fun recentTransactions(): Flow<List<Transaction>>
fun getAll(): Flow<List<Transaction>>
fun getAll(query: String? = null): Flow<List<Transaction>>
fun getById(id: Uuid): Flow<TransactionDetail?>
fun getAllByAccountId(accountId: Uuid): Flow<List<Transaction>>
}
Expand All @@ -40,12 +40,20 @@ internal class TransactionRepositoryImpl(
emitAll(dao.selectRecent(userIdProvider.userId.value))
}

override fun getAll(): Flow<List<Transaction>> =
flow { emitAll(dao.selectAll(userIdProvider.userId.value)) }
override fun getAll(query: String?): Flow<List<Transaction>> =
flow {
emitAll(
if (!query.isNullOrEmpty()) dao.selectAllLikeName(
userIdProvider.userId.value,
query
) else dao.selectAll(userIdProvider.userId.value)
)
}

override fun getById(id: Uuid): Flow<TransactionDetail?> = flow {
emitAll(dao.selectById(id))
}

override fun getAllByAccountId(accountId: Uuid): Flow<List<Transaction>> = flow {
emitAll(dao.selectAllByAccountId(accountId))
}
Expand Down
Loading

0 comments on commit 26683d4

Please sign in to comment.