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/text/layout/TextWithImageLayout.kt

package com.example.platform.ui.appwidgets.glance.layout.text.layout

import android.graphics.Bitmap
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.LocalSize
import androidx.glance.action.Action
import androidx.glance.action.clickable
import androidx.glance.appwidget.components.CircleIconButton
import androidx.glance.appwidget.components.Scaffold
import androidx.glance.appwidget.components.TitleBar
import androidx.glance.appwidget.cornerRadius
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
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.collections.layout.NoDataContent
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutDimensions.contentSize
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutDimensions.contentSpacing
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutDimensions.pictureRadius
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutDimensions.verticalTextsSpacing
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutDimensions.widgetPadding
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutSize.HorizontalLarge
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutSize.HorizontalSmall
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutSize.VerticalLarge
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutSize.VerticalSmall
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutTextStyles.primaryTextFontValues
import com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayoutTextStyles.secondaryTextFontValues
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
import com.example.platform.ui.appwidgets.glance.layout.utils.FontUtils
import com.example.platform.ui.appwidgets.glance.layout.utils.LargeWidgetPreview
import com.example.platform.ui.appwidgets.glance.layout.utils.MediumWidgetPreview
import com.example.platform.ui.appwidgets.glance.layout.utils.SmallWidgetPreview

/**
 * A layout focused on presenting a long text (~65 characters), its one line caption, a supporting
 * image and a secondary text in a [Scaffold] with an app-specific title bar.
 *
 * In compact spaces, the image, title bar, secondary text may not be shown.
 *
 * The layout serves as an implementation suggestion, but should be customized to fit your product's
 * needs. For instance, a ratings element may be additionally displayed.
 *
 * @param title the text to be displayed as title of the widget, e.g. name of your widget or app.
 * @param titleIconRes a tintable icon that represents your app or brand, that can be displayed
 * with the provided [title]. In this sample, we use icon from a drawable resource, but you should
 * use an appropriate icon source for your use case.
 * @param titleBarActionIconRes optional resource id of a tintable icon that can be displayed as
 * an icon button within the title bar area of the widget. For example, a search icon to find other
 * articles in an "article of the day" widget.
 * @param titleBarActionIconContentDescription description of the [titleBarActionIconRes] button
 * to be used by the accessibility services.
 * @param titleBarAction action to be performed on click of the [titleBarActionIconRes] button.
 * @param data the primary text, caption, secondary text and image to be displayed.
 *
 * @see [TextWithImageData] for accepted inputs.
 * @see [com.example.platform.ui.appwidgets.glance.layout.text.TextWithImageAppWidget]
 *
 */
@Composable
fun TextWithImageLayout(
  title: String,
  @DrawableRes titleIconRes: Int,
  @DrawableRes titleBarActionIconRes: Int? = null,
  titleBarActionIconContentDescription: String? = null,
  titleBarAction: (() -> Unit)? = null,
  data: TextWithImageData? = null,
) {
  fun titleBar(): @Composable (() -> Unit) = {
    TitleBar(
      startIcon = ImageProvider(titleIconRes),
      title = title.takeIf { LocalSize.current.width >= 230.dp } ?: "",
      iconColor = GlanceTheme.colors.primary,
      textColor = GlanceTheme.colors.onSurface,
      actions = {
        if (titleBarAction != null && titleBarActionIconRes != null) {
          CircleIconButton(
            imageProvider = ImageProvider(titleBarActionIconRes),
            contentDescription = titleBarActionIconContentDescription,
            contentColor = GlanceTheme.colors.secondary,
            backgroundColor = null, // transparent
            onClick = titleBarAction
          )
        }
      }
    )
  }

  val action = actionStartDemoActivity("TextWithImage: ${data?.textData?.key}")
  val layoutSize = TextWithImageLayoutSize.fromLocalSize()

  val titleBar = if (layoutSize.showTitleBar()) {
    titleBar()
  } else {
    null
  }
  val scaffoldTopPadding = if (layoutSize.showTitleBar()) {
    0.dp
  } else {
    widgetPadding
  }

  Scaffold(
    titleBar = titleBar,
    horizontalPadding = widgetPadding,
    backgroundColor = GlanceTheme.colors.widgetBackground,
    modifier = GlanceModifier
      .maybeClickable(action)
      .padding(
        bottom = widgetPadding,
        top = scaffoldTopPadding
      ),
  ) {
    when (data) {
      null -> NoDataContent()
      else -> {
        when (layoutSize) {
          VerticalSmall -> VerticalContent(
            data = data,
            showImage = true,
            showSecondaryText = false
          )

          VerticalLarge -> VerticalContent(
            data = data,
            showImage = true,
            showSecondaryText = true
          )

          HorizontalSmall -> HorizontalContent(
            data = data,
            showImage = false,
            showSecondaryText = false
          )

          HorizontalLarge -> HorizontalContent(
            data = data,
            showImage = true,
            showSecondaryText = true
          )
        }
      }
    }
  }
}

