Generated banner for Glance Canonical Layouts
Generated logo for Glance Canonical Layouts

Glance Canonical Layouts

Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)

  • Skill
  • Tool integration
  • Software development
  • Apache-2.0
  • Low risk
  • Has scripts

From the author

Use this skill to create home screen widgets using Jetpack Glance Canonical Layouts from the Android Platform Samples project, or when asked to implement standard widget layout patterns (including Check List, Image Grid, Snap Scrolling, and Standard Toolbar).
— the description by Google LLC

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a collection of reference implementations for Android home screen widgets using Jetpack Glance. It includes pre-built canonical layout patterns such as check lists, image grids, toolbars, and snap-scrolling layouts. Users can integrate these patterns into their Android projects by copying the provided reference files, configuring the widget provider, and registering the receiver in the Android manifest.

What it does

  • Provides source code for standard widget layout patterns like Action List, Check List, and Image Grid.
  • Offers guidance on configuring widget providers with SizeMode.Exact.
  • Includes necessary XML definitions for app widget providers.
  • Supplies supporting utility classes for theme, color, and action handling.
  • Details dependency requirements for Jetpack Glance and image loading libraries.
  • Provides troubleshooting steps for common issues like background color rendering.

When to use it

  • When you need to implement a standard widget layout pattern in an Android app using Jetpack Glance.
  • When you require a consistent design structure for home screen widgets.
  • When you need to integrate specific features like snap scrolling or search toolbars into your widget.
  • When you are setting up a new Glance-based widget and want to follow established reference implementations.

Advantages

  • Reduces development time by providing ready-to-use layout templates.
  • Ensures consistency with recommended Android widget design patterns.
  • Provides clear instructions on manifest registration and dependency management.
  • Includes modular components that can be adapted to specific app requirements.

Usage examples

“How do I implement a check list widget in my Android app?”
The agent will provide the CheckListAppWidget.kt, CheckListLayout.kt, and associated XML files, along with instructions to register the receiver in your AndroidManifest.xml.
“Create a search toolbar widget for my app.”
The agent will guide you to copy the SearchToolBarAppWidget.kt and SearchToolBarLayout.kt files and configure the widget provider with the required size mode.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/android/glance-canonical-layouts/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip glance-canonical-layouts.zip -d .agents/skills

Documentation by Google LLC

To implement a widget Canonical Layout that follows the recommended design patterns:

  1. The project MUST use Jetpack Glance.

  2. You MUST locate the requested layout from the list under Canonical Layout Reference Files.

  3. Copy the layout reference files into your project's target app module (e.g., src/main/java/ and src/main/res/) in one step to implement the exact design and reduce the number of tool calls.

  4. You MUST set override val sizeMode: SizeMode = SizeMode.Exact in the widget provider class.

  5. Validate that your project compiles, updating sample package declarations, import paths, and class names to match the target app's package name.

  6. Clean your project's widget code, removing unused classes (for example, unused buttons).

  7. You MUST register the widget receiver in AndroidManifest.xml inside <application> after <activity>, ensuring android:resource="@xml/..." matches the filename of the copied Widget Info XML. Otherwise, the widget won't appear in the widget picker:

<br />

<receiver android:name=".glance.MyReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/my_app_widget_info" />
</receiver>

<br />

Dependencies

Add Jetpack Glance dependencies to the app module's build.gradle.kts (dependencies block):

  • androidx.glance:glance-appwidget:1.2.0 or higher

  • androidx.glance:glance-material3:1.2.0 or higher

  • androidx.glance:glance-appwidget-preview:1.2.0 or higher

  • androidx.glance:glance-preview:1.2.0 or higher

Use version 1.3.0-alpha02 or higher with compileSdk = 37 and Android Gradle Plugin (AGP) 9.1.0 or higher (for example, agp = "9.1.0" in gradle/libs.versions.toml) when implementing Snap Scrolling.

If copying sample data repositories that load bitmaps with Coil (such as FakeImageGridDataRepository.kt, FakeImageTextListDataRepository.kt, or FakeTextWithImageRepository.kt), also add an image loading library dependency such as io.coil-kt:coil (e.g., implementation("io.coil-kt:coil:2.7.0")) if one doesn't already exist.

Canonical layout reference files

Action list layout (action_list)
Check list layout (check_list)
Expressive toolbar layout (expressive_toolbar)
Image grid layout (image_grid)
Image text list layout (image_text_list)
Long text layout (long_text)
Search toolbar layout (search_toolbar)
Full-bleed image layout and snap scrolling (snap_scrolling)
Standard toolbar layout (standard_toolbar)
Text with image layout (text_with_image)

