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!

Saving Multiple States for a List

Rin Orz asked:

I am writing a file manager, when I click on the folder, the LazyColumn items data will be reloaded to enter the folder, then how can I remember the list position of the previous page so that I can automatically display it to the correct one when I return next time position, for example, the path a/b/c, when go to c, I should save the location of a and b

[to navigate between pages, I] just replace the data through the viewModel to reorganize the LazyColumn, probably a fake navigation implementation

Google’s Ian Lake suggested:

It sounds like, if you’re using the same LazyColumn, then you’ll need to hoist and save the LazyListState for each of your lists (i.e., one for a, one for b, etc.), just passing down the right state with the right contents of the list as you swap between them

Rin was concerned about performance:

If there are too many file paths (pages), will holding a LazyListState for each lists cause performance problems? Maybe just save its initialFirstVisibleItemIndex and initialFirstVisibleItemScrollOffset for each lists?

Ian indicated that this was not necessary:

The LazyListState itself is very small; you’d want to hoist the whole object

Posted 2021-05-08, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1620141072414800


Avoid UI That Has Side Effects on Other UI

Igor Escodro asked:

Quick question regarding DisposableEffect. I have a ModalBottomSheetLayout with different contents based on user interaction. My idea is to clean all the content and clear the focus when the BottomSheet is hidden.

My current implementation is:

DisposableEffect(modalSheetState.currentValue) {
    onDispose {
        if (modalSheetState.isVisible.not()) {
            focusManager.clearFocus()
            sheetContentState = SheetContentState.Empty
        }
    }
}

Is it a correct implementation of DisposableEffect?

In addition to the code above which stays closer to the ModalBottomSheetLayout, each type of BottomSheet has a code similar to:

val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
    delay(600)
    focusRequester.requestFocus()
}
TaskInputTextField(
    text = taskInputText,
    onTextChange = { text -> taskInputText = text },
    modifier = Modifier.focusRequester(focusRequester)
)

Google’s Adam Powell was unconvinced:

[The first code snippet is] using recomposition to generate edge events and it’s not clear that it will be accurate/not generate duplicate events in some recompositions

effects (including disposal) always run after the composition has been committed; they can’t influence the current frame. sheetContentState = SheetContentState.Empty looks like the sort of thing where you’re expecting sheetContentState to be consumed somewhere else in the same recomposition where the DisposableEffect changes and thereby “fires”

at best you’ll get a frame of jank where the original source data and sheetContentState are out of sync.

Igor explained a bit more:

Basically I have a ModalBottomSheetLayout that changes the BottomSheet content when the tabs in the BottomAppBar changes. Also, I want to keep the BottomSheet opened when the user rotates the screen.

So I tried to create a mechanism where the Scaffold content communicates with the ModalBottomSheetLayout, updating the SheetContentState to be possible to have multiple BottomSheet.

It works… But every new bevavior I try to add on it is complex task. In this case I want to try to open the keyboard automatically when the BottomSheet is opened.

I added a DisposableEffect to close the keyboard every time the BottomSheet is closed. Now I added a new one to clean all the content, setting to Empty again.

Adam then gave is this article’s title:

as a rule of thumb try to avoid UI that has side effects on other UI

onDispose() is triggered by recomposition, causing some composable to no longer be needed. Ideally, onDispose() should not itself be mutating state that triggers further recomposition elsewhere in your hierarchy.

Or, as Adam put it:

in this case, the contents of the sheet leaving the composition isn’t the significant event, the significant event is either the same event that caused them to leave the composition, or in some cases something like the end of an animation somewhere

sheetContentState in particular looks like it wants to be driven somewhere else and not by a presentation like this going away

Later, Google’s Sean McQuillan added:

Don’t hesitate to hoist the controller-style objects (e.g. FocusRequester, SoftwareKeyboardController) to the thing managing events (the controller). They don’t need to be coupled with compose, and it’s expected to call methods on them in other places (e.g. a ViewModel).

A good way to think of these hoisted controller objects as encapsulated (state+events) you can use to add features to your controller (e.g. ViewModel, stateful composable) without having to wire up primitive state/events yourself (edited)

Posted 2021-05-01, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1619528291445700


Why is @Composable an Annotation?

Altynbek Nurtaza asked a question that comes up from time to time:

Hi everyone! in the videos people say composable functions are very similar to suspend functions. why is Composable a annotation rather than a kotlin keyword?

…[one] possible reason is that they didn’t reserve it: if a developer has a variable called “composable” in their code, they will potentially need to rewrite lot’s of code

Google’s Adam Powell pointed out that this is not the reason:

nah, to use another analogy with suspend, you can name variables suspend in your code too and there’s no issue. Kotlin has a lot of “soft keywords” like this that are only keywords in particular contexts, not at a super low level of lexical analysis.

You can read more about Kotlin’s soft keywords in the Kotlin docs.

Instead, the reason is what Albert Chang offered:

I believe it’s because adding a keyword is a huge language design change and can’t be done by a compiler plugin.

That led to a clarification from JetBrains’ Roman Elizarov (formerly Chief Coroutines Cook, and now King of All Kotlin):

It might be a great idea to have this kind of keyword in the language in the future, but it will take a huge amount of work. And this work is not about adding support for a new context-sensitive keyword (that’s trivial). It is the work of unbundling and abstracting of language support from the runtime library (just like it was done for coroutines).

In the end, the only(?) true language change for coroutines was introducing suspend as a keyword — most of the rest was handled by a library. That is extraordinarily tricky to pull off and probably only came about because coroutines were being developed by the same firm that maintains the language itself. Turning @Composable into composable would be another leap: allowing arbitrary third parties (e.g., Google) to define new soft keywords (composable) as part of a Kotlin compiler plugin. It sounds like this actually is being considered, but it remains to be seen if Google would migrate Compose to it if this capability is offered in the future.

Posted 2021-04-25, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1619100412381500


More!

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