Is a good way to use State<Boolean> in view model with Android Compose? - kotlin

I have read some sample codes for learning Compose.
I find many sample projects use Code A to create a StateFlow in view model, then convert it to State in #Composable function, the UI will be updated automatically when drawerOpen is changed.
1: I think both Code B and Code C can do the same thing, right? Why does many projects seldom to use them?
2: Is Code A a good way ?
3: I needn't to add rememberSaveable for variable drawerOpen in #Composable fun myRoute(...) because view model will store data, right?
Code A
class MainViewModel : ViewModel() {
private val _drawerShouldBeOpened = MutableStateFlow(false)
val drawerShouldBeOpened: StateFlow<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen by MainViewModel.drawerShouldBeOpened.collectAsState() //Do I need add rememberSaveable ?
...
}
Code B
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = mutableStateOf(false)
val drawerShouldBeOpened: State<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = MainViewModel.drawerShouldBeOpened //Do I need add rememberSaveable ?
...
}
Code C
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = false
val drawerShouldBeOpened: Boolean = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = rememberSaveable { mutableStateOf(MainViewModel.drawerShouldBeOpened)) //Can I remove rememberSaveable ?
}

There are multiple questions here.
Let me answer whatever is possible.
1. Where should you use remember / rememberSaveable? (Code A, B, or C)
Only in code C it is required.
(No issues in using in code A and B as well, but no advantages there)
Reason,
In code A and B - the state is maintained in the view model. Hence the value survives recomposition.
But in code C, the state is created and maintained inside the composable. Hence remember is required for the value to survive recomposition.
More details in Docs
2. Why Code C is not used much?
Composable recomposition happens whenever there is a change in state, not the value.
Given below is a simple example to demonstrate the same.
class ToggleViewModel : ViewModel() {
private val _enabledStateFlow = MutableStateFlow(false)
val enabledStateFlow: StateFlow<Boolean> = _enabledStateFlow
private val _enabledState = mutableStateOf(false)
val enabledState: State<Boolean> = _enabledState
private var _enabled = false
val enabled: Boolean = _enabled
fun setEnabledStateFlow(isEnabled: Boolean) {
_enabledStateFlow.value = isEnabled
}
fun setEnabledState(isEnabled: Boolean) {
_enabledState.value = isEnabled
}
fun setEnabled(isEnabled: Boolean) {
_enabled = isEnabled
}
}
#Composable
fun BooleanToggle(
viewmodel: ToggleViewModel = ToggleViewModel(),
) {
val enabledStateFlow by viewmodel.enabledStateFlow.collectAsState()
val enabledState by viewmodel.enabledState
val enabled by rememberSaveable {
mutableStateOf(viewmodel.enabled)
}
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledStateFlow) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledStateFlow(!enabledStateFlow) }) {
Text("Toggle State Flow")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledState) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledState(!enabledState) }) {
Text("Toggle State")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabled) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabled(!enabled) }) {
Text("Toggle Value")
}
}
}
}
You can see that the third text will NOT update on clicking the button.
The reason is that the mutable state inside the composable was created using an initial value from the view model data. But further updates to that data will not be reflected in the composable.
To get updates, we have to use reactive data like Flow, LiveData, State, and their variants.
3. Using StateFlow vs State.
From the docs, you can see that compose supports Flow, LiveData and RxJava.
You can see in the usage that we are using collectAsState() for StateFlow.
The method converts StateFlow to State. So both can be used.
Use Flow if the layers beyond ViewModel (like repo) are the data sources and they use Flow data type.
Else MutableState should be fine.

Related

Navigation with Arguments with Jetpack Compose

