I am writing a compose desktop app.
I have a main window:
Window(size = IntSize(600, 600)) {
// snip logic
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ChessBoard(board.value, modifier = Modifier.fillMaxWidth(0.8f))
Row(modifier = Modifier
.fillMaxSize(), horizontalArrangement = Arrangement.SpaceEvenly) {
Button(onClick = previousBoard) {
Text("<")
}
Button(onClick = nextBoard) {
Text(">")
}
}
}
}
Where ChessBoardis defined as
#Composable
fun ChessBoard(board: Board, modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
// snip logic, I checked that it does not influence the result
}
}
The chessboard takes up the correct amount of space, but the buttons overlap and aren't added at the bottom as expected.
I tried tweaking the modifiers on ChessBoard, but that did not change the fact that the buttons are at the top.
On the Row, remove the Modifier.fillMaxSize()
I think what you need is Modifier.fillMaxWidth()
The Row is taking up the entire space of the screen, and the buttons inside the row, by default stay at the top of the row. To check that this is the case, try adding a Modifier.align(CenterVerticlly) to the row, and the buttons will move to the center of the screen, since the row is still occupying the whole screen. Removing the Modifier.fillMaxSize() should do it, but if you still want to keep it for other Composables, use Modifier.align(Alignment) to fix it
Related
I have a simple Jetpack Compose application that uses the Navigation Component.
My UI is comprised of the following composables (redacted UI to be concise):
NavHost:
#Composable
fun NavHost(
navController: NavHostController = rememberNavController(),
startDestination: String = "main"
) {
NavHost(
navController = navController,
startDestination = startDestination) {
composable("main") {
SomeList(onNavigateToItemView = {
navController.navigate("listItem")
})
}
composable("listItem") {
ItemView()
}
}
}
SomeList:
#Composable
fun SomeList(onNavigateToItemView: () -> Unit) {
Column {
Row ( Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(text = Constants.APP_TITLE, fontSize = 30.sp, fontWeight = FontWeight.Bold)
}
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally
) {
items(items) { item->
ItemCard(item, onNavigateToItemView)
}
}
}
}
}
ItemCard:
#Composable
fun ItemCard(item: ItemModel, onNavigateToItemView: () -> Unit) {
Card(
border = BorderStroke(2.dp, Color.Cyan),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 5.dp)
.clickable {
onNavigateToItemView()
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
)
{
....
}
}
}
Now, whenever the user clicks on an ItemCard, he/she transitions to an ItemView.
What I am seeing is that the ItemView is being recomposed several times when navigating to it and also when navigating back from it.
According to the guide linked above,
The NavController's navigate function modifies the NavController's
internal state. To comply with the single source of truth principle as
much as possible, only the composable function or state holder that
hoists the NavController instance and those composable functions that
take the NavController as a parameter should make navigation calls.
Navigation events triggered from other composable functions lower in
the UI hierarchy need to expose those events to the caller
appropriately using functions.
And as you can see above, I am following that practice.
So, am I doing something wrong in my implementation or is this just how navigation and Jetpack Compose work together?
The multiple recomposition calls are affecting my UI unnecessarily.
Short Answer
Yes, navigating using compose navigation will recompose your composable each time you are navigating to it.
Workaround
When you are using navController.navigate() it will automatically recompose the targeted route. However, you can use an option to save the current state of the screen that you leave and restore the state of the screen that you target.
For example :
A->B->A->B
The first time you will load A & B you will recompose because you have no state saved yet. The moment you go from B to A (B->A) the state will be restored so you'll not recompose. Same things occurs the second time you go from A to B (A->B)
Use
Accord to documentation, you can make an extension function like this
fun NavController.popUpTo(destination: String) = navigate(destination) {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
// Restore state when reselecting a previously selected item
restoreState = true
}
And use it like this
SomeList(onNavigateToItemView = {
navController.popUpTo("listItem")
})
I'm trying to create a sky view in my Android app using Jetpack Compose. I want to display it inside a Card with a fixed height. During nigth time, the card background turns dark blue and I'd like to have some blinking stars spread over the sky.
To create the stars blinking animation, I'm using an InfiniteTransition object and a scale property with animateFloat that I apply to several Icons. Those Icons are created inside a BoxWithConstraints, to spread then randomly using a for loop. The full code I'm using is shown below:
#Composable
fun NightSkyCard() {
Card(
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(20.dp),
backgroundColor = DarkBlue
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
for (n in 0..20) {
val size = Random.nextInt(3, 5)
val start = Random.nextInt(0, maxWidth.value.toInt())
val top = Random.nextInt(10, maxHeight.value.toInt())
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = null,
modifier = Modifier
.padding(start = start.dp, top = top.dp)
.size(size.dp)
.scale(scale),
tint = Color.White
)
}
}
}
}
The problem with this code is that the BoxWithConstraints's scope is recomposing continously, so I get a lot of dots appearing and dissapearing from the screen very fast. I'd like the scope to just run once, so that the dots created at first time would blink using the scale property animation. How could I achieve that?
Instead of continuous recomposition you should look for least amount of recompositions to achieve your goal.
Compose has 3 phases. Composition, Layout and Draw, explained in official document. When you use a lambda you defer state read from composition to layout or draw phase.
If you use Modifier.scale() or Modifier.offset() both of three phases above are called. If you use Modifier.graphicsLayer{scale} or Modifier.offset{} you defer state read to layout phase. And the best part, if you use Canvas, which is a Spacer with Modifier.drawBehind{} under the hood, you defer state read to draw phase as in example below and you achieve your goal only with 1 composition instead of recomposing on every frame.
For instance from official document
// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSize().background(color))
Here the box's background color is switching rapidly between two
colors. This state is thus changing very frequently. The composable
then reads this state in the background modifier. As a result, the box
has to recompose on every frame, since the color is changing on every
frame.
To improve this, we can use a lambda-based modifier–in this case,
drawBehind. That means the color state is only read during the draw
phase. As a result, Compose can skip the composition and layout phases
entirely–when the color changes, Compose goes straight to the draw
phase.
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
Modifier
.fillMaxSize()
.drawBehind {
drawRect(color)
}
)
And how you can achieve your result
#Composable
fun NightSkyCard2() {
Card(
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(20.dp),
backgroundColor = Color.Blue
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
val stars = remember { mutableStateListOf<Star>() }
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue)
) {
SideEffect {
println("🔥 Recomposing")
}
LaunchedEffect(key1 = Unit) {
repeat(20) {
stars.add(
Star(
Random.nextInt(2, 5).toFloat(),
Random.nextInt(0, constraints.maxWidth).toFloat(),
Random.nextInt(10, constraints.maxHeight).toFloat()
)
)
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
if(stars.size == 20){
stars.forEach { star ->
drawCircle(
Color.White,
center = Offset(star.xPos, star.yPos),
radius = star.radius *(scale)
)
}
}
}
}
}
}
#Immutable
data class Star(val radius: Float, val xPos: Float, val yPos: Float)
One solution is to wrap your code in a LaunchedEffect so that the animation runs once:
#Composable
fun NightSkyCard() {
Card(
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(20.dp),
backgroundColor = DarkBlue
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
for (n in 0..20) {
var size by remember { mutableStateOf(0) }
var start by remember { mutableStateOf(0) }
var top by remember { mutableStateOf(0) }
LaunchedEffect(key1 = Unit) {
size = Random.nextInt(3, 5)
start = Random.nextInt(0, maxWidth.value.toInt())
top = Random.nextInt(10, maxHeight.value.toInt())
}
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = null,
modifier = Modifier
.padding(start = start.dp, top = top.dp)
.size(size.dp)
.scale(scale),
tint = Color.White
)
}
}
}
}
You then get 21 blinking stars.
What is the difference between these two approaches?
val result = remember(key1, key2) { computeIt(key1, key2) } (Docs)
val result by remember { derivedStateOf { computeIt(key1, key2) } } (Docs)
Both avoid re-computation if neither key1 nor key2 has changed
.
The second also avoids re-computations if downstream states are derived, but else, they are identical in their behavior, aren't they?
derivedStateOf {} is used when your state or key is changing more than you want to update your UI. It acts as a buffer for you, buffering out the changes you don't need. That is the primary difference between a keyed remember {} and derivedStateOf {}. With remember {}, you will still recompose as much as your key changes, which isn't a bad thing if that's what you need. If you don't need those recompositions though, that is where derivedStateOf {} comes in.
Take for example showing a button only if the user has scrolled a LazyColumn.
val isVisible = lazyListState.firstVisibleItemIndex > 0
firstVisibleItemIndex will change 0, 1, 2 etc as the user scrolls and cause a recomposition for every time it changes.
I only care about if it's 0 or not 0 and only want to recompose when that condition changes.
derivedStateOf is my buffer, it's buffering out all of those extra state changes I don't need and limiting the recomposition to only when the derivedStateOf changes.
val isVisible = remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } }
For the example given in the question, a remember(key1, key2) {} is the correct API to use there, not derivedStateOf {} because you want your UI to update any time the key changes, there isn't any change to buffer out.
Update: There is a detailed explanation of derivedStateOf in this talk https://www.youtube.com/watch?v=ahXLwg2JYpc&t=412s
AFAIK there is no difference here. It's just a coincidence that both constructs are doing the same thing here in this context. But, there are differences!
The biggest one is that derivedStateOf is not composable and it does no caching on it's own (remember does). So derivedStateOf is meant for long running calculations that have to be run only if key changes. Or it can be used to merge multiple states that are not in composable (in custom class for example).
I think the exact explanation is blurred for "outsiders", we need some input from some compose team member here :). My source for the above is this one thread on slack and my own experiments
EDIT:
Today i learned another derivedStateOf usage, very important one. It can be used to limit recomposition count when using some very frequently used value for calculation.
Example:
// we have a variable scrollState: Int that gets updated every time user scrolls
// we want to update our counter for every 100 pixels scrolled.
// To avoid recomposition every single pixel change we are using derivedStateOf
val counter = remember {
derivedStateOf {
(scrollState / 100).roundToInt()
}
}
// this will be recomposed only on counter change, so it will "ignore" scrollState in 99% of cases
Text(counter.toString()).
My source for that is as direct as it can be - from the author of compose runtime and the snapshot system, the Chuck Jazdzewski himself. I highly recommend watching stream with him here: https://www.youtube.com/watch?v=waJ_dklg6fU
EDIT2:
We finally have some official performance documentation with small mention of derivedStateOf. So the official purpose of derivedStateOf is to limit composition count (like in my example). sauce
val result = remember(key1, key2) { computeIt(key1, key2) } re-calculates when key1 or key2 changes but derivedStateOf is for tracking a change in one or more State/MutableState as stated in documents as
var a by remember { mutableStateOf(0) }
var b by remember { mutableStateOf(0) }
val sum = remember { derivedStateOf { a + b } }
// Changing either a or b will cause CountDisplay to recompose but not trigger Example
// to recompose.
CountDisplay(sum)
It's convenient to use derivedStateOf when you need to track a change in property of a State object. The value you store in State can be an object but when you need to track one or some properties of object you need to use derivedStateOf. And if it's not derived from a State/MutableState or object with an interface with #Stable annotation Composable won't recompose since recomposition requires a state change.
For instance an Input layout or number of items that you need to trigger another recomposition after a certain threshold or state.
var numberOfItems by remember {
mutableStateOf(0)
}
// Use derivedStateOf when a certain state is calculated or derived from other state objects.
// Using this function guarantees that the calculation will only occur whenever one
// of the states used in the calculation changes.
val derivedStateMax by remember {
derivedStateOf {
numberOfItems > 5
}
}
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "Amount to buy: $numberOfItems", modifier = Modifier.weight(1f))
IconButton(onClick = { numberOfItems++ }) {
Icon(imageVector = Icons.Default.Add, contentDescription = "add")
}
Spacer(modifier = Modifier.width(4.dp))
IconButton(onClick = { if (derivedStateMin) numberOfItems-- }) {
Icon(imageVector = Icons.Default.Remove, contentDescription = "remove")
}
}
if (derivedStateMax) {
Text("You cannot buy more than 5 items", color = Color(0xffE53935))
}
}
This is whatsapp text input that displays icons based on whether text is empty or not by reading from text
internal fun ChatInput(modifier: Modifier = Modifier, onMessageChange: (String) -> Unit) {
var input by remember { mutableStateOf(TextFieldValue("")) }
val textEmpty: Boolean by derivedStateOf { input.text.isEmpty() }
Row(
modifier = modifier
.padding(horizontal = 8.dp, vertical = 6.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.Bottom
) {
ChatTextField(
modifier = modifier.weight(1f),
input = input,
empty = textEmpty,
onValueChange = {
input = it
}
)
Spacer(modifier = Modifier.width(6.dp))
FloatingActionButton(
modifier = Modifier.size(48.dp),
backgroundColor = Color(0xff00897B),
onClick = {
if (!textEmpty) {
onMessageChange(input.text)
input = TextFieldValue("")
}
}
) {
Icon(
tint = Color.White,
imageVector = if (textEmpty) Icons.Filled.Mic else Icons.Filled.Send,
contentDescription = null
)
}
}
}
The Code A is from the official sample code here.
I know that in order to preserve state across recompositions, remember the mutable state using remember.
I think that the code val extraPadding by animateDpAsState(...) should be val extraPadding by remember { animateDpAsState(...) }, is it right?
BTW, val extraPadding by remember { animateDpAsState(...) } will cause the error
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState( //Should I add remember
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
No, you shouldn't. This function is marked with #Composable so it should be used directly in the view builder.
animateDpAsState will calculate its value depending on targetValue on each recomposition.
If you check it source code you'll see, that it uses remember inside, that's why it's marked with #Composable, and that's why you shouldn't bother about remembering some values manually.
For anyone that comes across this in the future, instead of attempting to remember the animateDpAsState it would be better to remember the expansion state. Because in this case we're trying to remember the state in an item of a list we use rememberSaveable:
var expanded by rememberSaveable { mutableStateOf(false) }
A very similar case of storing indexes across list items is shown in the official documentation here
I'm trying to make a chess engine desktop app with an ui component to it.
The game() method that I mention is a simple while loop that allows me to ask for moves in algebraic notation and make the moves if they are valid ones.
I have the following main and my problem is that if I uncomment and run the game() method inside the classe it wont start the App window, and if I instead try to uncomment the same method but from outside the Window it still wont start the desktop App.
On the other hand if i run it has it is, it will start the UI window.
fun main() = application {
resetBoard()
printBoardSmall()
Window(onCloseRequest = ::exitApplication, icon = painterResource("black_knight.png"), title = "Chess") {
ui()
//game()
}
//game()
}
#Composable
fun ui() {
var squarePair = false
Row {
Column {
for (n in 8 downTo 1) {
Row {
Text(
"" + n,
textAlign = TextAlign.Center,
modifier = Modifier.width(SIZE_TILE),
fontSize = FONT_SIZE_BOARD,
fontWeight = FontWeight.Bold
)
squarePair = boardLines(n, squarePair)
}
}
Row {
Text(" ", textAlign = TextAlign.Center, modifier = Modifier.width(SIZE_TILE))
for (n in 0..7) {
Text(
"" + ('A' + n),
textAlign = TextAlign.Center,
modifier = Modifier.width(SIZE_TILE),
fontSize = FONT_SIZE_BOARD,
fontWeight = FontWeight.Bold
)
}
}
}
Column {
Text(" Play", textAlign = TextAlign.Center, fontSize = 30.sp)
var move = ""
//var move by remember { mutableStateOf("") }
TextField(
value = move,
onValueChange = { move = it },
label = { Text("Move") },
maxLines = 1,
textStyle = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold),
modifier = Modifier.padding(20.dp)
)
print(move)
}
}
}
#Composable
fun board(n: Int, i: Int){
var team = ""
if(utils.isWhite(BOARD[n-1][i-1])) team = TEAM[0]
if(utils.isBlack(BOARD[n-1][i-1])) team = TEAM[1]
for(k in LOWER_CASE_LETTERS.indices) {
if (BOARD[n-1][i-1] == LOWER_CASE_LETTERS[k] || BOARD[n-1][i-1] == UPPER_CASE_LETTERS[k]) {
Image(painter = painterResource(team + "_" + PIECES[k] + ".png"), contentDescription = PIECES[k])
}
}
}
Im new to compose and I can't figure out whats the problem, expecially since im trying to run the method outside the Window
I'm no expert, but I'll try my best to help.
It doesn't work, when you call it in the Window, 'cause that's what it uses to init it's layout. So the Window won't show, 'till the game() stops.
I don't know, why it doesn't work, when you put it after the Window. Looking at fun main() = application { ... } I suspect, that initiating the application is similar to initiating the Window, so it also won't start, untill the game() is over.
You should try putting game() in a seperate thread. Something like this should work:
fun() main = application {
resetBoard()
printBoardSmall()
Window( ... ){
ui()
}
Thread {
game()
}.start()
}
P.S. I'm also very new to compose. And kinda new to programming. And really, really sorry for all the gross oversimplifications I made.
P.P.S. You could add a TextField to your app and use Button's onClick parameter to call a move checking function. Wouldn't need to play with Threads and coroutines then. And you could also use Buttons for the board squares, cause you can assign them a background and an image. If you would need help with any of that - HMU.