Blog Infos
Author
Published
Topics
, , , ,
Published

Hello Folks,

We are here again with Jetpack Compose a widely popular topic. Every day we use Image and there 3 different components of Image.

  1. ImageVector
  2. ImageBitmap
  3. painterResources

Each has a different sort of use, right? But the real question is: When should you use which one? 🤔

There are many discussions around this topic, but today we are going to understand this thing by looking at the under-the-hood code, no more assumptions, just look at the facts, like we always do.

Today we will focus only on ImageVector and painterResources. Once you understand everything about these 2, then you will figure out by yourself about ImageBitmap.

So, let’s hope in.

ImageVector:-

Vector Graphics: This API is for vector drawable resources. Using ImageVector directly is useful when you are positive that you’re working with vector graphics because it gives you control over features that are available with vectors.

Performance Considerations: Although vector graphics are resolution-independent, this is a boon in the use of various screen sizes and densities. While working with ImageVector, you assure yourself that you are directly editing properties.

Let’s look at the sample code to see how we are implementing this.

@AndroidEntryPoint
class MainActivity2 : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val mainViewModel = hiltViewModel<MainViewModel>()

            LaunchedEffect(key1 = Unit) {
                mainViewModel.processAction(MainAction.ContinueData)
            }
            
            ShowImage(mainViewModel)
        }

    }
}

@Composable
fun ShowImage(mainViewModel: MainViewModel) {
    val playerAText by mainViewModel.state.map { it.playerAText }.collectAsState(initial = "A-0")

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .padding(vertical = 16.dp)
            .fillMaxSize()
    ) {
        Image(imageVector = ImageVector.vectorResource(id = R.drawable.ic_launcher_background), contentDescription = null)

        Text(text = playerAText.plus("B"), fontSize = 12.sp, color = Color.Magenta)
    }
}

Recomposition Result:-

Explanation:-

  • Here, if you check the results then you see that only Text recomposition is happening, Image is not. (Not like painterResource). Even if you change the parameter, neither it will skip recompose or accept recomposition.

But how is the hack it’s pulling off this?

Well, to know about let’s deep dive into ImageVector.

To understand more about internal working, let’s first look at the code.

  • So if you look at the code carefully you will find that Image composable is marked with @NonRestartableComposable.

If I brief about @NonRestartableComposable:-

This annotation can be applied to Composable functions in order to prevent code from being generated which allows this function’s execution to be skipped or restarted. This may be desirable for small functions that just directly call another composable function have very little machinery in them directly, and are unlikely to be invalidated themselves.

  • If you decompile @NonRestartableComposable code, you will find that it uses replacableCompose rather than restartableCompose, which helps to prevent code from being generated which allows this function’s execution to be skipped or restarted.

Now let’s see what will happen if we remove @NonRestartableComposable .

@AndroidEntryPoint
class MainActivity2 : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val mainViewModel = hiltViewModel<MainViewModel>()

            LaunchedEffect(key1 = Unit) {
                mainViewModel.processAction(MainAction.ContinueData)
            }

            ShowImage(mainViewModel)
        }

    }
}

@Composable
fun ShowImage(mainViewModel: MainViewModel) {
    val playerAText by mainViewModel.state.map { it.playerAText }.collectAsState(initial = "A-0")

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .padding(vertical = 16.dp)
            .fillMaxSize()
    ) {
        ImageVector(
            imageVector = ImageVector.vectorResource(id = R.drawable.ic_launcher_background),
            contentDescription = null
        )

        Text(text = playerAText.plus("B"), fontSize = 12.sp, color = Color.Magenta)
    }
}


@Composable
fun ImageVector(
    imageVector: ImageVector,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null
) = Image(
    painter = rememberVectorPainter(imageVector),
    contentDescription = contentDescription,
    modifier = modifier,
    alignment = alignment,
    contentScale = contentScale,
    alpha = alpha,
    colorFilter = colorFilter
)