Troubleshooting

  1. Background Colors : Use GlanceModifier.background(Color) directly for solid background colors rather than ColorProvider, otherwise the background will be transparent.

  2. When using ColorProvider (outside of the background), avoid import errors :

    1. Import type: import androidx.glance.unit.ColorProvider

    2. Use fully qualified creator: androidx.glance.color.ColorProvider(dayColor, nightColor)

Checklist

  • [ ] Did you register the widget in the AndroidManifest?

  • [ ] Did you copy the corresponding Widget Info XML file and leave the given values including minWidth, minHeight, and minResizeHeight unchanged?

  • [ ] Did you use the given Glance Theme color values from the corresponding Layout Composable file?

  • [ ] Did you use dimension resource references for android:minWidth and android:minHeight in appwidget-provider XML definitions?

  • [ ] Did you override val sizeMode: SizeMode = SizeMode.Exact in the widget provider class?

Files

references/toolbars/layout/SearchToolBarLayout.kt

/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.platform.ui.appwidgets.glance.layout.toolbars.layout

import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.ColorFilter
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalSize
import androidx.glance.action.Action
import androidx.glance.action.clickable
import androidx.glance.appwidget.components.Scaffold
import androidx.glance.appwidget.cornerRadius
import androidx.glance.background
import androidx.glance.color.ColorProvider
import androidx.glance.layout.Alignment
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.size
import androidx.glance.layout.width
import androidx.glance.preview.ExperimentalGlancePreviewApi
import androidx.glance.preview.Preview
import androidx.glance.semantics.contentDescription
import androidx.glance.semantics.semantics
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import com.example.platform.ui.appwidgets.R
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.headerItemHeight
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.iconSize
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.itemsSpacing
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.minButtonSize
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.sideBarLeadingItemWidth
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.widgetPadding
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.Companion.canShowSearchText
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.Companion.canUseFilledButtons
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.Companion.numberOfItemsThatFit
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.HeaderTwoRowGrid
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.HorizontalRow
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.SideBarTwoRowGrid
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.TwoByTwoGrid
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.VerticalColumn
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity

/**
 * Layout focused on presenting a search entrypoint along with additional handy shortcuts that
 * provide quick access to frequently used functions.
 *
 * This serves as an implementation suggestion, but should be customized to fit your product's
 * needs.
 *
 * @param searchButton a button representing the search action in the widget displayed more
 *                     prominently than the trailing buttons..
 * @param trailingButtons list of 4 buttons representing handy shortcuts to frequently used
 *                     functions in your app.
 * @see SearchToolBarLayoutSize for supported breakpoints
 */
@Composable
fun SearchToolBarLayout(
  searchButton: SearchToolBarButton,
  // 4 items, as a list here for convenience, that you might inline in your implementation.
  trailingButtons: List<SearchToolBarButton>,
) {
  val searchButtonItem: @Composable () -> Unit = {
    if (canShowSearchText()) {
      SearchBar(searchButton = searchButton)
    } else {
      SearchIconButton(
        searchButton = searchButton,
        filled = canUseFilledButtons()
      )
    }
  }

  val trailingButtonItems: List<@Composable () -> Unit> =
    trailingButtons.map {
      {
        TrailingButton(
          button = it,
          filled = canUseFilledButtons()
        )
      }
    }

  Scaffold(
    modifier = GlanceModifier.padding(vertical = widgetPadding),
    horizontalPadding = widgetPadding
  ) {
    when (val layoutSize = SearchToolBarLayoutSize.fromLocalSize()) {
      HorizontalRow, VerticalColumn -> {
        val horizontal = (layoutSize == HorizontalRow)
        val numberOfItems = numberOfItemsThatFit(
          horizontal = horizontal,
          minItemSize = minButtonSize,
          spacing = itemsSpacing
        )
        val allItems = listOf(searchButtonItem) + trailingButtonItems
        val finalItems = allItems.take(numberOfItems)

        if (horizontal) {
          SpacedRow(
            items = finalItems,
            spacing = itemsSpacing,
            modifier = GlanceModifier.fillMaxSize()
          )
        } else {
          SpacedColumn(
            items = finalItems,
            spacing = itemsSpacing,
            modifier = GlanceModifier.fillMaxSize()
          )
        }
      }

      TwoByTwoGrid -> TwoRowGrid(
        // 4 items including search button
        items = listOf(searchButtonItem) + trailingButtonItems.take(3),
        spacing = itemsSpacing
      )

      SideBarTwoRowGrid -> SideBarTwoRowGrid(
        sideBarItem = searchButtonItem,
        items = trailingButtonItems.take(4),
        sideBarWidth = sideBarLeadingItemWidth,
        spacing = itemsSpacing
      )

      HeaderTwoRowGrid -> HeaderTwoRowGrid(
        headerItem = searchButtonItem,
        items = trailingButtonItems.take(4),
        headerHeight = headerItemHeight,
        spacing = itemsSpacing
      )
    }
  }
}

