One Off the Slack

These articles summarize the discussion on a particular thread in the #compose channel of JetBrains’ Slack workspace for Kotlin developers. If you would like to read the original thread, you will need access to that workspace — sign up at https://slack.kotlinlang.org!

Compose and try/catch

alorma asked an interesting question:

why is not possible to do a try inside a @Composable block?

I had never tried this and would not have expected a problem. And, in truth, it’s not a blanket ban. So, for example, this is fine:

@Composable
fun Content() {
  MyApplicationTheme {

    try {
      throw IllegalStateException("ick!")
    } catch (ex: Exception) {
      Log.d("hello", "world")
    }

    Surface(color = MaterialTheme.colors.background) {
      ShowSomethingCool()
    }
  }
}

However, this is not:

@Composable
fun Content() {
  MyApplicationTheme {
    try {
      Surface(color = MaterialTheme.colors.background) {
        ShowSomethingCool()
      }
    } catch (ex: Exception) {
      Log.d("hello", "world")
    }
  }
}

You cannot call a composable from within a try block of another composable. Calling from a catch block is OK, just not from try.

As various people on the thread pointed out, ideally, your composable is just declaring your UI. Anything with expected exceptions (disk I/O, network I/O, etc.) should be done outside of a composable anyway, such as in a viewmodel.

Google’s Jim Sproch explained that what seems like it might be simple… isn’t:

Supporting try-catch semantics is a lot of engineering work because the way Compose works with recomposition and parallelization, it is possible that a child is called in situations where the parent isn’t on the call stack. But when an exception is thrown, you want the control flow to pass back through the parent, even if the parent was skipped or executed on a different thread. Getting this right requires a decent amount of engineering, and so we had to prioritize other things that were more critical to an initial stable release. Hopefully in a followup release, this can be supported.

So, hopefully, in the future, this will be supported. In the meantime… just don’t write code that crashes, OK?

😁

Posted 2020-10-26, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1603737209131700


Z-Stacking?

Guy Bieber asked:

I have a colleague who zstacks all their screens and changes what is in front. I typically just switch what is being rendered in a composable with a if or case statement. Is doing stacking a best practice for compose?

Long-term, this sort of concern should be handled by navigation frameworks, including Jetpack’s own Navigation component. In the short term (hopefully measured in weeks), we have to worry about this sort of thing ourselves. And so the question is: do you change elevation (“zstack”) to control what is in front, or do you avoid rendering non-visible content in the first place?

According to Google’s Alexandre Elias, the answer is the latter:

if/case statements are the recommended best practice for Compose. that is what we are using in all our sample apps and optimizing Compose’s performance for

After all, why render nodes that the user will not see?

Alexandre pointed out a possible reason why somebody might try the elevation approach:

if the idea behind the zstacking approach is to cache/preload expensive data for performance reasons, the best practice would be to hoist up that state to a parent Composable of the screens so that it’s remembered even if the child screen falls into an “else” block

That’s one of the many differences between the classic View system and Compose. With views, we might want to pre-create the widgets even if they are not yet visible, so that they are ready when we need them. ViewPager and ViewPager2, for example, will set up enough pages to allow for animated transitions between them. Compose is much more of a “just in time” system, where you only define the UI nodes that you really need right now, based on the current state.

And, as Alexandre notes, playing elevation games might make things worse:

Compose doesn’t have much in the way of optimizations to avoid doing unnecessary work for z-occluded composables, so having tons of stacked invisible screens could cause more performance problems than it solves

Posted 2020-10-18, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1602877264121500


I Heard You Like Columns…

Colton Idle asked:

Why does compose have both Column and LazyColumnFor?

It seems like everyone could just potentially benefit from using LazyColumnFor and getting rid of Column (or putting LazyColumn functionality into Column, and removing Column) could work?

After all, a column is a column is a column… right?

Zach Klippenstein pointed out that there is a matter of weight:

My understanding is that Column is extremely cheap, even compared to LazyColumnFor, because it works in the same composition and is just a really straighforward layout, unconcerned with scrolling, gestures, animations, etc. Because it’s so lightweight, you can use it to construct more complicated layouts by nesting columns of small numbers of items, combining with other simple layouts, etc.

Or, as Google’s Romain Guy pointed out, it is roughly analogous to comparing a LinearLayout and a RecyclerView (with a LinearLayoutManager, presumably), or comparing LinearLayout and ConstraintLayout:

…even if there are no measurable performance benefits, using the simpler layouts/widgets can make the code easier to read/maintain and it also conveys an intent more clearly

Colton then drew a real-world analogy:

But I guess it’s kind of a “right tool for the job” e.g. I don’t need a 100lb sledgehammer for a nail in the wall. A regular hammer will do.

It’s not merely that a you do not need a sledgehammer for, say, hanging a picture. Trying to pound in a small nail with a sledgehammer may well put a large hole in your wall. LazyColumnFor() is not just more powerful than Column() — it is more expensive. Joost Klitsie pointed out one:

Also you cannot put a lazycolumn in a lazycolumn, but you can put a column in a lazycolumn, for example if one of your list items require a column then it is handy

So, a Column() can go in places where a LazyColumnFor() cannot.

For the same level of expense, a more-powerful component is better than its less-powerful counterpart. However, cost differences can be very important in their own right, and so having less-expensive (albeit less-powerful) constructs can be important, even if they seem redundant on the surface.

Posted 2020-10-11, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1602102472157600


More!

Older “One Off the Slack” articles can be found in the archives.