Hello guys, I am learning in <State> in jetpack co...
# compose
k
Hello guys, I am learning in State in jetpack compose. I found that State holders as source of truth. So created my some data can you guys guide me if I am doing wrong here.
PairViewModel.kt
Copy code
class PairViewModel : ViewModel() {

    var isBluetoothEnabled = mutableStateOf(false)
        private set

    fun onBluetoothEnable(value: Boolean) {
        isBluetoothEnabled.value = value
    }
}
PairScreen.kt
Copy code
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. Thanks
z
Things like registering broadcast receivers are considered side effects and should not be ran directly from composition. Interacting with system services is something that your UI composition, which is all part of your view layer, should generally not be responsible for. The name
checkBluetoothStatus
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.
k
Yes I used side effect in my code.
the function name
checkBluetoothStatus
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]
z
Copy code
rememberPairScreenState.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.
You don’t need to tag someone who’s already replied to a thread, they’ll automatically get notified about new messages
k
okk sure I'll never tag again
z
But
checkBluetoothStatus
also doesn’t register anything – it only creates an object. The actual register call is done in your composable on the next line.
k
Copy code
class 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..
Is anything wrong here?
Copy code
class BloodPressurePairViewModel : BaseViewModel() {

    var isBluetoothEnabled = mutableStateOf(false)
        private set

    fun onBluetoothEnable(value: Boolean) {
        isBluetoothEnabled.value = value
    }
}
z
A
ViewModel
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.
I would take everything in
PairScreenState
and put it in
BloodPressurePairViewModel
.
k
okk sure, I change now
into viewmodel then
Another question Can I pass
context
in viewmodel? Is it recommended way?
z
I think the usual pattern for that sort of thing is to put a suspend function on your
ViewModel
that can take whatever it needs from the composition, then call it from a
LaunchedEffect
.
k
Ok sure, I'll try now. I really appreciate it.