KotlinLeaner
10/04/2022, 12:07 PMclass PairViewModel : ViewModel() {
var isBluetoothEnabled = mutableStateOf(false)
private set
fun onBluetoothEnable(value: Boolean) {
isBluetoothEnabled.value = value
}
}
PairScreen.kt
class PairScreenState(context: Context, viewModel: PairViewModel) {
private val bluetoothManager: BluetoothManager = context.getSystemService(BluetoothManager::class.java)
private val bluetoothAdapter: BluetoothAdapter by lazy {
bluetoothManager.adapter
}
init {
viewModel.onBluetoothEnable(bluetoothAdapter.isEnabled)
}
fun checkBluetoothStatus(bluetoothStatus: MutableState<Boolean>): BroadcastReceiver {
return object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR
)) {
BluetoothAdapter.STATE_OFF -> {
bluetoothStatus.value = false
}
BluetoothAdapter.STATE_ON -> {
bluetoothStatus.value = true
}
}
}
}
}
}
}
@Composable
fun rememberPairScreenState(
context: Context,
viewModel: PairViewModel
) = remember {
PairScreenState(context, viewModel)
}
@Composable
fun PairContent(
context: Context = LocalContext.current,
viewModel: PairViewModel = getViewModel(),
rememberPairScreenState: PairScreenState = rememberPairScreenState(context, viewModel),
) {
AnimatedVisibility(visible = true) {
AppBarScaffold() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
rememberPairScreenState.checkBluetoothStatus(viewModel.isBluetoothEnabled).apply {
context.registerReceiver(this, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
}
if (viewModel.isBluetoothEnabled.value) {
println(">> Enable >>>")
} else {
println(">> Disable >>>")
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun PairContentPreview() {
PairContent()
}
I am using Bluetooth as example to understand state holder in my use case. Please guide me if you find anything wrong in my code. ThanksZach Klippenstein (he/him) [MOD]
10/04/2022, 2:47 PMcheckBluetoothStatus
is slightly misleading since that method doesn’t actually check anything, it just creates a glorified callback. It’s also recommended not to pass MutableStates around directly.
The way I would probably write this is to make your ViewModel or something in a layer above it know about the bluetooth service, be responsible for registering and unregistering the listener on lifecycle changes, and exposing the state you care about (either as a Flow or a State). Then all your composable would do is read that state.KotlinLeaner
10/04/2022, 2:50 PMcheckBluetoothStatus
changes to registerBluetoothStatus
and this function is callback. So you are suggesting to use this function method in viewmodel
instead of stateHolder
?Zach Klippenstein (he/him) [MOD]
10/04/2022, 2:52 PMrememberPairScreenState.checkBluetoothStatus(viewModel.isBluetoothEnabled).apply {
context.registerReceiver(this, IntentFilter(ACTION_STATE_CHANGED))
}
There’s no effect there. Also this code leaks the receiver since it never unregisters it – you’d want to at least use DisposableEffect
to do that.KotlinLeaner
10/04/2022, 2:54 PMZach Klippenstein (he/him) [MOD]
10/04/2022, 2:54 PMcheckBluetoothStatus
also doesn’t register anything – it only creates an object. The actual register call is done in your composable on the next line.KotlinLeaner
10/04/2022, 2:54 PMclass PairScreenState(
private val context: Context,
private val viewModel: BloodPressurePairViewModel
) {
private val bluetoothManager: BluetoothManager = context.getSystemService(BluetoothManager::class.java)
private val broadcastReceiver: BroadcastReceiver by lazy {
registerBluetoothStatus(viewModel.isBluetoothEnabled).apply {
context.registerReceiver(this, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
}
}
private val bluetoothAdapter: BluetoothAdapter by lazy {
bluetoothManager.adapter
}
val isBluetoothEnabled = viewModel.isBluetoothEnabled
private fun registerBluetoothStatus(bluetoothStatus: MutableState<Boolean>): BroadcastReceiver {
return object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR
)) {
BluetoothAdapter.STATE_OFF -> {
bluetoothStatus.value = false
}
BluetoothAdapter.STATE_ON -> {
bluetoothStatus.value = true
}
}
}
}
}
}
fun bluetoothEnable() {
viewModel.onBluetoothEnable(bluetoothAdapter.isEnabled)
}
fun unRegisterBluetoothStatus() {
(context as ComponentActivity).unregisterReceiver(broadcastReceiver)
}
}
@Composable
fun rememberPairScreenState(
context: Context = LocalContext.current,
viewModel: BloodPressurePairViewModel = getViewModel()
) = remember {
PairScreenState(context, viewModel)
}
@Composable
fun PairContent(
rememberPairScreenState: PairScreenState = rememberPairScreenState(),
) {
AnimatedVisibility(visible = true) {
AppBarScaffold(displayHomeAsUpEnabled = true) {
Column(
modifier = Modifier
.padding(dimensionResource(R.dimen.margin_screen_edge_sides))
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
EventsLifecycles(onResume = {
rememberPairScreenState.bluetoothEnable()
}, onStop = {
rememberPairScreenState.unRegisterBluetoothStatus()
})
if (rememberPairScreenState.isBluetoothEnabled.value) {
println(">> Enable >>>")
} else {
println(">> Disable >>>")
}
}
}
}
}
@Composable
fun EventsLifecycles(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onResume: () -> Unit,
onStop: () -> Unit
) {
DisposableEffect(lifecycleOwner) {
val lifeCycleEventObserver = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
onResume()
}
Lifecycle.Event.ON_PAUSE -> {
onStop()
}
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(lifeCycleEventObserver)
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifeCycleEventObserver)
}
}
}
@Preview(showBackground = true)
@Composable
fun PairContentPreview() {
PairContent()
}
I used like this way..class BloodPressurePairViewModel : BaseViewModel() {
var isBluetoothEnabled = mutableStateOf(false)
private set
fun onBluetoothEnable(value: Boolean) {
isBluetoothEnabled.value = value
}
}
Zach Klippenstein (he/him) [MOD]
10/04/2022, 2:56 PMViewModel
is a type of state holder. But not all state holders have to be `ViewModel`s. If you already have a ViewModel
, you don’t need to create a separate state holder class just for the sake of it.PairScreenState
and put it in BloodPressurePairViewModel
.KotlinLeaner
10/04/2022, 2:58 PMcontext
in viewmodel? Is it recommended way?Zach Klippenstein (he/him) [MOD]
10/04/2022, 3:20 PMViewModel
that can take whatever it needs from the composition, then call it from a LaunchedEffect
.KotlinLeaner
10/04/2022, 3:22 PM