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!

How Do We Show a Non-Compose Dialog?

Migrating an app to using Compose UI may require you to bridge to non-Compose bits of UI. You might be using an SDK that deals with activities, fragments, and custom views, for example. Or, perhaps you want to use something that already works but has no composable equivalent right now.

Dmitry Suzdalev had the latter scenario:

what is the safe way to show old-style android dialog from Composable function?

For example:

@Composable
fun ScreenContent(state: State) {
  if (state.showDialog) {
    // Need to show MaterialDatePicker here
  }
}

Dmitry wondered about possibly using SideEffect here. Google’s Zach Klippenstein suggested DisposableEffect instead:

if (state.showDialog) {
  DisposableEffect(dialogDetails) {
    // create dialog, call .show()
    onDispose {
      // Dismiss dialog
    }
  }
}

Albert Chang implemented this in an app and offered a more complete implementation:

if (showDialog) {
    val fragmentManager = (LocalContext.current as? FragmentActivity)?.supportFragmentManager
    DisposableEffect(fragmentManager) {
        var datePicker: MaterialDatePicker<*>? = null
        if (fragmentManager != null) {
            datePicker = MaterialDatePicker.Builder.datePicker()
                .setTitleText(title)
                .build()
                .apply {
                    addOnPositiveButtonClickListener {
                        date = it
                    }
                    addOnDismissListener { showDialog = false }
                    show(fragmentManager, javaClass.name)
                }
        }
        onDispose {
            datePicker?.dismiss()
        }
    }
}

Posted 2021-09-18, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1631896652024300


How Do We Implement Theme Overlays in Compose UI?

Bradleycorn asked:

I have an app that uses a Material theme in light and dark variants … pretty standard stuff. The light theme uses a white surface color and a pretty dark (navy blue) primary color. So far so good…

However, we have a few screens (formerly fragments) where the design calls for the color scheme to be inverted. That is, the dark (navy blue) color is used for the background (surface) and the content (text, textfields, etc) is white. I’m wondering what is the best way to deal with this? At first, I created a DarkSurface composable that wraps a surface and sets up some CompositionLocals to handle the color inversions. But the more I go down that path, the more cumbersome and wrong it seems. Now I’m wondering if I should create a new theme for these screens to use. Anyone else ran into something like this before and have any ideas on how to handle it?

In the classic View system, we might try accomplishing this via a theme overlay, but that’s not really a thing in Compose UI.

Google’s Nick Butcher points out the alternative: nested themes:

You can nest themes. See this sample which has a Pink theme at the root of the screen:

https://github.com/android/compose-samples/blob/v1.0.2/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt#L122

… but But then sets a Blue theme for a section of the UI

https://github.com/android/compose-samples/blob/v1.0.2/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt#L300

(note: in preparing this post, I swapped the original master links that Nick used with ones tied to the v1.0.2 tag)

PinkTheme() and BlueTheme() are defined as top-level functions in ui/theme/Theme.kt of the Owl sample project, and they are just wrappers around OwlTheme(). OwlTheme(), in turn, wraps a MaterialTheme() but also overrides the LocalElevations and LocalImages composition locals. But the colors from PinkTheme() and BlueTheme() are just passed into MaterialTheme(). MaterialTheme() sets up the design system for the composables it wraps, and you can nest MaterialTheme() calls to replace the design system for particular screens, portions of screens, etc.

Bradleycorn agreed with the plan:

Yep, that’s the path I’m going down now, thanks for the examples. It seems much more “correct” than creating a bunch of CompositionLocals and a wrapper composable to set them to provide colors. That got to be a mess fast.

Posted 2021-09-11, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1631116556030100


Why Do We Need SideEffect?

Florian Walther asked:

Even tho I read the documentation, I can’t wrap my head around why we need to use SideEffect here. The functionality works without the SideEffect call. Can someone tell me why we need it? https://google.github.io/accompanist/systemuicontroller/

The code in question is from the current Accompanist documentation for its system UI controller support:

// Remember a SystemUiController
val systemUiController = rememberSystemUiController()
val useDarkIcons = MaterialTheme.colors.isLight

SideEffect {
    // Update all of the system bar colors to be transparent, and use
    // dark icons if we're in light theme
    systemUiController.setSystemBarsColor(
        color = Color.Transparent,
        darkIcons = useDarkIcons
    )

    // setStatusBarsColor() and setNavigationBarsColor() also exist
}

Google’s Adam Powell attempted to explain:

Composition is a transaction. Anything you do in a @Composable function shouldn’t have observable side effects until after that transaction commits successfully. SideEffect {} does this, deferring its block of code until the composition transaction that scheduled it is successful.

SideEffect is also guaranteed to run that code on the applier thread (e.g. the Android UI thread) whereas composition itself may run on a background thread in the future

If composition fails, then nothing in that composition transaction is meant to have ever happened. By using SideEffect you can meet this contract. Otherwise you would need to ensure yourself that no mutations you make are visible externally before composition succeeds.

In this case, the system UI (e.g., status bar) is not even in our app, let alone in our composition. Anything that we do to it from a composable is, by definition, a side effect of that composable and should be handled via SideEffect (or perhaps some other type of Effect).

Florian seemed unconvinced:

So the difference is that without SideEffect the call to setSytemBarsColor would execute if the Composable was canceled for some reason?

but in reality it would probably not make any observable difference, is that correct?

Adam focused on correctness:

Writing wrong code that works by luck will generally come back to bite you eventually 🙂

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


More!

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