@Composable
private fun NoDataContent() {
  val context = LocalContext.current

  NoDataContent(
    noDataText = context.getString(R.string.sample_no_data_text),
    noDataIconRes = R.drawable.sample_no_data_icon,
    actionButtonText = context.getString(
      R.string.sample_learn_more_button_text
    ),
    actionButtonIcon = R.drawable.sample_info_icon,
    actionButtonOnClick = actionStartDemoActivity("on-click of info button in no data view")
  )
}

@Composable
private fun HorizontalContent(
  data: TextWithImageData,
  showImage: Boolean,
  showSecondaryText: Boolean,
) {
  val contentWidth = contentSize.width - contentSpacing
  val contentHeight = contentSize.height - (2 * verticalTextsSpacing)
  Row(
    verticalAlignment = Alignment.Vertical.Bottom,
    horizontalAlignment = Alignment.Horizontal.Start,
    modifier = GlanceModifier.fillMaxSize()
  ) {
    TextStack(
      data = data.textData,
      showSecondaryText = showSecondaryText,
      modifier = GlanceModifier.fillMaxHeight().defaultWeight(),
      availableSize = DpSize(
        // Use 40% of available width for text area if showing image on side.
        width = (0.4 * contentWidth).takeIf { showImage } ?: contentWidth,
        // Use 80% of vertical space for text area.
        height = (0.80 * contentHeight).takeIf { showImage } ?: contentHeight
      )
    )
    if (showImage) {
      Spacer(modifier = GlanceModifier.width(contentSpacing))
      Image(
        data = data.imageData,
        modifier = GlanceModifier.fillMaxHeight().width(contentWidth / 2)
      )
    }
  }
}

@Composable
private fun VerticalContent(
  data: TextWithImageData,
  showImage: Boolean,
  showSecondaryText: Boolean,
) {
  Column(
    verticalAlignment = Alignment.Vertical.Bottom,
    modifier = GlanceModifier.fillMaxHeight()
  ) {
    val contentWidth = contentSize.width
    val contentHeight = contentSize.height - (2 * verticalTextsSpacing)

    if (showImage) {
      Image(
        data = data.imageData,
        modifier = GlanceModifier.fillMaxWidth().defaultWeight()
      )
      Spacer(modifier = GlanceModifier.height(contentSpacing))
    }
    TextStack(
      data = data.textData,
      showSecondaryText = showSecondaryText,
      modifier = GlanceModifier.fillMaxWidth(),
      availableSize = DpSize(
        // Use 80% of available width to leave some offset for word break differences.
        width = 0.8 * contentWidth,
        // Use 40% vertical space for text area if showing image
        height = (0.4 * contentHeight).takeIf { showImage } ?: contentHeight
      )
    )
  }
}

@Composable
private fun TextStack(
  data: TextData,
  modifier: GlanceModifier,
  availableSize: DpSize,
  showSecondaryText: Boolean,
) {
  val (primaryTextFontSize, primaryTextMaxLines) = primaryTextFontValues(
    text = data.primary,
    availableSize = availableSize,
    showSecondaryText = showSecondaryText
  )

  Column(
    verticalAlignment = Alignment.Bottom,
    modifier = modifier
  ) {
    Text(
      text = data.caption,
      maxLines = 1,
      style = TextWithImageLayoutTextStyles.caption,
      modifier = GlanceModifier.fillMaxWidth()
    )
    Spacer(modifier = GlanceModifier.height(verticalTextsSpacing))
    Text(
      text = data.primary,
      maxLines = primaryTextMaxLines,
      style = TextWithImageLayoutTextStyles.primary.copy(fontSize = primaryTextFontSize),
    )
    if (showSecondaryText) {
      val (secondaryTextFontSize, secondaryTextMaxLines) = secondaryTextFontValues(
        text = data.secondary,
        availableSize = availableSize
      )

      Spacer(modifier = GlanceModifier.height(verticalTextsSpacing))
      Text(
        text = data.secondary,
        maxLines = secondaryTextMaxLines,
        style = TextWithImageLayoutTextStyles.secondary.copy(fontSize = secondaryTextFontSize),
      )
    }
  }
}

@Composable
private fun Image(
  data: ImageData,
  modifier: GlanceModifier,
) {
  val imageProvider = if (data.bitmap != null) {
    ImageProvider(data.bitmap)
  } else {
    ImageProvider(R.drawable.sample_placeholder_image)
  }

  Image(
    provider = imageProvider,
    contentDescription = data.contentDescription,
    contentScale = ContentScale.Crop,
    modifier = modifier.cornerRadius(pictureRadius),
  )
}

private fun GlanceModifier.maybeClickable(action: Action?): GlanceModifier {
  return if (action != null) {
    this.clickable(action)
  } else {
    this
  }
}

/**
 * Data object holding information displayed in
 * [com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayout].
 */
data class TextWithImageData(
  val textData: TextData,
  val imageData: ImageData,
)