I've been troubleshooting this issue for couple of days now about navigating with args in jetpack compose. I feel like I'm missing something simple, really simple that I can't figure out somehow.
I tried making sure I'm coding it correctly but to no avail I still get the error.
So the error:
Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/second_screen/Item Six } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0x442b361f) route=home_screen}
I feel like I'm done everything correct but still cant get through this error. Or there is something simple that I'm missing.
My NavHost:
interface NavigationDestination{
val route: String
}
#Composable
fun MainNavHost(
){
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = HomeScreenNavigation.route,
){
composable(HomeScreenNavigation.route){
HomeScreen(onItemClick = {nameString->
navController.navigate("${SecondNavigationDestination.route}/${nameString}")
{
launchSingleTop = true
}
})
}
composable(
route = SecondNavigationDestination.routeWithArg,
arguments = listOf(navArgument(SecondNavigationDestination.nameArg)
{type = NavType.StringType})
){
SecondScreen()
}
}
}
My Screens
First Screen:
object HomeScreenNavigation: NavigationDestination{
override val route = "home_screen"
}
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
){
ItemList(
itemList = LocalData.itemList,
onItemClick = onItemClick
)
}
#Composable
fun ItemList(
itemList: List<ItemModel>,
onItemClick: (String) -> Unit,
modifier: Modifier = Modifier
){
LazyColumn(
verticalArrangement = Arrangement.spacedBy(9.dp)
){
items(
items = itemList,
key = {it.id}
){
Item(
item = it,
onItemClick = onItemClick
)
}
}
}
#Composable
fun Item(
item: ItemModel,
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
){
Row(
modifier = modifier
.fillMaxWidth()
.padding(9.dp)
.clickable { onItemClick(item.name) },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
){
Text(
text = item.name
)
}
}
///////////////////////////////////////////////////////
Second Screen:
object SecondNavigationDestination: NavigationDestination{
override val route = "second_screen"
const val nameArg = "nameArg"
val routeWithArg = "${route}/$nameArg"
}
#Composable
fun SecondScreen(
){
val viewModel: MainViewModel = viewModel()
Box(
contentAlignment = Alignment.Center
){
Text(
viewModel.id
)
}
}
/////////////////////////////////////////////////
ViewModel:
class MainViewModel(
savedStateHandle: SavedStateHandle
): ViewModel(){
val id: String = savedStateHandle[SecondNavigationDestination.nameArg] ?: "empty"
}
Change this
val routeWithArg = "${route}/$nameArg"
to this
val routeWithArg = "${route}/{$nameArg}"
For more info: docs

jetpack Compose row/column onClick navigation

Please bear with me I don't know how to properly phrase my question, but I'll try my best.
I made the screen above by putting muliple composables together.
I gave the composable carrying the row an onClick function;
#Composable
fun MenuGrid(
modifier: Modifier = Modifier,
onMenuCardClick: (Int) -> Unit = {},
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(3),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
.height(200.dp)
) {
items(MenuData) { item ->
MenuCard(
drawable = item.drawable,
text = item.text,
modifier = Modifier
.height(56.dp)
.clickable { onMenuCardClick(item.drawable + item.text) }
)
}
}
}
Like I said earlier I put multiple composables together, to form the image above. I arranged the multiple composables in a composable, I called MenuContentScreen;
#Composable
fun MenuContentScreen(modifier: Modifier = Modifier) {
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)) {
MenuQuote()
MenuContentSection(title = R.string.favorite_collections) {
MenuGrid()
}
}
}
Then I referenced/called the MenuContentScreen on the main Composable of that screen MenuScreen. (The one defined in NavHost)
#Composable
fun MenuScreen() {
MenuContentScreen(Modifier.padding())
}
Which is where the problem is, since the onClick function was defined in another composable, I can't use the onClick function for the MenuScreen;
So MY QUESTION is there a way I can link the onClick function on MenuGrid to MenuScreen, maybe creating an parameter on MenuScreen and assigning it to the MenuGrid's onClick function (which I have tried and got val cannot be assigned), or Any thing at all.
I will greatly appreciate any help. I have been on this like forever. No information is too small please.
Thanks in Advance.
You need to carry onClick lambda till your MenuScreen Composable such as
#Composable
fun MenuContentScreen(modifier: Modifier = Modifier,
onMenuCardClick: () -> Unit
) {
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)) {
MenuQuote()
MenuContentSection(title = R.string.favorite_collections) {
MenuGrid(){
onMenuCardClick()
}
}
}
#Composable
fun MenuScreen(
onMenuCardClick: () -> Unit
) {
MenuContentScreen(Modifier.padding(), onClick= onMenuCardClick)
}
And in you nav graph without passing navController to MenuScreen
call
MenuContentScreen {
navController.navigate()
}

