Skip to content

Commit dd2d116

Browse files
authored
feat: finish glance support (#239)
1 parent b004b33 commit dd2d116

17 files changed

+431
-111
lines changed

README.md

+165-88
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,86 @@ let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID")
6262
```
6363
</details>
6464

65-
<details><summary>Android</summary>
65+
<details><summary>Android (Jetpack Glance)</summary>
66+
67+
### Add Jetpack Glance as a dependency to you app's Gradle File
68+
```groovy
69+
implementation 'androidx.glance:glance-appwidget:LATEST-VERSION'
70+
```
71+
72+
### Create Widget Configuration into `android/app/src/main/res/xml`
73+
```xml
74+
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
75+
android:initialLayout="@layout/glance_default_loading_layout"
76+
android:minWidth="40dp"
77+
android:minHeight="40dp"
78+
android:resizeMode="horizontal|vertical"
79+
android:updatePeriodMillis="10000">
80+
</appwidget-provider>
81+
```
82+
83+
### Add WidgetReceiver to AndroidManifest
84+
```xml
85+
<receiver android:name=".glance.HomeWidgetReceiver"
86+
android:exported="true">
87+
<intent-filter>
88+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
89+
</intent-filter>
90+
<meta-data
91+
android:name="android.appwidget.provider"
92+
android:resource="@xml/home_widget_glance_example" />
93+
</receiver>
94+
```
95+
96+
### Create WidgetReceiver
97+
98+
To get automatic Updates you should extend from [HomeWidgetGlanceWidgetReceiver](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt)
99+
100+
Your Receiver should then look like this
101+
102+
```kotlin
103+
package es.antonborri.home_widget_example.glance
104+
105+
import HomeWidgetGlanceWidgetReceiver
106+
107+
class HomeWidgetReceiver : HomeWidgetGlanceWidgetReceiver<HomeWidgetGlanceAppWidget>() {
108+
override val glanceAppWidget = HomeWidgetGlanceAppWidget()
109+
}
110+
```
111+
112+
### Build Your AppWidget
113+
114+
```kotlin
115+
116+
class HomeWidgetGlanceAppWidget : GlanceAppWidget() {
117+
118+
/**
119+
* Needed for Updating
120+
*/
121+
override val stateDefinition = HomeWidgetGlanceStateDefinition()
122+
123+
override suspend fun provideGlance(context: Context, id: GlanceId) {
124+
provideContent {
125+
GlanceContent(context, currentState())
126+
}
127+
}
128+
129+
@Composable
130+
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
131+
// Use data to access the data you save with
132+
val data = currentState.preferences
133+
134+
135+
// Build Your Composable Widget
136+
Column(
137+
...
138+
}
139+
140+
```
141+
142+
</details>
143+
144+
<details><summary>Android (XML)</summary>
66145

67146
### Create Widget Layout inside `android/app/src/main/res/layout`
68147

@@ -103,22 +182,6 @@ which will give you access to the same SharedPreferences
103182
### More Information
104183
For more Information on how to create and configure Android Widgets, check out [this guide](https://developer.android.com/develop/ui/views/appwidgets) on the Android Developers Page.
105184
106-
### Jetpack Glance
107-
In Jetpack Glance, you have to write your receiver (== provider), that returns a widget.
108-
Add it to AndroidManifest the same way as written above for android widgets.
109-
110-
```kotlin
111-
class MyReceiver : GlanceAppWidgetReceiver() {
112-
override val glanceAppWidget: GlanceAppWidget get() = MyWidget()
113-
}
114-
```
115-
116-
If you need to access HomeWidget shared preferences, use this:
117-
118-
```kotlin
119-
HomeWidgetPlugin.getData(context)
120-
```
121-
122185
</details>
123186
124187
## Usage
@@ -145,14 +208,17 @@ HomeWidget.updateWidget(
145208
);
146209
```
147210

148-
The name for Android will be chosen by checking `qualifiedAndroidName`, falling back to `<packageName>.androidName` and if that was not provided it
149-
will fallback to `<packageName>.name`.
211+
The name for Android will be chosen by checking `qualifiedAndroidName`, falling back to `<packageName>.androidName` and if that was not provided it will fallback to `<packageName>.name`.
150212
This Name needs to be equal to the Classname of the [WidgetProvider](#Write-your-Widget)
151213

152214
The name for iOS will be chosen by checking `iOSName` if that was not provided it will fallback to `name`.
153215
This name needs to be equal to the Kind specified in you Widget
154216

155-
#### Android
217+
#### Android (Jetpack Glance)
218+
219+
If you followed the guide and use `HomeWidgetGlanceWidgetReceiver` as your Receiver, `HomeWidgetGlanceStateDefinition` as the AppWidgetStateDefinition, `currentState()` in the composable view and `currentState.preferences` for data access. No further work is necessary.
220+
221+
#### Android (XML)
156222
Calling `HomeWidget.updateWidget` only notifies the specified provider.
157223
To update widgets using this provider,
158224
update them from the provider like this:
@@ -173,36 +239,6 @@ class HomeWidgetExampleProvider : HomeWidgetProvider() {
173239
}
174240
```
175241

176-
#### Jetpack Glance
177-
Updating widgets in Jetpack Glance is a bit more tricky,
178-
widgets are only updated when their state changes,
179-
therefore simple update will not refresh them.
180-
To update them, you have to fake state update like this:
181-
182-
```kotlin
183-
class MyWidgetReceiver : GlanceAppWidgetReceiver() {
184-
override val glanceAppWidget: GlanceAppWidget get() = MyWidget()
185-
186-
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
187-
super.onUpdate(context, appWidgetManager, appWidgetIds)
188-
189-
runBlocking {
190-
appWidgetIds.forEach {
191-
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it)
192-
MyWidget().apply {
193-
// Must update widget state otherwise it update has no effect for some reason.
194-
updateAppWidgetState(context, glanceId) { prefs ->
195-
prefs[stringPreferencesKey("___FAKE_UPDATE___")] = Random.nextULong().toString()
196-
}
197-
198-
// Update widget.
199-
update(context, glanceId)
200-
}
201-
}
202-
}
203-
}
204-
}
205-
```
206242

207243
### Retrieve Data
208244
To retrieve the current Data saved in the Widget call `HomeWidget.getWidgetData<String>('id', defaultValue: data)`
@@ -301,7 +337,40 @@ Android and iOS (starting with iOS 17) allow widgets to have interactive Element
301337
This code tells the system to always perform the Intent in the App and not in a process attached to the Widget. Note however that this will start your Flutter App using the normal main entrypoint meaning your full app might be run in the background. To counter this you should add checks in the very first Widget you build inside `runApp` to only perform necessary calls/setups while the App is launched in the background
302338
</details>
303339
304-
<details><summary>Android</summary>
340+
341+
<details><summary>Android Jetpack Glance</summary>
342+
343+
1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file
344+
```
345+
<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver" android:exported="true">
346+
<intent-filter>
347+
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
348+
</intent-filter>
349+
</receiver>
350+
<service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
351+
android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true"/>
352+
```
353+
2. Create a custom Action
354+
```kotlin
355+
class InteractiveAction : ActionCallback {
356+
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
357+
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked"))
358+
backgroundIntent.send()
359+
}
360+
}
361+
```
362+
3. Add the Action as a modifier to a view
363+
```kotlin
364+
Text(
365+
title,
366+
style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold),
367+
modifier = GlanceModifier.clickable(onClick = actionRunCallback<InteractiveAction>()),
368+
)
369+
```
370+
371+
</details>
372+
373+
<details><summary>Android XML </summary>
305374
306375
1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file
307376
```
@@ -413,7 +482,25 @@ To retrieve the image and display it in a widget, you can use the following Swif
413482
<img width="522" alt="Screenshot 2023-06-07 at 12 57 28 PM" src="https://github.com/ABausG/home_widget/assets/21065911/f7dcdea0-605a-4662-a03a-158831a4e946">
414483
</details>
415484