/**
 * Data object holding texts displayed in
 * [com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayout].
 *
 * @param key a unique identifier for the data being displayed as primary content e.g. article ID,
 * in an "article of the day" widget. This may not be necessary for your use case; in this sample,
 * we use this key to differentiate between placeholder and real data when demonstrating clicks.
 * @param primary text representing primary information being conveyed to the user via the
 * widget
 * @param secondary text supporting the primary text; for example, in an "article of the day"
 * widget, this could be a brief description of the article.
 * @param caption shorter text (couple of words) that can fit in one line; e.g. author's name in an
 * "article of the day" widget.
 */
data class TextData(
  val key: String,
  val primary: String,
  val secondary: String,
  val caption: String,
)

/**
 * Data object holding raw information about the image accompanying the [TextData] in
 * [com.example.platform.ui.appwidgets.glance.layout.text.layout.TextWithImageLayout].
 *
 * @param resId an image accompanying the [TextData] content; for example, a preview image of
 * an article in an "article of the day" widget.
 * @param contentDescription an optional description about the [resId] that can be
 * used by the accessibility services
 */
data class ImageData(
  val bitmap: Bitmap? = null,
  val contentDescription: String? = null,
)

private enum class TextWithImageLayoutSize {
  // No title bar or secondary text
  HorizontalSmall,

  // No title bar or secondary text
  VerticalSmall,

  // Text with Image on the side
  HorizontalLarge,

  // Image with text below it
  VerticalLarge;

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

      return if (isTall && size.height <= 300.dp) {
        VerticalSmall
      } else if (isTall) {
        VerticalLarge
      } else if (size.width <= 165.dp) {
        HorizontalSmall
      } else {
        HorizontalLarge
      }
    }
  }

    @Composable
    fun showTitleBar() = LocalSize.current.height >= 180.dp
}


private object TextWithImageLayoutTextStyles {
  val primary: TextStyle
    @Composable get() = TextStyle(
      fontWeight = FontWeight.Medium,
      color = GlanceTheme.colors.onSurface
    )

  val secondary: TextStyle
    @Composable get() = TextStyle(
      color = GlanceTheme.colors.onSurfaceVariant
    )

  val caption: TextStyle
    @Composable get() =
      TextStyle(color = GlanceTheme.colors.secondary)

  @Composable
  fun primaryTextFontValues(
    text: String,
    availableSize: DpSize,
    showSecondaryText: Boolean,
  ): Pair<TextUnit, Int> {
    val availableHeight = if (showSecondaryText) {
      // Within the text area, 30% space is used by primary text.
      0.30 * availableSize.height
    } else {
      0.60 * availableSize.height
    }

    return FontUtils.calculateFontSizeAndMaxLines(
      context = LocalContext.current,
      text = text,
      availableWidth = availableSize.width,
      availableHeight = availableHeight,
      maxFontSize = 24.sp,
      minFontSize = 16.sp
    )
  }

  @Composable
  fun secondaryTextFontValues(text: String, availableSize: DpSize): Pair<TextUnit, Int> {
    return FontUtils.calculateFontSizeAndMaxLines(
      context = LocalContext.current,
      text = text,
      availableWidth = availableSize.width,
      // Within the text area, 25% space is used by secondary text.
      availableHeight = 0.25 * availableSize.height,
      maxFontSize = 24.sp,
      minFontSize = 12.sp
    )
  }
}

private object TextWithImageLayoutDimensions {
  /** Padding that visually appears between the widget outline and anything inside. */
  val widgetPadding = 16.dp

  /** Corner radius to be applied to an image. */
  val pictureRadius = 16.dp

    /**
     * Space between larger building blocks within the layout. e.g space between text stack and
     * the image.
     */
    val contentSpacing = 12.dp

    /**
     * Vertical spacing between text items
     */
    val verticalTextsSpacing = 4.dp

  /** Height of the title bar. */
  private val titleBarHeight: Dp
    @Composable get() = if (TextWithImageLayoutSize.fromLocalSize().showTitleBar()) {
      0.dp
    } else {
      56.dp
    }

  /** Height and width in dp available to main content (excluding title bar, padding, spacing). */
  val contentSize: DpSize
    @Composable get() {
      val size = LocalSize.current

      return DpSize(
        width = size.width - (2 * widgetPadding),
        height = size.height - titleBarHeight - widgetPadding
      )
    }
}

@SmallWidgetPreview
@MediumWidgetPreview
@LargeWidgetPreview
@Composable
private fun TextWithImagePreview() {
  val context = LocalContext.current

  TextWithImageLayout(
    title = context.getString(R.string.sample_text_and_image_app_widget_name),
    titleIconRes = R.drawable.sample_text_icon,
    titleBarActionIconRes = R.drawable.sample_refresh_icon,
    titleBarActionIconContentDescription = context.getString(
      R.string.sample_refresh_icon_button_label
    ),
    titleBarAction = {},
    data = TextWithImageData(
      textData = TextData(
        key = "1",
        primary = "This allows for a longer text string.",
        secondary = "Specifically because the focus in this, layout is on the primary text.",
        caption = "Ut mollis amet cursus",
      ),
      imageData = ImageData()
    )
  )
}

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