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.Sean McQuillan [G]
12/04/2020, 10:59 PMLilly
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 AMSean McQuillan [G]
12/05/2020, 1:09 AMonConnected
is called by recomposition instead of an event handler. Trying to figure out how to advise an alternativeSean McQuillan [G]
12/05/2020, 1:12 AMviewModel
and onConnected
Sean McQuillan [G]
12/05/2020, 1:14 AMonConnected
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 recompositionSean McQuillan [G]
12/05/2020, 1:15 AMonConnected
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 AMSean McQuillan [G]
12/05/2020, 1:16 AMSean McQuillan [G]
12/05/2020, 1:17 AMSean McQuillan [G]
12/05/2020, 1:17 AMSean McQuillan [G]
12/05/2020, 1:18 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 stuffSean McQuillan [G]
12/05/2020, 1:20 AMSean McQuillan [G]
12/05/2020, 1:21 AMnull
)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 AMSean McQuillan [G]
12/05/2020, 1:24 AMLilly
12/05/2020, 1:25 AMLilly
12/05/2020, 1:26 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 AMSean McQuillan [G]
12/05/2020, 1:27 AMSean McQuillan [G]
12/05/2020, 1:28 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 AMSean McQuillan [G]
12/05/2020, 1:30 AMuiState
is driving recomposition of this composable, then as a side-effect of recomposition it's calling onConnected
Sean McQuillan [G]
12/05/2020, 1:30 AMonConnected
and uiState
are hoisted to different levelsSean McQuillan [G]
12/05/2020, 1:31 AMuiState
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 AMSean McQuillan [G]
12/05/2020, 1:34 AMitems
, and the event to notify the viewModel about an item clickSean McQuillan [G]
12/05/2020, 1:34 AMLilly
12/05/2020, 1:35 AMLilly
12/05/2020, 1:52 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
Sean McQuillan [G]
12/05/2020, 1:55 AMSean McQuillan [G]
12/05/2020, 1:56 AMonConnected
safely (though since it's an event I'd recommend ensuring it's on Main dispatcher before calling it)Sean McQuillan [G]
12/05/2020, 1:57 AMSean McQuillan [G]
12/05/2020, 1:58 AMLilly
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 AMSean McQuillan [G]
12/05/2020, 1:59 AMLilly
12/05/2020, 2:00 AMSean McQuillan [G]
12/05/2020, 2:00 AMSean McQuillan [G]
12/05/2020, 2:04 AMSean McQuillan [G]
12/05/2020, 2:04 AMSean McQuillan [G]
12/05/2020, 2:05 AMSean McQuillan [G]
12/05/2020, 2:06 AMSean McQuillan [G]
12/05/2020, 2:06 AMSean McQuillan [G]
12/05/2020, 2:07 AMSean McQuillan [G]
12/05/2020, 2:08 AMuiState
Sean McQuillan [G]
12/05/2020, 2:08 AMLilly
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???Lilly
12/05/2020, 2:13 AMScannerScreen(onConnected = {
backStack.push(Routing.Test)
})
Sean McQuillan [G]
12/05/2020, 2:28 AMSean McQuillan [G]
12/05/2020, 2:29 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
}
}
}
}
Lilly
12/05/2020, 3:23 AMLilly
12/05/2020, 3:31 AMScreenContent
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 PMSean McQuillan [G]
12/07/2020, 4:58 PMSean McQuillan [G]
12/07/2020, 5:00 PMSean McQuillan [G]
12/07/2020, 5:01 PMSean McQuillan [G]
12/07/2020, 5:07 PMLilly
12/07/2020, 6:28 PMLilly
12/07/2020, 6:31 PMScaffold
for instance?Sean McQuillan [G]
12/07/2020, 8:10 PMSean 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