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
.
- ImageVector
- ImageBitmap
- 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 acceptrecomposition
.
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
- 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 theImageVector
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, thisremember
block returns the value that was last computed; it does not re-compute the value and thus avoids the overhead ofrecomposition
. - Efficient Resource Handling:
Use ofTypedValue
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 thevector
drawable each time. Here it would parse the resource XML into a reusableImageVector
, minimizing the overhead of re-parsing on each recomposition. - Skipping Unnecessary Recompositions:
Because the vector resource is cached viaremember
, Jetpack Compose can skip recompositions when the vector resource hasn’t changed, which avoids re-parsing the XML resource and re-creating theImageVector
.
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 inloadVectorResourceInner
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