Configurable Widgets

Create configurable Widgets and connect them to your Flutter App

Configurable Widgets

Sometimes you want to give your users the ability to configure their HomeScreenWidgets to their liking. For example a weather widget that can show the weather for different locations or a todo list that can show different lists.

iOS

On iOS there are two ways to make your App Configurable. The new and recommended way by Apple is to use WidgetConfigurationIntent you can also provide backwards compatibility using SiriKit Intents

WidgetConfigurationIntent

  • Create Widget Extension

    If you create a new Widget add an extension by going

    File

    New

    Target

    Widget Extension

    This time checking the box for

    Include Configuration App Intent

    Setting up the Extension with Configuration Intent

    The generated Widget code includes the following classes:

    • AppIntentTimelineProvider - Provides a Timeline of entries at which the System will update the Widget automatically using an AppIntent for configuration - TimelineEntry - Represents the Data Object used to build the Widget. The date field is necessary and defines the point in time at which the Timeline would update - View - The Widget itself, which is built with SwiftUI - Widget - Configuration: Make note of the kind you set in the Configuration as this is what's needed to update the Widget from Flutter.

    In a separate file there is a WidgetConfigurationIntent defined that will determine the configuration options of the Widget in AppIntent.swift.

  • Adjusting the Configuration Intent

    You can add fields that appear in the configuration by changing the generated ConfigurationAppIntent class.

    swift
    @Parameter(title: "Name", default: "World")
    var name: String

    You can access the fields of the configuration when building the Widget UI

    swift
    struct ConfigurableWidgetEntryView : View {
        var entry: Provider.Entry
    
        var body: some View {
            VStack {
                Text("Hello")
                Text(entry.configuration.name)
            }
        }
    }
        </div>
    
  • Getting Data from Flutter to the Configuration

    App Groups

    In order for sending Data from your Flutter App to the Widget we need to use

    App Groups

    .

    Create a new App Group

    Visit the Apple Developer Portal and create a new App Group.

    Enable App Groups in XCode

    Add the App Group capability to

    both the App Target (Runner) and your Widget Extension

    in XCode.

    Enable App Groups in XCode
    Register App Group in Flutter

    In your Flutter code register the App Group with home_widget

    dart
    import 'package:home_widget/home_widget.dart';
    void main() {
        WidgetFlutterBinding.ensureInitialized();
        HomeWidget.setAppGroupId('group.YOUR_APP_GROUP_ID');
        runApp(MyApp());
    }

    Send Data

    You can store data from Flutter for the Widget using HomeWidget.saveWidgetData to use it as options in the configuration panel.

    dart
    final punctuations = [
        '!',
        '!!!',
        '.',
        '?',
        // Wave Emoji
        '\u{1F44B}',
    ];
    await HomeWidget.saveWidgetData(
        'punctuations',
        jsonEncode(punctuations),
    );

    Use Values from Flutter as Options in Customize Panel

    In XCode and Swift add an AppEntity and matching EntityQuery

    swift
    
    @Parameter(title: "Punctuation")
    var punctuation: PunctuationEntity
    }
    
    // Make Entity Codable so home_widget
    // That way home_widget can best extract the values from a configuration
    struct PunctuationEntity: AppEntity, Codable {
    
    let id: String
    
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Punctuation"
    static var defaultQuery = PunctuationQuery()
    
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(id)")
    }
    }
    
    struct PunctuationQuery: EntityQuery {
    
    func punctuations() -> [PunctuationEntity] {
        let userDefaults = UserDefaults(suiteName: "YOUR_APP_GROUP")
    
        do {
        let jsonPunctuations = (userDefaults?.string(forKey: "punctuations") ?? "[\"!\"]").data(
            using: .utf8)!
        let stringArray = try JSONDecoder().decode([String].self, from: jsonPunctuations)
        return stringArray.map { punctuation in
            PunctuationEntity(id: punctuation)
    
        }
        } catch {
        return [PunctuationEntity(id: "!")]
        }
    
    }
    
    func entities(for identifiers: [PunctuationEntity.ID]) async throws -> [PunctuationEntity] {
        let results = punctuations().filter { identifiers.contains($0.id) }
        return results
    }
    
    func suggestedEntities() async throws -> [PunctuationEntity] {
        return punctuations()
    }
    
    func defaultResult() async -> PunctuationEntity? {
        try? await suggestedEntities().first
    }
    
    }

    Now you can access this new parameter in your Widget Code

    swift
    struct ConfigurableWidgetEntryView: View {
    var entry: Provider.Entry
    
    var body: some View {
        VStack {
        Text("Hello")
        Text(entry.configuration.name)
        Text(entry.configuration.punctuation.id)
        }
    }
    }
  • Seeing Configuration in Flutter

    You need to let home_widget know about the relation of the Intent and the kind (what you need for updating the widget).

    Add Runner as a Target of the Widget

    Open the Intent Swift File and in the Details Pane enable the Target Membership for the Runner Target

    Enable Target Membership for Runner
    Register Configuration in AppDelegate

    In your AppDelegate.swift register the Configuration Intent together with the kind of your Widget

    swift
    import Flutter
    import UIKit
    import home_widget
    
    @main
    @objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        if #available(iOS 17.0, *) { 
        HomeWidgetPlugin.setConfigurationLookup(to: [ 
            "ConfigurableWidget": ConfigurationAppIntent.self
        ])
        } 
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    }
    

    To get the configuration a user made to a Widget you can use HomeWidget.getInstalledWidgets().

    dart
    final configuration = await HomeWidget.getInstalledWidgets();

    this will give you a list of HomeWidgetInfo in there there is a Map<String, dynamic>? configuration.

    For our Greeting Example the Configuration would be:

    json
    {
        "name": "Documentation",
        "punctuation": {
            "id": "!!!"
        }
    }