Layout inspector result:-

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

  • You can see that it stopped to prevent it from being skipped or restarted because we have removed @NonRestartableComposable.

Unlike painterResources, it does not create new objects of Painter on each recomposition, there will only single instance and that’s the reason it will skip until the value gets changed.

How ImageVector is managing it?

Let’s look at the code for that, just click on vectorResource and you will find this below code.

@Composable
fun ImageVector.Companion.vectorResource(@DrawableRes id: Int): ImageVector {
    val context = LocalContext.current
    val res = resources()
    val theme = context.theme

    return remember(id, res, theme, res.configuration) {
        vectorResource(theme, res, id)
    }
}

fun ImageVector.Companion.vectorResource(
    theme: Resources.Theme? = null,
    res: Resources,
    resId: Int
): ImageVector {
    val value = TypedValue()
    res.getValue(resId, value, true)

    return loadVectorResourceInner(
        theme,
        res,
        res.getXml(resId).apply { seekToStartTag() },
        value.changingConfigurations
    ).imageVector
}

@Throws(XmlPullParserException::class)
@SuppressWarnings("RestrictedApi")
internal fun loadVectorResourceInner(
    theme: Resources.Theme? = null,
    res: Resources,
    parser: XmlResourceParser,
    changingConfigurations: Int
): ImageVectorCache.ImageVectorEntry {
    val attrs = Xml.asAttributeSet(parser)
    val resourceParser = AndroidVectorParser(parser)
    val builder = resourceParser.createVectorImageBuilder(res, theme, attrs)

    var nestedGroups = 0
    while (!parser.isAtEnd()) {
        nestedGroups = resourceParser.parseCurrentVectorNode(
            res,
            attrs,
            theme,
            builder,
            nestedGroups
        )
        parser.next()
    }
    return ImageVectorCache.ImageVectorEntry(builder.build(), changingConfigurations)
}

Recomposition for Themed Vector Drawables:

  • This is an ImageVector load function with an overridden compose that prevents potential recomposition when images are loaded in Jetpack Compose. How does this improve performance?
  • Resource Caching with remember:
    Wrapping the expensive logic of resource loading inside the remember block (remember(id, res, theme, res. configuration)) means you will be doing this work only if it needs to happen. The remember function caches the ImageVector resource in composition so that it won’t do the computation if any of the inputs is id, res, theme, or res. configuration. It therefore limits unnecessary recomposition triggered by states that have not changed.
    Unless the inputs change, this remember block returns the value that was last computed; it does not re-compute the value and thus avoids the overhead of recomposition.
  • Efficient Resource Handling:
    Use of TypedValue to read resource metadata that may include changing configurations, etc. This manages and then efficiently loads the vector resources.
    In contrast, this function optimizes the resource lookup instead of reloading and parsing the vector drawable each time. Here it would parse the resource XML into a reusable ImageVector, minimizing the overhead of re-parsing on each recomposition.
  • Skipping Unnecessary Recompositions:
    Because the vector resource is cached via remember, Jetpack Compose can skip recompositions when the vector resource hasn’t changed, which avoids re-parsing the XML resource and re-creating the ImageVector.
    This implies that meaningful modifications to the app’s configuration or resource ID can result in a recomposition, but generally, the app responds faster and less often to UI updates made unnecessarily.
  • Inner Parsing (loadVectorResourceInner):
    The custom parsing logic in loadVectorResourceInner gives you control over how vector XMLs are parsed. This allows you to fine-tune the resource loading process, potentially leading to faster parsing and reduced memory usage, especially for complex vector drawables.

painterResources:-

Most people are using painterResources to display images. Why? because it’s very easy and doesn’t need to handle conditions for bitmap and vector images. painterResoruces handles automatically.

How does it do that?

For that let’s look at code.

Explanation:-

  • Here if you check the code, you will notice that there is an if and else condition, and based on the condition it starts to load the image.
  • So if you have dynamic images (vector and bitmap, png, jpg), and don’t want to handle too many conditions then painterResources is your partner.

