I have 2 Mutable States in my ViewModel and want them to observe the textfields in my composable.
ViewModel:
#HiltViewModel
class AddEditTransactionViewModel #Inject constructor(
private val moneyManagerUseCases: MoneyManagerUseCases
) : ViewModel() {
private val _transactionDescription = mutableStateOf("")
val transactionDescription: State<String> = _transactionDescription
private val _transactionAmount = mutableStateOf("")
val transactionAmount: State<String> = _transactionAmount
Composable:
#Composable
fun AddEditTransactionScreen(
navController: NavController,
viewModel: AddEditTransactionViewModel = hiltViewModel(),
) {
// This works
var descriptionState by remember { mutableStateOf("") }
var amountState by remember { mutableStateOf("") }
// This doesn't work
var viewModelDescriptionState = viewModel.transactionDescription.value
var viewModelAmountState = viewModel.transactionAmount.value
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
value = viewModelDescriptionState,
onValueChange = { viewModelDescriptionState = it },
label = { Text(text = "Description") })
Spacer(modifier = Modifier.padding(20.dp))
TextField(
value = viewModelAmountState,
onValueChange = { viewModelAmountState = it },
label = { Text(text = "Amount") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
It works if I use the state from my composable but that kind of beats the purpose of the ViewModel.
The first line:
var descriptionState by remember { mutableStateOf(") }
It works because you're updating the value of mutableState via delegation. It's easier to understand if you look at how you would do same without delegation:
val descriptionState = remember { mutableStateOf("") }
//...
descriptionState.value = "new value"
remember will save same object between recompositions, but this object have value field which is changing.
On the other hand, here:
var viewModelDescriptionState = viewModel.transactionDescription.value
you create a local variable, and any changes will not be saved between recompositions.
This is usually solved by creating a setter function within the view model. You can also use delegation in the view model, if you want to restrict it to update without a setter, you can add private set, instead of having private _variable and public variable:
var transactionDescription by mutableStateOf("")
private set
fun updateTransactionDescription(newValue: String) {
transactionDescription = newValue
}
This is considered best practice because the setter can contain more logic than a simple update value. In the case where you only need to store it, you can remove the private part and update it directly, without the setter function.
You can find more information about the state in documentation Compose, including this youtube video which explains the basic principles.
Solution
AddEditTransactionScreen:
#Composable
fun AddEditTransactionScreen(
navController: NavController,
viewModel: AddEditTransactionViewModel = hiltViewModel()
) {
val descriptionState = viewModel.transactionDescription
val amountState = viewModel.transactionAmount
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
value = descriptionState.value,
onValueChange = { viewModel.updateTransactionDescription(it) },
label = { Text(text = "Description") })
Spacer(modifier = Modifier.padding(20.dp))
TextField(
value = amountState.value,
onValueChange = { viewModel.updateTransactionAmount(it) },
label = { Text(text = "Amount") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
}
AddEditTransactionViewModel:
#HiltViewModel
class AddEditTransactionViewModel #Inject constructor() : ViewModel() {
private val _transactionDescription = mutableStateOf("")
val transactionDescription: State<String> = _transactionDescription
private val _transactionAmount = mutableStateOf("")
val transactionAmount: State<String> = _transactionAmount
fun updateTransactionDescription(value: String) {
_transactionDescription.value = value
}
fun updateTransactionAmount(value: String) {
_transactionAmount.value = value
}
}
Explanation
Its transactionDescription and transactionAmount attributes are of the State<String> type, the State type cannot have its value changed, unlike the MutableState type, a simple solution that maintains the change protection only by the view model, is creating a method for each attribute that you want to change and make this change in the attribute that is MutableState.
Related
I am using compose LazyColumn with viewModel updating the list items by having inside my viewModel:
data class ContactsListUiState(
val contacts: MutableList<Contact>
)
#HiltViewModel
class ContactsViewModel #Inject constructor(savedStateHandle: SavedStateHandle) : ViewModel() {
private val _contactsListUiState = MutableStateFlow(ContactsListUiState(mutableListOf()))
val contactsListUiState: StateFlow<ContactsListUiState> = _contactsListUiState.asStateFlow()
private fun updateContactsList(newContacts: MutableList<Contact>) {
_contactsListUiState.update{ currentState ->
currentState.copy(
contacts = newContacts
)
}
}
such that my updateContactsList() function updates the stateFlow and I am supposed to collect the contacts list with collectAsStateWithLifecycle() inside my composable function inside MainActivity
#OptIn(ExperimentalLifecycleComposeApi::class, ExperimentalFoundationApi::class)
#Composable
fun ContactsListScreen(
navController: NavController,
modifier: Modifier = Modifier
) {
Log.d("ViewModel", "ContactsListScreen recomposed")
val uiState by contactsViewModel.contactsListUiState.collectAsStateWithLifecycle()
Box(modifier = modifier) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
Column {
TextField(
value = searchText,
singleLine = true,
onValueChange = {
searchText = it
Log.d("MainActivity", "calling filter with $it")
contactsViewModel.filterContactsByString(it)
},
leadingIcon = if (searchText.isNotBlank()) searchLeadingIcon else null,
trailingIcon = if (searchText.isBlank()) searchTrailingIcon else null,
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
)
LazyColumn(
Modifier.fillMaxWidth(),
state = listState,
contentPadding = PaddingValues(bottom = 80.dp)
) {
items(uiState.contacts) { contact ->
ContactListItem(
navController,
contact = contact,
modifier = Modifier.fillMaxWidth()
)
Divider(thickness = 0.5.dp, color = colorResource(id = R.color.blue))
}
}
...
where contactsListViewModel is declared on top of the activity which I inject using hilt :
#AndroidEntryPoint
class MainActivity : ComponentActivity(), LoaderManager.LoaderCallbacks<Cursor> {
private val contactsViewModel: ContactsViewModel by viewModels()
For some reason uiState.contacts is empty inside the composable function, but it does contain items when I logged it inside the viewModel function and therefore my list stays empty..
Any suggestions on what could have gone wrong?
Thanks
Please use SnapshotStatelist/(mutableStateListOf()) instead of an ordinary list (mutableList).
private val _contactsListUiState = MutableStateFlow(ContactsListUiState(mutableStateListOf()))
Also please check this post this one and this one
I'm new on jetpack compose and I'm sure that I'm missing something but I don't know what?
my State model:
data class ChoiceSkillsState(
val isLoading: Boolean = false,
val errorWD: ErrorWD? = null,
val skills: List<Skill> = emptyList(),
)
The Skill model:
#Parcelize
data class Skill(
val id: Int,
val name: String,
val imageUrl: String? = null,
var children: List<SkillChild>? = null,
) : Parcelable {
#Parcelize
data class SkillChild(
val id: Int,
val name: String,
val imageUrl: String? = null,
var note: Int? = null,
) : Parcelable
}
fun Skill.asChildNoted(): Boolean {
if (!children.isNullOrEmpty()) {
children!!.forEach {
if (it.note != null) return true
}
}
return false
}
on my viewModel
private val _state = mutableStateOf(ChoiceSkillsState())
val state: State<ChoiceSkillsState> = _state
On some event I update my skillList on my state : ChoiceSkillState.
When I log, my data is updated correctly but my view is not recomposed..
There is my LazyColumn:
#Composable
private fun LazyColumnSkills(
skills: List<Skill>,
onClickSkill: (skill: Skill) -> Unit,
) {
LazyColumn(
contentPadding = PaddingValues(bottom = MaterialTheme.spacing.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small),
) {
items(
items = skills,
) { skill ->
ItemSkillParent(
skill = skill,
onClickSkill = onClickSkill
)
}
}
}
Then here is my ItemSkillParent:
#Composable
fun ItemSkillParent(
skill: Skill,
onClickSkill: (skill: Skill) -> Unit
) {
val backgroundColor =
if (skill.asChildNoted()) Orange
else OrangeLight3
val endIconRes =
if (skill.asChildNoted()) R.drawable.ic_apple
else R.drawable.ic_arrow_right
Box(
modifier = Modifier
.fillMaxWidth()
.clip(shape = MaterialTheme.shapes.itemSkill)
.background(backgroundColor)
.clickable { onClickSkill(skill) },
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 7.dp, horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier
.weight(1f)
.size(50.dp)
.clip(shape = MaterialTheme.shapes.itemSkillImage),
painter = rememberAsyncImagePainter(model = skill.imageUrl),
contentDescription = "Image skill",
contentScale = ContentScale.Crop
)
Text(
modifier = Modifier
.weight(6f)
.padding(horizontal = 10.dp),
text = skill.name,
style = MaterialTheme.typography.itemSkill
)
ButtonIconRoundedMini(
iconRes = endIconRes,
contentDesc = "Icon arrow right",
onClick = { onClickSkill(skill) }
)
}
}
}
My onClickSkill() will open a new Screen then pass result, then I will update my data with this :
fun updateSkill(skill: Skill) {
val skillsUpdated = _state.value.skills
skillsUpdated
.filter { it.id == skill.id }
.forEach { it.children = skill.children }
_state.value = _state.value.copy(skills = skillsUpdated)
}
As you can see, the background color and the iconResource should be changed, it's changing when only when I scroll.
Can someone explain me what's happening there ?
You should never use var in class properties if you want a property update to cause recomposition.
Check out Why is immutability important in functional programming?.
In this case you are updating the children property, but skillsUpdated and _state.value.skills are actually the same object - you can check the address, so Compose thinks it has not been changed.
After updating your children to val, you can use copy to update it.
val skillsUpdated = _state.value.skills.toMutableList()
for (i in skillsUpdated.indices) {
if (skillsUpdated[i].id != skill.id) continue
skillsUpdated[i] = skillsUpdated[i].copy(children = skill.children)
}
_state.value = _state.value.copy(skills = skillsUpdated.toImmutableList())
Note that converting a mutable list into an immutable list is also critical here, because otherwise the next time you try to update it, the list will be the same object: both toList and toMutableList return this when applied to a mutable list.
make your properties as state
val backgroundColor by remember {
mutableStateOf (if (skill.asChildNoted()) Orange
else OrangeLight3)
}
val endIconRes by remember {
mutableStateOf (if (skill.asChildNoted()) R.drawable.ic_apple else R.drawable.ic_arrow_right)
}
I was developing an App where I try to implement some new technologies, as Jetpack Compose. And in general, it's a great tool, except the fact that it has hard pre-visualize system (#Preview) thn the regular xml design files.
My problem comes when I try to create a #Preview of the component which represent the different rows, where I load my data recover from network.
In my case I made this:
#Preview(
name ="ListScreenPreview ",
showSystemUi = true,
showBackground = true,
device = Devices.NEXUS_9)
#Composable
fun myPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
PokedexEntry(
model = pokemonMokData,
navController = rememberNavController(),
viewModel = hiltViewModel())
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Cacamon",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
To represent this #Composable:
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
Surface(
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxSize()
)
{
Column {
Spacer(modifier = Modifier.height(20.dp))
Image(
painter = painterResource(id = R.drawable.ic_international_pok_mon_logo),
contentDescription = "Pokemon",
modifier = Modifier
.fillMaxWidth()
.align(CenterHorizontally)
)
SearchBar(
hint = "Search...",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
viewModel.searchPokemonList(it)
}
Spacer(modifier = Modifier.height(16.dp))
PokemonList(navController = navController,
viewModel = viewModel)
}
}
}
#Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = " ",
onSearch: (String) -> Unit = { }
) {
var text by remember {
mutableStateOf("")
}
var isHintDisplayed by remember {
mutableStateOf(hint != "")
}
Box(modifier = modifier) {
BasicTextField(value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
textStyle = TextStyle(color = Color.Black),
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
.padding(horizontal = 20.dp, vertical = 12.dp)
.onFocusChanged {
isHintDisplayed = !it.isFocused
}
)
if (isHintDisplayed) {
Text(
text = hint,
color = Color.LightGray,
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp)
)
}
}
}
#Composable
fun PokemonList(
navController: NavController,
viewModel: PokemonListViewModel
) {
val pokemonList by remember { viewModel.pokemonList }
val endReached by remember { viewModel.endReached }
val loadError by remember { viewModel.loadError }
val isLoading by remember { viewModel.isLoading }
val isSearching by remember { viewModel.isSearching }
LazyColumn(contentPadding = PaddingValues(16.dp)) {
val itemCount = if (pokemonList.size % 2 == 0) {
pokemonList.size / 2
} else {
pokemonList.size / 2 + 1
}
items(itemCount) {
if (it >= itemCount - 1 && !endReached && !isLoading && !isSearching) {
viewModel.loadPokemonPaginated()
}
PokedexRow(rowIndex = it, models = pokemonList, navController = navController, viewModel = viewModel)
}
}
Box(
contentAlignment = Center,
modifier = Modifier.fillMaxSize()
) {
if (isLoading) {
CircularProgressIndicator(color = MaterialTheme.colors.primary)
}
if (loadError.isNotEmpty()) {
RetrySection(error = loadError) {
viewModel.loadPokemonPaginated()
}
}
}
}
#SuppressLint("LogNotTimber")
#Composable
fun PokedexEntry(
model: PokedexListModel,
navController: NavController,
modifier: Modifier = Modifier,
viewModel: PokemonListViewModel
) {
val defaultDominantColor = MaterialTheme.colors.surface
var dominantColor by remember {
mutableStateOf(defaultDominantColor)
}
Box(
contentAlignment = Center,
modifier = modifier
.shadow(5.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.aspectRatio(1f)
.background(
Brush.verticalGradient(
listOf(dominantColor, defaultDominantColor)
)
)
.clickable {
navController.navigate(
"pokemon_detail_screen/${dominantColor.toArgb()}/${model.pokemonName}/${model.number}"
)
}
) {
Column {
CoilImage(
imageRequest = ImageRequest.Builder(LocalContext.current)
.data(model.imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}.build(),
imageLoader = ImageLoader.Builder(LocalContext.current)
.availableMemoryPercentage(0.25)
.crossfade(true)
.build(),
contentDescription = model.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
loading = {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val indicator = createRef()
CircularProgressIndicator(
//Set constrains dynamically
modifier = Modifier.constrainAs(indicator) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
},
// shows an error text message when request failed.
failure = {
Text(text = "image request failed.")
}
)
Log.d("pokemonlist", model.imageUrl)
Text(
text = model.pokemonName,
fontFamily = RobotoCondensed,
fontSize = 20.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
#Composable
fun PokedexRow(
rowIndex: Int,
models: List<PokedexListModel>,
navController: NavController,
viewModel: PokemonListViewModel
) {
Column {
Row {
PokedexEntry(
model = models[rowIndex * 2],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
Spacer(modifier = Modifier.width(16.dp))
if (models.size >= rowIndex * 2 + 2) {
PokedexEntry(
model = models[rowIndex * 2 + 1],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
#Composable
fun RetrySection(
error: String,
onRetry: () -> Unit,
) {
Column() {
Text(error, color = Color.Red, fontSize = 18.sp)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { onRetry() },
modifier = Modifier.align(CenterHorizontally)
) {
Text(text = "Retry")
}
}
}
I try to annotate with the #Nullable navController and viewmodel of the PokemonListScreen #Composable, but doesn't work either. I'm still seeing an empty screen:
So I try to search into the Jetpack documentation but, it's just defining quite simple Composables.
So if you have some more knowledge about it and can help, thanks in advance !
The main problem is if I wanna Preview that #Composable, although I made #Nullable to the viewmodel parameter, which I guess it's the problem here, AS still demand to initialize. Because I guess the right way to pass argument to a preview is by #PreviewArgument annotation.
[EDIT]
After some digging, I found AS is returning the following error under the Preview Screen:
So, there anyway to avoid viewmodel error??
[SOLUTION]
Finally a apply the following solution which make works, because the cause of the problem is due to Hilt have some inconpatibilities with Jetpack Compose previews:
Create an interface of the your ViewModel which recover all the variables and methods.
Make yourcurrent viemodel class extends of the interface.
Create a 2ยบ class which extends on the interface and pass that to your #Preview
#SuppressLint("UnrememberedMutableState")
#Preview(
name ="ListScreenPreview",
showSystemUi = true,
showBackground = true,
device = Devices.PIXEL)
#Composable
fun MyPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
JetpackComposePokedexTheme {
PokedexRow(
rowIndex = 0,
models = PokemonListScreenProvider().values.toList(),
navController = rememberNavController(),
viewModel = PokemonListViewModelMock(
0, mutableStateOf(""), mutableStateOf(value = false),
mutableStateOf(false), mutableStateOf(listOf(pokemonMokData))
)
)
}
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Machasaurio",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
PokemonListViewModelInterface
interface PokemonListViewModelInterface {
var curPage : Int
var loadError: MutableState<String>
var isLoading: MutableState<Boolean>
var endReached: MutableState<Boolean>
var pokemonList: MutableState<List<PokedexListModel>>
fun searchPokemonList(query: String)
fun loadPokemonPaginated()
fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit)
}
PokemonListViewModelMock
class PokemonListViewModelMock (
override var curPage: Int,
override var loadError: MutableState<String>,
override var isLoading: MutableState<Boolean>,
override var endReached: MutableState<Boolean>,
override var pokemonList: MutableState<List<PokedexListModel>>
): PokemonListViewModelInterface{
override fun searchPokemonList(query: String) {
TODO("Not yet implemented")
}
override fun loadPokemonPaginated() {
TODO("Not yet implemented")
}
override fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit) {
TODO("Not yet implemented")
}
}
The actual Preview is the following, and although the image doesn't display, is shown correctly:
You could create another composable which invokes the viewmodel logic via lambda functions instead of using the viewmodel itself. Extract your uiState to a separate class, so it can be used as a StateFlow in your viewmodel, which in turn can be observed from the composable.
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
/*
rememberStateWithLifecyle is an extension function based on
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
*/
val uiState by rememberStateWithLifecycle(viewModel.uiState)
PokemonListScreen(
uiState = uiState,
onLoadPokemons = viewModel::loadPokemons,
onSearchPokemon = {viewModel.searchPokemon(it)},
onCalculateDominantColor = {viewModel.calcDominantColor(it)},
onNavigate = {route -> navController.navigate(route, null, null)},
)
}
#Composable
private fun PokemonListScreen(
uiState: PokemonUiState,
onLoadPokemons:()->Unit,
onSearchPokemon: (String) -> Unit,
onCalculateDominantColor: (Drawable) -> Color,
onNavigate:(String)->Unit,
) {
}
#HiltViewModel
class PokemonListViewModel #Inject constructor(/*your datasources*/) {
private val loading = MutableStateFlow(false)
private val loadError = MutableStateFlow(false)
private val endReached = MutableStateFlow(false)
private val searching = MutableStateFlow(false)
private val pokemons = MutableStateFlow<Pokemon?>(null)
val uiState: StateFlow<PokemonUiState> = combine(
loading,
loadError,
endReached,
searching,
pokemons
) { loading, error, endReached, searching, pokemons ->
PokemonUiState(
isLoading = loading,
loadError = error,
endReached = endReached,
isSearching = searching,
pokemonList = pokemons,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = PokemonUiState.Empty,
)
}
data class PokemonUiState(
val pokemonList: List<Pokemon> = emptyList(),
val endReached: Boolean = false,
val loadError: Boolean = false,
val isLoading: Boolean = false,
val isSearching: Boolean = false,
) {
companion object {
val Empty = PokemonUiState()
}
}
I'm not sure of the depth of this application, but a potential idea would be to code to an interface and not an implementation.
That is, create an interface with all of the functions you need (that may already exist in your ViewModel), have your PokemonListViewModel implement it, and create another mock class that implements it as well. Pass the mock into your preview and leave the real implementation with PokemonListViewModel
interface PokeListViewModel {
...
// your other val's
val isLoading: Boolean
fun searchPokemonList(pokemon: String)
fun loadPokemonPaginated()
// your other functions
...
}
Once you create your interface you can simply update your composables to be expecting an object that "is a" PokeListViewModel, for example.
Hopefully this helps
I want to call "onLogin" function and pass user but I can't access "onLogin" in ViewModel , I tried to use mutableLiveData but I couldn't,I don't know should I pass onLogin to viewmodel or this is a bad practice
there is button whose title is "Sign In" , it calls method in ViewModel called "Submit" use apollo (graphql) to get the user
SignInScreen
#Composable
fun SignInScreen(
onNavigateToSignUp:() -> Unit,
onLogin:(User) -> Unit
){
val viewModel:SignInViewModel = viewModel()
Scaffold(
bottomBar = {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(bottom = 10.dp)
.fillMaxWidth()
) {
Text(text = "Don't have an account?")
Text(
text = "Sign Up.",
modifier = Modifier
.padding(start = 5.dp)
.clickable { onNavigateToSignUp() },
fontWeight = FontWeight.Bold
)
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Instagram")
Spacer(modifier = Modifier.size(30.dp))
Input(viewModel.username,placeholder = "username"){
viewModel.username = it
}
Spacer(modifier = Modifier.size(20.dp))
Input(viewModel.password,placeholder = "Password"){
viewModel.password = it
}
Spacer(modifier = Modifier.size(30.dp))
Button(onClick = {viewModel.submit()},modifier = Modifier.fillMaxWidth()) {
Text(text = "Sign In")
}
}
}
}
ViewModel
class SignInViewModel(application:Application):AndroidViewModel(application) {
var username by mutableStateOf("")
var password by mutableStateOf("")
private val context = application.applicationContext
private val _user = MutableLiveData<User>(null)
val user:LiveData<User> get() = _user
fun submit(){
viewModelScope.launch {
val response = apolloClient.mutate(LoginMutation(username = Input.fromNullable(username),password = Input.fromNullable(password))).await()
_user.value = response.data?.login?.user as User
}
}
}
This is how I did it.
1. First I created this class to communicate from ViewModel to view(s) and to have stateful communication where the UI knows what to show with every update and through one live data.
sealed class UIState<out T>() {
class Idle() : UIState<Nothing>()
class Loading(val progress: Int = 0) : UIState<Nothing>()
class Success<out T>(val data: T?) : UIState<T>()
class Error(
val error: Throwable? = null,
val message: String? = null,
val title: String? = null
) : UIState<Nothing>()
}
2. Then Of course create the live data in ViewModel and also an immutable copy for the view:
private val _loginState by lazy { MutableLiveData<UIState<ResponseUser>>() }
val loginState: LiveData<UIState<ResponseUser>> = _loginState
fun performLogin(username: String, password: String) {
viewModelScope.launch {
_loginState.postValue(loading)
// your login logic here
if ("login was successful") {
_loginState.postValue(UIState.Success("your login response if needed in UI"))
} else {
_loginState.postValue(UIState.Error("some error here"))
}
}
}
3. Now in the UI I need to observe this live data as a state, which is pretty easy we have delegate literally called observeAsState. But here is the catch and that's if you are doing something like navigation, which you only want to happen only once:
#Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginState by viewModel.loginState.observeAsState(UIState.Idle())
val hasHandledNavigation = remember { mutableStateOf(false)}
if (loginState is UIState.Success && !hasHandledNavigation.value ) {
navigateToWelcomeScreen()
else {
LoginScreenUI(loginState) { username, password ->
viewModel.performLogin(username, password)
}
}
}
4. in the UI you want, among other things, two text fields and a button, and you want to remember the username and password that entered:
#Composable
fun LoginScreenUI(
state: UIState<ResponseUser>, onLoginButtonClicked: (username: String, password: String) -> Unit
) {
Column() {
var username by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = username,
onValueChange = { username = it },
)
var password by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = password,
onValueChange = { password = it },
)
Button(
onClick = {
onLoginButtonClicked(
username, password
)
}
) {
Text(text = "Login")
}
if (state is UIState.Error) {
AlertDialogComponent(state.title, state.message)
}
}
}
I hope I've covered everything :D
My solution is to use the LaunchedEffect because the Android developer documentation is mentioning showing SnackBar as an example which is a single time event, code example following the same as Amin Keshavarzian Answer
just change the part 3 to use LaunchedEffect instead of the flag state hasHandledNavigation
#Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginState by viewModel.loginState.observeAsState(UIState.Idle())
LaunchedEffect(key1 = loginState) {
if (loginState is UIState.Success)
navigateToWelcomeScreen()
}
LoginScreenUI(loginState) { username, password ->
viewModel.performLogin(username, password)
}
}
In my viewModel I have "state" for every single screen. e.g.
class MainState(val commonState: CommonState) {
val text = MutableStateFlow("text")
}
I pass viewModel to my JetpackCompose screen.
#Composable
fun MainScreen(text: String, viewModel: HomeViewModel) {
val textState = remember { mutableStateOf(TextFieldValue()) }
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = viewModel.state.mainState.text.value,
color = Color.Blue,
fontSize = 40.sp
)
Button(
onClick = { viewModel.state.mainState.text.value = "New text" },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Green
),
modifier = Modifier.padding(16.dp)
) {
Text(text)
}
TextField(
value = textState.value,
onValueChange = { textState.value = it },
label = { Text("Input text") }
)
}
}
When I click button I change value of state and I expect UI will update but it does not. I have to click on TextField and then text in TextView updates.
Any suggestion why UI does not update automatically?
That's how I pass components and start whole screen in startActivity;
class HomeActivity : ComponentActivity() {
private val viewModel by viewModel<HomeViewModel>()
private val homeState: HomeState get() = viewModel.state
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RateMeAppTheme {
ContentScreen(viewModel, homeState)
}
}
}
}
In this simple case u should use mutableStateOf("text") in class MainState instead of mutableStateFlow
class MainState(val commonState: CommonState) {
val text = mutableStateOf("text")
}
Using MutableStateFlow
To use MutableStateFlow (which is not required in the current scenario) , we need to collect the flow.
Like the following:-
val state = viewModel.mainState.text.collectAsState() // we can collect a stateflow as state in a composable function
Then we can use the observed state value in the Text using:-
Text(text = state.value, ..)
Finally your composable function should look like:-
#Composable
fun MainScreen(text: String, viewModel: HomeViewModel) {
val textState = remember { mutableStateOf(TextFieldValue()) }
val state = viewModel.mainState.text.collectAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.value,
color = Color.Blue,
fontSize = 40.sp
)
Button(
onClick = { viewModel.mainState.text.value = "New text" },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Green
),
modifier = Modifier.padding(16.dp)
) {
Text(text)
}
TextField(
value = textState.value,
onValueChange = { textState.value = it },
label = { Text("Input text") }
)
}
}