Expose value from SharedPreferences as Flow

I'm trying to get a display scaling feature to work with JetPack Compose. I have a ViewModel that exposes a shared preferences value as a flow, but it's definitely incorrect, as you can see below:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val _densityFactor: MutableStateFlow<Float> = MutableStateFlow(1.0f)
val densityFactor: StateFlow<Float>
get() = _densityFactor.asStateFlow()
private fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
// This is what I look at and go, "this is really bad."
private fun densityFactorFlow(): Flow<Float> = flow {
while (true) {
emit(getDensityFactorFromSharedPrefs())
}
}
init {
viewModelScope.launch(Dispatchers.IO) {
densityFactorFlow().collectLatest {
_densityFactor.emit(it)
}
}
}
}
Here's my Composable:
#Composable
fun MyPageRoot(
modifier: Modifier = Modifier,
viewModel: MyViewModel = hiltViewModel()
) {
val densityFactor by viewModel.densityFactor.collectAsState(initial = 1.0f)
CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density * densityFactor
)
) {
// Content
}
}
And here's a slider that I want to slide with my finger to set the display scaling (the slider is outside the content from the MyPageRoot and will not change size on screen while the user is using the slider).
#Composable
fun ScreenDensitySetting(
modifier: Modifier = Modifier,
viewModel: SliderViewModel = hiltViewModel()
) {
var sliderValue by remember { mutableStateOf(viewModel.getDensityFactorFromSharedPrefs()) }
Text(
text = "Zoom"
)
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
onValueChangeFinished = { viewModel.setDisplayDensity(sliderValue) },
enabled = true,
valueRange = 0.5f..2.0f,
steps = 5,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colors.secondary,
activeTrackColor = MaterialTheme.colors.secondary
)
)
}
The slider composable has its own viewmodel
#HiltViewModel
class PersonalizationMenuViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
fun setDisplayDensity(density: Float) {
viewModelScope.launch {
val sharedPreference = context.getSharedPreferences(
"MEAL_ASSEMBLY_PREFS",
Context.MODE_PRIVATE
)
val editor = sharedPreference.edit()
editor.putFloat("density", density)
editor.apply()
}
}
}
I know that I need to move all the shared prefs code into a single class. But how would I write the flow such that it pulled from shared prefs when the value changed? I feel like I need a listener of some sort, but very new to Android development.
Your comment is right, that's really bad. :) You should create a OnSharedPreferenceChangeListener so it reacts to changes instead of locking up the CPU to constantly check it preemptively.
There's callbackFlow for converting listeners into Flows. You can use it like this:
fun SharedPreferences.getFloatFlowForKey(keyForFloat: String) = callbackFlow<Float> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (keyForFloat == key) {
trySend(getFloat(key, 0f))
}
}
registerOnSharedPreferenceChangeListener(listener)
if (contains(key)) {
send(getFloat(key, 0f)) // if you want to emit an initial pre-existing value
}
awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
}.buffer(Channel.UNLIMITED) // so trySend never fails
Then your ViewModel becomes:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
val densityFactor: StateFlow<Float> = sharedPreferences
.getFloatFlowForKey("density")
.stateIn(viewModelScope, SharingStarted.Eagerly, 1.0f)
}

Can I wrap collectAsState with remember when I use Jetpack Compose?

