Cooking Window Inset with Jetpack Compose sauce and a pinch of View — part 2
Published: 2023-03-07 23:39:50

Hey! My name is Timur, I am an Android developer at [KTS](https://www.kts.solutions/).

This article is a continuation. In the previous part, we figured out what _edge-to-edge_ mode is in mobile apps and how to work with _WindowInsets_ in Android.

Now we’ll cover examples of how to handle insets not only in View, but also in _Jetpack Compose_. And 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](https://github.com/TimurChikishev/Insets/).

Content of this article:

- **Examples of processing insets:**
- 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
- **Conclusion**

**System Window Insets**

This type of insets is the main one. They’re needed to handle elements like the _Status Bar_ and _Navigation Bar_. For example, with full screen rendering, the toolbar will be under the Status Bar.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cpbv9ufv5mkuoke04blv.jpeg)

In this case, we need to set the _insets_ for the toolbar that’s in the [AppBarLayout](https://developer.android.com/reference/com/google/android/material/appbar/AppBarLayout) with a magenta background. This will give us the effect of extending the AppBar behind the Status Bar. The example below uses the [insetter](https://github.com/chrisbanes/insetter) library.

```

toolbar.applyInsetter {
   type(navigationBars = true, statusBars = true) {
       padding(horizontal = true)
       margin(top = true)
   }
}
```
In _Jetpack Compose_, you can achieve the same effect by setting insets in _contentPadding_ of the _TopAppBar_ function. In this example, we use _WindowInsets.systemBars_ for horizontal and top so that when the screen is rotated, the Navigation Bar doesn’t overlap the beginning of the header or the exit button.

```

TopAppBar(
    contentPadding = WindowInsets.systemBars
        .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
        .asPaddingValues(),
    backgroundColor = MaterialTheme.colors.primary
) {
    // content...
}
```
Jetpack Compose also has many extensions for [Modifier](https://developer.android.com/jetpack/compose/modifiers), such as [systemBarsPadding()](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).systemBarsPadding()), [navigationBarsPadding()](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).navigationBarsPadding()), [statusBarsPadding()](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).statusBarsPadding()) and others.

After installing _insets_, the toolbar will look like this:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhmp464hp7sda3iy2rkv.jpeg)

**Ime Insets (Keyboard Processing)**

The keyboard is handled with _WindowInsetsCompat.Type.ime()_. Also for _ime insets_, it’s possible to additionally handle keyboard animation with the new API _ViewCompat.setWindowInsetsAnimationCallback_. You can learn more about the animation features of ime insets here.

The _setWindowInsetsAnimationCallback_ call is implemented in the [insetter](https://github.com/chrisbanes/insetter) library (activated by the _animated_ flag on padding/margin) and allows you to additionally link the animation not only to the View on which the DSL is called, but also to other View in order to synchronize the animation on several UI elements (_syncTranslationTo_ method).


An example of keypad processing with [insetter](https://github.com/chrisbanes/insetter) animation.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d77tpzf8431ce6o51iod.gif)

```
private fun setupInsets() = with(binding) {
   messageWrapper.applySystemBarsImeInsetter(syncTranslationView = list) {
       margin(horizontal = true, bottom = true, animated = true)
   }
}

inline fun View.applySystemBarsImeInsetter(
   syncTranslationView: View? = null,
   crossinline insetterApply: InsetterApplyTypeDsl.() -> Unit
) {
   applyInsetter {
       type(ime = true, navigationBars = true, statusBars = true) {
           insetterApply()
       }
       syncTranslationView?.let {
           syncTranslationTo(it)
       }
   }
}
```

To achieve this effect in _Jetpack Compose_, you need to use the [imePadding()](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).imePadding()) extension for the [Modifier](https://developer.android.com/jetpack/compose/modifiers) (don’t forget to inset the Navigation Bar with [navigationBarsPadding())](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).navigationBarsPadding()):

```
@Composable
fun BottomEditText(
    placeholderText: String = "Type text here..."
) {
    val text = rememberSaveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
    Surface(elevation = 1.dp) {
        OutlinedTextField(
            value = text.value,
            onValueChange = { text.value = it },
            placeholder = { Text(text = placeholderText) },
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp, vertical = 8.dp)
                .navigationBarsPadding()
                .imePadding()
        )
    }
}
```

_Jetpack Compose_ also allows you to open the keyboard with a scroll using [imeNestedScroll()](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).imeNestedScroll()):

```
LazyColumn(
    contentPadding = contentPadding,
    reverseLayout = true,
    modifier = Modifier
        .weight(1f)
        .imeNestedScroll()
) {
    items(listItems) { SampleListItem(it) }
}
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7704njezoxks4gmeom87.gif)

**Stable Insets**

Stable Insets are only useful in full-screen applications such as video players, galleries, and games. In player mode, you may have noticed that you have the entire UI system hidden, including the Status Bar, which moves across the edge of the screen. But as soon as you touch the screen, the Status Bar appears at the top. The peculiarity of the insets in the gallery or player is that empty _insets_ come into _View_ when you show/hide the UI system (when the UI system is hidden). For this reason, the Ui elements of the application that work with _insets_ can jump back and forth, as you can see in the following image.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/efq9gg9spkyirn4k9j6b.gif)

Therefore, there’s a special type of _Stable Insets_ that the system always outputs with values as if the system UI were displayed. The [insetter](https://github.com/chrisbanes/insetter) has a method _ignoreVisibility_ which tells the system that there should be _Stable Insets_ for this _View_.

```
toolbar.applyInsetter {
    type(navigationBars = true, statusBars = true) {
        padding(horizontal = true)
            margin(top = true)
    }
    ignoreVisibility(true)
}
```
Surprisingly, there’s no ready-made solution for _Stable Insets_ in _Jetpack Compose_, but we can implement it as follows:

```
class StableStatusBarsInsetsHolder {
   private var stableStatusBarsInsets: WindowInsets = WindowInsets(0.dp)

   val stableStatusBars: WindowInsets
       @Composable
       get() {
           val density = LocalDensity.current
           val layoutDirection = LocalLayoutDirection.current
           val statusBars = WindowInsets.statusBars
           return remember {
               derivedStateOf {
                   if (statusBars.exclude(stableStatusBarsInsets).getTop(density) > 0) {
                       stableStatusBarsInsets 
                                   = statusBars.deepCopy(density, layoutDirection)
                   }
                   stableStatusBarsInsets
               }
           }.value
       }
 
}

private fun WindowInsets.deepCopy(density: Density, layoutDirection: LayoutDirection): WindowInsets {
   return WindowInsets(
       left = getLeft(density, layoutDirection),
       top = getTop(density),
       right = getRight(density, layoutDirection),
       bottom = getBottom(density)
   )
}
```
Example of use:

```
val stableInsetsHolder = remember { StableStatusBarsInsetsHolder()}
SampleTopBar(
   titleRes = R.string.insets_sample_fullscreen_stable,
   contentPadding = stableInsetsHolder.stableStatusBars
       .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
       .asPaddingValues(),
)
```
Now, when we use _Stable Insets_, the processing _insets_ elements don’t bounce.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2mfubx814punhuwibzx.gif)

**Immersive mode (full screen mode without UI elements)**

To show or hide the Ui instead of putting certain UI_FLAGS combinations, a new class _WindowInsetsController_ (its compat version _WindowInsetsControllerCompat_) is used since API 30 which has a convenient API based on the new API 30 classes.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cx4ej6i6xks4m7jxkt3r.gif)

How you can return the system Ui to the screen is set with the WindowInsetsController flags (set via the _setSystemBarsBehavior_ method):

- BEHAVIOR_SHOW_BARS_BY_SWIPE — A swipe is required to return SystemUi, remove on your own;
- BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE — A swipe is required to return the system Ui, hiding automatically after a while.

There are the following extension functions with the ability to add calls to the [WindowInsetsController](https://developer.android.com/reference/android/view/WindowInsetsController) via extraAction.

**To hide the UI**

```
fun Window.hideSystemUi(extraAction:(WindowInsetsControllerCompat.() -> Unit)? = null) {
    WindowInsetsControllerCompat(this, this.decorView).let { controller ->
        controller.hide(WindowInsetsCompat.Type.systemBars())
        extraAction?.invoke(controller)
    }
}

// Usage
hideSystemUi{
    systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
```
To show the UI

```
fun Window.showSystemUi(extraAction: (WindowInsetsControllerCompat.() -> Unit)? = null) {
    WindowInsetsControllerCompat(this, this.decorView).let { controller ->
        controller.show(WindowInsetsCompat.Type.systemBars())
        extraAction?.invoke(controller)
    }
}

// Usage
showSystemUi()
```
In _Jetpack Compose_, you can use the _rememberSystemUiController_() from the [accompanist system ui controller](https://google.github.io/accompanist/systemuicontroller/) library to hide or show the system user interface.

```
val systemUiController = rememberSystemUiController()

InsetsExamplesTheme {
   FullscreenCutoutSample(
       systemUiController.toggleUi
   )
}

val SystemUiController.toggleUi: () -> Unit
   get() = {
       isSystemBarsVisible = !isSystemBarsVisible
   }
```
But this library doesn’t allow changing flags of [WindowInsetsControllerCompat](https://developer.android.com/reference/androidx/core/view/WindowInsetsControllerCompat). So a class based on _rememberSystemUiController_() from the [accompanist systemui controller](https://google.github.io/accompanist/systemuicontroller/) was written. In this implementation, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE is set for [WindowInsetsControllerCompat](https://developer.android.com/reference/androidx/core/view/WindowInsetsControllerCompat).

[Example](https://gist.github.com/lockbooks/2f83a5c9a62180fe6934cf52556b884b)

Example of use:

```
val systemUiVisibilityController = rememberSystemUiVisibilityController()
InsetsExamplesTheme {
   FullscreenCutoutSample(
       systemUiVisibilityController.toggleUi
   )
}
```
**Display Cutouts (Display cutout support)**

Bangs and cutouts are appearing more and more frequently on cell phones. They’re located in different places on the screen and can have different sizes and shapes.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/43413xjz45b7zqfow4zi.jpg)

Android 9 (api 28) introduced the _DisplayCutout_ class, which allows you to handle the cutout area. In addition, there’s a set of flags in _WindowManager.LayoutParams_ that allow you to enable different behavior around the cutouts.

To set the flags, [layoutInDisplayCutoutMode](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#layoutInDisplayCutoutMode) is used to determine how your content is displayed in the cutout area. There’re the following values:

- [LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) — in portrait mode the content is displayed below the cutout area, and in landscape mode there will be a black bar.
- [LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) — Content is displayed in the cutout area in both portrait and landscape modes.
- [LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER) — Content is never displayed in the cutout area.
- [LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) — In this mode, the window expands beneath the cutouts at all edges of the display in both portrait and landscape orientations, regardless of whether the window hides the system panels ( [LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) full analogue, but available with 28 api when [LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) appeared only in 30 api).

Example of use:

```
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    window.attributes.layoutInDisplayCutoutMode =  WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
```
There is a flag for _displayCutout_ in the [insetter](https://chrisbanes.github.io/insetter/) library, so you can handle padding and margin from the cutout area.

```
applyInsetter {
   type(displayCutout = true) {
       // specify the sides you need
       // for example padding(top = true)
       padding() 
   } 
}
```
In _Jetpack Compose_ you also need to use _layoutInDisplayCutoutMode_ to set the mode for _DisplayCotout_. In compose, you can use [WindowInsets.displayCutout](https://developer.android.com/reference/android/view/WindowInsets#getDisplayCutout()) or [Modifier.displayCutoutPadding()](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).displayCutoutPadding()) to handle padding.

If you suddenly need to get a cutout area, you can use [DisplayCutoutCompat.getBoundingRects()](https://developer.android.com/reference/androidx/core/view/DisplayCutoutCompat#getBoundingRects()) to do that. This method returns a list of rectangles, each of which is a bounding rectangle for a non-functional area on the display.

```
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
   val boundingRects = insets.displayCutout?.boundingRects
   insets
}
```
In Jetpack Compose, you can’t get boundingRects with [WindowInsets.displayCutout](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.foundation.layout.WindowInsets.Companion).displayCutout()).

I think that arranging elements relative to the cutout makes no sense for the following reasons:

- where there’s no cutout is information from the Status Bar (time, icons)
- api returns a list, which means there can be several cutouts
- cutouts come in different shapes and sizes

In general, everything says that it’s better not to do so, but if you really need, you can do it.

**System Gesture Insets**

This kind of insets appeared in Android 10. They return the gesture areas home from below and back to the right and left of the screen.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnf0s001z6z9dmr766te.jpg)

If there’re scroll elements in these areas, users can, for example, accidentally trigger a backward gesture. Therefore, if you can’t move the interface element away from the edge, but you need to fix the problem with gestures, use this type of inserts

For example: in the gif below, the user wants to scroll but will trigger an exit without excluding gestures. You should also remember that large areas with gesture exclusions can lead to incomprehensible behavior for the user, so it’s worth excluding gestures only in places where it’s necessary. If we look at the example below, a gesture should only be excluded for an element with a horizontal scroll.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6y1md3chnjfodat3k9x.gif)

While implementing the example for View, I encountered a problem that windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures()) returns an empty value. I agonized over this for a long time, until I decided to run the example on an emulator and saw that everything worked. To make sure, I sent the apk to my colleagues and got the answer: everything works. The problem was that I was testing the example on my device (Realme c21). Vendors are messing around as usual.

[Code](https://gist.github.com/lockbooks/cf44c65595cfcc27f8aefd0eac34f00e)

To exclude gestures in Jetpack Compose, you can use [Modifier.systemGestureExclusion](https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#(androidx.compose.ui.Modifier).systemGestureExclusion())().

```
LazyRow(
   modifier = Modifier
       .padding(vertical = 16.dp)
       .systemGestureExclusion(),
   contentPadding = PaddingValues(horizontal = 16.dp),
   horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    //...
}
```

**Mandatory System Gesture Insets**

This type of insets appeared in Android 10 and is a subtype of System Gesture Insets. They specify areas of the screen where system gesture behavior will always take priority over in-app gestures. Mandatory gesture areas can never be excluded by apps (with android 10, the mandatory gesture area is the home gesture area). Mandatory insets move content away from mandatory gesture inserts. For example, if you have a seekbar at the bottom of the screen, you need to use Mandatory System Gesture Insets to avoid calling gestures.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fi5umyqt6lgdp6e1f3r0.gif)

In View, mandatory _insets_ can be processed using the [insetter](https://chrisbanes.github.io/insetter/) library:

```
seekBar.applyInsetter {
   type(mandatorySystemGestures = true) {
       padding(bottom = true)
   }
}
```

In Jetpack Compose, mandatory gestures can be obtained with _WindowInsets.mandatorySystemGestures_:

```
Surface(
   modifier = Modifier
       .fillMaxWidth()
       .windowInsetsPadding(
           WindowInsets.mandatorySystemGestures
               .only(WindowInsetsSides.Bottom)
               .union(
                   WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
               )
       ),
   color = Color.LightGray.copy(alpha = 0.3f)
) {
   // content
}
```

**Tappable element insets**

This type of insets appeared in android 10 and is needed to handle different Navigation Bar modes. This type of _insets_ is rarely used, because the differences from the _system window insets_ are minimal. But you should agree that it’s nice to know that your application is written in the coolest way possible.

In the picture below you can see that _tappable element_ insets and _system window insets_ work the same when the device is set to navigate with buttons. The difference can only be noticed in gesture-based navigation mode.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/al0e0fpj0pse2q25u965.jpeg)

The point is that when navigating with gestures, we don’t click, but swipe from bottom to top. This means that we can use interactive elements in this area (e.g. [FloatingActionButton](https://m2.material.io/components/buttons-floating-action-button/android)), which means that we don’t need to indent and tappable element insets will return 0.

An example of using the c view (still using the [insetter](https://chrisbanes.github.io/insetter/) library):

```
fab.applyInsetter {
   type(tappableElement = true) {
       margin(bottom = true)
   }
}
```

In _Jetpack Compose_ we use [WindowInsets.tappableElement](https://developer.android.com/reference/kotlin/androidx/core/view/WindowInsetsCompat.Type#tappableElement()):

```
Scaffold(
   floatingActionButton = {
      FloatingActionButton(
          modifier = Modifier.padding(
              bottom = WindowInsets.tappableElement
                  .only(WindowInsetsSides.Bottom)
                  .asPaddingValues()
                  .calculateBottomPadding()
          ),
          backgroundColor = backgroundColor,
          onClick = onClick
      ) {
          Icon(
              imageVector = Icons.Filled.Add,
              contentDescription = null
          )
      }
){ // content... }
```

**Conclusion**

You can find all the code from the article in [this](https://github.com/TimurChikishev/Insets) repository.

We looked at the nuances of implementing _e2e_ in Android mobile apps and an example of implementation using the [insetter](https://chrisbanes.github.io/insetter/) library for View, and used the built-in insets in _Jetpack Compose_. In this article I wanted to let all android developers know that _e2e_ is in fact very easy to implement and worth the time invested. I hope you found this article useful and more and more developers will adopt _e2e_ in their applications.

Do you implement edge-to-edge in your applications? Do you use libraries to work with insets? What problems have you faced when implementing edge-to-edge?

Thanks to Vadim for his help in preparing this article.