But wait, what about recomposition? we didn’t check for painterResources.

Let’s look at the result, with the same example which we took above.

@AndroidEntryPoint
class MainActivity2 : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val mainViewModel = hiltViewModel<MainViewModel>()

            LaunchedEffect(key1 = Unit) {
                mainViewModel.processAction(MainAction.ContinueData)
            }

            ShowImage(mainViewModel)
        }

    }
}

@Composable
fun ShowImage(mainViewModel: MainViewModel) {
    val playerAText by mainViewModel.state.map { it.playerAText }.collectAsState(initial = "A-0")

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .padding(vertical = 16.dp)
            .fillMaxSize()
    ) {

        Image(
            painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null
        )

        Text(text = playerAText.plus("B"), fontSize = 12.sp, color = Color.Magenta)
    }
}

Layout inspector results:-

As you can see, the whole different result than our ImageVector. It is recomposing every time. It creates a new object of Painter every time. and because of that reason, it is recomposing. Which is very very performance inefficient.

here we access directly painterResources as composable not via class if you check you will notice that ImageVector was an Immutable class and painterResources is directly composable.

How can we overcome this problem?

Well, there is one solution for this. we can create differently composable for Image .

@AndroidEntryPoint
class MainActivity2 : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val mainViewModel = hiltViewModel<MainViewModel>()

            LaunchedEffect(key1 = Unit) {
                mainViewModel.processAction(MainAction.ContinueData)
            }

            ShowImage(mainViewModel)
        }

    }
}

@Composable
fun ShowImage(mainViewModel: MainViewModel) {
    val playerAText by mainViewModel.state.map { it.playerAText }.collectAsState(initial = "A-0")

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .padding(vertical = 16.dp)
            .fillMaxSize()
    ) {

        ImageConstants(imageConstants = getData(R.drawable.ic_launcher_background))

        BackgroundImage()

        Text(text = playerAText.plus("B"), fontSize = 12.sp, color = Color.Magenta)
    }
}

@Composable
private fun BackgroundImage() {
    Image(
        imageVector = ImageVector.vectorResource(id = R.drawable.ic_launcher_background),
        contentDescription = null
    )
}

Layout inspector result:-

Now if you will see the results then you’ll find that BackgroundImage has started skipping recomposition, so with this approach Image with painterResources also works well now, until and unless there is no parameter change, it will continue to skip.

How this happened, you already know if you are using Jetpack Compose.

Is there any way to achieve this behavior without creating different composable?

Well, that is not recommended but still, we will see.

we will copy that internal code and paste with little change. We will add @NonRestartableComposable .


@NonRestartableComposable
@Composable
fun ImagePainter(
    painter: Painter,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null
) {
    val semantics = if (contentDescription != null) {
        Modifier.semantics {
            this.contentDescription = contentDescription
            this.role = Role.Image
        }
    } else {
        Modifier
    }

    // Explicitly use a simple Layout implementation here as Spacer squashes any non fixed
    // constraint with zero
    Layout(
        {},
        modifier.then(semantics).clipToBounds().paint(
            painter,
            alignment = alignment,
            contentScale = contentScale,
            alpha = alpha,
            colorFilter = colorFilter
        )
    ) { _, constraints ->
        layout(constraints.minWidth, constraints.minHeight) {}
    }
}

We will use ImagePainter instead of Image and with this approach, we will achieve the same result as ImageVector. Try it yourself.

So if someone tells you to just use ImageVector do not restart with every recomposition like painterResource so we should use only ImageVector not painterResource then that is not the whole truth.

It’s based on your use case whether you have dynamic image allocation or vector image only.

painterResource is also optimized where they do have a caching mechanism and everything.

So do not make your decision based on whatever rumors you hear, do some research and start reading internal code.

If you have any questions, just drop a comment, and I’ll get back to you ASAP. We’ll dive deeper into Jetpack Compose soon.

Until then, happy coding!

This article is previously published by proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu