Lilly
12/04/2020, 1:17 AM@Composable
fun BluetoothDeviceListAdapterComponent(
items: List<BluetoothDeviceWrapper>,
connectVM: ConnectViewModel,
onConnected: () -> Unit
) {
// initial state is a problem here
val connectState: BluetoothConnectState by connectVM.state.collectAsState()
onCommit(connectState) {
Timber.w("connectState changed")
when(connectState) {
is BluetoothConnectState.Loading -> {
Timber.w("Loading")
}
is BluetoothConnectState.Success -> {
Timber.w("GoTo next screen.")
onConnected()
}
is BluetoothConnectState.Failure -> {
// how to call toast from here?
}
}
}
state
is a MutableStateFlow
and can be Loading, Success or Failure(val errorMessage). First problem is that MutableStateFlow
needs an initial value which triggers onCommit
on first start but it should only be triggered when viewModel changes its state explicitly. Second problem is when state is Success and next screen is started: When I get back to the screen, onCommit
is trigged with value Success and starts the next screen again, although the state hasn't changed. Last question would be how to call a Toast from onCommit
? Is this approach even optimal for my use case?Sean McQuillan [G]
12/04/2020, 10:56 PMconnectedState
as an event in this code and trying to modify other states in response to it changing. This route will make UDF hard to implement.
There's a few bits of state here that I see, that you may find easier if you model them separately:
1. The current screen
2. A toast message
Then, in compose consume the states that describe the UI at the present time.Lilly
12/05/2020, 1:06 AMIt appears you're treatingThe scenario is a little bit different:as an event in this code and trying to modify other states in response to it changing.connectedState
@Composable
fun BluetoothDeviceListAdapterComponent(
items: List<BluetoothDeviceWrapper>,
viewModel: ConnectViewModel,
onConnected: () -> Unit
) {
val uiState: BluetoothConnectState by viewModel.state.collectAsState() /* state */
when (uiState) {
is BluetoothConnectState.Loading -> Text("Loading...")
is BluetoothConnectState.Success -> onConnected() // --> next screen
is BluetoothConnectState.Failure -> // TODO
}
LazyColumnFor(items) { item ->
BluetoothDeviceListItemComponent(item, onActionClick = { device ->
viewModel.connect(device) /* event */
}
})
}
Every BluetoothDeviceListItemComponent
has a Button which triggers the onActionClick
event which in turn triggers a non-suspending function in the VM that is consuming a flow. The results are stored in a StateFlow<BluetoothConnectState>
which is observed by the BluetoothDeviceListAdapterComponent
. The first result is Loading, when bt device is connected, state changes to Success otherwise Failure. I can't figure out how to handle the initial state. A Loading composable should only be displayed after event onActionClick
is triggered because the flow itself already exposes a Loading state at the right time.Sean McQuillan [G]
12/05/2020, 1:08 AMonConnected
is called by recomposition instead of an event handler. Trying to figure out how to advise an alternativeviewModel
and onConnected
onConnected
as an event changes state that's hoisted somewhere higher in the tree. This leads you to build a composition -> event bridge like here where you call onConnected
by recompositiononConnected
may be called multiple times due to recomposition and (2) onConnected
may be called during a composition that is later discarded when it shouldn'tLilly
12/05/2020, 1:16 AMBluetoothDeviceListAdapterComponent
composable. It could be also in the highest levelSean McQuillan [G]
12/05/2020, 1:16 AMonConnectDevice: (Device) -> Unit
as a parameter to this composableLilly
12/05/2020, 1:20 AMWhat's happening is thatas an event changes state that's hoisted somewhere higher in the treeonConnected
Sean McQuillan [G]
12/05/2020, 1:20 AMitems: ..., uiState: LoadingState, onConnectDevice: ...)
and simply displays stuffnull
)Lilly
12/05/2020, 1:24 AMWhat's happening is thatWhat state doesas an event changes state that's hoisted somewhere higher in the treeonConnected
onConnected
change?Sean McQuillan [G]
12/05/2020, 1:24 AMLilly
12/05/2020, 1:25 AMIn this case, the "current screen" state should be determined at the same place as the connected status state -> which is in the ViewModelWhat is the current screen state here?
Sean McQuillan [G]
12/05/2020, 1:27 AMLilly
12/05/2020, 1:28 AMSean McQuillan [G]
12/05/2020, 1:28 AMLilly
12/05/2020, 1:29 AMSean McQuillan [G]
12/05/2020, 1:29 AMuiState
is driving recomposition of this composable, then as a side-effect of recomposition it's calling onConnected
onConnected
and uiState
are hoisted to different levelsuiState
to the same place as the state onConnected
modifies
2. Call onConnected
from an event handler (instead of in response to state changes)Lilly
12/05/2020, 1:32 AMSean McQuillan [G]
12/05/2020, 1:33 AMitems
, and the event to notify the viewModel about an item clickLilly
12/05/2020, 1:35 AMitems
and a onConnectDevice: (Device) -> Unit
parameter. This will hoist the state up to a higher level but how to go further from here. Fact is that onConnected
is a reaction to a state change not? or what do you mean with
it seems likely that it's this viewModels responsibility to dispatch the follow on event
Sean McQuillan [G]
12/05/2020, 1:54 AMviewModel.connect
in the code you pasted) eventually calls onConnected
Lilly
12/05/2020, 1:54 AMprivate val _state: MutableStateFlow<ConnectBluetoothDeviceUseCase.BluetoothConnectState> =
MutableStateFlow(
ConnectBluetoothDeviceUseCase.BluetoothConnectState.Loading
)
val state: StateFlow<ConnectBluetoothDeviceUseCase.BluetoothConnectState> = _state.asStateFlow()
fun connect(device: BluetoothDevice) {
viewModelScope.launch {
connectBluetoothDevicesUseCase.connect(device).collect {
_state.value = it
}
}
}
Sean McQuillan [G]
12/05/2020, 1:55 AMLilly
12/05/2020, 1:55 AMSean McQuillan [G]
12/05/2020, 1:55 AM_state.value = it
onConnected
safely (though since it's an event I'd recommend ensuring it's on Main dispatcher before calling it)Lilly
12/05/2020, 1:58 AM_state.value
part. onConnected
is this:
@Composable
fun DcInternEntryPoint(defaultRouting: Routing) {
Router(defaultRouting) { backStack ->
when (backStack.last()) {
is Routing.Scanner -> {
ScannerScreen(onConnected = {
backStack.push(Routing.Test)
})
}
}
}
}
Sean McQuillan [G]
12/05/2020, 1:59 AMLilly
12/05/2020, 2:00 AMSean McQuillan [G]
12/05/2020, 2:00 AMuiState
Lilly
12/05/2020, 2:13 AMonConnect
in VM but then I'm aksing how should this look like. I have no reference to the backStack
in VM???ScannerScreen(onConnected = {
backStack.push(Routing.Test)
})
Sean McQuillan [G]
12/05/2020, 2:28 AMScannerScreenViewModel
, but you can call onConnected()
from itLilly
12/05/2020, 3:22 AMfun connect(device: BluetoothDevice, onConnected: () -> Unit) {
viewModelScope.launch {
connectBluetoothDevicesUseCase.connect(device).collect { connectState ->
when (connectState) {
BluetoothConnectState.Loading -> _state.value = connectState
BluetoothConnectState.Success -> onConnected()
is BluetoothConnectState.Failure -> _state.value = connectState
}
}
}
}
ScreenContent
function would display additional elements like a Button, Text or whatever. Now with the current implementation, every time discoveryState
is changed, not only the BluetoothDeviceListAdapterComponent
composable recomposes but also the additional elements like Button, Text would recompose. --> So wouldn't it be better to keep the discoveryState
in BluetoothDeviceListAdapterComponent
so that this is the only composable that is recomposed?Sean McQuillan [G]
12/07/2020, 4:57 PMLilly
12/07/2020, 6:28 PMScaffold
for instance?Sean McQuillan [G]
12/07/2020, 8:10 PMLilly
12/07/2020, 8:51 PMThe state->event trampoline always introduces one recomposition delay to trigger the event.and what skipping do you mean here?
Iff you profiled and found that this state was a trigger for unacceptable levels of work after skipping there's a few solutions
Sean McQuillan [G]
12/09/2020, 6:12 AMLilly
12/10/2020, 10:57 AM