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 onConnectedSean 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 onConnectedSean 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 onConnectedLilly
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 = itSean 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 AMuiStateSean 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