SiriKit Intents

Before iOS 17 SiriKit Intents where used to build configurable widgets. You can use this to add backwards compatibility.

Basic setup

  • Create Intent

    Create a new SiriKit Intent File by going to

    File

    New

    SiriKit Intent File

    Create a new Intent using the

    +

    button in the bottom left.

    In the Intent set

    Category

    to

    View

    and enable

    Intent is eligible for widgets

    Add a simple parameter to the Intent. FOr our example a String parameter called name

  • Using Intent in Widget

    To use our custom generated Intent in the generated Widget we need to to adjust the automatically generated code to conform to the correct APIs.

    Adjust your SimpleEntry to use properties instead of the Intent so we can use both the AppIntent and the SiriIntent.

    swift
    struct SimpleEntry: TimelineEntry {
        let date: Date
        let configuration: ConfigurationAppIntent 
        let name: String
        var punctuation: String? = nil
    }    

    Create an IntentTimelineProvider

    swift
    struct IntentProvider: IntentTimelineProvider {
        typealias Entry = SimpleEntry
    
        typealias Intent = GreetingIntentIntent
    
        func placeholder(in context: Context) -> SimpleEntry {
            SimpleEntry(date: Date(), name: "World")
        }
    
        func getSnapshot(
            for configuration: GreetingIntentIntent, in context: Context,
            completion: @escaping (SimpleEntry) -> Void
        ) {
            completion(SimpleEntry(date: Date(), name: configuration.Name))
        }
    
        func getTimeline(
            for configuration: GreetingIntentIntent, in context: Context,
            completion: @escaping (Timeline<SimpleEntry>) -> Void
        ) {
            var entries: [SimpleEntry] = []
    
            // Generate a timeline consisting of five entries an hour apart, starting from the current date.
            let currentDate = Date()
            for hourOffset in 0..&lt;5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, name: configuration.Name)
            entries.append(entry)
            }
    
            completion(Timeline(entries: entries, policy: .atEnd))
        }
    }

    In the Widget definition add code to use the appropriate configuration provider.

    swift
    var body: some WidgetConfiguration {
        if #available(iOS 17.0, *) { 
        AppIntentConfiguration(
            return AppIntentConfiguration(
                kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()
            ) {
                entry in
                ConfigurableWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
            }
        } else { 
            return IntentConfiguration(
                kind: kind, 
                intent: GreetingIntentIntent.self, 
                provider: IntentProvider()
            ) { entry in
                ConfigurableWidgetEntryView(entry: entry)
            } 
        } 
    }
  • Getting Data from Flutter to the Configuration

    With SiriKit Intents you need to add a new IntentHandler that can handle using Data you send from home_widget.

    For this add a new

    Intents Extension

    to your App. Using

    File

    New

    Target

    Intents Extension

    In your SiriKit Intents definition add the newly created Extension as a Target

    In the same configurations file add a new Type and configure it to the options you need.

    Create a Field that uses this new type. Ensuring to enable

    Options are provided dynamically

    Implement the IntentHandler to handle the new Intent

    swift
    import Intents
    
    class IntentHandler: INExtension, GreetingIntentIntentHandling {
    
    func providePunctuationOptionsCollection(for intent: GreetingIntentIntent) async throws -> INObjectCollection<Punctuation> {
        let userDefaults = UserDefaults(suiteName: "YOUR_APP_GROUP")
        
        do {
            let jsonPunctuations = (userDefaults?.string(forKey: "punctuations") ?? "[\"!\"]").data(using: .utf8)!
            let stringArray = try JSONDecoder().decode([String].self, from: jsonPunctuations)
            let items = stringArray.map { punctuation in
                Punctuation(identifier: punctuation, display: punctuation)
            }
            return INObjectCollection(items: items)
            
        } catch {
            return INObjectCollection(items: [Punctuation(identifier: "!", display: "!")])
        }
    }
    }
  • Seeing Configuration in Flutter

    Similar to the WidgetConfigurationIntent you can use HomeWidget.getInstalledWidgets() to get the configuration of the Widget.

    dart
    final configuration = await HomeWidget.getInstalledWidgets();

    this will give you a list of HomeWidgetInfo in there there is a Map<String, dynamic>? configuration.

    For our Greeting Example the Configuration would be:

    json
    {
        "Name": "Siri",
        "Punctuation": {
            "identifier": "👋",
            "displayString": "👋"
        }
    }

