Cooking Window Inset with Jetpack Compose sauce and a pinch of View — part 1

Devchik
Lonto
Published in
6 min readJan 16, 2023

Hey! My name is Timur, I am an Android developer at Lonto.

Unfortunately, there’re still Android applications that don’t support edge-to-edge. I feel that developers either don’t know about this possibility or are afraid to work with WindowInsets. In fact, Edge-to-Edge isn’t difficult to implement, and thanks to this article you’ll understand this topic much faster.

Today I’ll explain what edge-to-edge mode is in mobile apps and how to work with WindowInsets in Android. In the next article, we’ll also cover examples of how to handle insets not only in View, but also in Jetpack Compose.

When you can find the articles about working with Insets in View on the web, the information about working with them in Jetpack Compose can only be found in the official documentation.

All the examples from the article can be viewed in this repository.

The content of the article:

1️⃣ What is edge-to-edge?
2️⃣ Steps for setting up edge-to-edge
3️⃣ Changing the color of the system UI
4️⃣ Request a rendering of the application under the system UI
5️⃣ Eliminate visual conflicts
6️⃣ WindowInsets vs fitSystemWindow?

What is edge-to-edge?

Today, mobile applications are increasingly displayed across the entire visible surface of the display, beyond the system UI. These applications use an edge-to-edge approach, where the application is displayed below the system UI, i.e., the status bar and the navigation bar.

Why, you ask? To create a more attractive and modern user interface. Will you agree that everyone is happier when they use a nicer application?

Now let’s move on to the edge-to-edge implementation.

Steps for setting up edge-to-edge

To implement edge-to-edge mode in your application you’ll need to:

  • change the color of the system UI
  • request a rendering of the application under the system UI
  • eliminate visual conflicts

Changing the color of the system UI

As of Android 5 (API 21), it’s now possible to set the color for the Status Bar and Navigation Bar. Use the following theme attributes for this purpose:

<item name="android:statusBarColor">@color/colorAccent</item>
<item name="android:navigationBarColor">@color/colorAccent</item>

We can also make the color of the system UI transparent or semi-transparent. To achieve semi-transparency, just set android:windowTranslucentStatus and android:windowTranslucentNavigation:

<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>

To get a fully transparent system interface you need to set android:navigationBarColor and android:statusBarColor with transparent color and disable contrast with the following attributes android:enforceNavigationBarContrast, android:enforceStatusBarContrast. Disabling contrast is necessary because since version 10 Android provides sufficient contrast to the Navigation Bar.

<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>

In the screenshot above, you may see that the Navigation Bar buttons are barely visible. The Status Bar would have the same problem if it weren’t for the magenta color. To fix this, use the attributes android:windowLightStatusBar, android:windowLightNavigationBar. Note that windowLightStatusBar is available with 23 api and windowLightNavigationBar with 27 api.

In Jetpack Compose, you can use the System UI Controller library to change the color, which provides simple tools for changing the color of the UI system. You can change the color of the UI system using this library as follows:

private val BlackScrim = Color(0f, 0f, 0f, 0.3f) // 30% opaque black

@Composable
fun TransparentSystemBars() {
val systemUiController = rememberSystemUiController()
val useDarkIcons = MaterialTheme.colors.isLight
SideEffect {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = useDarkIcons,
isNavigationBarContrastEnforced = false,
transformColorForLightContent = { original ->
BlackScrim.compositeOver(original)
}
)
}
}

Example of using the TransparentSystemBars function:

override fun onCreate(savedInstanceState: Bundle?) { 
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
TransparentSystemBars()
Sample()
}
}

The setSystemBarsColor method allows you to:

  • set the color for the system UI
  • specify when to use light or dark icons
  • you can disable contrast with isNavigationBarContrastEnforced
  • you can use the transformColorForLightContent lambda, which will be called to convert colors if dark icons are requested but not available. The default behavior in transformColorForLightContent is black, so you don’t need to write that).

Request a rendering of the application under the system UI

In addition to changing the color of the system interface, you need to tell the system to draw the application in full-screen mode. Android has special View SYSTEM_UI_FLAGS for this purpose (hereafter UI _FLAGS). They’ve been deprecated since API 30, and you should now use the new WindowCompat class, which contains the required flags on earlier versions of the API.

The full screen mode request via WindowCompat looks like this:

WindowCompat.setDecorFitsSystemWindows(window, false)

If you apply this in the activity, the framework won’t replace inserts for the content of your application and we’ll have to do it manually.

It makes sense to request rendering mode for the entire application (in onCreate() of your Activity, if you use the single-activity approach).

In Jetpack Compose, this step is no different.

Eliminate visual conflicts

If you follow the previous steps and run the application, you can see that the system no longer accounts for the space for the system UI. Now we have to do it ourselves:

Android applications use WindowInsets to manage the system UI. Insets is an object that represents an area of a window that conflicts with the application. Conflicts can be different, and there’re different types of insets for that (gesture handling areas, system panels, bangs, etc.).

The WindowInsetsCompat class with backward compatibility and convenient separation into insets types is used for insets processing.

The processing itself consists of attaching a listener to the View, to which the system passes an insets object. After you get the object, you can apply the desired padding or margin to the View:

ViewCompat.setOnApplyWindowInsetsListener(navBar) { view, insets ->
val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(
bottom = systemBarInsets.bottom,
left = systemBarInsets.left,
right = systemBarInsets.right
)
insets
}

In Lonto, we use the insetter library, which works on the basis of this approach. The library implements a handy Kotlin DSL applyInsetter to handle insets.

toolbar.applyInsetter {
type(navigationBars = true, statusBars = true) {
padding(horizontal = true, top = true)
}
}

Jetpack Compose used to use the library from the accompanist repository to install instes, but it’s obsolete now that official support for insets is available in Compose 1.2.0. If you were already using accompanist, there’s a detailed migration guide on the website.

TopAppBar( 
contentPadding = WindowInsets.systemBars
.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
.asPaddingValues(),
backgroundColor = MaterialTheme.colors.primary
) {
// content…
}

WindowInsets vs fitSystemWindow?

Android has the fitSystemWindow flag. If you set it to “true”, this flag adds padding to the container for which you specified the flag.

FitsSystemWindows confuses many developers.

For example, this flag works with CoordinatorLayout and doesn’t work for FrameLayout. FitsSystemWindows = true doesn’t move your content under the status bar, but it works for layouts like CoordinatorLayout and DrawerLayout because they override the default behavior. Under the hood, they set the flags setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) if fitsSystemWindows is true.

If we set these flags to our rootLayout, everything should work even on FrameLayout.

Also, hierarchy is important when using fitSystemWindow: if any parent has set this flag to “true”, its propagation won’t be taken into account further, because the container has already applied the indents.

This is actually not all the nuances why using the fitSystemWindow flag isn’t recommended (you can read about other problems in this article), so better use WindowInsets to avoid different problems and non-obvious behavior.

What’s next?

That’s all for now, but stay tuned ✌️
In the next part we will look at examples of processing different types of inserts:

  • System Window Insets
  • Ime Insets (Keyboard Processing)
  • Stable Insets
  • Immersive mode (full screen mode without UI elements)
  • To hide the UI
  • To show the UI
  • Display Cutouts (Display cutout support)
  • System Gesture Insets
  • Mandatory System Gesture Insets
  • Tappable element insets

See you later!

--

--