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") }
)
}
}
Related
I am writing a small gallery app for my cat. It has a button by clicking on which a new PhotoItem is added to the displayed list, but it appears only after phone rotation and I want it to appear on the screen right after button was clicked.
Right now everything is stored in a mutableList inside savedStateHandle.getStateFlow but I also tried regular MutableStateFlow and mutableStateOf and it didn't help. I havent really used jatpack compose and just can't figure what to do (
App
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun BebrasPhotosApp() {
val galaryViewModel = viewModel<GalaryViewModel>()
val allPhotos by galaryViewModel.loadedPics.collectAsState()
Scaffold(topBar = { BebraTopAppBar() }, floatingActionButton = {
FloatingActionButton(
onClick = { galaryViewModel.addPicture() },
backgroundColor = MaterialTheme.colors.onBackground
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Add Photo",
tint = Color.White,
)
}
}) {
LazyColumn(modifier = Modifier.background(color = MaterialTheme.colors.background)) {
items(allPhotos) {
PhotoItem(bebra = it)
}
}
}
}
ViewModel
class GalaryViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val loadedPics = savedStateHandle.getStateFlow(
"pics", initialValue = mutableListOf<Bebra>(
Bebra(R.string.photo_1, R.string.desc_1, R.drawable.bebra_pic_1, R.string.add_desc_1),
Bebra(R.string.photo_2, R.string.desc_2, R.drawable.bebra_pic_2, R.string.add_desc_2),
Bebra(R.string.photo_3, R.string.desc_3, R.drawable.bebra_pic_3, R.string.add_desc_3)
)
)
fun addPicture() {
val additionalBebraPhoto = Bebra(
R.string.photo_placeholder,
R.string.desc_placeholder,
R.drawable.placeholder_cat,
R.string.add_desc_placeholder
)
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
}
PhotoItem
#Composable
fun PhotoItem(bebra: Bebra, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
Card(elevation = 4.dp, modifier = modifier
.padding(8.dp)
.clickable { expanded = !expanded }) {
Column(
modifier = modifier
.padding(8.dp)
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
) {
Text(
text = stringResource(id = bebra.PicNumber),
style = MaterialTheme.typography.h1,
modifier = modifier.padding(bottom = 8.dp)
)
Text(
text = stringResource(id = bebra.PicDesc),
style = MaterialTheme.typography.body1,
modifier = modifier.padding(bottom = 8.dp)
)
Image(
painter = painterResource(id = bebra.Picture),
contentDescription = stringResource(id = bebra.PicDesc),
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(256.dp)
.clip(RoundedCornerShape(12))
)
if (expanded) {
BebraAdditionalDesc(bebra.additionalDesc)
}
}
}
}
Bebra Data class
data class Bebra(
#StringRes val PicNumber: Int,
#StringRes val PicDesc: Int,
#DrawableRes val Picture: Int,
#StringRes val additionalDesc: Int
)
So, I am also not super familiar with JC, but from first glance it looks like your method, addPicture() - which is called when the user taps on the button, does not update the state, therefore there's no recomposition happening, so the UI does not get updated.
Check:
fun addPicture() {
// ...
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
So here you are basically adding a new item to savedStateHandle, which I assume does not trigger a recomposition.
What I think you need to do, is to update loadedPics, somehow.
However, loadedPics is a StateFlow, to be able to update it you would need a MutableStateFlow.
For simplicity, this is how you would do it if you were operating with a list of strings:
// declare MutableStateFlow that can be updated and trigger recomposition
val _loadedPics = MutableStateFlow(
savedStateHandle.get<MutableList<String>>("pics") ?: mutableListOf()
)
// use this in the JC layout to listen to state changes
val loadedPics: StateFlow<List<String>> = _loadedPics
// addPicture:
val prevList = _loadedPics.value
prevList.add("item")
_loadedPics.value = prevList // triggers recomposition
// here you probably will want to save the item in the
// `savedStateHandle` as you already doing.
I have one textfield for user phone number, and I want to use this phone number for both register users to Firestore and using it for mobile authentication, so I have two ViewModel and I want to use ShareViewModel for this number textfield, is it possible to use it in android jetpack compose?
AuthViewModel:
#HiltViewModel
class AuthenticationViewModel #Inject constructor(
val auth: FirebaseAuth
) : ViewModel() {
.....
}
RegisterViewModel :
#HiltViewModel
class RegisterViewModel #Inject constructor(
val db: FirebaseFirestore,
val auth: FirebaseAuth,
) : ViewModel() {
......
}
RegisterScreen :
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun RegisterScreen(
navController: NavController,
model: RegisterViewModel
) {
val phoneNumberState = remember { mutableStateOf("") }
OutlinedTextField(
value = phoneNumberState.value,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = white,
focusedIndicatorColor = Grey,
unfocusedIndicatorColor = Grey,
focusedLabelColor = Grey,
unfocusedLabelColor = Grey,
cursorColor = custom,
textColor = custom,
),
onValueChange = { phoneNumberState.value = it },
label = { Text(text = "Phone Number") },
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
}),
placeholder = { Text(text = "Phone Number") },
singleLine = true,
modifier = Modifier.fillMaxWidth(0.8f)
)
Button(
modifier = Modifier
.width(205.dp)
.height(35.dp),
onClick = {
focus.clearFocus(force = true)
model.onSignUp(
phoneNumberState.value,
)
navController.navigate("otp")
},
colors = ButtonDefaults.buttonColors(
backgroundColor = custom
),
shape = RoundedCornerShape(30)
) {
Text(
text = "Next",
style = TextStyle(
fontSize = 11.sp,
color = white,
))
}}
PhoneVerifyScreen:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun PhoneVerifyScreen(
navController: NavController,
modelAuthentication: AuthenticationViewModel,
onClick: (mobileNum: String, otp: String) -> Unit
) {
val focusManager = LocalFocusManager.current
val phoneNumberOTP = remember { mutableStateOf("") }
val context = LocalContext.current
LaunchedEffect(Unit) {
println("found activity? ${context.findActivity()}")
val activity = context.findActivity() ?: return#LaunchedEffect
modelAuthentication.setActivity(activity)
}
Column(
Modifier.fillMaxSize()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {keyboardController?.hide()}
,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = phoneNumberOTP.value,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = white,
focusedIndicatorColor = Grey,
unfocusedIndicatorColor = Grey,
focusedLabelColor = Grey,
unfocusedLabelColor = Grey,
cursorColor = custom,
textColor = custom,
),
onValueChange = { phoneNumberOTP.value = it },
label = { Text(text = "Verify code") },
placeholder = { Text(text = "Verify code") },
singleLine = true,
modifier = Modifier.fillMaxWidth(0.8f),
)
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
Button(
modifier = Modifier
.width(285.dp)
.height(55.dp),
onClick = {
modelAuthentication.otpVerification(phoneNumberOTP.value)
navController.navigate("profileScreen")
},
colors = ButtonDefaults.buttonColors(
backgroundColor = custom2
),
shape = RoundedCornerShape(60),
) {
Text(
text = "Next",
style = TextStyle(
fontSize = 18.sp,
color = white,
))
}}}}
create a viewModel in your MainActivity, and this viewModel will be the shared viewModel, you can pass it to other screens or just pass function that set phone number to that sharedViewModel
class MainActivity : ComponentActivity() {
private val sharedViewModel by viewModels<SharedViewModel>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App(sharedViewModel = sharedViewModel)
}
}
}
I have a login page with two TextFields, one for the username and one for the password. When the submit button is clicked, I call my loginViewModel's login(username,passwd) method in order to asynchronously authenticate the user. Once the authentication result is received, I post the value to the ViewModel's mutableLiveData in order for the value to be observed in my Login composable through loginViewModel.wasLoginSuccessful.observeAsState(false).
The problem occurs with the if (isUserAuthenticated) onSuccessfulLogin() since when the authentication result is true, I successfully get "redirected" to the home page but everything is super laggy, and using the profiler I can see the Memory & CPU usage fluctuating a lot. If I remove the if statement and make it so that the onSuccessfullLogin() is called unconditionally, everything works just fine without any issues. I'm guessing that this is a side-effect through recomposition and/or a memory leak but I can't pinpoint it.
Login page:
#Composable
fun Login(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
LoginContent() {
navController.navigate(Screens.DASHBOARD_MYEWAY.navRoute) {
popUpTo(0)
}
}
LoginFooter() {
navController.navigate(NavigationGraphs.ACTIVATION.route)
}
}
}
#Composable
private fun LoginContent(onSuccessfulLogin: () -> Unit) {
val loginViewModel: LoginViewModel = remember { getKoin().get() }
val isUserAuthenticated by loginViewModel.wasLoginSuccessful.observeAsState(false)
val username = remember { mutableStateOf("") }
val password = remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
TopBarPadding()
Spacer(modifier = Modifier.padding(24.fixedDp()))
Text(
text = LocalContext.current.getString(R.string.welcome_plural),
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100),
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.padding(8.fixedDp()))
Text(
text = LocalContext.current.getString(R.string.please_enter_password_to_proceed),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100)
)
Spacer(modifier = Modifier.padding(30.fixedDp()))
InputField(
label = LocalContext.current.getString(R.string.username),
onTextChangeCallback = { username.value = it })
Spacer(modifier = Modifier.padding(26.fixedDp()))
InputField(
label = LocalContext.current.getString(R.string.password),
isPassword = true,
canReveal = true,
onTextChangeCallback = { password.value = it })
Spacer(modifier = Modifier.padding(32.fixedDp()))
MyButton(text = LocalContext.current.getString(R.string.login),
buttonType = MyButtonType.PRIMARY,
onClick = {
scope.launch {
loginViewModel.login(username.value, password.value)
}
})
Spacer(modifier = Modifier.padding(24.fixedDp()))
Link(text = LocalContext.current.getString(R.string.forgot_my_password))
}
if (isUserAuthenticated) onSuccessfulLogin()
}
#Composable
private fun LoginFooter(onClickEwayActivation: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = LocalContext.current.getString(R.string.you_have_no_username),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100)
)
Row {
Text(
text = LocalContext.current.getString(R.string.do_string),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100)
)
Link(
text = LocalContext.current.getString(R.string.my_eway_activation),
onClick = { onClickEwayActivation() })
}
}
Spacer(modifier = Modifier.padding(40.fixedDp()))
}
}
LoginViewModel:
class LoginViewModel(
private val authenticationUseCase: AuthenticationControllerUseCase
) : ViewModel() {
private val _loginResult = MutableLiveData(false)
val wasLoginSuccessful: LiveData<Boolean> = _loginResult
fun login(username: String, password: String) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = authenticationUseCase.validateUser(username, password)) {
is ResponseResult.Success -> {
response.data?.userLoginId!!.saveStringThroughKeystore(PreferenceKeys.ACCOUNT_ID.key)
_loginResult.postValue(true)
}
is ResponseResult.Error -> {
_loginResult.postValue(false)
Log.d(
"USER_VALIDATION_ERROR",
"Network Call Failed With Exception: " + response.exception.message
)
}
}
}
}
}
New to Compose and struggling hard with more complex state cases.
I cant seem to change the text dynamically in these cards, only set the text manually. Each button press should change the text in a box, starting left to right, leaving the next boxes empty.
What is wrong here?
UI:
val viewModel = HomeViewModel()
val guessArray = viewModel.guessArray
#Composable
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxWidth()
) {
WordGrid()
Keyboard()
}
}
#Composable
fun WordGrid() {
CardRow()
}
#Composable
fun Keyboard() {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
MyKeyboardButton(text = "A", 35)
MyKeyboardButton(text = "B", 35)
}
}
#Composable
fun MyCard(text: String) {
Card(
modifier = Modifier
.padding(4.dp, 8.dp)
.height(55.dp)
.aspectRatio(1f),
backgroundColor = Color.White,
border = BorderStroke(2.dp, Color.Black),
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = text,
fontSize = 20.sp,
)
}
}
}
#Composable
fun CardRow() {
guessArray.forEach { rowCards ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
rowCards.forEach {
MyCard(it)
println(it)
}
}
}
}
#Composable
fun MyKeyboardButton(text: String, width: Int) {
Button(
onClick = {
guessArray[viewModel.currentRow][viewModel.column] = text
viewModel.column += 1
},
modifier = Modifier
.width(width.dp)
.height(60.dp)
.padding(0.dp, 2.dp)
) {
Text(text = text, textAlign = TextAlign.Center)
}
}
ViewModel:
class HomeViewModel : ViewModel() {
var currentRow = 0
var guessArray = Array(5) { Array(6) { "" }.toMutableList() }
var column = 0
}
The grid is created, but the text is never changed.
To make Compose view recompose with the new value, a mutable state should be used.
You're already using it with remember in your composable, but it also needs to be used in your view model for properties, which should trigger recomposition.
class HomeViewModel : ViewModel() {
var currentRow = 0
val guessArray = List(5) { List(6) { "" }.toMutableStateList() }
var column = 0
}
I have a ViewModel that keeps a timer:
class GameViewModel #Inject constructor(application: Application) : AndroidViewModel(application), PlayStopWatch.PlayClockListener {
val playTime = MutableLiveData<Long>()
override fun onPlayClockTick(elapsedTime: Long) {
playTime.value = elapsedTime
}
}
I would like to use it to update a clock in this composable but I don't think I'm understanding the documentation properly:
#Composable
fun PlayTime(viewModel: GameViewModel) {
val playTime by remember { mutableStateOf(viewModel.gameTime)}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.icon_timer),
contentDescription = "Timer Icon",
tint = yellow,
modifier = Modifier
.size(64.dp)
)
Text(
"$playTime",
color = yellow,
fontFamily = Montserrat,
fontWeight = FontWeight(700),
fontSize = 60.sp,
letterSpacing = -0.5.sp,
lineHeight = 72.sp,
)
}//: End Row
Text(
text = "Tap Screen to Start Play",
color = white_97,
style = ff.h5
)
}
The view model is passed down from a view tree that includes this view
#Composable
fun StartPlayView(viewModel:GameViewModel) {
Surface(
color = background_primary,
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(heightWeight(32f)))
MenuBar()
Spacer(modifier = Modifier.weight(heightWeight(32f)))
ScoresBar()
Spacer(modifier = Modifier.weight(heightWeight(76f)))
PauseButton()
Spacer(modifier = Modifier.weight(heightWeight(200f)))
PlayTime(viewModel)
Spacer(modifier = Modifier.weight(heightWeight(200f)))
BottomBar()
Spacer(modifier = Modifier.weight(heightWeight(16f)))
}
}
}
Which gets passed the viewModel from a NavGraphBuilder via Android's build in HiltViewModel:
#Composable
fun SetupNavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.StartPlay.route
){
// Login View
composable(
route = Screen.Login.route
){
composable(route = Screen.StartPlay.route){ StartPlayView(hiltViewModel()) }
}
}
How do I keep the TextView in the PlayTime view display the current time on the timer in real time?
you could try something like this. the delay() is in millis, so the playTime will only be updated every seconds. just change that based on your needs
fun startPlayTimeCounter() {
job = coroutineScope.launch {
val initialTime = System.currentTimeMillis()
var playTime: Long
while (true) {
playTime = System.currentTimeMillis() - initialTime
delay(1000L)
}
}
}
and stop the timer by cancelling the job
fun stopPlayTimeCounter() {
job?.cancel()
}