Android

Use a dedicated FlutterActivity for widget configuration and a second Dart entry point (e.g. configureMain) so configuration always runs in a clean isolate and you do not mix “open app” with “configure widget.” The steps below describe that setup.

For a full example, check out the configurable_widget example app.

  • Create a configuration Activity

    Subclass FlutterActivity and override the Dart entry point name so Flutter runs configureMain instead of main:

    kotlin
    package es.antonborri.configurable_widget
    
    import io.flutter.embedding.android.FlutterActivity
    
    class WidgetConfigurationActivity : FlutterActivity() {
      override fun getDartEntrypointFunctionName(): String = "configureMain"
    }

    Keep your launcher MainActivity on the default main entry point.

  • Register the Activity in the widget provider

    In your widget’s appwidget-provider XML, set reconfigurable and point android:configure at the fully qualified class name of your configuration activity:

    xml
    <appwidget-provider
        xmlns:android="http://schemas.android.com/apk/res/android"
        ...
        android:widgetFeatures="reconfigurable"
        android:configure="com.example.app.WidgetConfigurationActivity" />

    This enables the widget’s configuration / “Settings” flow from the system UI.

  • Declare the Activity in AndroidManifest.xml

    Register the configuration activity with an intent filter for android.appwidget.action.APPWIDGET_CONFIGURE (same action as AppWidgetManager.ACTION_APPWIDGET_CONFIGURE). Mirror the Flutter embedding setup you use on MainActivity (LaunchTheme, NormalTheme, configChanges, etc.):

    xml
    <activity
        android:name=".WidgetConfigurationActivity"
        android:exported="true"
        android:launchMode="singleTop"
        ... >
        <meta-data
            android:name="io.flutter.embedding.android.NormalTheme"
            android:resource="@style/NormalTheme" />
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
        </intent-filter>
    </activity>
  • Add `configureMain` and listen for configuration

    Define a second entry point with @pragma('vm:entry-point') so tree shaking keeps it. The name must match getDartEntrypointFunctionName():

    dart
    @pragma('vm:entry-point')
    Future<void> configureMain() async {
      WidgetsFlutterBinding.ensureInitialized();
      final configuredWidgetId =
          await HomeWidget.initiallyLaunchedFromHomeWidgetConfigure();
    
      if (configuredWidgetId != null) {
        return runApp(MaterialApp(
          home: YourConfigurationScreen(widgetId: configuredWidgetId),
        ));
      }
      return main();
    }

    Call HomeWidget.initiallyLaunchedFromHomeWidgetConfigure() as early as possible after WidgetsFlutterBinding.ensureInitialized(). If the user opened this activity to configure a widget, the Future resolves to that instance’s widget ID (string); otherwise null (you can fall through to main()).

    When a configure launch is detected, the plugin sets the activity result to RESULT_CANCELED until you finish successfully—so canceling or leaving the flow behaves correctly.

  • Build the configuration UI and persist with HomeWidget

    In your configuration screen, use the widget ID to scope stored keys per instance (for example name.$widgetId and punctuation.$widgetId), read/write them with HomeWidget.getWidgetData / HomeWidget.saveWidgetData, and refresh the widget with HomeWidget.updateWidget (pass qualifiedAndroidName for your AppWidgetProvider / receiver class if needed).

  • Finish configuration

    When the user saves (or you are done applying settings), call:

    dart
    await HomeWidget.finishHomeWidgetConfigure();

    This completes the configuration activity with RESULT_OK, returns the EXTRA_APPWIDGET_ID to the system, and closes the activity.

For platform details and UI expectations, see the Android App Widget configuration guide.