https://kotlinlang.org logo
k

KotlinLeaner

10/04/2022, 12:07 PM
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

Zach Klippenstein (he/him) [MOD]

10/04/2022, 2:47 PM
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

KotlinLeaner

10/04/2022, 2:50 PM
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

Zach Klippenstein (he/him) [MOD]

10/04/2022, 2:52 PM
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

KotlinLeaner

10/04/2022, 2:54 PM
okk sure I'll never tag again
z

Zach Klippenstein (he/him) [MOD]

10/04/2022, 2:54 PM
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

KotlinLeaner

10/04/2022, 2:54 PM
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

Zach Klippenstein (he/him) [MOD]

10/04/2022, 2:56 PM
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

KotlinLeaner

10/04/2022, 2:58 PM
okk sure, I change now
into viewmodel then
Another question Can I pass
context
in viewmodel? Is it recommended way?
z

Zach Klippenstein (he/him) [MOD]

10/04/2022, 3:20 PM
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

KotlinLeaner

10/04/2022, 3:22 PM
Ok sure, I'll try now. I really appreciate it.
15 Views