/**
 * Data class representing different buttons displayed in the search toolbar widget.
 * @param iconRes Resource id of the icon button.
 * @param contentDescription description about the button that can be used by the accessibility
 *                           services.
 * @param onClick action to perform on click of the button.
 * @param text optional text that can be displayed if space is available.
 */
data class SearchToolBarButton(
  @DrawableRes val iconRes: Int,
  val contentDescription: String,
  val onClick: Action,
  val text: String? = null,
)

/**
 * Search entrypoint in style of a rounded icon button.
 */
@Composable
private fun SearchIconButton(
  searchButton: SearchToolBarButton,
  filled: Boolean,
) {
  RectangularIconButton(
    imageProvider = ImageProvider(searchButton.iconRes),
    contentDescription = searchButton.contentDescription,
    backgroundColor = if (filled) {
      GlanceTheme.colors.tertiary
    } else {
      ColorProvider(Color.Transparent, Color.Transparent)
    },
    contentColor = if (filled) {
      GlanceTheme.colors.onTertiary
    } else {
      GlanceTheme.colors.onSecondaryContainer
    },
    iconSize = iconSize,
    roundedCornerShape = RoundedCornerShape.FULL,
    onClick = searchButton.onClick,
    modifier = GlanceModifier.fillMaxSize()
  )
}

/**
 * Search entrypoint in style of a search bar.
 */
@Composable
private fun SearchBar(searchButton: SearchToolBarButton) {
  Row(
    horizontalAlignment = Alignment.Start,
    verticalAlignment = Alignment.CenterVertically,
    modifier = GlanceModifier
      .fillMaxSize()
      .padding(horizontal = 16.dp, vertical = 12.dp)
      .background(GlanceTheme.colors.secondaryContainer)
      .cornerRadius(RoundedCornerShape.FULL.cornerRadius)
      .semantics { this.contentDescription = searchButton.contentDescription }
      .clickable(searchButton.onClick),
  ) {
    // Search or brand icon
    Image(
      provider = ImageProvider(searchButton.iconRes),
      contentDescription = null,
      colorFilter = ColorFilter.tint(GlanceTheme.colors.primary),
      modifier = GlanceModifier.size(iconSize)
    )
    // Followed by text
    searchButton.text?.let {
      Spacer(GlanceModifier.width(8.dp))
      Text(
        text = it,
        maxLines = 1,
        style = TextStyle(
          color = GlanceTheme.colors.onSecondaryContainer,
          fontSize = 16.sp,
          fontWeight = FontWeight.Medium
        ),
      )
    }
  }
}

/**
 * Rounded action buttons that support [filled] background when specified.
 */
@Composable
private fun TrailingButton(
  button: SearchToolBarButton,
  filled: Boolean = true,
) {
  RectangularIconButton(
    imageProvider = ImageProvider(button.iconRes),
    contentDescription = button.contentDescription,
    backgroundColor = if (filled) {
      GlanceTheme.colors.secondaryContainer
    } else {
      ColorProvider(Color.Transparent, Color.Transparent)
    },
    contentColor = GlanceTheme.colors.onSecondaryContainer,
    onClick = button.onClick,
    iconSize = iconSize,
    roundedCornerShape = RoundedCornerShape.MEDIUM,
    modifier = GlanceModifier.fillMaxSize()
  )
}

// Breakpoints based on the UX design.
private enum class SearchToolBarLayoutSize {
  // Row of search button followed by action buttons that fit horizontally.
  HorizontalRow,

  // Column of search button and action buttons that fit vertically.
  VerticalColumn,

  // A two row, two column grid containing search button and 3 trailing buttons.
  TwoByTwoGrid,

  // A side bar for search followed by a 2x2 grid of trailing buttons.
  SideBarTwoRowGrid,

  // A header containing header bar followed by a 2x2 grid of trailing buttons.
  HeaderTwoRowGrid;

