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()
}
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 created a Pokemon App with Jetpack Compose. I have two screens. One where the names of the pokemon is being displayed, and a second screen where the details of a selected pokemon will be shown. What I want to achieve is passing the name of the pokemon to a url when it's selected, and getting the information on to the second screen.
const val POKE_DETAILS = "api/v2/pokemon/{name}/"
Network File
interface NetworkingService{
#GET(Endpoints.POKE_LIST)
suspend fun getListResponse(
#Query("limit") limit: String = "151"
): Response<GetPokemonListResponse>
#GET(Endpoints.POKE_DETAILS)
suspend fun getDetailsResponse(
#Path("name") name: String
): GetPokemonDetailResponse
}
object PokemonNetworkObject{
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val retroFit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
val networkingServices: NetworkingService by lazy {
retroFit.create(NetworkingService::class.java)
}
val apiClient = ApiClient(networkingServices)
}
Compose Screens
#Preview
#Composable
fun PokemonListPage(pokemonViewModel: PokemonListViewModel = viewModel(), navController: NavController){
val pokeList = pokemonViewModel.pokemonStateData.collectAsState().value
Surface(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(),
) {
Scaffold(topBar =
{
TopAppBar(
backgroundColor = Color.DarkGray,
elevation = 0.dp,
) {
Row(modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
){
Text(
text = "POKEDEX",
fontWeight = FontWeight.Bold,
fontSize = 25.sp,
color = Color.Magenta
)
}
}
}, backgroundColor = Color.DarkGray){
PokemonCards(pokemons = pokeList!!, navController = navController))
}
// PokemonCards(pokemons = pokeList!!)
}
}
#Composable
private fun PokemonCards(pokemons: List<PokeListEntity>, navController: NavController) {
LazyColumn{
items(pokemons){ pokemon ->
PokemonCard(pokemon = pokemon, navController = navController)
}
}
}
#Composable
private fun PokemonCard(pokemon: PokeListEntity, navController: NavController) {
Card(
modifier = Modifier
.width(500.dp)
.requiredHeight(100.dp)
.padding(10.dp),
backgroundColor = Color.Cyan,
shape = RoundedCornerShape(corner = CornerSize(10.dp)),
elevation = 5.dp
) {
Row(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = pokemon.name,
fontSize = 25.sp,
fontWeight = FontWeight.Bold
)
Button(
onClick = {
navController.navigate(route = "pokemon_detail_screen/${pokemon.name}")
}) {
Text(text = "INFO")
}
}
}
}
Details Screen
#Preview
#Composable
fun PokemonDetailScreen(
detailViewModel: DetailViewModel = viewModel(),
navController: NavController,
pokemonName: String // This Variable
) {
val details by detailViewModel.detailState.observeAsState()
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
color = Color.DarkGray
) {
DetailCard(field = details!!)
}
}
#Composable
private fun DetailCard(field: GetPokemonDetailResponse) {
Card(
modifier = Modifier
.requiredHeight(300.dp)
.fillMaxWidth()
.padding(10.dp),
backgroundColor = Color.Cyan,
shape = RoundedCornerShape(corner = CornerSize(15.dp)),
elevation = 10.dp
) {
Column(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(10.dp),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {
DetailFields("Name:", field = field.name)
Divider(thickness = 4.dp)
DetailFields("Height:", field = "${field.height}.in")
Divider(thickness = 4.dp)
DetailFields("Weight:", field = "${field.weight}.lbs")
}
}
}
#Composable
private fun DetailFields(title: String, field: String) {
Column() {
Text(
title,
fontSize = 20.sp
)
Text(
field,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}
}
MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PokeDexComposeTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "pokemon_list_page"
) {
composable("pokemon_list_page") {
PokemonListPage(navController = navController)
}
composable(
"pokemon_detail_screen/{name}",
arguments = listOf(
navArgument("name") {
type = NavType.StringType
}
)
) {
// The navBackStackEntry contains the arguments
val pokemonName = remember {
it.arguments?.getString("name")
}
PokemonDetailScreen(
pokemonName = pokemonName?.lowercase() ?: "",
navController = navController)
}
}
// val pokeListViewModel = viewModel<PokemonListViewModel>()
// PokemonListPage(pokeListViewModel)
}
}
}
}
}
This is still pretty new to me. Any Suggestions are definitely welcomed. Thanks.
Update
I have created a Navigation Component, but i don't know where to pass the pokemonName String value in the Details Screen
*Here is a copy of the project, if you want to test some things out.
https://drive.google.com/drive/folders/1gLH_Q3jYLhn-6ad0LMBeZHb8HyT8FszC?usp=sharing
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
}
My composable is recomposing endlessly after flow collect and navigating to a new screen.
I can't understand why.
I'm using Firebase for Auth with Email and Password.
I had to put some Log.i to test my function and my composable, and yes, my Main composable (SignUp) is recomposing endlessly after navigating.
ViewModel
// Firebase auth
private val _signUpState = mutableStateOf<Resources<Any>>(Resources.success(false))
val signUpState: State<Resources<Any>> = _signUpState
fun firebaseSignUp(email: String, password: String) {
job = viewModelScope.launch(Dispatchers.IO) {
firebaseAuth.firebaseSignUp(email = email, password = password).collect {
_signUpState.value = it
Log.i("balito", "polipop")
}
}
}
fun stop() {
job?.cancel()
}
SignUp
#Composable
fun SignUp(
navController: NavController,
signUpViewModel: SignUpViewModel = hiltViewModel()
) {
val localFocusManager = LocalFocusManager.current
Log.i("salut", "salut toi")
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.padding(16.dp)
.background(color = PrimaryColor)
) {
BackButton(navController = navController)
Spacer(modifier = Modifier.height(30.dp))
Text(
text = stringResource(id = R.string.sinscrire),
fontFamily = visby,
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
color = Color.White
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = stringResource(R.string.prenez_votre_sante_en_main),
fontFamily = visby,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
color = Grey
)
Spacer(modifier = Modifier.height(20.dp))
Email(signUpViewModel = signUpViewModel, localFocusManager = localFocusManager)
Spacer(modifier = Modifier.height(16.dp))
Password(signUpViewModel = signUpViewModel, localFocusManager = localFocusManager)
Spacer(modifier = Modifier.height(30.dp))
Button(value = stringResource(R.string.continuer), type = Type.Valid.name) {
localFocusManager.clearFocus()
signUpViewModel.firebaseSignUp(signUpViewModel.emailInput.value, signUpViewModel.passwordInput.value)
}
Spacer(modifier = Modifier.height(16.dp))
Button(value = stringResource(R.string.inscription_avec_google), type = Type.Other.name) {
}
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
ClickableTextInfo(stringResource(id = R.string.deja_un_compte_se_connecter), onClick = {})
}
}
Response(navController = navController, signUpViewModel = signUpViewModel)
DisposableEffect(key1 = signUpViewModel.signUpState.value == Resources.success(true)) {
onDispose {
signUpViewModel.stop()
Log.i("fin", "fin")
}
}
}
#Composable
private fun Response(
navController: NavController,
signUpViewModel: SignUpViewModel
) {
when (val response = signUpViewModel.signUpState.value) {
is Resources.Loading<*> -> {
//WaitingLoaderProgress(loading = true)
}
is Resources.Success<*> -> {
response.data.also {
Log.i("lolipop", "lolipopi")
if (it == true) {
navController.navigate(Screen.SignUpConfirmation.route)
}
}
}
is Resources.Failure<*> -> {
// response.throwable.also {
// Log.d(TAG, it)
// }
}
}
}
During navigation transition recomposition happens multiple times because of animations, and you call navController.navigate on each recomposition.
You should not cause side effects or change the state directly from the composable builder, because this will be performed on each recomposition, which is not expected in cases like animation.
Instead you should use side effects. In your case, LaunchedEffect should be used.
if (response.data) {
LaunchedEffect(Unit) {
Log.i("lolipop", "lolipopi")
navController.navigate(Screen.SignUpConfirmation.route)
}
}
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") }
)
}
}