I hope to share a parameter val isCanAddRecord by mViewMode.isCanAddRecord.collectAsState() among #Composable functions.
The Code A is based the article How can I share info among #Composable function in Android Studio?
I know collectAsState() is wrapped with remember, you can see the Source Code.
Now you will find the object watchState is wrapped with remember, and watchState.isCanAddRecord which is assiged to mViewMode.isCanAddRecord.collectAsState() is wrapped with remember again.
Will the Code A cause error?
Code A
#Composable
fun ScreenHome_Watch(
modifier: Modifier = Modifier,
mViewMode: SoundViewModel,
watchState:WatchState = rememberWatchState(mViewMode)
){
...
}
class WatchState(
val isCanAddRecord: State<Boolean>,
...
){
...
}
#Composable
fun rememberWatchState(mViewMode: SoundViewModel): WatchState {
val watchState = WatchState(mViewMode.isCanAddRecord.collectAsState())
return remember {
watchState
}
}
Source Code
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
Of course you can remember your mutableState or any other remember or anything that is not Composable. You can remember measurePolicy or even another code block as lambda for drawing like Modifier.drawWithCache does. Jetnews App sample does what you about. This is a matter of preference, you can store anything that is not Composable inside your remember block.
/**
* Remembers the content for each tab on the Interests screen
* gathering application data from [InterestsViewModel]
*/
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
// UiState of the InterestsScreen
val uiState by interestsViewModel.uiState.collectAsState()
// Describe the screen sections here since each section needs 2 states and 1 event.
// Pass them to the stateless InterestsScreen using a tabContent.
val topicsSection = TabContent(Sections.Topics) {
val selectedTopics by interestsViewModel.selectedTopics.collectAsState()
TabWithSections(
sections = uiState.topics,
selectedTopics = selectedTopics,
onTopicSelect = { interestsViewModel.toggleTopicSelection(it) }
)
}
val peopleSection = TabContent(Sections.People) {
val selectedPeople by interestsViewModel.selectedPeople.collectAsState()
TabWithTopics(
topics = uiState.people,
selectedTopics = selectedPeople,
onTopicSelect = { interestsViewModel.togglePersonSelected(it) }
)
}
val publicationSection = TabContent(Sections.Publications) {
val selectedPublications by interestsViewModel.selectedPublications.collectAsState()
TabWithTopics(
topics = uiState.publications,
selectedTopics = selectedPublications,
onTopicSelect = { interestsViewModel.togglePublicationSelected(it) }
)
}
return listOf(topicsSection, peopleSection, publicationSection)
}
val tabContent = rememberTabContent(interestsViewModel)
val (currentSection, updateSection) = rememberSaveable {
mutableStateOf(tabContent.first().section)
}
Remember lambda
fun Modifier.drawWithCache(
onBuildDrawCache: CacheDrawScope.() -> DrawResult
) = composed(
inspectorInfo = debugInspectorInfo {
name = "drawWithCache"
properties["onBuildDrawCache"] = onBuildDrawCache
}
) {
val cacheDrawScope = remember { CacheDrawScope() }
this.then(DrawContentCacheModifier(cacheDrawScope, onBuildDrawCache))
}
Remember layout policy which is widely used with layouts
#Composable
#UiComposable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content:
#Composable #UiComposable BoxWithConstraintsScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
SubcomposeLayout(modifier) { constraints ->
val scope = BoxWithConstraintsScopeImpl(this, constraints)
val measurables = subcompose(Unit) { scope.content() }
with(measurePolicy) { measure(measurables, constraints) }
}
}

android modern development: how can I understand user did not change any data in a screen?

