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

Product edit #4

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

/**
* Created by MD on 29.12.22.
*/
abstract class BaseViewModel<STATE : ViewState, EVENT : ViewEvent, COMMAND : ViewCommand>(val initialState: STATE) : ViewModel() {
abstract class BaseViewModel<STATE : ViewStateDTO, EVENT : ViewEvent, COMMAND : ViewCommand>(val initialState: STATE) : ViewModel() {

// todo - use explicit backing fields when project is switched to Kotlin 2.0, see https://github.com/Kotlin/KEEP/blob/explicit-backing-fields-re/proposals/explicit-backing-fields.md

Expand All @@ -29,8 +28,8 @@ abstract class BaseViewModel<STATE : ViewState, EVENT : ViewEvent, COMMAND : Vie
val eventChannel = Channel<EVENT>(Channel.UNLIMITED)

// For handling commands from VM to UI
private val _commandFlow = MutableSharedFlow<COMMAND>()
val commandFlow: Flow<COMMAND> = _commandFlow.asSharedFlow()
private val commandChannel = Channel<COMMAND>()
val commandFlow: Flow<COMMAND> = commandChannel.receiveAsFlow()

/**
* This is the job for all coroutines started by this ViewModel.
Expand Down Expand Up @@ -89,8 +88,10 @@ abstract class BaseViewModel<STATE : ViewState, EVENT : ViewEvent, COMMAND : Vie
/**
* Post a command to the UI.
*/
protected suspend fun sendCommand(command : COMMAND) {
_commandFlow.emit(command)
protected fun sendCommand(command : COMMAND) {
defaultScope.launch {
commandChannel.send(command)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cz.damat.thebeercounter.commonUI.base

import androidx.annotation.StringRes


/**
* Created by MD on 08.11.23.
*/
sealed class State {
object Loading : State()
object Content : State()
data class Error(@StringRes val message: Int) : State()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ import androidx.compose.runtime.Immutable
* Created by MD on 23.04.23.
*/
@Immutable
interface ViewState
interface ViewStateDTO
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cz.damat.thebeercounter.commonUI.compose.component

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cz.damat.thebeercounter.commonUI.base.State


/**
* Created by MD on 08.11.23.
*/
@Composable
fun StateWrapper(
modifier: Modifier = Modifier,
state: State,
customLoading: @Composable (BoxScope.() -> Unit)? = null,
customError: @Composable (BoxScope.() -> Unit)? = null,
content: @Composable () -> Unit,
) {
Box(modifier = modifier) {
when (state) {
State.Content -> content()
is State.Error -> customError?.invoke(this) ?: Error(message = stringResource(id = state.message))
State.Loading -> customLoading?.invoke(this) ?: Loading()
}
}
}

@Composable
private fun BoxScope.Loading(
) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(64.dp),
)
}

@Composable
private fun BoxScope.Error(
message: String,
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = message
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import androidx.lifecycle.flowWithLifecycle
import cz.damat.thebeercounter.commonUI.base.BaseViewModel
import cz.damat.thebeercounter.commonUI.base.ViewCommand
import cz.damat.thebeercounter.commonUI.base.ViewEvent
import cz.damat.thebeercounter.commonUI.base.ViewState
import cz.damat.thebeercounter.commonUI.base.ViewStateDTO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
Expand All @@ -25,7 +26,7 @@ import kotlinx.coroutines.plus
* Collects the [BaseViewModel.stateFlow] by using [collectAsStateWithLifecycle] and the [BaseViewModel.initialState].
*/
@Composable
fun <T : ViewState> BaseViewModel<T, *, *>.collectStateWithLifecycle(): State<T> {
fun <T : ViewStateDTO> BaseViewModel<T, *, *>.collectStateWithLifecycle(): State<T> {
return stateFlow.collectAsStateWithLifecycle(initialValue = this.initialState)
}

Expand Down Expand Up @@ -58,9 +59,7 @@ fun <T : ViewCommand> BaseViewModel<*, *, T>.collectCommand(
) {
val lifecycleOwner = LocalLifecycleOwner.current

LaunchedEffect(key1 = Unit) {
// unchanging key makes sure that the collection is started only once

LaunchedEffect(lifecycleOwner.lifecycle, commandFlow) {
commandFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.onEach(block) // since the command flow in not a StateFlow the block is called only at the time something is posted to the flow.
.launchIn(this + baseCoroutineExceptionHandler)
Expand Down
4 changes: 4 additions & 0 deletions commonUI/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<string name="history">History</string>
<string name="more">More</string>

<string name="action_edit">Edit</string>
<string name="action_reset">Reset</string>
<string name="action_delete">Delete</string>
<string name="action_set_count">Set count</string>
Expand All @@ -17,11 +18,14 @@

<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="back">Back</string>
<string name="clear">Clear</string>
<string name="expand">Expand</string>
<string name="collapse">Collapse</string>

<string name="name">Name</string>
<string name="beer">Beer</string>
<string name="clear_all_confirm_message">Do you really want to delete all items?</string>
<string name="edit_product">Edit product</string>
<string name="error_product_not_found">Product not found</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import cz.damat.thebeercounter.commonlib.room.entity.HistoryItemType
import cz.damat.thebeercounter.commonlib.room.entity.INITIAL_ITEM_ID
import cz.damat.thebeercounter.commonlib.room.entity.Product
import cz.damat.thebeercounter.componentCounter.domain.repository.ProductRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext


/**
Expand All @@ -18,6 +20,10 @@ import kotlinx.coroutines.flow.first

class ProductRepositoryImpl(private val db: AppDatabase, private val productDao: ProductDao, private val historyItemDao: HistoryItemDao) : ProductRepository {

override suspend fun getProduct(id: Int): Product? {
return productDao.getProduct(id)
}

override fun getShownProductsFlow() = productDao.getProductsFlow(true)

//todo - use for allowing the user to delete currently not shown products
Expand Down Expand Up @@ -64,6 +70,26 @@ class ProductRepositoryImpl(private val db: AppDatabase, private val productDao:
}
}

override suspend fun updateProductNameAndSetCount(id: Int, name: String, count: Int) {
db.withTransaction {
productDao.getProduct(id)?.let { product ->
val oldCount = product.count
productDao.upsert(product.copy(name = name, count = count))

if (oldCount != count) {
historyItemDao.upsert(
HistoryItem(
productId = id,
oldCount = oldCount,
newCount = count,
type = HistoryItemType.MANUAL
)
)
}
}
}
}

override suspend fun hideProduct(id: Int) {
db.withTransaction {
productDao.getProduct(id)?.let { product ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import org.koin.core.component.KoinComponent

interface ProductRepository : KoinComponent {

suspend fun getProduct(id: Int): Product?

fun getShownProductsFlow(): Flow<List<Product>>

//todo - use for allowing the user to delete currently not shown products
Expand All @@ -23,6 +25,8 @@ interface ProductRepository : KoinComponent {

suspend fun setProductCount(id: Int, count: Int, type: HistoryItemType)

suspend fun updateProductNameAndSetCount(id: Int, name: String, count: Int)

suspend fun hideProduct(id: Int)

suspend fun clearAllAndAddInitialProduct(initialItemName: String)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cz.damat.thebeercounter.featureCounter

import cz.damat.thebeercounter.featureCounter.scene.counter.CounterScreenViewModel
import cz.damat.thebeercounter.featureCounter.scene.edit.EditScreenViewModel
import cz.damat.thebeercounter.featureCounter.scene.history.HistoryViewModel
import org.koin.android.ext.koin.androidApplication
import org.koin.androidx.viewmodel.dsl.viewModel
Expand All @@ -14,6 +15,11 @@ val featureCounterKoinModule = module {
viewModel {
CounterScreenViewModel(get(), androidApplication().resources)
}

viewModel { (productId: Int) ->
EditScreenViewModel(productId, get())
}

viewModel {
HistoryViewModel(get())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ sealed class CounterCommand : ViewCommand {
object ShowAddNewDialog : CounterCommand()
object PerformHapticFeedback : CounterCommand()
data class ShowSetCountDialog(val product: Product) : CounterCommand()
data class OpenEdit(val productId: Int) : CounterCommand()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import cz.damat.thebeercounter.commonUI.R
import cz.damat.thebeercounter.commonUI.compose.component.CardThemed
import cz.damat.thebeercounter.commonUI.compose.component.ConfirmDialog
Expand All @@ -31,6 +32,7 @@ import cz.damat.thebeercounter.featureCounter.scene.counter.dialog.AddNewProduct
import cz.damat.thebeercounter.featureCounter.scene.counter.dialog.SetCountDialog
import cz.damat.thebeercounter.commonUI.compose.theme.disabled
import cz.damat.thebeercounter.commonUI.compose.utils.vibrateStrong
import cz.damat.thebeercounter.featureCounter.scene.dashboard.RouteEdit
import org.koin.androidx.compose.get
import java.text.NumberFormat
import java.util.*
Expand All @@ -40,11 +42,18 @@ import java.util.*
* Created by MD on 23.04.23.
*/
@Composable
fun CounterScreen() {
fun CounterScreen(
navController: NavController
) {
val viewModel: CounterScreenViewModel = get()
val viewState = viewModel.collectStateWithLifecycle()
val onEvent = viewModel.getOnEvent()
CommandCollector(viewModel = viewModel, onEvent)

CommandCollector(
viewModel,
navController,
onEvent
)

CounterScreenContent(viewState = viewState.value, onEvent = onEvent)
}
Expand All @@ -58,6 +67,7 @@ private fun Preview() {
@Composable
private fun CommandCollector(
viewModel: CounterScreenViewModel,
navController: NavController,
onEvent: OnEvent
) {
val showSetCountDialogForProduct = remember {
Expand All @@ -84,6 +94,9 @@ private fun CommandCollector(
CounterCommand.PerformHapticFeedback -> {
view.vibrateStrong()
}
is CounterCommand.OpenEdit -> {
navController.navigate("$RouteEdit/${it.productId}")
}
}
}

Expand Down Expand Up @@ -262,10 +275,10 @@ private fun ProductDropdown(shown: MutableState<Boolean>, product: Product, onEv
}

enum class MenuItem(@StringRes val titleRes: Int) {
Edit(R.string.action_edit),
Reset(R.string.action_reset),
Hide(R.string.action_delete),
SetCount(R.string.action_set_count),
//todo - modify product item and dialog
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class CounterScreenViewModel(
private fun onMenuItemClick(menuItem: MenuItem, id: Int) {
ioScope.launch {
when (menuItem) {
MenuItem.Edit -> {
sendCommand(CounterCommand.OpenEdit(id))
}
MenuItem.Reset -> {
productRepository.setProductCount(id, 0, HistoryItemType.RESET)
}
Expand All @@ -69,9 +72,7 @@ class CounterScreenViewModel(
}
MenuItem.SetCount -> {
currentState().products?.firstOrNull { it.id == id }?.let {
defaultScope.launch {
sendCommand(CounterCommand.ShowSetCountDialog(it))
}
sendCommand(CounterCommand.ShowSetCountDialog(it))
}
}
}
Expand All @@ -85,9 +86,7 @@ class CounterScreenViewModel(
}

private fun onClearAllClicked() {
defaultScope.launch {
sendCommand(CounterCommand.ShowClearAllConfirmDialog)
}
sendCommand(CounterCommand.ShowClearAllConfirmDialog)
}

private fun onClearAllConfirmed() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package cz.damat.thebeercounter.featureCounter.scene.counter

import cz.damat.thebeercounter.commonUI.base.ViewState
import cz.damat.thebeercounter.commonUI.base.ViewStateDTO
import cz.damat.thebeercounter.commonlib.room.entity.Product
import kotlinx.collections.immutable.ImmutableList

Expand All @@ -10,4 +10,4 @@ import kotlinx.collections.immutable.ImmutableList
*/
data class CounterViewState(
val products : ImmutableList<Product>? = null
) : ViewState
) : ViewStateDTO
Loading
Loading