1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.glance.appwidget 18 19 import androidx.compose.ui.unit.DpSize 20 import androidx.glance.LocalSize 21 import androidx.glance.appwidget.SizeMode.Exact 22 import androidx.glance.appwidget.SizeMode.Responsive 23 24 /** 25 * Modes describing how the [GlanceAppWidget] should handle size specification. 26 * 27 * Note: Size modes that support multiple sizes ([Exact], [Responsive]) run the composable passed to 28 * to [provideContent] concurrently for each size. This has a number of important implications. 29 * Since an instance of the content is running for each size, all of the State objects in the 30 * content will have an instance for each size. 31 * 32 * For example, in Exact mode, let's say the AppWidgetHost asks for 2 sizes, portrait and landscape. 33 * In the code below, there will end up being two instances of the `count` variable, one for each 34 * size: 35 * ``` 36 * provideContent { 37 * var count by remember { mutableStateOf(0) } 38 * Button( 39 * text = "Increment count: $count", 40 * onClick = { count++ } 41 * ) 42 * } 43 * ``` 44 * 45 * If the button is clicked while the widget is displayed in portrait size, the `count` variable is 46 * updated for both sizes. This is so that, if the device orientation changes and the host displays 47 * the landscape layout, it will be consistent with the state in portrait. This works because 48 * lambdas that are at the same place in the composition will be mapped to the same default key in 49 * all sizes. So triggering one will trigger the corresponding lambdas in other sizes. 50 * 51 * This means that lambdas will be called multiple times when they are triggered, which can have 52 * unexpected effects if state is not handled correctly. In order to prevent some external action 53 * from being triggered multiple times at once, you should conflate requests so that only one is 54 * active at a time, e.g. using MutableStateFlow. 55 * 56 * To prevent this behavior, you can use the [androidx.compose.runtime.key] composable to set 57 * different default lambda keys for each size, so that they do not trigger each other: 58 * ``` 59 * provideContent { 60 * key(LocalSize.current) { 61 * var count by remember { mutableStateOf(0) } 62 * Button( 63 * text = "Increment count: $count", 64 * onClick = { count++ } 65 * ) 66 * } 67 * } 68 * ``` 69 * 70 * To disable this behavior on a per-lambda basis, use [androidx.glance.action.action] to set a 71 * custom lambda key based on the current size: 72 * ``` 73 * provideContent { 74 * var count by remember { mutableStateOf(0) } 75 * Button( 76 * text = "Increment count: $count", 77 * onClick = action("incrementCount-${LocalSize.current}") { count++ } 78 * ) 79 * } 80 * ``` 81 * 82 * In both of the last two examples, when the button is clicked, only the lambda for the currently 83 * visible size will be triggered. 84 * 85 * Note that the above does not work for effects, which will always be triggered for each size. Use 86 * effects to update state variables in the composition, otherwise be sure to handle any duplicate 87 * triggering that may occur. 88 */ 89 sealed interface SizeMode { 90 /** 91 * The [GlanceAppWidget] provides a single UI. 92 * 93 * The [LocalSize] will be the minimum size the App Widget can be, as defined in the App Widget 94 * provider info (see [android.appwidget.AppWidgetManager.getAppWidgetInfo]). 95 */ 96 object Single : SizeMode, PreviewSizeMode { toStringnull97 override fun toString(): String = "SizeMode.Single" 98 } 99 100 /** 101 * The [GlanceAppWidget] provides a UI for each size the App Widget may be displayed at. The 102 * list of sizes is provided by the options bundle (see 103 * [android.appwidget.AppWidgetManager.getAppWidgetOptions]). 104 * 105 * The composable will be run concurrently for each size. In each sub-composition, the 106 * [LocalSize] will be the one for which the UI is generated. See the note in [SizeMode] for 107 * more info. 108 */ 109 object Exact : SizeMode { 110 override fun toString(): String = "SizeMode.Exact" 111 } 112 113 /** 114 * The [GlanceAppWidget] provides a UI for a fixed set of sizes. 115 * 116 * On Android 12 and later, the composable will be run concurrently for each size provided and 117 * the mapping from size to view will be sent to the system. The framework will then decide 118 * which view to display based on the current size of the App Widget (see 119 * [android.widget.RemoteViews] for details) 120 * 121 * Before Android 12, the composable will be run concurrently for each size at which the app 122 * widget may be displayed (like for [Exact]). For each size, the best view will be chosen, 123 * which is the largest one that fits in the available space, or the smallest one if none fit. 124 * 125 * See the note in [SizeMode] for more info about handling concurrent runs for multiple sizes. 126 * 127 * @param sizes List of sizes to use, must not be empty. 128 */ 129 class Responsive(val sizes: Set<DpSize>) : SizeMode, PreviewSizeMode { 130 131 init { <lambda>null132 require(sizes.isNotEmpty()) { "The set of sizes cannot be empty" } 133 } 134 equalsnull135 override fun equals(other: Any?): Boolean { 136 if (this === other) return true 137 if (javaClass != other?.javaClass) return false 138 139 other as Responsive 140 141 if (sizes != other.sizes) return false 142 143 return true 144 } 145 hashCodenull146 override fun hashCode(): Int = sizes.hashCode() 147 148 override fun toString(): String = "SizeMode.Responsive(sizes=$sizes)" 149 } 150 } 151 152 /** This marker interface determines which [SizeMode]s can be used for preview compositions. */ 153 sealed interface PreviewSizeMode : SizeMode 154