Interactive Widgets
Android and iOS (starting with iOS 17) allow widgets to have interactive Elements like Buttons. Using home_widget you can register Dart Callbacks that get invoked from the Native Widgets so you only have to write that logic once.
Flutter Setup
-
Write a static function that takes a Uri as an argument. This will get called when a user clicks on the View
@pragma("vm:entry-point") FutureOr<void> backgroundCallback(Uri? data) async { // do something with data ... }
@pragma('vm:entry-point')
must be placed above thecallback
function to avoid tree shaking in release mode. -
Register the callback function by calling
HomeWidget.registerInteractivityCallback(backgroundCallback);
iOS Setup
-
Adjust your Podfile to add
home_widget
as a dependency to your WidgetExtensiontarget 'YourWidgetExtension' do use_frameworks! use_modular_headers! pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios' end
-
To be able to use plugins with the Background Callback add this to your AppDelegate's
application
functionif #available(iOS 17, *) { HomeWidgetBackgroundWorker.setPluginRegistrantCallback { registry in GeneratedPluginRegistrant.register(with: registry) } }
-
Create a custom
AppIntent
in your App Target (Runner) and make sure to select both your App and your WidgetExtension in the Target Membership panelIn this Intent you should import
home_widget
and callHomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!)
in the perform method.url
andappGroup
can be either hardcoded or set as parameters from the Widgetimport AppIntents import Flutter import Foundation import home_widget @available(iOS 16, *) public struct BackgroundIntent: AppIntent { static public var title: LocalizedStringResource = "HomeWidget Background Intent" @Parameter(title: "Widget URI") var url: URL? @Parameter(title: "AppGroup") var appGroup: String? public init() {} public init(url: URL?, appGroup: String?) { self.url = url self.appGroup = appGroup } public func perform() async throws -> some IntentResult { await HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!) return .result() } }
-
Add a Button to your Widget. This Button might be encapsulated by a Version check. Pass in an instance of the
AppIntent
created in the previous stepButton( intent: BackgroundIntent( url: URL(string: "homeWidgetExample://titleClicked"), appGroup: widgetGroupId) ) { Text(entry.title).bold().font( /*@START_MENU_TOKEN@*/.title /*@END_MENU_TOKEN@*/) }.buttonStyle(.plain)
-
With the current setup the Widget is now Interactive as long as the App is still in the background. If you want to have the Widget be able to wake the App up you need to add the following to your
AppIntent
file@available(iOS 16, *) @available(iOSApplicationExtension, unavailable) extension BackgroundIntent: ForegroundContinuableIntent {}
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
Android Setup
- Add the necessary Receiver and Service to your
AndroidManifest.xml
file<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver" android:exported="true"> <intent-filter> <action android:name="es.antonborri.home_widget.action.BACKGROUND" /> </intent-filter> </receiver> <service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true"/>
- Create a custom Action
class InteractiveAction : ActionCallback { override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked")) backgroundIntent.send() } }
- Add the Action as a modifier to a view
Text( title, style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), modifier = GlanceModifier.clickable(onClick = actionRunCallback<InteractiveAction>()), )
Example Blog Article
A more in depth guide on how to build Interactive Widgets can be found here