I built a fragment, using jetpack compose for adding views on screen and state and view model classes for saving state.
first time when I navigate to this fragment, fetch API is called and If I have any value on this API, I filled text fields with them. It is possible that the API is empty and do not return any value.
Users can enter any data on this text fields or can ignore them because filling the text fields is optional and when he/she clicks on submit button; the data is saved in server by calling save API and shows a successful message that data is saved.
my question is that when user navigates to this fragment and does not enter or change any value,
when clicks on submit button, I do not want to show that toast.
how can I handle it? thanks.
First you will need to create a data class to hold the values for your screen as a mutable state with a default value.
data class SampleState(
val isLoading: Boolean = true,
val someText1: String = "",
val someText2: String = ""
)
It is also necessary to create a sealed class with the possible screen actions, such as changing someText1 and someText2 and also the click of a button.
sealed class SampleAction {
object TestButton : SampleAction()
data class ChangeSomeText1(val text1: String) : SampleAction()
data class ChangeSomeText2(val text2: String) : SampleAction()
}
And finally, a sealed class to contain the possible one-time-event that can be triggered through some action, like after clicking on the button with the fields properly filled in.
sealed class SampleChannel {
object ValidFields : SampleChannel()
}
The view model will be responsible for managing all the logic:
#HiltViewModel
class SampleViewModel #Inject constructor() : ViewModel() {
var state by mutableStateOf(value = SampleState())
private set
private val _channel = Channel<SampleChannel>()
val channel = _channel.receiveAsFlow()
init {
loadInitialFakeData()
}
fun onAction(action: SampleAction) {
when (action) {
is SampleAction.TestButton -> testButtonClicked()
is SampleAction.ChangeSomeText1 -> state = state.copy(someText1 = action.text1)
is SampleAction.ChangeSomeText2 -> state = state.copy(someText2 = action.text2)
}
}
private fun loadInitialFakeData() = viewModelScope.launch {
delay(timeMillis = 1000)
if (Random.nextBoolean()) state = state.copy(
someText1 = "fake text 1 from api",
someText2 = "fake text 2 from api"
)
state = state.copy(isLoading = false)
}
private fun testButtonClicked() {
val hasBlankField = listOf(
state.someText1,
state.someText2
).any { string ->
string.isBlank()
}
if (hasBlankField) return
viewModelScope.launch {
_channel.send(SampleChannel.ValidFields)
}
}
}
In this case there is a loadInitialFakeData function that is executed as soon as the view model is instantiated, which simulates a delay only for testing purposes and according to a random boolean assigns or not values in the state class.
There is also a function testButtonClicked that will do the logic if any of the fields is empty, if any of them is empty nothing happens (I left it that way so as not to extend the code too much) and if it is not empty we notify the channel with the ValidFields.
The only public function in the view model is the onAction, which is responsible for receiving actions through the composable screen.
Now on the composable screen we can do it like this:
#Composable
fun SampleScreen(
viewModel: SampleViewModel = hiltViewModel()
) {
val state = viewModel.state
val scaffoldState = rememberScaffoldState()
LaunchedEffect(key1 = Unit) {
viewModel.channel.collect { channel ->
when (channel) {
SampleChannel.ValidFields -> {
scaffoldState.snackbarHostState.showSnackbar(
message = "all right!"
)
}
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
Column(modifier = Modifier.fillMaxSize()) {
ComposeLoading(isLoading = state.isLoading)
ComposeForm(
changeSomeText1 = {
viewModel.onAction(SampleAction.ChangeSomeText1(it))
},
changeSomeText2 = {
viewModel.onAction(SampleAction.ChangeSomeText2(it))
},
testButtonClick = {
viewModel.onAction(SampleAction.TestButton)
},
state = state
)
}
}
}
#Composable
private fun ComposeLoading(
isLoading: Boolean
) = AnimatedVisibility(
modifier = Modifier.fillMaxWidth(),
visible = isLoading,
enter = expandVertically(animationSpec = tween()),
exit = shrinkVertically(animationSpec = tween())
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(height = 16.dp))
Text(
text = "loading...",
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
#Composable
private fun ComposeForm(
changeSomeText1: (String) -> Unit,
changeSomeText2: (String) -> Unit,
testButtonClick: () -> Unit,
state: SampleState
) = Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
ComposeSimpleTextField(
value = state.someText1,
onValueChange = {
changeSomeText1(it)
},
label = "text field 1"
)
Spacer(modifier = Modifier.height(height = 16.dp))
ComposeSimpleTextField(
value = state.someText2,
onValueChange = {
changeSomeText2(it)
},
label = "text field 2"
)
Spacer(modifier = Modifier.height(height = 16.dp))
Button(
modifier = Modifier.align(alignment = Alignment.End),
onClick = testButtonClick
) {
Text(text = "Test Button")
}
}
#Composable
private fun ComposeSimpleTextField(
value: String,
onValueChange: (String) -> Unit,
label: String
) = TextField(
modifier = Modifier.fillMaxWidth(),
value = value,
onValueChange = { onValueChange(it) },
label = {
Text(text = label)
}
)
The main thing here is the LaunchedEffect to collect all possible events from the SampleChannel (on this example there is only one) and the calls to screen events when the text of some field is changed or when the button is clicked... The rest is just a compose boilerplate that doesn't matter much for the main logic.
Note: I used Hilt for dependency injection in this example, so I had access to #HiltViewModel and hiltViewModel()
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-android-compiler:2.42'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'