416-
<details><summary>Android</summary>
485+
<details><summary>Android (Jetpack Glance)</summary>
486+
487+
```kotlin
488+
// Access data
489+
val data = currentState.preferences
490+
491+
// Get Path
492+
val imagePath = data.getString("lineChart", null)
493+
494+
// Add Image to Compose Tree
495+
imagePath?.let {
496+
val bitmap = BitmapFactory.decodeFile(it)
497+
Image(androidx.glance.ImageProvider(bitmap), null)
498+
}
499+
```
500+
501+
</details>
502+
503+
<details><summary>Android (XML)</summary>
417504

418505
1. Add an image UI element to your xml file:
419506
```xml
@@ -484,57 +571,47 @@ Text(entry.message)
484571
In order to only detect Widget Links you need to add the queryParameter`homeWidget` to the URL
485572
</details>
486573

487-
<details><summary>Android</summary>
574+
<details><summary>Android Jetpack Glance</summary>
575+
488576
Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest`
489577
```
490578
<intent-filter>
491579
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
492580
</intent-filter>
493581
```
494582

495-
In your WidgetProvider add a PendingIntent to your View using `HomeWidgetLaunchIntent.getActivity`
583+
Add the following modifier to your Widget (import from HomeWidget)
496584
```kotlin
497-
val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity(
498-
context,
499-
MainActivity::class.java,
500-
Uri.parse("homeWidgetExample://message?message=$message"))
501-
setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData)
585+
Text(
586+
message,
587+
style = TextStyle(fontSize = 18.sp),
588+
modifier = GlanceModifier.clickable(
589+
onClick = actionStartActivity<MainActivity>(
590+
context,
591+
Uri.parse("homeWidgetExample://message?message=$message")
592+
)
593+
)
594+
)
502595
```
503596

