I'm trying to port a rather complex Android View to Compose and I've managed to do a naive implementation by basically using a Canvas and moving the onDraw() code there. I've ran into issues when trying to optimize this to make it skip unneeded parts of the recomposition.
The view is a board for the game of GO (it would be the same for chess). I'm trying to get things such as the board's background to not redraw every time a move is made, as the background does not change. As my understanding of the docs is, if I pull the drawBackground() from the onDraw and just put it in an Image() composable, the Image() composable should not get recomposed unless its parameter (which is just the bitmap) changes. However, breakpoints show the method getting called every single time the position changes (e.g. the player makes a move). Am I doing something wrong? How could I take advantage of Compose here?
Code:
#Composable
fun Board(modifier: Modifier = Modifier, boardSize: Int, position: Position?, candidateMove: Point?, candidateMoveType: StoneType?, onTapMove: ((Point) -> Unit)? = null, onTapUp: ((Point) -> Unit)? = null) {
val background: ImageBitmap = imageResource(id = R.mipmap.texture)
Box(modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
Image(bitmap = background) // Expecting this to run only once, but gets run every time Board() gets recomposed!!!
var width by remember { mutableStateOf(0) }
val measurements = remember(width, boardSize) { doMeasurements(width, boardSize, drawCoordinates) }
var lastHotTrackedPoint: Point? by remember { mutableStateOf(null) }
Canvas(modifier = Modifier.fillMaxSize()) {
if (measurements.width == 0) {
return#Canvas
}
//... lots of draw code here
}
}
}
Any Jetpack Compose guru can help me understand why is it not skipping that recomposition?
Try putting Image(bitmap = background) in a separate composable function. Or moving Canvas to another function.
Related
so I wasted a good couple of days on this, and my deadline's tomorrow
basically, I have a mutableLiveData var which is a companion object,
and when the data is updated it calls the observe function inside the grid.
The observe function gets called fine, it does the print statements which you can see,
but it completely skips everything in the compose "items()" method.
Can someone please help me? I could not find anything useful online...
#ExperimentalFoundationApi
#Composable
fun ItemsGrid() {
LazyVerticalGrid(
cells = GridCells.Fixed(3),
contentPadding = PaddingValues(8.dp)
) {
mProductsByCategoryID.observe(viewLifecycleOwner,
{ products ->
println("DATA CHANGGED ${products[0].name}")
println(mProductsByCategoryID.value?.get(0)?.name)
items(products) {
println("INSIDE ITEMS for products ${it.name}") // never gets inside of here except for the first time the view loads
DemoCards(demo = it)
}
}
)
}
}
In Compose you shouldn't observe LiveData directly inside a #Composable, but observe it as State. Instead of callbacks to update UI, now we have Recomposition (#Composable function getting called automatically over and over again every time an observed value in the #Composable function changes).
More info here
Your code should look more like:
#Composable
fun ItemsGrid() {
val productsByCategory = mProductsByCategoryID.observeAsState()
LazyVerticalGrid(
cells = GridCells.Fixed(3),
contentPadding = PaddingValues(8.dp)
) {
//here goes code that does what your callback was doing before.
//when val productsByCategory.value changes, ItemsGrid()
//gets recomposed (called again) and this code will execute
}
}
In Jetpack Compose I'm using AndroidView to display an ad banner from a company called Smart.IO.
At the moment the banner shows when first initialised, but then fails to recompose when user comes back to the screen it's displayed on.
I'm aware of using the update function inside compose view, but I can't find any parameters I could use to essentially update on Banner to trigger the recomposition.
AndroidView(
modifier = Modifier
.fillMaxWidth(),
factory = { context ->
Banner(context as Activity?)
},
update = {
}
)
This could be a library error. You can check if this view behaves normally in normal Android XML.
Or maybe you need to use some API from this library, personally I haven't found any decent documentation or Android SDK source code.
Anyway, here is how you can make your view update.
You can keep track of life-cycle events, as shown in this answer, and only display your view during ON_RESUME. This will take it off the screen when it is paused, and make it create a new view when it resumes:
val lifeCycleState by LocalLifecycleOwner.current.lifecycle.observeAsSate()
if (lifeCycleState == Lifecycle.Event.ON_RESUME) {
AndroidView(
modifier = Modifier
.fillMaxWidth(),
factory = { context ->
Banner(context as Activity?)
},
update = {
}
)
}
Lifecycle.observeAsSate:
#Composable
fun Lifecycle.observeAsSate(): State<Lifecycle.Event> {
val state = remember { mutableStateOf(Lifecycle.Event.ON_ANY) }
DisposableEffect(this) {
val observer = LifecycleEventObserver { _, event ->
state.value = event
}
this#observeAsSate.addObserver(observer)
onDispose {
this#observeAsSate.removeObserver(observer)
}
}
return state
}
The Code A is from the project ThemingCodelab, you can see full code here.
I think that the keyword remember is not necessary in Code A.
I have tested the Code B, it seems that I can get the same result just like Code A.
Why need the author to add the keyword remember in this #Composable ?
Code A
#Composable
fun Home() {
val featured = remember { PostRepo.getFeaturedPost() }
val posts = remember { PostRepo.getPosts() }
MaterialTheme {
Scaffold(
topBar = { AppBar() }
) { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
item {
Header(stringResource(R.string.top))
}
item {
FeaturedPost(
post = featured,
modifier = Modifier.padding(16.dp)
)
}
item {
Header(stringResource(R.string.popular))
}
items(posts) { post ->
PostItem(post = post)
Divider(startIndent = 72.dp)
}
}
}
}
}
Code B
#Composable
fun Home() {
val featured =PostRepo.getFeaturedPost()
val posts = PostRepo.getPosts()
...//It's the same with the above code
}
You need to use remember to prevent recomputation during recomposition.
Your example works without remember because this view will not recompose while you scroll through it.
But if you use animations, add state variables or use a view model, your view can be recomposed many times(when animating up to once a frame), in which case getting data from the repository will be repeated many times, so you need to use remember to save the result of the computation between recompositions.
So always use remember inside a view builder if the calculations are at least a little heavy, even if right now it looks like the view is not gonna be recomposed.
You can read more about the state in compose in documentation, including this youtube video, which explains the basic principles.
I completely confused with compose conception.
I have a code
#Composable
fun HomeScreen(viewModel: HomeViewModel = getViewModel()) {
Scaffold {
val isTimeEnable by viewModel.isTimerEnable.observeAsState()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
) {
Switch(
checked = isTimeEnable ?: false,
onCheckedChange = {
viewModel.setTimerEnable(it)
},
)
Clock(viewModel.timeSelected.value!!) {
viewModel.setTime(it)
}
}
}
}
#Composable
fun Clock(date: Long, selectTime: (date: Date) -> Unit) {
NumberClock(Date(date)) {
val time = SimpleDateFormat("HH:mm", Locale.ROOT).format(it)
Timber.d("Selected time: time")
selectTime(it)
}
}
Why Clock widget recomposes when I tap switch. If I remove line selectTime(it) from Clock widget callback recomposition doesn't happen.
Compose version: 1.0.2
This is because in terms of compose, you are creating a new selectTime lambda every time, so recomposition is necessary. If you pass setTime function as a reference, compose will know that it is the same function, so no recomposition is needed:
Clock(viewModel.timeSelected.value!!, viewModel::setTime)
Alternatively if you have more complex handler, you can remember it. Double brackets ({{ }}) are critical here, because you need to remember the lambda.
Clock(
date = viewModel.timeSelected.value!!,
selectTime = remember(viewModel) {
{
viewModel.setTimerEnable(it)
}
}
)
I know it looks kind of strange, you can use rememberLambda which will make your code more readable:
selectTime = rememberLambda(viewModel) {
viewModel.setTimerEnable(it)
}
Note that you need to pass all values that may change as keys, so remember will be recalculated on demand.
In general, recomposition is not a bad thing. Of course, if you can decrease it, you should do that, but your code should work fine even if it is recomposed many times. For example, you should not do heavy calculations right inside composable to do this, but instead use side effects.
So if recomposing Clock causes weird UI effects, there is probably something wrong with your NumberClock that cannot survive the recomposition. If so, please add the NumberClock code to your question for advice on how to improve it.
This is the intended behaviour. You are clearly modifying the isTimeEnabled field inside your viewmodel when the user toggles the switch (by calling vm.setTimeenabled). Now, it is apparent that the isTimeEnabled in your viewmodel is a LiveData instance, and you are referring to that instance from within your Composable by calling observeAsState on it. Hence, when you modify the value from the switch's onValueChange, you are essentially modifying the state that the Composable depends on. Hence, to render the updated state, a recomposition is triggered
How can I go about making a composable deep down within the render tree full screen, similar to how the Dialog composable works?
Say, for example, when a use clicks an image it shows a full-screen preview of the image without changing the current route.
I could do this in CSS with position: absolute or position: fixed but how would I go about doing this in Jetpack Compose? Is it even possible?
One solution would be to have a composable at the top of the tree that can be passed another composable as an argument from somewhere else in the tree, but this sounds kind of messy. Surely there is a better way.
From what I can tell you want to be able to draw from a nested hierarchy without being limited by the parent constraints.
We faced similar issues and looked at the implementation how Composables such as Popup, DropDown and Dialog function.
What they do is add an entirely new ComposeView to the Window.
Because of this they are basically starting from a blank canvas.
By making it transparent it looks like the Dialog/Popup/DropDown appears on top.
Unfortunately we could not find a Composable that provides us the functionality to just add a new ComposeView to the Window so we copied the relevant parts and made following.
#Composable
fun FullScreen(content: #Composable () -> Unit) {
val view = LocalView.current
val parentComposition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
val id = rememberSaveable { UUID.randomUUID() }
val fullScreenLayout = remember {
FullScreenLayout(
view,
id
).apply {
setContent(parentComposition) {
currentContent()
}
}
}
DisposableEffect(fullScreenLayout) {
fullScreenLayout.show()
onDispose { fullScreenLayout.dismiss() }
}
}
#SuppressLint("ViewConstructor")
private class FullScreenLayout(
private val composeView: View,
uniqueId: UUID
) : AbstractComposeView(composeView.context) {
private val windowManager =
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val params = createLayoutParams()
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
init {
id = android.R.id.content
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
ViewTreeSavedStateRegistryOwner.set(this, ViewTreeSavedStateRegistryOwner.get(composeView))
setTag(R.id.compose_view_saveable_id_tag, "CustomLayout:$uniqueId")
}
private var content: #Composable () -> Unit by mutableStateOf({})
#Composable
override fun Content() {
content()
}
fun setContent(parent: CompositionContext, content: #Composable () -> Unit) {
setParentCompositionContext(parent)
this.content = content
shouldCreateCompositionOnAttachedToWindow = true
}
private fun createLayoutParams(): WindowManager.LayoutParams =
WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
token = composeView.applicationWindowToken
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
fun show() {
windowManager.addView(this, params)
}
fun dismiss() {
disposeComposition()
ViewTreeLifecycleOwner.set(this, null)
windowManager.removeViewImmediate(this)
}
}
Here is an example how you can use it
#Composable
internal fun Screen() {
Column(
Modifier
.fillMaxSize()
.background(Color.Red)
) {
Text("Hello World")
Box(Modifier.size(100.dp).background(Color.Yellow)) {
DeeplyNestedComposable()
}
}
}
#Composable
fun DeeplyNestedComposable() {
var showFullScreenSomething by remember { mutableStateOf(false) }
TextButton(onClick = { showFullScreenSomething = true }) {
Text("Show full screen content")
}
if (showFullScreenSomething) {
FullScreen {
Box(
Modifier
.fillMaxSize()
.background(Color.Green)
) {
Text("Full screen text", Modifier.align(Alignment.Center))
TextButton(onClick = { showFullScreenSomething = false }) {
Text("Close")
}
}
}
}
}
The yellow box has set some constraints, which would prevent the Composables from inside to draw outside its bounds.
Using the Dialog composable, I have been able to get a proper fullscreen Composable in any nested one. It's quicker and easier than some of other answers.
Dialog(
onDismissRequest = { /* Do something when back button pressed */ },
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false, usePlatformDefaultWidth = false)
){
/* Your full screen content */
}
If I understand correctly you just don't want to navigate anywhere. Id something like this.
when (val viewType = viewModel.viewTypeGallery.get()) {
is GalleryViewModel.GalleryViewType.Gallery -> {
Gallery(viewModel, scope, installId, filePathModifier, fragment, setImageUploadType)
}
is GalleryViewModel.GalleryViewType.ImageViewer -> {
Row(Modifier.fillMaxWidth()) {
Image(
modifier = Modifier
.fillMaxSize(),
painter = rememberCoilPainter(viewType.imgUrl),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
}
}
I just keep track of what type the view is meant to be. In my case I'm not displaying a dialog I'm removing my entire gallery and showing an image instead.
Alternatively you could just have an if(viewImage) condition below your call your and layer the 'dialog' on top of it.
After notice that, at least for now, we don't have any Composable to do "easy" fullscreen, I decided to implement mine one, mostly based on ideas from #foxtrotuniform6969 and #ntoskrnl. Also, I tried to do it most possible without to use platform dependent functions then I think this is very suiteable to Desktop/Android.
You can check the basic implementation in this GitHub repository.
By the way, the implementation idea was just:
Create a composable to wrap the target composables tree that can call an FullScreen composable;
Retrieve the full screen dimensions/size from a auxiliary Box matched to the root screen size using the .onGloballyPositioned() modifier;
Store the full screen size and all FullScreen composables created in the tree onto appropriated compositionLocalOf instances (see documentation).
I tried to use this in a Desktop project and seems to be working, however I didn't tested in Android yet. The repository also contains a example.
Feel free to navigate in the repository and sent a pull request if you can. :)