  companion object {
    @Composable
    fun fromLocalSize(): SearchToolBarLayoutSize {
      val size = LocalSize.current
      val height = size.height
      val width = size.width

      return if (height < 128.dp) {
        HorizontalRow
      } else if (width < 128.dp) {
        VerticalColumn
      } else if (height < 188.dp && width < 188.dp) {
        TwoByTwoGrid
      } else if (height < 188.dp) {
        SideBarTwoRowGrid
      } else {
        HeaderTwoRowGrid
      }
    }

    /**
     * Helper to decide whether to show search text in current widget size.
     */
    @Composable
    fun canShowSearchText(): Boolean {
      val localSize = LocalSize.current

      // Per breakpoints in the UX design
      return localSize.width >= 184.dp && localSize.height >= 188.dp
    }

    /**
     * Helper to decide whether to show filled icons vs without containers in current widget size.
     */
    @Composable
    fun canUseFilledButtons(): Boolean {
      val localSize = LocalSize.current

      // Per breakpoints in the UX design
      return localSize.height >= 72.dp && localSize.width >= 72.dp
    }

    /**
     * Helper to decide how many items to show in the available space in given orientation.
     *
     * @param horizontal if its a horizontal orientation
     * @param minItemSize min size to maintain for each item when identify how many to fit
     * @param spacing spacing to between items
     */
    @Composable
    fun numberOfItemsThatFit(horizontal: Boolean, minItemSize: Dp, spacing: Dp): Int {
      val size = if (horizontal) {
        LocalSize.current.width
      } else {
        LocalSize.current.height
      }

      // n buttons have n-1 content spacers, so, we add one to total width to make the width division
      // simpler.
      val normalizedWidth: Dp = size + spacing
      val normalizedButtonWidth: Dp = minItemSize + spacing
      // Number of equally wide buttons that fit in a row
      return ((normalizedWidth / normalizedButtonWidth)).toInt()
    }
  }
}

// Various dimensions coming from the UX design
private object SearchToolBarLayoutDimens {
  /** Minimum size needed for buttons / clickable areas for accessibility. */
  val minButtonSize = 48.dp

  /** Padding around the content within the widget. */
  val widgetPadding = 12.dp

  /** Spacing between buttons in all layouts. */
  val itemsSpacing = 8.dp

  /** Size of icons in all buttons */
  val iconSize = 24.dp

  /** Height of side bar in the [SideBarTwoRowGrid] layout. */
  val sideBarLeadingItemWidth = 52.dp

  /** Height of header in the [HeaderTwoRowGrid] layout. */
  val headerItemHeight = 52.dp
}

/**
 * Previews for various breakpoints for this search toolbar layout.
 */
@OptIn(ExperimentalGlancePreviewApi::class)
@Preview(widthDp = 56, heightDp = 56) // only search icon
@Preview(widthDp = 128, heightDp = 48) // reveals one additional button - no background
@Preview(widthDp = 128, heightDp = 72) // w/ background colors
@Preview(widthDp = 296, heightDp = 48) // more buttons - no background
@Preview(widthDp = 296, heightDp = 72) // more buttons - w/ background colors
@Preview(widthDp = 72, heightDp = 228) // vertical
@Preview(widthDp = 128, heightDp = 128) // 2x2 grid
@Preview(widthDp = 296, heightDp = 128) // search sidebar + 2x2 grid
@Preview(widthDp = 128, heightDp = 228) // search on top (no text) + 2x2 grid
@Preview(widthDp = 240, heightDp = 228) // search on top w/ text + 2x2 grid
@Composable
private fun SearchToolbarPreview() {
    SearchToolBarLayout(
        searchButton = SearchToolBarButton(
            iconRes = R.drawable.sample_search_icon,
            contentDescription = "Search notes",
            text = "Search",
            onClick = actionStartDemoActivity("search notes button")
        ),
        trailingButtons = listOf(
            SearchToolBarButton(
                iconRes = R.drawable.sample_mic_icon,
                contentDescription = "audio",
                onClick = actionStartDemoActivity("audio button")
            ),
            SearchToolBarButton(
                iconRes = R.drawable.sample_videocam_icon,
                contentDescription = "video note",
                onClick = actionStartDemoActivity("video note button")
            ),
            SearchToolBarButton(
                iconRes = R.drawable.sample_camera_icon,
                contentDescription = "camera",
                onClick = actionStartDemoActivity("camera button")
            ),
            SearchToolBarButton(
                iconRes = R.drawable.sample_share_icon,
                contentDescription = "share",
                onClick = actionStartDemoActivity("share button")
            ),
        )
    )
}

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Google LLC
Publisher
Android (Google) (Official)
Distributed by
Android (Google)
Source
android/skills · system/glance-canonical-layouts · commit bf68e2d
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE.txt)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Google LLC or Android (Google).

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download glance-canonical-layouts.zip