HomeAndroidDownside fixing in Compose Textual content. On this weblog we take a...

Downside fixing in Compose Textual content. On this weblog we take a look at a sensible use… | by Alejandra Stamato | Android Builders | Apr, 2023


CASE STUDIES

Given a requirement, typically a number of alternate options could also be accessible to implement it. On this weblog we take a look at a sensible use case for positioning textual content in Compose. We’ll analyze a number of alternate options to resolve it, utilizing completely different Compose textual content APIs. We’ll overview professionals and cons and, as a bonus, the efficiency of every answer.

Caveat: We don’t anticipate you to undertake such an evaluation for each piece of code you write, nonetheless we thought-about it fascinating to share our method to find what choices you need to remedy a concrete situation and the way we went about selecting amongst them.

Take into account the next use case:

  • Vertically middle a Textual content composable in a container.
  • Textual content has a most of two strains.
  • The container’s peak is all the time the peak of two strains of textual content, irrespective of the precise variety of strains the textual content would occupy. This is the requirement which made the implementation non trivial.
Design requirement we wish to construct.

By default the textual content peak adjustments primarily based on the variety of strains, and wrap round them. That is the “default conduct” case.

“Default conduct”, textual content peak adjustments primarily based on the variety of strains, and wrap round them.

Compose 1.4 launched a minLines parameter in Textual content and TextField.
Setting each minLines and maxLines to 2 could remedy our challenge. However does it? 🤔Let’s see:

Outcome utilizing Textual content minlines

The textual content shouldn’t be vertically aligned within the measure area. There’s a characteristic request to offer extra vertical alignment configuration.

Usually, a easy heuristic we wish might be:

At this level you may assume why don’t we simply set the container peak to a set worth? Whereas this works in precept, the answer shouldn’t be precise and doesn’t assure 2 strains are all the time seen, for instance when the person adjustments font dimension for higher accessibility.

For simplicity, we assume that our textual content shouldn’t be multi-styled so calculating two strains with any set of characters would give us the identical peak in all circumstances.

Which textual content may we measure that’s all the time 2 strains in peak? This query has a simple reply: a string containing newline char, or “n”, represents 2 empty strains.

Discovering the perfect answer typically entails hanging a stability between various factors, akin to the next:

  • Readability: how simply the code will be learn, understood and maintained by different builders.
  • Compose-idiomacy: how effectively the code follows the conventions and patterns of the programming language or framework getting used, Compose in our case.
  • Efficiency: how effectively the code performs when it comes to body rendering pace and reminiscence utilization.

Whereas optimising for one issue could enhance the general high quality of the code, it could possibly come on the expense of different points. On the finish we decide the one which achieves the optimum stability between these, leading to code that’s straightforward to know, environment friendly, and follows greatest practices.

Now take just a few moments earlier than we begin and assume: how would you’ve gotten solved this drawback?

Accomplished? Are you prepared? Let’s go! 🚀

What follows is an exploration of a number of the alternate options we got here up:

  • #1: Two Textual content composables. Including a relentless 2 strains Textual content composable as spacer.
  • #2: TextMeasurer. Measuring ”n” in composition, after which utilizing this measurement to set Textual content peak.
  • #3: TextLayoutResult and onTextLayout lambda. Utilizing a structure callback to recompose with the proper Textual content peak.
  • #4: structure modifier. Measure and set the proper Textual content peak in the course of the structure part.
  • 🚫#5: Drawing Textual content on Canvas with drawText with the proper peak, centred manually in each instructions.
  • 🚫#6: Subcomposition. Measuring ”n” in subcomposition, after which utilizing this measurement to set Textual content peak.

The most suitable choice was #1. Why? Learn beneath!

We add two Textual content composables, one on prime of the opposite utilizing a Field. One has the textual content to show, and the opposite has the textual content “n”, which is all the time 2 strains excessive and doesn’t modify something visually on the display screen.

Execs

  • Achieves the required design by including a relentless padding.
  • Brief code, straightforward to know and preserve.

Cons

  • We’re all the time including this further textual content composable. You wouldn’t want it when the textual content is 2 strains excessive.

To keep away from all the time including an additional composable like we do in choice #1, we are able to get the peak of “n” in composition and use this peak for one single Textual content.

To do that we’d like to have the ability to measure textual content someway. Fortunately we now have a really helpful and highly effective API for this: TextMeasurer, chargeable for measuring a textual content in order that it’s able to be drawn.

We will create a TextMeasurer with the rememberTextMeasurer() operate. Then we use it to measure “n” and set this peak to the textual content we wish to show.