504-
#### Jetpack Glance
505-
Create an `ActionCallback`:
506-
507-
```kotlin
508-
class OpenAppAction : ActionCallback {
509-
companion object {
510-
const val MESSAGE_KEY = "OpenAppActionMessageKey"
511-
}
512-
513-
override suspend fun onAction(
514-
context: Context, glanceId: GlanceId, parameters: ActionParameters
515-
) {
516-
val message = parameters[ActionParameters.Key<String>(MESSAGE_KEY)]
597+
</details>
517598

518-
val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity(
519-
context, MainActivity::class.java, Uri.parse("homeWidgetExample://message?message=$message")
520-
)
599+
<details><summary>Android XML</summary>
521600

522-
pendingIntentWithData.send()
523-
}
524-
}
601+
Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest`
602+
```
603+
<intent-filter>
604+
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
605+
</intent-filter>
525606
```
526607

527-
and use it like this:
528-
608+
In your WidgetProvider add a PendingIntent to your View using `HomeWidgetLaunchIntent.getActivity`
529609
```kotlin
530-
Button(
531-
text = "Open App",
532-
onClick = actionRunCallback<OpenConfigurationAction>(
533-
actionParametersOf(
534-
ActionParameters.Key<String>(OpenAppAction.MESSAGE_KEY) to "your message"
535-
)
536-
)
537-
)
610+
val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity(
611+
context,
612+
MainActivity::class.java,
613+
Uri.parse("homeWidgetExample://message?message=$message"))
614+
setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData)
538615
```
539616

540617
</details>

android/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ android {
5151

5252
dependencies {
5353
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
54+
implementation 'androidx.glance:glance-appwidget:1.0.0'
5455
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import android.content.Context
2+
import android.content.SharedPreferences
3+
import android.os.Environment
4+
import androidx.datastore.core.DataStore
5+
import androidx.glance.state.GlanceStateDefinition
6+
import es.antonborri.home_widget.HomeWidgetPlugin
7+
import kotlinx.coroutines.flow.Flow
8+
import kotlinx.coroutines.flow.flow
9+
import java.io.File
10+
11+
class HomeWidgetGlanceState(val preferences: SharedPreferences)
12+
13+
class HomeWidgetGlanceStateDefinition : GlanceStateDefinition<HomeWidgetGlanceState> {
14+
override suspend fun getDataStore(context: Context, fileKey: String): DataStore<HomeWidgetGlanceState> {
15+
val preferences = context.getSharedPreferences(HomeWidgetPlugin.PREFERENCES, Context.MODE_PRIVATE)
16+
return HomeWidgetGlanceDataStore(preferences)
17+
}
18+
19+
override fun getLocation(context: Context, fileKey: String): File {
20+
return Environment.getDataDirectory()
21+
}
22+
23+
}
24+
25+
private class HomeWidgetGlanceDataStore(private val preferences: SharedPreferences) : DataStore<HomeWidgetGlanceState> {
26+
override val data: Flow<HomeWidgetGlanceState>
27+
get() = flow { emit(HomeWidgetGlanceState(preferences)) }
28+
29+
override suspend fun updateData(transform: suspend (t: HomeWidgetGlanceState) -> HomeWidgetGlanceState): HomeWidgetGlanceState {
30+
return transform(HomeWidgetGlanceState(preferences))
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import android.appwidget.AppWidgetManager
2+
import android.content.Context
3+
import androidx.glance.appwidget.GlanceAppWidget
4+
import androidx.glance.appwidget.GlanceAppWidgetManager
5+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
6+
import androidx.glance.appwidget.state.updateAppWidgetState
7+
import kotlinx.coroutines.runBlocking
8+
9+
abstract class HomeWidgetGlanceWidgetReceiver<T : GlanceAppWidget> : GlanceAppWidgetReceiver() {
10+
11+
abstract override val glanceAppWidget: T
12+
13+
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
14+
super.onUpdate(context, appWidgetManager, appWidgetIds)
15+
runBlocking {
16+
appWidgetIds.forEach {
17+
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it)
18+
glanceAppWidget.apply {
19+
if (this.stateDefinition is HomeWidgetGlanceStateDefinition) {
20+
// Must Update State
21+
updateAppWidgetState<HomeWidgetGlanceState>(context = context, this.stateDefinition as HomeWidgetGlanceStateDefinition, glanceId) { currentState -> currentState }
22+
}
23+
// Update widget.
24+
update(context, glanceId)
25+
}
26+
}
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)