So, I have implemented a lazycolumnfor to work with a list of recipe elements, the thing is that it does not smooth scroll, if I just scroll fast it stutters till the last element appears and not smooth scroll.
Is this an error from my side or do I need to add something else?
data class Recipe(
#DrawableRes val imageResource: Int,
val title: String,
val ingredients: List<String>
)
val recipeList = listOf(
Recipe(R.drawable.header,"Cake1", listOf("Cheese","Sugar","water")),
Recipe(R.drawable.header,"Cake2", listOf("Cheese1","Sugar1","Vanilla")),
Recipe(R.drawable.header,"Cake3", listOf("Bread","Sugar2","Apple")))
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RecipeList(recipeList = recipeList)
}
}
}
#Composable
fun RecipeCard(recipe:Recipe){
val image = imageResource(R.drawable.header)
Surface(shape = RoundedCornerShape(8.dp),elevation = 8.dp,modifier = Modifier.padding(8.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth().clip(shape = RoundedCornerShape(8.dp))
Image(asset = image,modifier = imageModifier,contentScale = ContentScale.Crop)
Spacer(modifier = Modifier.preferredHeight(16.dp))
Text(text = recipe.title,style = typography.h6)
for(ingredient in recipe.ingredients){
Text(text = ingredient,style = typography.body2)
}
}
}
}
#Composable
fun RecipeList(recipeList:List<Recipe>){
LazyColumnFor(items = recipeList) { item ->
RecipeCard(recipe = item)
}
}
#Preview
#Composable
fun RecipePreview(){
RecipeCard(recipeList[0])
}
Currently (version 1.0.0-alpha02) Jetpack Compose has 2 Composable functions for loading image resources:
imageResource(): this Composable function, load an image resource synchronously.
loadImageResource(): this function loads the image in a background thread, and once the loading finishes, recompose is scheduled and this function will return deferred image resource with LoadedResource or FailedResource
So your lazyColumn is not scrolling smoothly since you are loading images synchronously.
So you should either use loadImageResource() or a library named Accompanist by Chris Banes, which can fetch and display images from external sources, such as network, using the Coil image loading library.
UPDATE:
Using CoilImage :
First, add Accompanist Gradle dependency, then simply use CoilImage composable function:
CoilImage(data = R.drawable.header)
Using loadImageResource() :
val deferredImage = loadImageResource(
id = R.drawable.header,
)
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth()
.clip(shape = RoundedCornerShape(8.dp))
deferredImage.resource.resource?.let {
Image(
asset = it,
modifier = imageModifier
)
}
Note: I tried both ways in a LazyColumnFor, and although loadImageResource() performed better than imageResource() but still it didn't scroll smoothly.
So I highly recommend using CoilImage
Note 2: To use Glide or Picasso, check this repository by Vinay Gaba
On the other note, LazyColumn haven't been optimised for scrolling performance yet, but I've just tested on 1.0.0-beta07 release and can confirm it's way smoother than 1.0.0-beta06
Compose.UI 1.0.0-beta07 relevant change log:
LazyColumn/Row will now keep up to 2 previously visible items active (not disposed) even when they are scrolled out already. This allows the component to reuse the active subcompositions when we will need to compose a new item which improves the scrolling performance. (Ie5555)
Related
I want to read from the Room database as updates are made and render in Compose UI. I am only getting the initial state and updates to the table are not showing up unless I go to the previous screen and come back. What am I doing wrong?
Note: I understand I need to have a ViewModel in between but I suppose the issue will still be there.
UI:
#Composable
fun ListScreen(itemKey: String, itemRepository: ItemRepository) {
val itemList by itemRepository.getItemData(itemKey).collectAsState(initial = listOf())
LazyColumn(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 10.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(itemList) {
Text(it, style = MaterialTheme.typography.body2)
}
}
}
Repository:
fun getItemData(itemKey: String): Flow<List<String>> = dao.getItems(itemKey)
DAO:
#Query("SELECT value FROM ItemEntity WHERE itemKey = :itemKey")
fun getItems(itemKey: String): Flow<List<String>>
Found the issue. The code that writes to the database and the one that reads from it were using different RoomDatabase objects. After making sure they are the same, live updates started to show up.
I have a simple app with a single screen, displaying movies in a Composable items list:
I use Android's paging3 library in order to load the movies page by page, and things seem to be working well:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
MoviesList(lazyMovieItems)
}
#Composable
fun MoviesList(lazyPagedMovies: LazyPagingItems<Movie>) {
LazyColumn(modifier = Modifier.padding(horizontal = 16.dp)) {
itemsIndexed(lazyPagedMovies) { index, movie ->
MoviesListItem(index, movie!!)
}
}
}
In an attempt to add a progress indicator to the initial loading phase (e.g. as explained in an Android code-lab), I've tried applying the following conditional, based on loadState.refresh:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
// Added: Show a progress indicator while the data is loading
if (lazyPagedMovies.loadState.refresh is LoadState.Loading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
MoviesList(lazyMovieItems)
}
Instead of displaying the progress indicator, this naive addition seem to be putting the paging loader into an infinite loop, where the first page gets fetched over and over indefinitely, without any items effectively being loaded (let alone displayed) into the list.
Side note: Just to rule out that this all has something to do with the condition itself, it appears that even adding as little as this log: Log.i("DBG", ""+lazyPagesMovies.loadState) with no conditions at all, introduces the undesired behavior.
I'm using Kotlin version 1.7.10 and the various Compose libraries in version 1.3.1.
Seems that with this simple code I might have somehow hit some Compose related edge-case. I've managed to work around things by introducing the progress-indicator conditional under a sub-function (composable) that accepts the paging items directly:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
MoviesScreen(lazyMovieItems) // was: MoviesList(lazyMovieItems)
}
// Newly added intermediate function
#Composable
fun MoviesScreen(lazyPagedMovies: LazyPagingItems<Movie>) {
MoviesList(lazyPagedMovies)
if (lazyPagedMovies.loadState.refresh is LoadState.Loading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
#Composable
fun MoviesList(lazyPagedMovies: LazyPagingItems<Movie>) {
// ... (unchanged)
}
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.
Animating items in LazyColumn and LazyRow in Compose is not yet supported:
https://developer.android.com/jetpack/compose/lists#item-animations
Follow issue tracker:
https://issuetracker.google.com/issues/150812265
However I created a small POC on a potential workaround until it's officially supported (Check answer), it's far from production ready and definitely contains bugs but just thought of sharing my small playground project
EDIT: The issue tracker now has an update with a solution using a modifier
Just made a small POC of a workaround on animating items in LazyColumn and LazyRow until a proper support is added:
https://github.com/RoudyK/AnimatedLazyColumn
DEF not production ready and happy to get any feedback
EDIT:
Example usage:
data class MainItem(
val id: String,
val text: String
)
val items = List(10) { MainItem(UUID.randomUUID().toString(), UUID.randomUUID().toString()) }
val state = rememberLazyListState()
AnimatedLazyColumn(
state = state,
items = items.map {
AnimatedLazyListItem(key = it.id, value = it.text) {
TextItem(viewModel, it)
}
}
)
AnimatedLazyRow(
state = state,
items = items.map {
AnimatedLazyListItem(key = it.id, value = it.text) {
TextItem(viewModel, it)
}
}
)
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. :)