Discover how we’re utilizing the identical textual content model to measure “n” because the Textual content beneath, so we make sure that we measure with the identical constraints. Failing to do that may trigger getting a peak that’s completely different than the required.

TextMeasurer has an inner cache. So long as the textual content content material, textual content model and constraints don’t change, it’ll return the identical measurement consequence, so it’s tremendous to name it in composition (or no matter part you want). We will even go a step additional in our case and use bear in mind to retailer the results of the measurement as an optimization, so the calculation survives recompositions and we don’t must hit its cache. Usually although, we ought to be conscious when it might make sense to invalidate the bear in mind cache. Right here it’s not vital because the textual content is static.

Execs

  • Achieves the required design.
  • Code is straightforward to know and preserve.
  • Compose idiomatic.

Cons

  • Requires measuring with the identical model you’re drawing to keep away from a design distinction
  • Requires contemplating to invalidate cache manually or not in the course of the lifecycle of this composable.

To forestall an additional composable (choice #1), a substitute for measuring the textual content explicitly (choice #2) is utilizing Textual content’s onTextLayout lambda which returns TextLayoutResult. This object incorporates measurement data obtained in the course of the structure part in regards to the textual content being rendered, like the data we’d like: line rely and line peak.

You possibly can examine the Compose Phases documentation and the Compose phases episode of the MAD abilities collection on Layouts & Modifiers to be taught extra about Compose phases. However as a fast recap, Compose has three principal phases to remodel information into UI:

Within the onTextLayout block we are able to question if the textual content has 1 line with lineCount. If it does, we duplicate the peak, change the peak mutable state which triggers a recomposition. If the textual content already has 2 strains we don’t want to do that. This processing occurs in the course of the structure part.

This answer has 2 downsides.

First, this course of will make us lose a body for texts the place line rely is 1. The body shouldn’t be skipped (it received’t affect the frozen body charge), however as a result of we’re making a change in state earlier than the drawing part, there’s a hole the place nothing has been drawn. Compose wants to attend until the subsequent body to execute its 3 phases. Relying on the gadget, your customers could discover a blink. You may additionally see different composables affected, because the mother or father composable recalculates the place to put the youngsters, after considered one of them has simply modified its dimension.

And second, the consequence shouldn’t be good:

Outcome after utilizing onTextLayout lambda and multiplied peak *2 for a single-lined textual content

Discover the textual content on the left for which we made the calculation is taller than the one on the proper. To know why we have to shortly revisit textual content metrics.

On the time of writing, by default in Compose, there’s padding added on prime of the primary line and backside of the final line of the textual content, because of a legacy configuration known as includeFontPadding. By measuring one line of textual content:

And multiplying it by 2 you’re including this font padding twice (left consequence), when in actuality a two lined textual content would have a shorter ensuing peak (proper consequence).

The ensuing peak of stacking 2 strains of textual content is bigger than one single textual content with two strains.

These values are decided by the font metrics, that means this distinction may be extra apparent or not relying on which font you utilize.

One fascinating factor to strive is setting includeFontPadding to false to take away the additional padding on prime of the primary and backside of the final line of textual content. We received’t go into particulars of this answer right here. You possibly can examine the gist and the Fixing Font Padding in Compose Textual content weblog submit for those who’re to be taught extra about font metrics and textual content padding in Compose.

Let’s repair our code so we get the required design. As a substitute of getting the peak of the only line and multiplying it by two, we are able to measure the textual content “n” utilizing onTextLayout on the primary body, retailer this worth and use it for all different frames as our peak for the precise textual content.

We’re nonetheless dropping a body however the design is the one we wish. The code remains to be easy to keep up and we’re not including an additional Textual content composable once we don’t must.

Execs

  • Achieves the required design.
  • Code is straightforward to keep up.
  • Compose idiomatic.

Cons

  • The preliminary mistake was straightforward to make, if you’re unaware of the textual content metrics.
  • We lose a body by altering state earlier than the drawing part. It may be noticeable relying on the structure, gadget you’re operating, and so on.

You should utilize the structure modifier to switch how a component is measured and laid out, in our case, to measure and set the peak to be 2 strains of textual content. Then we are able to place the textual content within the middle of the container.

We can not use bear in mind operate contained in the structure block as we’re not in composition at this level. As talked about earlier than, TextMeasurer comes with an inner cache, so measuring a number of occasions shouldn’t be an issue, however it’ll take a toll.

We will additionally use the TextMeasurer to measure in composition, bear in mind the results of the calculation, after which use the consequence in the course of the structure part, just like what we did in choice 2 to get a greater efficiency.

To be taught extra in regards to the structure modifier, see the Use the structure modifier web page in our documentation and the Superior structure ideas episode of the MAD Abilities collection on Layouts and Modifiers.

Execs

  • Achieves the required design.
  • Compose- idiomatic, by leveraging the ability of the drawing part.

Cons

  • Handbook vertical alignment by calculating relative place.
  • Measuring and remembering this measure in composition is extra environment friendly than utilizing TextMeasurer inner cache.

Whereas drawing the textual content in Canvas straight is feasible, this selection shouldn’t be appropriate for our use case, because it skips over the fabric and basis layers of Compose textual content, which implements core options like textual content choice and accessibility.
Drawing textual content in Canvas is primarily for inventive functions, akin to ornamental textual content or customized animated typography.
For all this we received’t add it to the ultimate shortlist of choices to think about. We’ll write it only for enjoyable.

The right API to attract Compose Textual content on Canvas is drawText. This API receives a TextLayoutResult as obtained from measuring textual content with TextMeasurer with the mandatory configurations.

Step one might be to acquire a “n”. Then we are able to use drawWithCache modifier in a Field, to entry a cached draw scope. Whereas on this scope, we are able to carry out the second measurement on the textual content we have to show, utilizing the constraints of the Canvas.

Lastly in the course of the onDrawBehind block we name drawText, with the results of the second measurement. We use the topLeft parameter to put our textual content so it’s vertically and horizontally centered, utilizing the canvas measures.

Once more we use TextMeasurer to measure “n” to get the peak of our container like we did within the TextMeasurer step, and use bear in mind so we don’t calculate the measurement in every recomposition.

Execs

  • Achieves the required design.
  • Compose- idiomatic, by manually drawing textual content on Canvas in the course of the drawing part.

Cons

  • Drawing straight on Canvas is unsuitable for advanced makes use of, as your Textual content will lack primary core options like a11n and choice.
  • Handbook alignment by calculating relative positions.

As an iteration of choice #1, as an alternative of including the composable Textual content(n) all the time on the display screen, we are able to simply compose the aspect, get its peak by way of subcomposition and apply the obtained peak to the subsequent Textual content composable. We technically can do that utilizing SubcomposeLayout. However, as we’ll see a bit later, this selection shouldn’t be appropriate for our use case.

By utilizing SubcomposeLayout you’re capable of do a measurement go first for a given youngster composable after which use that data to find out whether or not to compose different youngsters or not.

If that is how the three phases of Compose go for almost all of layouts:

Discover that the structure part entails measurement and placement steps.

In SubcomposeLayout there’s a elementary change like this:

With SubcomposeLayout you’re capable of situation the composition of a kid in the course of the structure part of a principal composable, with the measurement data obtained.

Again to our code, we are able to create a composable that receives mainText which is the textual content we wish to show. By utilizing SubcomposeLayout, we are able to subcompose and measure the peak of “n”. Then we set the peak we obtained because the max peak within the structure block of the SubcomposeLayout. We will place mainText vertically within the middle by doing a calculation between the max peak and the present textual content’s peak.

This code may appear to be this:

This code is more durable to learn in comparison with our earlier answer. Additionally as a result of we’re deferring one composition after one other youngster’s structure part, we anticipate a poorer efficiency consequence for the benchmark.

Lastly, our use case doesn’t match the utilization of SubcomposeLayout right here. We’re composing each “n” and the true textual content, deferring the composition of the true textual content and solely putting considered one of them all the time.

As talked about, we use SubcomposeLayout when not less than one youngster’s composition will depend on the results of one other youngster’s measurement, e.g. utilizing the values calculated in the course of the measurement as params for the composition of the youngsters.

What we actually want is one youngster’s measurement to measure different youngsters, which is why utilizing the structure modifier is sufficient, as explored in choice #4.

See the Superior structure ideas episode of the MAD Abilities collection on Layouts and Modifiers to be taught extra about SubcomposeLayout and its utilization.

We didn’t contemplate this selection in our remaining comparability, however thought it was fascinating to discover.

Execs

  • Achieves the required design.
  • Prevents including an additional Textual content to the composition for alignment functions.

Cons

  • Suboptimal utilization of SubcomposeLayout.
  • Complicated code, more durable to know.
  • Worst performing answer.

We wrote and ran a macrobenchmark take a look at on an actual Pixel 7 Professional gadget. The take a look at consists of a LazyColumn containing a set quantity of things (the composable we’re testing), after which scrolling it a set quantity of occasions from prime to backside. The benchmark outcomes embrace this overhead.

Whereas we anticipate all options to carry out effectively, we intention to evaluate the impact of utilising completely different APIs by evaluating them towards one another.

We analysed two metrics: frameDurationCpuMs and frameOverrunMs.

  • frameDurationCpuMs — How a lot time the body took to be produced on the CPU, the smaller this quantity, the higher as they point out that the UI is rendering extra effectively.
  • frameOverrunMs — How a lot time a given body missed its deadline by. We’ll be getting adverse numbers right here, the extra adverse the higher as they point out how a lot sooner than the deadline a body was.

For extra on these metrics, see the Seize Macrobenchmark Metrics documentation.

The outcomes of the benchmark for the “default conduct” (with out utilizing any particular API) are:

+--------------------+----------+---------+---------+--------+
| | P50 | P90 | P95 | P99 |
+--------------------+----------+---------+---------+--------+
| frameDurationCpuMs | 5.1 | 6.6 | 7.1 | 8.5 |
| frameOverrunMs | -10.1 | -8.6 | -7.9 | -5.9 |
+--------------------+----------+---------+---------+--------+

Macrobenchmark provides us percentile benchmark outcomes. To get P90 as an illustration, the framework collects the outcomes from operating the take a look at a set variety of iterations, orders them in ascending order, and discards the underside 90% of them. P90 is the primary consequence left after doing this, e.g. frameDurationCpuMs had been 6.6 ms or larger for 90% of the runs.

This result’s our baseline, and we in contrast all of the efficiency options beneath to it and towards each other.

These are the outcomes for frameDurationCpuMs:

+--------------------+----------+---------+---------+--------+
| frameDurationCpuMs | P50 | P90 | P95 | P99 |
+--------------------+----------+---------+---------+--------+
| default | 5.1 | 6.6 | 7.1 | 8.5 |
| Two texts | 5.4 | 7.1 | 7.8 | 9.6 |
| Textual content measurer | 5.3 | 6.8 | 7.4 | 8.7 |
| TextLayoutResult | 5.3 | 7.1 | 8.0 | 9.6 |
| structure modifier | 5.4 | 7.1 | 7.8 | 9.6 |
| drawText | 5.3 | 7.0 | 7.7 | 9.5 |
| Subcomposition | 4.8 | 7.0 | 8.2 | 11.1 |
+--------------------+----------+---------+---------+--------+

These are the outcomes for frameOverrunMs:

+--------------------+----------+---------+---------+--------+
| frameOverrunMs | P50 | P90 | P95 | P99 |
+--------------------+----------+---------+---------+--------+
| default | -10.1 | -8.6 | -7.9 | -5.9 |
| Two texts | -9.8 | -8.0 | -7.0 | -4.2 |
| TextMeasurer | -9.7 | -8.1 | -7.5 | -5.3 |
| TextLayoutResult | -9.8 | -7.8 | -7.0 | -4.2 |
| structure modifier | -9.8 | -8.0 | -7.0 | -4.7 |
| drawText | -9.7 | -7.9 | -7.1 | -4.6 |
| Subcomposition | -10.1 | -7.7 | -5.9 | -2.4 |
+--------------------+----------+---------+---------+--------+

From looking at these 2 metrics we are able to extract few key conclusions:

  • TextMeasurer answer (choice #2) appears to carry out the closest to the default conduct throughout the board.
  • The remainder of them, besides the subcomposition choice, carry out equally. Utilizing all these completely different APIs doesn’t appear to have an effect on the median body time in comparison with the default conduct, nonetheless it had a larger impact on frames in P99.
  • Utilizing subcomposition is the worst performing answer in comparison with the remaining. That is unsurprising as a result of this structure defers the composition of a kid composable until after the results of a principal composition.

Now we’ll revisit our shortlist and decide the perfect one in accordance with the factors we outlined: readability, efficiency, and Compose idiomacy. It’s vital to do not forget that every answer has strengths and weaknesses.

Our choices:

For our use case we determined to go for the intuitive answer, or choice #1 🎉 as it’s the most straightforward to learn and preserve, and carried out effectively in comparison with virtually all the remainder of the alternate options 🚀

Phew, we made it! We explored an actual drawback we had, proposed a number of choices to sort out it, in contrast and selected the one which fits greatest, given a standards. We additionally did a efficiency evaluation of all options for analysis functions.

Our drawback? Vertically align a textual content in a container with a peak decided by the measure of one other textual content.

We picked the answer that maximizes instinct, readability, simplicity, has efficiency, contemplating all tradeoffs and learnt about shortcomings of every method.

Now, how would you’ve gotten carried out this part? Do you agree with our decide? Which might’ve been yours and why? Tell us right here or catch me on Twitter together with your feedback and questions.

Pleased composing! 🚀



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments