<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Anton Borries on Medium]]></title>
        <description><![CDATA[Stories by Anton Borries on Medium]]></description>
        <link>https://medium.com/@ABausG?source=rss-3158ce720fe3------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*qlo7Ur139HwCZDdaHeYqDw.jpeg</url>
            <title>Stories by Anton Borries on Medium</title>
            <link>https://medium.com/@ABausG?source=rss-3158ce720fe3------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 16:38:50 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@ABausG/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Building a Duolingo Style Streak Widget for Flutter]]></title>
            <link>https://medium.com/@ABausG/building-a-duolingo-style-streak-widget-for-flutter-e0fd7f0531ad?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/e0fd7f0531ad</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[streaks]]></category>
            <category><![CDATA[duolingo]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Sun, 04 Oct 2026 22:20:46 GMT</pubDate>
            <atom:updated>2026-10-04T22:20:46.030Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building a Duolingo-Style Streak Widget for Flutter</h3><p><em>Disclaimer: This Article was fully written by hand. AI was used only for grammar and typo correction</em></p><p><a href="https://medium.com/@ABausG/a-new-way-to-create-homescreen-widgets-for-flutter-apps-d4953b0ad829">Recently I announced a new way to use the home_widget ecosystem</a> to write Homescreen Widgets for Flutter Apps fully in Dart without needing to touch native code.</p><p>With some recent additions it is now possible to write a Duolingo-style Streak Widget fully in Dart. This has always been something I wanted to do because trying out Duolingo’s Widgets is what actually got me started using Duolingo (current streak is 1058 for those wondering)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MY2q0tMkQwT-nfB_3Dl-Zg.png" /><figcaption>An example of Dasholingo</figcaption></figure><h3>The characteristics of a Duolingo-style widget</h3><p>The key features for a Duolingo-style Widget are:</p><ul><li>Show current streak and if completed for today or not</li><li>Background Image getting more and more unhinged throughout the day</li><li>All of that using a Bird Mascot</li></ul><p>Especially the Bird fact makes it the perfect case to build Dasholingo. It’s Flutter after all!</p><h3>The home_widget features making this work</h3><p>Most features were already possible in the new home_widget way of writing widgets but what was missing was always having automatic updates based on time.</p><p>This can be useful to swap out a picture of a bird getting angrier throughout the day, but is also really handy if you want to display any kind of timed data throughout the day like calendar events or weather forecasts.</p><p>On iOS this was quite straightforward as iOS Homescreen Widgets operate using a <a href="https://developer.apple.com/documentation/widgetkit/timeline">Timeline</a>. You define data points and iOS will update the widget automatically.</p><p>For Android no such concept exists out of the box. However as home_widget_generator is based on code generation home_widget will now generate the needed infrastructure automatically to update the widget with the needed data automatically using automatically scheduled alarms.</p><h3>In Practice</h3><p>To get these timed updates working in home_widget there are two parts to it. Defining it in the widget and using it in the generated helper to send data.</p><h4>Defining the Widget</h4><p>When you want to use timed data in a Widget it is as simple as wrapping your data field with a HWTimedData.</p><pre>import &#39;package:home_widget_generator/home_widget_generator.dart&#39;;<br><br>@HomeWidget(<br>  name: &#39;Streak&#39;,<br>  android: HomeWidgetAndroidConfiguration(),<br>  iOS: HomeWidgetIOSConfiguration(<br>    groupId: &#39;group.com.example.app&#39;,<br>    supportedFamilies: [HWWidgetFamily.systemSmall],<br>  ),<br>  widget: HWStack(<br>    fit: HWStackFit.expand,<br>    children: [<br>      // Timed Data to update the Background Image<br>      HWImage(<br>        HWTimedData(HWImageData(&#39;mascot&#39;)),<br>        fit: HWImageFit.cover,<br>      ),<br>      HWAlign(<br>        alignment: HWAlignment.topStart,<br>        child: HWText(<br>          // Non timed Data value for the streak<br>          HWInt(&#39;streak&#39;, defaultValue: 0),<br>          style: HWTextStyle(fontSize: 28, fontWeight: HWFontWeight.bold),<br>        ),<br>      ),<br>      HWAlign(<br>        alignment: HWAlignment.bottomCenter,<br>        child: HWText(<br>          // Timed Data for an optional Message<br>          HWTimedData(HWString(&#39;message&#39;, defaultValue: &#39;Open the app&#39;)),<br>        ),<br>      ),<br>    ],<br>  ),<br>)<br>class Streak {}</pre><h4>Sending timed data</h4><p>The widget helper that gets generated will then contain a field for timed data which you pass in a Map&lt;DateTime, WidgetData&gt; where WidgetData is a generated object containing all fields you defined as being time dependent.</p><p>In our streak example this looks like this:</p><pre>await StreakHomeWidget.saveData(<br>  streak: 82,<br>  timedData: {<br>    now: StreakTimedData(<br>      mascot: AssetImage(&#39;assets/dash/time_wait.png&#39;),<br>      message: &#39;Dash is waiting&#39;,<br>    ),<br>    // …more slots<br>  },<br>);</pre><h3>More Info</h3><p>Check out the <a href="https://github.com/ABausG/home_widget/tree/main/examples/streak_widget">full example in the GitHub Repo of home_widget</a></p><p>If you like my work on home_widget and it is helpful for your project please consider sponsoring:</p><p><a href="https://github.com/sponsors/abausg">Sponsor @ABausG on GitHub Sponsors</a></p><p>Reach out to me if you have more questions:</p><ul><li><a href="https://x.com/abausg">Anton Borries (@ABausG) on X</a></li><li><a href="https://www.linkedin.com/in/abausg/">Anton Borries - 1KOMMA5° | LinkedIn</a></li><li><a href="https://bsky.app/profile/abausg.bsky.social">Anton (@abausg.bsky.social)</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e0fd7f0531ad" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A new way to create Homescreen Widgets for Flutter Apps]]></title>
            <link>https://medium.com/@ABausG/a-new-way-to-create-homescreen-widgets-for-flutter-apps-d4953b0ad829?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/d4953b0ad829</guid>
            <category><![CDATA[home-screen-widget]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Mon, 08 Jun 2026 16:02:38 GMT</pubDate>
            <atom:updated>2026-06-08T16:02:38.954Z</atom:updated>
            <content:encoded><![CDATA[<p>In November 2020 I published the first version of <a href="https://pub.dev/packages/home_widget">home_widget</a> to make writing Homescreen Widgets easier for Flutter Apps. The package has been successful in achieving this functionality however there were some things that always bothered me and I think where things preventing people from writing Homescreen Widgets (<a href="https://github.com/ABausG/home_widget/discussions/categories/show-and-tell">although many have!</a>)</p><ol><li>Setting up a new Widget requires native additions and clicking around in the Xcode UI</li><li>You have to write Widget Code natively and crucially twice</li></ol><p><strong>I am very excited to announce some new additions to the home_widget ecosystem solving both of these issues.</strong></p><h3>home_widget_cli</h3><p>I created a new cli package <a href="https://pub.dev/packages/home_widget_cli">home_widget_cli</a> that helps in adding all the necessary boilerplate for setting up widgets for both Android and iOS. No opening of Android Studio or Xcode required.</p><h4>Install the cli</h4><pre>dart pub global activate home_widget_cli</pre><h4>Create a widget</h4><pre>home_widget create WeatherForecastWidget</pre><p>This will then wire up all the necessary code for you to start building out the UI. Sending data and updating widgets still works the same way using the home_widget package.</p><h3>home_widget_generator</h3><p>The cli also provides a generate command which combines with a new <a href="https://pub.dev/packages/home_widget_generator">home_widget_generator</a> to allow writing Homescreen Widgets fully in Dart.</p><ol><li>UI Code on the native side for both iOS and Android</li><li>Simple Dart helper classes to update the Widgets</li></ol><h4>Basic Example</h4><p>Add home_widget_generator as a dependency in pubspec.yaml</p><pre>dev_dependencies:<br>  home_widget_generator: ^0.0.1<br>  # You can also add the cli as a dev dependency to do dart run home_widget generate<br>  home_widget_cli: ^0.0.1<br></pre><p>Create a basic Widget Definition next to your your lib folder</p><pre>my_project/<br>  home_widget/<br>    greeting_widget.dart &lt;-- Add the file here<br>  lib/  <br>  test/ </pre><pre>import &#39;package:home_widget_generator/home_widget_generator.dart&#39;;<br><br>@HomeWidget(<br>  name: &#39;Greeting&#39;,<br>  android: HomeWidgetAndroidConfiguration(),<br>  iOS: HomeWidgetIOSConfiguration(<br>    groupId: &#39;group.es.antonborri.generatorBasics&#39;,<br>    supportedFamilies: [HWWidgetFamily.systemSmall],<br>  ),<br>  widget: HWColumn(<br>    crossAxisAlignment: HWCrossAxisAlignment.start,<br>    children: [<br>      HWText.fixed(<br>        &#39;Hello&#39;,<br>        style: HWRoleTextStyle(role: HWTextStyleRole.caption),<br>      ),<br>      HWText(<br>        HWString(&#39;name&#39;, defaultValue: &#39;world&#39;),<br>        style: HWRoleTextStyle(<br>          role: HWTextStyleRole.title,<br>          fontWeight: HWFontWeight.bold,<br>        ),<br>      ),<br>    ],<br>  ),<br>)<br>class Greeting {}</pre><p>Running home_widget generate (or dart run home_widget generate it you added the cli dependency) this will produce native Homescreen Widgets on both iOS and Android</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/594/1*7WZ1Kn80Fe-Unpg43QtMzg.jpeg" /><figcaption>Simple Greeting Widget on iOS</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/533/1*ZSU8zkXUH3n7zDwNkd3KIg.jpeg" /><figcaption>Simple Greeting Widget on Android</figcaption></figure><p>To update you simply need to call the generated helpers</p><pre>await GreetingHomeWidget.saveData(name: &#39;Anton&#39;);<br>await GreetingHomeWidget.updateWidget();</pre><h4>Conditionally showing and Formatting</h4><p>home_widget_generator also allows generating Widgets that rely on specific data being set or which values they have. For example you can check if a value is stored and if it is true/false</p><pre>import &#39;package:home_widget_generator/home_widget_generator.dart&#39;;<br><br>@HomeWidget(<br>  name: &#39;Conditional Status&#39;,<br>  android: HomeWidgetAndroidConfiguration(),<br>  iOS: HomeWidgetIOSConfiguration(<br>    groupId: &#39;group.es.antonborri.generatorBasics&#39;,<br>    supportedFamilies: [HWWidgetFamily.systemSmall],<br>  ),<br>  widget: HWFill(<br>    child: HWDataExists(<br>      data: HWBool(&#39;hasData&#39;),<br>      whenPresent: HWBoolConditional(<br>        data: HWBool(&#39;enabled&#39;, defaultValue: true),<br>        whenTrue: HWColumn(<br>          mainAxisAlignment: HWMainAxisAlignment.center,<br>          crossAxisAlignment: HWCrossAxisAlignment.center,<br>          children: [<br>            HWText.fixed(<br>              &#39;Enabled&#39;,<br>              style: HWRoleTextStyle.headline(<br>                color: HWColor.fixed(0xFF16A34A),<br>              ),<br>            ),<br>          ],<br>        ),<br>        whenFalse: HWColumn(<br>          mainAxisAlignment: HWMainAxisAlignment.center,<br>          crossAxisAlignment: HWCrossAxisAlignment.center,<br>          children: [<br>            HWText.fixed(<br>              &#39;Disabled&#39;,<br>              style: HWRoleTextStyle.headline(<br>                color: HWColor.fixed(0xFFDC2626),<br>              ),<br>            ),<br>          ],<br>        ),<br>      ),<br>      whenAbsent: HWColumn(<br>        mainAxisAlignment: HWMainAxisAlignment.center,<br>        crossAxisAlignment: HWCrossAxisAlignment.center,<br>        children: [<br>          HWText.fixed(&#39;No Data&#39;, style: HWRoleTextStyle.headline()),<br>          HWText.fixed(<br>            &#39;Open the app&#39;,<br>            style: HWRoleTextStyle(<br>              role: HWTextStyleRole.caption,<br>              color: HWDefaultColor(HWColorRole.contentSecondary),<br>            ),<br>          ),<br>        ],<br>      ),<br>    ),<br>  ),<br>)<br>class ConditionalStatus {}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X4YZ32STye92P-SNi0v6cw.jpeg" /><figcaption>Different State Widgets on iOS</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dmcdl_2l3OysQviNzPtGxA.jpeg" /><figcaption>Different State Widgets on Android</figcaption></figure><p>These are the first Preview releases of the Packages. More Info and Features are coming soon. Please try them out, <a href="https://docs.page/abausg/home_widget/generator">read the documentation</a> and <a href="https://github.com/abausg/home_widget">leave feedback via GitHub</a></p><p>Please consider <strong>supporting the future work of home_widget through GitHub Sponsors</strong></p><p><a href="https://github.com/sponsors/abausg">Sponsor @ABausG on GitHub Sponsors</a></p><p>If you have any questions feel free to reach out via social media as well:</p><p><a href="https://bsky.app/profile/abausg.bsky.social">Bluesky</a><br><a href="https://x.com/abausg">Twitter</a><br><a href="https://linkedin.com/in/abausg">LinkedIn</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d4953b0ad829" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Configurable Homescreen Widgets on Android for Flutter Apps using home_widget]]></title>
            <link>https://medium.com/@ABausG/configurable-homescreen-widgets-on-android-for-flutter-apps-using-home-widget-244515cb0f55?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/244515cb0f55</guid>
            <category><![CDATA[home-screen-widget]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Sun, 12 Apr 2026 21:57:49 GMT</pubDate>
            <atom:updated>2026-04-12T21:57:49.525Z</atom:updated>
            <content:encoded><![CDATA[<p>Configurable Widgets are a great way to give users control over what they want to see on their home screens. E.g a weather app may change which city to display weather for, a todo list select a list or a calendar a specific calendar.</p><p>A while back I wrote about how to build <a href="https://medium.com/@ABausG/configurable-home-screen-widget-for-flutter-apps-on-ios-3dc3062cdfc2">Configurable Home Screen Widget for Flutter Apps on iOS</a>.</p><p><a href="https://medium.com/@ABausG/configurable-home-screen-widget-for-flutter-apps-on-ios-3dc3062cdfc2">Configurable Home Screen Widget for Flutter Apps on iOS</a></p><p>I’m super happy to announce that, thanks to mainly a community contribution, <strong>home_widget now supports this also for Android Apps</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J2TNspf4zhY01kEgyXIvgQ.gif" /></figure><p>This Article will cover the necessary steps in order to make a Widget configurable.</p><h3>The concept</h3><p>Homescreen widgets on Android use a dedicated <a href="https://developer.android.com/develop/ui/views/appwidgets/configuration">Configuration Activity</a> to launch the app indicating that a user wants to configure a specific Widget.</p><p>You can detect when your App was launched this way using a new callback in Flutter. However I would recommend to configure it to launch in a dedicated Flutter entrypoint meaning you don’t have to launch the Full App just for these configuration changes as well as worry less about life cycle issues.</p><p>This Guide assumes you already have created the basic Widget Scaffold e.g <a href="https://docs.page/abausg/home_widget/setup/android">following this guide</a></p><p>Ensure you use at least home_widget 0.9.1</p><pre>// pubspec.yaml<br><br>dependencies:<br>  ...<br>  home_widget: ^0.9.1</pre><h3>Android Setup</h3><p>On Android we need to do three steps in order to tell the OS that a specific Widget is configurable.</p><ol><li>Create a Configuration Activity</li><li>Tell the Widget to use this Activity for Configuration</li><li>Ensure the AndroidManifest knows that this Activity can be used for configuration</li></ol><h4>Create a Configuration Activity</h4><p>Next to your MainActivity.kt create a new dedicated Activity for the Configuration</p><pre>package com.example.app<br><br>import io.flutter.embedding.android.FlutterActivity<br><br>class WidgetConfigurationActivity : FlutterActivity() {<br>  override fun getDartEntrypointFunctionName(): String = &quot;configureMain&quot;<br>}</pre><p>We will create a new Dart Entry Point with configureMain later.</p><h4>Tell the Widget to use this Activity for Configuration</h4><p>In the widget’s configuration xml make the widget configurable and point it to the newly created activity</p><pre>&lt;appwidget-provider<br>  xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;<br>  ...<br>  android:widgetFeatures=&quot;reconfigurable&quot;<br>  android:configure=&quot;com.example.app.WidgetConfigurationActivity&quot; /&gt;</pre><h4>Adjust AndroidManifest</h4><p>Add the new Activity to the AndroidManifest.xml similar to how the Main Activity is present</p><pre>&lt;activity<br>    android:name=&quot;.WidgetConfigurationActivity&quot;<br>    android:exported=&quot;true&quot;<br>    android:launchMode=&quot;singleTop&quot;<br>    ... &gt;<br>    &lt;meta-data<br>        android:name=&quot;io.flutter.embedding.android.NormalTheme&quot;<br>        android:resource=&quot;@style/NormalTheme&quot; /&gt;<br>    &lt;intent-filter&gt;<br>        &lt;action android:name=&quot;android.appwidget.action.APPWIDGET_CONFIGURE&quot; /&gt;<br>    &lt;/intent-filter&gt;<br>&lt;/activity&gt;</pre><p>Ensure you add the &lt;action android:name=&quot;android.appwidget.action.APPWIDGET_CONFIGURE&quot; /&gt; intent filter to the activity so the System knows how to launch this activity.</p><p>Now when the User adds the Widget to the Homescreen they will be prompted to configure it</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UAzYrT1YFqCqmotPmMd9CA.png" /></figure><h3>Flutter Setup</h3><p>As already hinted above we will create a dedicated dart entry point that will be called whenever the user wants to configure the Homescreen Widget. We need to do the following steps</p><ol><li>Create dedicated Entrypoint to launch the configuration ui</li><li>Present the configuration UI</li><li>Finish the configuration</li></ol><h4>Create Entrypoint</h4><p>In your main.dart file add a new global method next to the existing main()</p><pre>  @pragma(&#39;vm:entry-point&#39;)<br>  Future&lt;void&gt; configureMain() async {<br>    WidgetsFlutterBinding.ensureInitialized();<br>    // Validate that the App was indeed launched for configuration<br>    // If this is non-null it is the id of the widget the user wants to configure<br>    final configuredWidgetId =<br>        await HomeWidget.initiallyLaunchedFromHomeWidgetConfigure();<br><br>    if (configuredWidgetId != null) {<br>      // Call runApp with whatever UI you want to present for configuration<br>      return runApp(MaterialApp(<br>        home: YourConfigurationScreen(widgetId: configuredWidgetId),<br>      ));<br>    }<br>    // As a fallback launch the usual main app<br>    return main();<br>  }</pre><h4>Present the configuration UI</h4><p>HomeWidget.initiallyLaunchedFromHomeWidgetConfigure() will return a non null widgetId for the widget that should be configured. Use this Widget id to store configured values specifically for this widget.</p><p>e.g in the UI you may have a dropdown field so select a specific value. Use the widgetId when you save it using something like HomeWidget.saveWidgetData(&#39;name.$widgetId&#39;, &#39;World&#39;)</p><pre>ListTile(<br>  title: const Text(&#39;Name&#39;),<br>  trailing: DropdownButton(<br>    value: name,<br>    onChanged: (value) {<br>      saveName(value);<br>    },<br>    items: [&#39;World&#39;, &#39;Universe&#39;, &#39;Flutter Community&#39;]<br>        .map((option) =&gt; DropdownMenuItem(<br>              value: option,<br>              child: Text(option),<br>            ))<br>        .toList(),<br>  ),<br>); <br><br>...<br><br>Future&lt;void&gt; saveName(String? name) async {<br>  // Use the widgetId returned from HomeWidget.initiallyLaunchedFromHomeWidgetConfigure()<br>  await HomeWidget.saveWidgetData(&#39;name.$widgetId&#39;, name);<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oHjqLq4jNqd9i7Fz4wnzwA.png" /></figure><h4>Finish the configuration</h4><p>Once the user has finished making the configurations and you have saved all the values to Home Widget, update the widget and then crucially call HomeWidget.finishHomeWidgetConfigure() to finish the configuration. Without this Android will not properly mark the widget as configured.</p><p>Ideally instead of saving the configuration on each edit store it in a local state and only save the data and update the widget once the user makes a “Save” Action</p><pre>Future&lt;void&gt; _saveConfiguration() async {<br>  await HomeWidget.saveWidgetData(&#39;name.$widgetId&#39;, name);<br>  await HomeWidget.saveWidgetData(&#39;punctuation.$widgetId&#39;, punctuation);<br>  await HomeWidget.updateWidget(name: &#39;MyWidget&#39;);<br>  // Don&#39;t forget to call this<br>  await HomeWidget.finishHomeWidgetConfigure();<br>}</pre><h3>Closing the loop</h3><p>Back on the Android side, when building the Compose UI, use the key with the `widgetId` to get the user configured value when showing the widget:</p><pre>class ConfigurableWidgetHomeWidget : GlanceAppWidget() {<br>  override val stateDefinition = HomeWidgetGlanceStateDefinition()<br><br>  override suspend fun provideGlance(context: Context, id: GlanceId) {<br>    val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)<br>    provideContent { WidgetContent(appWidgetId, currentState()) }<br>  }<br><br>  @Composable<br>  private fun WidgetContent(appWidgetId: Int, currentState: HomeWidgetGlanceState) {<br>    val prefs = currentState.preferences<br>    val nameKey = &quot;name.$appWidgetId&quot;<br>    val name = prefs.getString(nameKey, &quot;World&quot;)<br><br>    Column() {<br>      Text(text = &quot;Hello&quot;)<br>      if (!name.isNullOrEmpty()) {<br>        Text(text = name)<br>      }<br>    }<br>  }<br>}</pre><h3>Full Example</h3><p>For a full example check out the android/ implementation of the <a href="https://github.com/ABausG/home_widget/tree/main/examples/configurable_widget">Configurable Widget Example in the home_widget repository</a></p><p><a href="https://github.com/ABausG/home_widget/tree/main/examples/configurable_widget">home_widget/examples/configurable_widget at main · ABausG/home_widget</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/865/1*ivAKZBOlgPeOGQ_TlfnbcA.png" /></figure><h3>Other new Features of home_widget</h3><p>home_widget now supports saving files and images directly. This makes it a lot easier to e.g pass API Responses as data for widgets or using bundled assets or network Images to present them on native widgets.</p><pre>await HomeWidget.saveFile(&#39;key&#39;, someBytesAsUint8List);<br><br>await HomeWidget.saveImage(&#39;key&#39;, imageProvider);</pre><p>This will store the path to the file for key similar to how renderFlutterWidget did it.</p><p>You can find an <a href="https://github.com/ABausG/home_widget/tree/main/examples/file_and_images">example of this on Github</a></p><p>Thanks for reading. If you like home_widget and want to support future development <strong>consider sponsoring on Github</strong></p><p><a href="https://github.com/sponsors/ABausG/">Sponsor @ABausG on GitHub Sponsors</a></p><p>If you have any questions reach out to me on social media:</p><p><a href="https://bsky.app/profile/abausg.bsky.social">Anton (@abausg.bsky.social)</a></p><p><a href="https://www.linkedin.com/in/abausg/">https://www.linkedin.com/in/abausg/</a></p><p><a href="https://x.com/ABausG">JavaScript is not available.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=244515cb0f55" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Apple Watch Companion for Flutter Apps]]></title>
            <link>https://medium.com/@ABausG/apple-watch-companion-for-flutter-apps-8b46b0aa8b35?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/8b46b0aa8b35</guid>
            <category><![CDATA[apple-watch]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Fri, 24 Oct 2025 14:26:09 GMT</pubDate>
            <atom:updated>2025-10-24T14:26:09.039Z</atom:updated>
            <content:encoded><![CDATA[<p>At Fluttercon Europe 2025 I gave a talk on how to build a companion app for Apple Watch for a Flutter App. You can find <a href="https://youtu.be/0HR0lZVx6go">the recording</a> and <a href="https://abausg.github.io/flutter_apple_watch_talk/">the slides</a>. If you want to get straight to a code example you can find an <a href="https://github.com/ABausG/flutter_apple_watch_talk/tree/main/example">example app</a>.</p><p>In this Article I will go through the necessary setup steps, how to share data and how to display data from your Flutter App in an Apple Watch App.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_N2zF6Hi4kiy046PXWUnvw.png" /></figure><h3>Adding an Apple Watch App</h3><p>In order to get started with adding a watch App open Xcode and go : File &gt; New &gt; Target &gt; watchOS &gt; App and fill out the metadata ensuring you select Watch App for Existing iOS App and selecting your Flutter Runner App</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8ZE5PASxlm2m_IfZ8IGoIw.png" /><figcaption>New Target</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fSzxkZ9HYtz4tVsAi0lfXA.png" /><figcaption>watchOS Target</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*84FYORu77gwmF3juilA2HA.png" /><figcaption>Watch App for Existing iOS App</figcaption></figure><h3>WatchConnectivity</h3><p>When you now run your App on an iPhone you can see your App available for installation in the Watch App on your phone. However simply having the companion app is not really what we want. We want to share data between the apps on the phone and the watch.</p><p>Ever since watchOS 2 (which was released in 2015) you need to use <a href="https://developer.apple.com/documentation/watchconnectivity">WatchConnectivity</a>. While the API has changed in some ways, the basic concept is still centered around having a <a href="https://developer.apple.com/documentation/watchconnectivity/wcsession">WCSession</a> and a <a href="https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate">WCSessionDelegate</a>. How it works is to connect a SessionDelegate to a session from both the phone app as well as the watch app.</p><p>Of course you could wrap the native APIs yourself to make sure your Flutter app can connect to the session. However you can also use the great Flutter Community and use packages from pub.dev where someone may already have wrapped it. In the case of Watch Connectivity there are actually multiple plugins already for this. For the remainder of this article I will be using the package <a href="https://pub.dev/packages/flutter_watch_os_connectivity">flutter_watch_os_connectivity</a></p><h4>Connecting to the Session from Flutter</h4><p>Using the plugin it is actually super easy to connect and activate the session.</p><pre>final watchConnectivity = FlutterWatchOsConnectivity();<br>await watchConnectivity.configureAndActivateSession()</pre><p>That’s it. Your flutter app is connected to a WatchSession. The plugin also offers callbacks like getActivateState for checking the session state, getPairedDeviceInfo for checking if a watch is currently paired and getReachability to check if the watch app is currently paired/reachable. All of these functions also offer listening to changes via a standard Dart Stream</p><h4>Session Delegate on the Watch</h4><p>As mentioned the session also needs to be connected to from the watch app.</p><p>To do that add a new Swift File to create the delegate. And add the following code:</p><pre>class FlutterWatchDelegate: NSObject, WCSessionDelegate {<br>  override init() {<br>    super.init()<br>    // Get the default session<br>    let session = WCSession.default<br>    // Set this instance as the delgate<br>    session.delegate = self<br>    // Activate the session<br>    session.activate()<br>  }<br><br>  func session(<br>    _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,<br>    error: (any Error)?<br>  ) {<br>    print(&quot;Activation Completed: \(session.activationState)&quot;)<br>    // Once connected you can initiate with initial data<br>  }<br>}</pre><p>To use the delegate (and have the initialization logic called), add it as a variable in your watch app</p><pre>@main<br>struct FlutterAppleWatchApp: App {<br>var delegate: FlutterWatchDelegate = FlutterWatchDelegate()<br><br>  var body: some Scene {<br>    WindowGroup {<br>      ContentView()<br>    }<br>  }<br>}</pre><h3>Share Data</h3><p>There are multiple ways of sharing data between the phone app and the watch app.</p><h4>Shared Application Context</h4><p>Apple provides a way of having a shared application context which is a key‑value store between the apps on both phone and watch. Especially useful if you want to update data while not both apps are open at the same time.</p><p><strong>Sending Data from Flutter to the Watch</strong></p><p>The plugin offers a way to simply send a Map&lt;String, dynamic&gt; from Flutter to the Application Context.</p><pre><br>final data = &lt;String, dynamic&gt;{&#39;key&#39;: value};<br>await watchConnectivity.updateApplicationContext(data);</pre><p><strong>Receive Data on the Watch</strong></p><p>The WCSessionDelegate has a function you can override which will get called when the ApplicationContext is updated.</p><pre>class FlutterWatchDelegate: NSObject, WCSessionDelegate {<br>  // ...<br>  var myData: Int = 0<br>  <br>  // Override didReceiveApplicationContext to be called when the context gets updated<br>  func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {<br>    myData = applicationContext[&quot;key&quot;] as Int?<br>  }<br>}</pre><p>When the session is first activated you can also use the session to get the current application context as well.</p><pre><br>func session(<br>  _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,<br>  error: (any Error)?<br>) {<br>  print(&quot;[WATCH] Activation Completed: \(session.activationState)&quot;)<br>  if session.activationState == WCSessionActivationState.activated {<br>    let initialContext = session.applicationContext<br>    // Use the context for what you need it<br>  }<br>}</pre><p><strong>Send Data from the Watch to Flutter</strong></p><p>Sending Data the other way around is quite straightforward. In order to update the application context the other way around you can simply call updateApplicationContext on the session like this:</p><pre>func updateData(value: Any?) {<br>  try? WCSession.default.updateApplicationContext([&quot;key&quot;: value])<br>}</pre><p><strong>Receive Data from the Watch in Flutter</strong></p><p>The plugin offers a way to get the applicationContext in Flutter both as a Future and a Stream</p><pre>// As Future<br>final receivedContext = await watchConnectivity.getApplicationContext();<br><br>// Updates as a Stream<br>final updates = watchConnectivity.applicationContextUpdated<br>    .listen((applicationContext) {<br>      // Handle updates<br>    });  </pre><p>Note that the received object has a currentContext and a receivedContext. This mirrors the underlying structure in the Swift API. This can get slightly confusing, especially when both the phone and the watch app are open at the same time and sending data both ways. So especially in these cases you should opt for different data exchange approaches.</p><p>For more details on the shared application context check out the <a href="https://developer.apple.com/documentation/watchconnectivity/wcsession/applicationcontext">Apple Documentation</a>.</p><h4>Sending Messages</h4><p>One other ways for sharing data is sending messages in the form of Map&lt;String, dynamic&gt; around the two apps. Note that before sending you should check that the counterpart app is reachable . The syntax to use it so send receive messages is quite similar to sending application context</p><pre>// Send Message from Flutter to Apple Watch<br>final messageData = &lt;String, dynamic&gt;{};<br>await watchConnectivity.sendMessage(messageData);<br><br>// Receive Data in Stream<br>watchConnectivity.messageReceived.listen((message) async {    <br>    /// New message is received, you can read it data map<br>    receivedMessageData = message.data;<br>});</pre><p><a href="https://pub.dev/packages/flutter_watch_os_connectivity#sending-and-handling-messages-a-namesend_message">Check out the plugin’s documentation</a> on more Info and also on how to reply to messages</p><p>On the Apple Watch Site sending/receiving messages is also quite similar</p><pre>// Override in Delegate for receiving messages<br>func session(<br>    _ session: WCSession,<br>    didReceiveMessage message: [String : Any]<br>) {<br>  // Handle received Message<br>}<br><br>// Sending Messages<br>func sendMessage(message: [String : Any]) {<br>  try? WCSession.default.sendMessage(message)<br>}</pre><p>Check the Apple Documentation for more Info on <a href="https://developer.apple.com/documentation/watchconnectivity/wcsession/sendmessage(_:replyhandler:errorhandler:)">sending Messages</a> and <a href="https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceivemessage:)">receiving messages</a>.</p><h4>User Info</h4><p>User Info is yet another way in which Apple allows you to send data as a Map&lt;String, dynamic&gt;. The main difference here is that Apple has slightly different rules on how receiving UserInfo can notify a backgrounded app, making User Info the prime candidate for using it to send data for <a href="https://developer.apple.com/design/human-interface-guidelines/complications">complications</a>. As a heads up I will write a follow up article on the topic of connecting Flutter with and Apple Watch App and Complications in another article so be sure to follow me for updates when this is out.</p><p>Sending Data works again quite similarly</p><pre>await watchConnectivity.transferUserInfo({<br>    &quot;key&quot;: &quot;value&quot;<br>});</pre><p>Note: You may be tempted to use the parameter isComplication when you want to send data for a complication, however the <a href="https://developer.apple.com/forums/thread/759389?answerId=795157022#795157022">underlying technology is not (yet) supported after apple switched complications to WidgetKit</a>. However in my experiments updating the complication also worked without this. More on this in the follow up article</p><p>Receiving data from the watch is similarly done by overriding the respective function on the delegate. More Info again in <a href="https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceiveuserinfo:)">Apple’s Documentation</a></p><pre>func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {<br>  // Handle user Info for Complication<br>}</pre><h4>Files</h4><p>WatchOS Connectivity also offers transferring files between platforms. This can be used to e.g send a screenshot of a Flutter Widget to display that on the watch (e.g your hero graphics) or also to transfer database files or backend responses. Sending a file is again following the other methods.</p><pre>final baseDirectory = await getApplicationSupportDirectory();    <br>final file = File(&#39;${baseDirectory.path}/$myData.json&#39;);<br>final transfer = await watchConnectivity.transferFileInfo(file);<br><br>transfer?.setOnProgressListener((progress) {<br>  debugPrint(&#39;Progress: ${progress.currentProgress}&#39;);<br>});</pre><p>Note that when receiving a File, this File is only available during the respective function invocation in the Watch App Delegate. So, in order to not lose it, you need to store it in the app’s document directory and use that copy when using the file outside of the function.</p><pre>func session(_ session: WCSession, didReceive file: WCSessionFile) {<br>  let sourceURL = file.fileURL<br>  let fileManager = FileManager.default<br>  let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]<br>  let destinationURL = documentsURL.appendingPathComponent(sourceURL.lastPathComponent)<br><br>  try? fileManager.moveItem(at: sourceURL, to: destinationURL)<br>    <br>  useFile()<br>}</pre><p>To read the file e.g when the App connects to the session on the phone you can do something like:</p><pre>func session(<br>  _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,<br>  error: (any Error)?<br>) {<br>  if session.activationState == WCSessionActivationState.activated {<br>    checkFile()<br>  }<br>}<br><br>var fileUrl : Url?<br><br><br>func checkFile() {<br>  let fileManager = FileManager.default<br>  let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]<br>  let imageUrl = documentsURL.appendingPathComponent(&quot;myImage.png&quot;)<br><br>  if fileManager.fileExists(atPath: imageUrl.path()) {<br>    // File exists<br>    fileUrl = imageUrl<br>  } else {<br>    // File does not exist<br>    fileUrl = nil<br>  }<br>}</pre><h3>Display Data from Flutter in the Apple Watch App</h3><p>So now that we have covered how to send data. Displaying data and updating the UI can be quite easy as well using the built-in state management solutions from SwiftUI. We can use the @Observable and @State annotation to automatically pass updates down to the UI Layer.</p><h4>Observing the SessionDelegate</h4><p>By using the @Observable annotation on the FlutterWatchDelegate it can get updates whenever a variable (in the example case the count changes)</p><pre>// Mark as Observable for updates<br>@Observable<br>class FlutterWatchDelegate: NSObject, WCSessionDelegate {<br>  <br>  // Count variable<br>  var count: Int = 0<br><br>  override init() {<br>    super.init()<br>    let session = WCSession.default<br>    session.delegate = self<br>    session.activate()<br>  }<br><br>  func session(<br>    _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,<br>    error: (any Error)?<br>  ) {<br>    if session.activationState == WCSessionActivationState.activated {<br>      // Get initial count<br>      updateCount(applicationContext: session.applicationContext)<br>    }<br>  }<br><br>  // Update Count when new application context is received<br>  func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any])<br>  {<br>    updateCount(applicationContext: applicationContext)<br>  }<br><br>  func updateCount(applicationContext: [String: Any]) {<br>    if let newCount = applicationContext[&quot;count&quot;] as? Int {<br>      count = newCount<br>    }<br>  }<br>}</pre><h4>Using the delegate as State</h4><p>Now that the FlutterWatchDelegate can be observed we can use it in the Watch Application to automatically receive updates</p><pre>@main<br>struct FlutterAppleWatchApp: App {<br>  @State var delegate: FlutterWatchDelegate = FlutterWatchDelegate()<br><br>  var body: some Scene {<br>    WindowGroup {<br>      ContentView(<br>        delegate: delegate<br>      )<br>    }<br>  }<br>}</pre><h4>Using and displaying the data</h4><p>Passing down the delegate to the View we can rebuild the ui with the value.</p><pre>struct ContentView: View {<br>  let delegate: FlutterWatchDelegate<br><br>  var body: some View {<br>    List {<br>      // Using the count from the delegate<br>      // .description is the Swift equivalent of toString()<br>      Text(delegate.count.description)<br>      Button(<br>        &quot;Increment&quot;,<br>        action: delegate.incrementCounter<br>      )     <br>    }<br>  }<br>}</pre><p>As you can see in the example above we call an incrementCounter method on the delegate. This is an easy way to enable two way data transfers as described in the previous section.</p><p>The respective method on the delegate may look something like this</p><pre>func incrementCounter() {<br>  count = count + 1<br><br>  try? WCSession.default.updateApplicationContext([&quot;count&quot;: count])<br>}</pre><h3>Things to look out for</h3><p>There are a couple of issues that may arise that could stop you from building the app (especially in release mode)</p><h4>Ensure the “Thin Binary” phase happens after “Embed Watch Content”</h4><p>Drag the <strong>Thin Binary</strong> step after the <strong>Embed Watch Content</strong> step in Runner &gt; Build Phases</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0sT32Sw1KduBTu-7yVo9vQ.png" /><figcaption>Drag and Drop the “<strong>Thin Binary</strong>” to the end of the step list</figcaption></figure><h4>Ensure Watch Target Build Settings target the correct Platform</h4><p>There is <a href="https://github.com/flutter/flutter/issues/160622">an issue</a> where Flutter overrides the Watch Platform and can then not correctly build the App.</p><p>To work around this you have to manually set the Watch App &gt; Build Settings &gt; Supported Platforms for Profile and Release builds to <strong>watchOS</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FNdldHCBzL4NsslF2ZSERQ.png" /><figcaption>Set <strong>Supported Platforms </strong>to <strong>watchOS </strong>for Profile and Release</figcaption></figure><h3>Closing thoughts</h3><p>While the initial setup can be a bit finicky and working around Xcode build errors is certainly not fun, this process shows that Watch Companion Apps for Flutter Apps is possible.</p><p>Does this mean you should all build Watch Companion Apps right away? Maybe! It all depends on your app’s use cases and, crucially, if it is something that brings a benefit to your users?</p><p>In the end a Flutter App is installed just like a native app. So we can use all the benefits of Flutter (write code once run it anywhere, Hot Reload, …) <strong>plus</strong> use the APIs for platform integration combined with the vast ecosystem on pub.dev to make the best use of platform specific features to provide the best User Experience possible!</p><p>—</p><p>Follow me here or on Social Media to get notified when the second part of this article is published about adding Complications</p><h4>Bluesky</h4><p><a href="https://bsky.app/profile/abausg.bsky.social">Anton (@abausg.bsky.social)</a></p><h4>Twitter</h4><p><a href="https://x.com/abausg">Anton Borries (@ABausG) on X</a></p><h4>Consider sponsoring to support my work</h4><p><a href="https://github.com/sponsors/ABausG">Sponsor @ABausG on GitHub Sponsors</a></p><h4>Example App</h4><p><a href="https://github.com/ABausG/flutter_apple_watch_talk">GitHub - ABausG/flutter_apple_watch_talk: Talk about Flutter on Apple Watch</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8b46b0aa8b35" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Configurable Home Screen Widget for Flutter Apps on iOS]]></title>
            <link>https://medium.com/@ABausG/configurable-home-screen-widget-for-flutter-apps-on-ios-3dc3062cdfc2?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/3dc3062cdfc2</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[home-screen-widget]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Sun, 25 May 2025 21:14:51 GMT</pubDate>
            <atom:updated>2025-05-25T21:14:51.659Z</atom:updated>
            <content:encoded><![CDATA[<p>Want to give your users more control over their Home Screen Widgets? Imagine a weather widget where they can choose their city, or a to-do list widget displaying specific lists. This level of customization enhances user experience and makes your widgets truly personal.</p><figure><img alt="A gif showing how the Widget is configurable. Long pressing the widget opens a pane to edit the name. The Name is changed to “Flutter” and gets updated on the Widget" src="https://cdn-images-1.medium.com/max/720/1*tpZHpsSO_4y6bLnRRO3z8w.gif" /><figcaption>Changing the Name parameter using a simple Input field</figcaption></figure><p>This article will guide you through the process of implementing configurable iOS Home Screen Widgets in your Flutter application using the home_widget package.</p><p>For this we will make use of <a href="https://developer.apple.com/documentation/AppIntents/WidgetConfigurationIntent">WidgetConfigurationIntent</a>, Apple’s newer and recommended way of writing configurable widgets. home_widget also supports the older SiriKit intent. Documentation for this can be found in the <a href="https://docs.page/abausg/home_widget/features/configurable-widgets#sirikit-intents">home_widgets docs</a>.</p><h3><strong>Prerequisites:</strong></h3><p>Before we begin, ensure you have added the home_widget plugin as a dependency in your pubspec.yaml file. For this guide, we&#39;ll assume you&#39;re using version ^0.8.0.</p><pre>dependencies:<br>  flutter:<br>    sdk: flutter<br>  home_widget: ^0.8.0</pre><p>Run flutter pub get to fetch the dependency.</p><p>We’ll be spending most of our time in Xcode. For that open the iOS workspace in Xcode.</p><h3>Step 1: Creating Your Widget Extension with Configuration Intent</h3><p>The first step is creating a new WidgetExtension and adding that to the App</p><ul><li>Navigate to File&gt; New &gt; Target</li><li>Select Widget Extension</li><li>Crucially, <strong>check the box for </strong><strong>Include Configuration App Intent</strong>.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mWNyPTjFNb1mR3ncJuFWsw.jpeg" /></figure><p>Upon creation, Xcode will generate the foundational files for your widget, including:</p><ul><li>AppIntentTimelineProvider: This class is responsible for providing a timeline of widget updates. When using WidgetConfigurationIntent, it utilizes an AppIntent for configuration.</li><li>TimelineEntry: This struct represents the data that will be displayed in your widget at a specific point in time. It always includes a date field to schedule updates.</li><li>Your Widget’s View (built with SwiftUI): This defines the visual presentation of your widget.</li><li>Your Widget’s main Widget struct: Pay close attention to the kind property you define here, as this identifier will be used later in your Flutter code to reference and update this specific widget.</li></ul><p>In a separate Swift file (likely named YourWidgetNameIntent.swift or within AppIntent.swift), you&#39;ll find a ConfigurationAppIntent: WidgetConfigurationIntent protocol definition. This is where you&#39;ll define the configurable options for your widget.</p><h3>Step 2: Adjusting the Configuration Intent</h3><p>The generated ConfigurationAppIntent class is for defining the customizable aspects of your widget. To add a configuration field, simply declare a property with the @Parameter attribute:</p><pre>@available(iOS 17.0, *)<br>struct ConfigurationAppIntent: WidgetConfigurationIntent {<br>  static var title: LocalizedStringResource = &quot;Configuration&quot;<br>  static var description = IntentDescription(&quot;This is an example widget.&quot;)<br><br>  // An example simple parameter<br>  @Parameter(title: &quot;Name&quot;, default: &quot;World&quot;)<br>  var name: String<br><br>  func perform() async throws -&gt; Never {<br>    return Never()<br>  }<br>}</pre><p>In this example, we’ve added a name parameter with a default value of &quot;<em>World</em>&quot;. This will appear as a text field in the widget&#39;s configuration options when the user adds or edits the widget.</p><p>Accessing these configuration values within your widget’s UI is straightforward:</p><pre>struct ConfigurableWidgetEntryView : View {<br>    var entry: Provider.Entry<br>    var body: some View {<br>        VStack {<br>            Text(&quot;Hello&quot;)<br>            Text(entry.configuration.name)<br>        }<br>    }<br>}</pre><p>Here, entry.configuration.name retrieves the value the user has set in the configuration.</p><figure><img alt="iOS HomeScreen Widget showing “Hello World”. The app’s name is “Configurable Widget”" src="https://cdn-images-1.medium.com/max/352/1*DXSygQNdLHlWU0Uo8zJawA.png" /><figcaption>Widget showing Hello World</figcaption></figure><figure><img alt="A gif showing how the Widget is configurable. Long pressing the widget opens a pane to edit the name. The Name is changed to “Flutter” and gets updated on the Widget" src="https://cdn-images-1.medium.com/max/720/1*tpZHpsSO_4y6bLnRRO3z8w.gif" /><figcaption>Changing the Name parameter using a simple Input field</figcaption></figure><h3>Step 3: Getting Data from Flutter to the Configuration</h3><p>Often, you’ll want to provide dynamic configuration options based on data within your Flutter app. The home_widget plugin allows you to save data that can be used in your widget&#39;s configuration panel using HomeWidget.saveWidgetData</p><p>In order to enable synchronisation between Flutter, the iOS App and the Widget you need to register an App Group. For this you need a paid Apple Developer account.</p><ol><li>Register a new App Group on the <a href="https://developer.apple.com/account/resources/identifiers/applicationGroup/add/">Apple Developer Portal</a></li><li>Add the App Group Capability to both the App and the WidgetExtension<br>Runner &gt; Signing &amp; Capabilities &gt; + Capability &gt; App Groups <br>ConfigurableWidgetExtension &gt; Signing &amp; Capabilities &gt; + Capability &gt; App Groups</li><li>Register the App Group with home_widget in Flutter<br>await HomeWidget.<em>setAppGroupId</em>(‘group.com.example.appgroup’);</li></ol><pre>final punctuations = [<br>    &#39;!&#39;,<br>    &#39;!!!&#39;,<br>    &#39;.&#39;,<br>    &#39;?&#39;,<br>    // Wave Emoji<br>    &#39;\u{1F44B}&#39;,<br>];<br>await HomeWidget.saveWidgetData(<br>    &#39;punctuations&#39;,<br>    jsonEncode(punctuations),<br>);</pre><p>In this Dart code, we’re saving a list of punctuations as a JSON string with the key &#39;punctuations&#39;. But you could also use this to provide a list of locations for weather or a list of to-do lists.</p><h4>Using Flutter Values as Options in the Customize Panel</h4><p>To make these saved values appear as selectable options in the widget’s configuration, you need to define an AppEntity and a corresponding EntityQuery in your Swift code:</p><pre>@available(iOS 17.0, *)<br>struct ConfigurationAppIntent: WidgetConfigurationIntent {<br>  ...<br><br>  @Parameter(title: &quot;Punctuation&quot;)<br>  var punctuation: PunctuationEntity<br>}</pre><pre>// Make Entity Codable so home_widget<br>// That way home_widget can best extract the values from a configuration<br>struct PunctuationEntity: AppEntity, Codable {<br>  let id: String<br>  static var typeDisplayRepresentation: TypeDisplayRepresentation = &quot;Punctuation&quot;<br>  static var defaultQuery = PunctuationQuery()<br>  var displayRepresentation: DisplayRepresentation {<br>      DisplayRepresentation(title: &quot;\(id)&quot;)<br>  }<br>}<br><br>struct PunctuationQuery: EntityQuery {<br>  func punctuations() -&gt; [PunctuationEntity] {<br>    let userDefaults = UserDefaults(suiteName: &quot;YOUR_APP_GROUP&quot;)<br>      do {<br>        let jsonPunctuations = (userDefaults?.string(forKey: &quot;punctuations&quot;) ?? &quot;[\&quot;!\&quot;]&quot;).data(<br>          using: .utf8)!<br>        let stringArray = try JSONDecoder().decode([String].self, from: jsonPunctuations)<br>        return stringArray.map { punctuation in<br>          PunctuationEntity(id: punctuation)<br>        }<br>      } catch {<br>        return [PunctuationEntity(id: &quot;!&quot;)]<br>      }<br>  }<br><br>  func entities(for identifiers: [PunctuationEntity.ID]) async throws -&gt; [PunctuationEntity] {<br>      let results = punctuations().filter { identifiers.contains($0.id) }<br>      return results<br>  }<br><br>  func suggestedEntities() async throws -&gt; [PunctuationEntity] {<br>      return punctuations()<br>  }<br><br>  func defaultResult() async -&gt; PunctuationEntity? {<br>      try? await suggestedEntities().first<br>  }<br>}</pre><p>Here’s what’s happening:</p><ul><li>PunctuationEntity: This struct conforms to AppEntity. It represents a single punctuation option with an id. The displayRepresentation defines how this entity will be displayed to the user in the configuration panel. Codable is used for home_widget to easily parse this value again so it can be read again from within Flutter to detect which option a user chose.</li><li>PunctuationQuery: This struct conforms to EntityQuery. Its methods are responsible for fetching the available punctuation options.</li><li>punctuations(): This function retrieves the JSON string we saved from Flutter using UserDefaults(make sure to use your App Group suite name) and decodes it into an array of PunctuationEntityobjects.</li><li>entities(for identifiers:): This function fetches specific entities based on their IDs.</li><li>suggestedEntities(): This provides a list of suggested entities for the configuration.</li><li>defaultResult(): This sets a default entity if the user doesn&#39;t make a selection.</li></ul><p>Now you can access the selected punctuation in your widget’s code:</p><pre>struct ConfigurableWidgetEntryView: View {<br>  var entry: Provider.Entry<br>  var body: some View {<br>    VStack {<br>      Text(&quot;Hello&quot;)<br>      Text(entry.configuration.name)<br>      Text(entry.configuration.punctuation.id)<br>    }<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/890/1*22x9IGuNY58LfviUCAqw7Q.jpeg" /></figure><h3>Step 4: Seeing Configuration in Flutter</h3><p>To access the user’s widget configurations within your Flutter app, you need to establish a connection between your AppIntent and the App.</p><h4>Adding Runner as a Target of the Widget</h4><p>Open your Intent Swift file (e.g., YourWidgetNameIntent.swift or AppIntent.swift) in Xcode. In the <strong>Details Pane</strong>(usually on the right), find the <strong>Target Membership</strong> section and <strong>enable the </strong><strong>Runner target</strong> for this file.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/598/1*1RW--px_phrkWgW7vRP9lQ.jpeg" /><figcaption>Add Runner as one of the target membership</figcaption></figure><h4>Registering Configuration in AppDelegate</h4><p>In your Flutter project’s ios/Runner/AppDelegate.swift file, import the home_widget plugin and register your ConfigurationAppIntent with the corresponding widget kind:</p><pre>import Flutter<br>import UIKit<br>import home_widget<br><br>@main<br>@objc class AppDelegate: FlutterAppDelegate {<br>override func application(<br>    _ application: UIApplication,<br>    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?<br>) -&gt; Bool {<br>    GeneratedPluginRegistrant.register(with: self)<br>    if #available(iOS 17.0, *) { <br>      HomeWidgetPlugin.setConfigurationLookup(to: [<br>        &quot;ConfigurableWidget&quot;: ConfigurationAppIntent.self<br>      ])<br>    }<br>    return super.application(application, didFinishLaunchingWithOptions: launchOptions)<br>  }<br>}</pre><p>Replace &quot;ConfigurableWidget&quot; with the actual kind you defined in your widget&#39;s main Swift file.</p><p>Now, within your Flutter code, you can retrieve the configurations of all installed widgets using HomeWidget.getInstalledWidgets():</p><pre>final configuration = await HomeWidget.getInstalledWidgets();</pre><p>This will return a list of HomeWidgetInfo objects. Each HomeWidgetInfo contains a Map&lt;String, dynamic&gt;? configuration property, which holds the user&#39;s settings for that specific widget.</p><pre>{<br>    &quot;name&quot;: &quot;Documentation&quot;,<br>    &quot;punctuation&quot;: {<br>        &quot;id&quot;: &quot;!!!&quot;<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-rytTgTo7bTt5wtdFyOrxA.jpeg" /><figcaption>Two widgets on the homescreen with different configurations</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SETn4M1RlzjH047Jf77EHA.jpeg" /><figcaption>See the configuration in Flutter</figcaption></figure><blockquote><em>On iOS Simulators, </em><em>HomeWidget.getInstalledWidgets() will always return an empty list. To fully test the configuration retrieval, you </em><strong><em>must test on a real iOS device</em></strong><em>.</em></blockquote><h3>Conclusion</h3><p>By leveraging WidgetConfigurationIntent with the home_widget plugin, you can create powerful and personalized Home Screen Widgets for your iOS users. This approach provides a clean and efficient way to offer customization options, enhancing the engagement and utility of your widgets. Remember to test thoroughly on a physical device to experience the full functionality of configurable widgets.</p><h4>Do you like this article and my work? Consider supporting via Github Sponsors</h4><p><a href="https://github.com/sponsors/ABausG">Sponsor @ABausG on GitHub Sponsors</a></p><h4>More Info on home_widget and configurable widgets</h4><p><strong>Documentation</strong></p><p><a href="https://docs.page/abausg/home_widget/features/configurable-widgets">Configurable Widgets</a></p><p><strong>Example App</strong></p><p><a href="https://github.com/abausg/home_widget/tree/main/examples/configurable_widgets">https://github.com/abausg/home_widget/tree/main/examples/configurable_widgets</a></p><p><strong>home_widget on pub.dev</strong></p><p><a href="https://pub.dev/packages/home_widget">home_widget | Flutter package</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3dc3062cdfc2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Jetpack Glance Home Screen Widgets with Flutter]]></title>
            <link>https://medium.com/@ABausG/jetpack-glance-home-screen-widgets-with-flutter-810c5121422f?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/810c5121422f</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[jetpack-compose]]></category>
            <category><![CDATA[jetpack-glance]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[homescreen]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Mon, 25 Mar 2024 18:30:02 GMT</pubDate>
            <atom:updated>2024-03-25T18:32:22.352Z</atom:updated>
            <content:encoded><![CDATA[<p>Back in December of 2021 the <a href="https://android-developers.googleblog.com/2021/12/announcing-jetpack-glance-alpha-for-app.html">first Alpha of Jetpack Glance</a> was announced to write Home Screen Widgets on Android using Compose Syntax. Finally <a href="https://pub.dev/packages/home_widget">home_widget</a> now supports Jetpack Glance making it a lot easier to write the native Android part when creating Home Screen Widgets for your Flutter Apps.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HvLgN-RiOvtBx9T4GZMRZw.png" /></figure><p>In this article we’ll take a look at the home_widget_counter App created for <a href="https://medium.com/@ABausG/interactive-homescreen-widgets-with-flutter-using-home-widget-83cb0706a417">iOS Interactivity</a> and add another Android Widget now using Glance technology.</p><p>Scroll down to the end to see what else is new in home_widget</p><h3>Quick Summary of the Flutter Part of the App</h3><p>The App is basically the classic Flutter Counter App. When counting up the App uses home_widget to store the count and updates the Home Screen Widget.</p><p>The method that is invoked looks like this:</p><pre>Future&lt;void&gt; _sendAndUpdate([int? value]) async {<br>  await HomeWidget.saveWidgetData(_countKey, value);<br> <br>  await HomeWidget.updateWidget(<br>    iOSName: &#39;CounterWidget&#39;,<br>    androidName: &#39;CounterWidgetProvider&#39;,<br>  );<br>}</pre><p>For this to work you’ll need the latest Versions of home_widget.</p><pre># For Flutter Stable<br>home_widget: ^0.5.0<br><br># For Flutter Beta<br>home_widget: ^0.6.0</pre><h3>Writing the Jetpack Compose Widget</h3><h4>Prepare Gradle File</h4><p>Jetpack Glance requires us a bit of adjustments to the Gradle File in order to compile the App:</p><ol><li>Add Glance Dependency to the build.gradle File in your app directory</li></ol><pre>dependencies {<br> implementation ‘androidx.glance:glance-appwidget:1.0.0’<br>}</pre><p>2. Enable Compose Support. Note that depending on your kotlin version you might have to also adjust the compose option.</p><pre>buildFeatures {<br>  compose true<br>}<br><br>// For the example this is needed with defining the Kotlin Version to 1.9.20<br>composeOptions {<br>  kotlinCompilerExtensionVersion = &quot;1.5.4&quot;<br>}</pre><p>Add this point it might be best to quickly run your app to ensure that the app still compiles and runs.</p><h4>Add the necessary Files</h4><p>We’ll need to create the following files</p><ul><li>A configuration xml file</li><li>A WidgetReceiver</li><li>The actual Widget</li><li>We also need to define the Configuration File and the Receiver in the App’s Manifest file</li></ul><p>The <strong>configuration file</strong> needs to be created in android/app/src/main/res/xml . In there you can customized and configure options like min/max sizes and loading behaviour. For our Counter example it looks like this:</p><pre>&lt;appwidget-provider xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;<br>    android:initialLayout=&quot;@layout/glance_default_loading_layout&quot;<br>    android:minWidth=&quot;203dp&quot;<br>    android:minHeight=&quot;90dp&quot;<br>    android:resizeMode=&quot;horizontal|vertical&quot;<br>    android:updatePeriodMillis=&quot;10000&quot; /&gt;</pre><p>The <strong>WidgetReceiver </strong>is handling the updating behavior of the widget. home_widget offers an abstraction for this so you only need to provide simple references to the Widget (which will be created in the next step). Thanks to these abstractions for our example the file is super short:</p><pre>import HomeWidgetGlanceWidgetReceiver<br><br>class CounterGlanceWidgetReceiver : HomeWidgetGlanceWidgetReceiver&lt;CounterGlanceWidget&gt;() {<br>    override val glanceAppWidget = CounterGlanceWidget()<br>}</pre><p>The WidgetReceiver is already referencing the final file our <strong>AppWidget</strong>. This is whats actually building the Widget. Using Jetpack Compose the syntax with which you can now write the widgets is a lot closer to how you would write Flutter UI Code:</p><pre>class CounterGlanceWidget : GlanceAppWidget() {<br><br>    // Needed for Updating   <br>    override val stateDefinition = HomeWidgetGlanceStateDefinition()<br><br>    override suspend fun provideGlance(context: Context, id: GlanceId) {<br>        provideContent {<br>            GlanceContent(context, currentState())<br>        }<br>    }<br><br>    @Composable<br>    private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {<br>        val data = currentState.preferences<br>        val count = data.getInt(&quot;counter&quot;, 0)<br><br>        Box(modifier = GlanceModifier.background(Color.White).padding(16.dp).clickable(onClick = actionStartActivity&lt;MainActivity&gt;(context))) {<br>            Column(<br>                    modifier = GlanceModifier.fillMaxSize(),<br>                    verticalAlignment = Alignment.Vertical.CenterVertically,<br>                    horizontalAlignment = Alignment.Horizontal.CenterHorizontally,<br>            ) {<br>                Text(<br>                        &quot;You have pushed the button this many times:&quot;,<br>                        style = TextStyle(fontSize = 14.sp, textAlign = TextAlign.Center),<br>                )<br>                Text(<br>                        count.toString(),<br>                        style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center),<br>                )<br><br>            }<br>        }<br>    }<br>}</pre><p>Please take special note of the override val stateDefinition = HomeWidgetGlanceStateDefinition() as this is vital for home_widget to update the widget’s UI</p><p>As a last step we need to tell our App about our new Widget in the <strong>Android Manifest file</strong>. Here we tie together the Configuration File and the Widget Receiver. Simply add this block inside the application section of the manifest file:</p><pre>&lt;receiver<br>    android:name=&quot;.CounterGlanceWidgetReceiver&quot;<br>    android:label=&quot;Glance&quot;<br>    android:exported=&quot;true&quot;&gt;<br>    &lt;intent-filter&gt;<br>        &lt;action android:name=&quot;android.appwidget.action.APPWIDGET_UPDATE&quot; /&gt;<br>    &lt;/intent-filter&gt;<br>    &lt;meta-data<br>        android:name=&quot;android.appwidget.provider&quot;<br>        android:resource=&quot;@xml/counter_glance_widget&quot; /&gt;<br>&lt;/receiver&gt;</pre><p>That’s it. Now you should be able to add the Widget to your Home Screen and see the current Count!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AAMf3aGJuLjONompMsTJig.png" /><figcaption>Displaying the current count</figcaption></figure><h4>Updating the Widget</h4><p>Going from the example we need to adjust (or add) the HomeWidget.updateWidget method to now target our new CounterGlanceWidgetReceiver</p><pre>Future&lt;void&gt; _sendAndUpdate([int? value]) async {<br>  await HomeWidget.saveWidgetData(_countKey, value);<br> <br>  await HomeWidget.updateWidget(<br>    iOSName: &#39;CounterWidget&#39;,<br>    androidName: &#39;CounterGlanceWidgetReceiver&#39;,<br>  );<br>}</pre><h3>Adding Interactivity</h3><p>Next up we want to add interactivity to the widget such that we can count up directly from the HomeScreen.</p><h4>Preparing the Flutter App</h4><p>On the Flutter side of things we need to register an interactivity callback that will then increment the count and update the widget again. Important thing to note are that the interactive callback is <strong>accessible statically </strong>and marked with @pragma(‘vm:entry-point’)</p><pre>Future&lt;void&gt; main() async {<br>  WidgetsFlutterBinding.ensureInitialized();<br> await HomeWidget.registerInteractivityCallback(interactiveCallback);<br>  runApp(const MyApp());<br>}<br><br>@pragma(&#39;vm:entry-point&#39;)<br>Future&lt;void&gt; interactiveCallback(Uri? uri) async {<br>  // We check the host of the uri to determine which action should be triggered.<br>  if (uri?.host == &#39;increment&#39;) {<br>    await _increment();<br>  } else if (uri?.host == &#39;clear&#39;) {<br>    await _clear();<br>  }<br>}<br><br>/// Saves that new value<br>Future&lt;int&gt; _increment() async {<br>  final oldValue = await _value;<br>  final newValue = oldValue + 1;<br>  await _sendAndUpdate(newValue);<br>  return newValue;<br>}<br><br>/// Clears the saved Counter Value<br>Future&lt;void&gt; _clear() async {<br>  await _sendAndUpdate(null);<br>}<br><br>/// Stores [value] in the Widget Configuration<br>Future&lt;void&gt; _sendAndUpdate([int? value]) async {<br>  await HomeWidget.saveWidgetData(_countKey, value);<br>  await HomeWidget.updateWidget(<br>    iOSName: &#39;CounterWidget&#39;,<br>    androidName: &#39;CounterGlanceWidgetReceiver&#39;,<br>  );<br>}</pre><h4>Adjusting the Glance UI</h4><p>Glance supports interactivity using ActionCallbacks let’s create action callbacks for the <strong>increment </strong>and the <strong>clear</strong> action.</p><pre>class IncrementAction : ActionCallback {<br>    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {<br>        val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(<br>                context,<br>                Uri.parse(&quot;homeWidgetCounter://increment&quot;))<br>        backgroundIntent.send()<br>    }<br>}<br><br>class ClearAction : ActionCallback {<br>    override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {<br>        val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(<br>                context,<br>                Uri.parse(&quot;homeWidgetCounter://clear&quot;))<br>        backgroundIntent.send()<br>    }<br>}</pre><p>To use these Actions in the UI we need to add a GlanceModifier to the Elements that we want to invoke these Actions with.</p><pre>Box(<br>  modifier = GlanceModifier.clickable(onClick = actionRunCallback&lt;IncrementAction&gt;())<br>) {<br>  Image(<br>    provider = ImageProvider(R.drawable.baseline_add_24), contentDescription = null,<br>    colorFilter = ColorFilter.tint(ColorProvider(Color.White)),<br>    modifier = GlanceModifier.size(32.dp).background(<br>      imageProvider = ImageProvider(R.drawable.fab_shape)<br>    )<br>  )<br>}</pre><p>So now our GlanceContent looks like this to include our Buttons</p><pre>  @Composable<br>  private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {<br>    val data = currentState.preferences<br>    val count = data.getInt(&quot;counter&quot;, 0)<br><br>    Box(modifier = GlanceModifier.background(Color.White).padding(16.dp).clickable(onClick = actionStartActivity&lt;MainActivity&gt;(context))) {<br>      Column(<br>          modifier = GlanceModifier.fillMaxSize(),<br>          verticalAlignment = Alignment.Vertical.CenterVertically,<br>          horizontalAlignment = Alignment.Horizontal.CenterHorizontally,<br>      ) {<br>        Text(<br>            &quot;You have pushed the button this many times:&quot;,<br>            style = TextStyle(fontSize = 14.sp, textAlign = TextAlign.Center),<br>        )<br>        Spacer(GlanceModifier.defaultWeight())<br>        Text(<br>            count.toString(),<br>            style = TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center),<br>        )<br>        Spacer(GlanceModifier.defaultWeight())<br>        Row(<br>            modifier = GlanceModifier.fillMaxWidth()<br>        ) {<br>          Box(<br>              modifier = GlanceModifier.clickable(onClick = actionRunCallback&lt;ClearAction&gt;())<br>          ) {<br>            Image(<br>                provider = ImageProvider(R.drawable.baseline_close_24), contentDescription = null,<br>                colorFilter = ColorFilter.tint(ColorProvider(Color.White)),<br>                modifier = GlanceModifier.size(32.dp).background(<br>                    imageProvider = ImageProvider(R.drawable.fab_shape)<br>                )<br>            )<br>          }<br>          Spacer(GlanceModifier.defaultWeight())<br>          Box(<br>              modifier = GlanceModifier.clickable(onClick = actionRunCallback&lt;IncrementAction&gt;())<br>          ) {<br>            Image(<br>                provider = ImageProvider(R.drawable.baseline_add_24), contentDescription = null,<br>                colorFilter = ColorFilter.tint(ColorProvider(Color.White)),<br>                modifier = GlanceModifier.size(32.dp).background(<br>                    imageProvider = ImageProvider(R.drawable.fab_shape)<br>                )<br>            )<br>          }<br>        }<br>      }<br>    }<br>  }</pre><p>In order for the App to know that it can handle the home_widget background workers we need to add the following section to the App’s Manifest.</p><pre>&lt;receiver<br>    android:name=&quot;es.antonborri.home_widget.HomeWidgetBackgroundReceiver&quot;<br>    android:exported=&quot;true&quot;&gt;<br>    &lt;intent-filter&gt;<br>        &lt;action android:name=&quot;es.antonborri.home_widget.action.BACKGROUND&quot; /&gt;<br>    &lt;/intent-filter&gt;<br>&lt;/receiver&gt;<br><br>&lt;service<br>    android:name=&quot;es.antonborri.home_widget.HomeWidgetBackgroundService&quot;<br>    android:exported=&quot;true&quot;<br>    android:permission=&quot;android.permission.BIND_JOB_SERVICE&quot; /&gt;</pre><p>That’s it. Now we can can increment and clear the counter directly from the HomeScreen!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5EjewmSbOmrkHTKswo4oWQ.gif" /></figure><h3>More new Features in home_widget 0.5.0</h3><h4>Pinning Home Screen Widgets</h4><p>As an additional new feature home_widget now supports <strong>pinning a Widget to the Home Screen</strong> directly from the App. This feature is only available on Android <em>(if the User’s launcher supports it</em>)</p><p>To request to pin the widget simply invoke HomeWidget.requestPinWidget()</p><pre>Future&lt;void&gt; _requestToPinWidget() async {<br>  final isRequestPinSupported = await HomeWidget.isRequestPinWidgetSupported();<br>  if (isRequestPinSupported == true) {<br>    await HomeWidget.requestPinWidget(androidName: &#39;CounterGlanceWidgetReceiver&#39;);<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ywB8hoQWnbj_Bo9vLRCNWw.gif" /></figure><h4>Widgets Analytics</h4><p>You can now get information about the widgets a user currently has installed/pinned on their Home or Lock Screens. This is extremely useful for analytics and/or if you want to reward your users if they install your Widget.</p><p>To query the currently installed widgets of your app simply call</p><pre>Future&lt;void&gt; _checkInstalledWidgets() async {<br>  final installedWidgets = await HomeWidget.getInstalledWidgets();  debugPrint(installedWidgets.toString());<br>}</pre><pre>// Output Android:<br>I/flutter (28322): [HomeWidgetInfo{iOSFamily: null, iOSKind: null, androidWidgetId: 22, androidClassName: .CounterGlanceWidgetReceiver, androidLabel: Glance}]</pre><pre>// Output iOS (HomeScreen and LockScreen):<br>flutter: [HomeWidgetInfo{iOSFamily: systemSmall, iOSKind: CounterWidget, androidWidgetId: null, androidClassName: null, androidLabel: null}, HomeWidgetInfo{iOSFamily: accessoryCircular, iOSKind: CounterWidget, androidWidgetId: null, androidClassName: null, androidLabel: null}]</pre><h3>Closing</h3><p>Thanks so much to all of the contributors in this new release.</p><p>home_widget on <a href="https://pub.dev/packages/home_widget">pub.dev</a> and on <a href="https://github.com/ABausG/home_widget">github.com</a></p><p>If you or your company is benefitting from home_widget consider sponsoring me on GitHub</p><p><a href="https://github.com/sponsors/ABausG">Sponsor @ABausG on GitHub Sponsors</a></p><p>You can find the code for this example on Github</p><p><a href="https://github.com/ABausG/home_widget_counter">GitHub - ABausG/home_widget_counter: An example App for home_widget rebuilding the Flutter Counter App as a HomeScreen Widget</a></p><p>If you have any questions about home_widget feel free to reach out to me on X/Twitter</p><p><a href="https://twitter.com/ABausG">https://twitter.com/ABausG</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=810c5121422f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Getting Material Base Components into Widgetbook]]></title>
            <link>https://medium.com/widgetbook/getting-material-base-components-into-widgetbook-c0e1f874b14b?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/c0e1f874b14b</guid>
            <category><![CDATA[widgetbook]]></category>
            <category><![CDATA[storybook]]></category>
            <category><![CDATA[material-design]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Wed, 22 Nov 2023 09:09:01 GMT</pubDate>
            <atom:updated>2024-01-15T10:06:32.326Z</atom:updated>
            <content:encoded><![CDATA[<h3>Displaying Material Base Components in Widgetbook</h3><p>Styling Material Widgets can be tricky. Using the widgetbook_base_components mason brick you can get an overview inside Widgetbook how they will look, feel and behave</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OvsdFzV_vK1Mzglv2YjvGA.png" /><figcaption>Buttons from widgetbook_base_components in the Widgetbook Groceries Example</figcaption></figure><h3>Struggling with styling ThemeData</h3><p>Personally I am a big fan of using the default Material Widgets in Flutter Apps as they are easy to use, offer great support for Accessibility and relying on theme is great if you are onboarding new team members as they can just use the default blocks without having to make sure they use the correct widget.</p><p>However one big caveat with this can be what <a href="https://twitter.com/RydMike">Mike Rydstrom</a> put into great words during his workshop at <a href="https://www.flutterfriends.dev/">Flutter &amp; Friends</a> (Material for the Workshop can be found <a href="https://twitter.com/messages/87799983-568573618">here</a>)</p><blockquote>Flutter Material theming is fun and easy — but tedious</blockquote><p>This really resonated with me as that basically reflects my experience with it. Adjusting ThemeData to match a given Design or just to your preference can indeed be tricky to get all the nuances right. However things like Mike’s <a href="https://rydmike.com/flexcolorscheme/themesplayground-v5/#/">Theme Playground</a> and <a href="https://pub.dev/packages/flex_color_scheme">flex_color_scheme</a> can really help you nail your ThemeData. When you have your Theme just right and you are able to just use Material Widgets without having to do custom styling when using them it feels great to use, is quick and ensures visual consistency between your uses of these Widgets.</p><h3>The idea</h3><p>widgetbook_base_components is a <a href="https://github.com/felangel/mason">mason</a> brick that generates UseCases for your Widgetbook to show you how these widgets would appear and behave in your App.</p><p>In the first version the following components are supported: Buttons, TextStyles, Controls, Card, AlertDialog, Indicators, Chips, TextFields, ListTile, BottomNavigationBar</p><p>You might notice that these UseCases in Widgetbook have a lot more info than you are normally used to. This is because the idea behind widgetbook_base_components is to get a quick overview of how certain Material Widget Groups like buttons/switches/etc behave if used in an app only relying on the style set by the encapsulting ThemeData.</p><p>For some examples you can also check out a Widgetbook with these components here:</p><p><a href="https://abausg.github.io/bricks/widgetbook_base_components/web/">https://abausg.github.io/bricks/widgetbook_base_components/web/</a></p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2FtXL4FHPSnVJ0A%2Ftwitter%2Fiframe&amp;display_name=Giphy&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExY2licXNrNnkyeHdoOTk3ejRnYm1zdTR6bDBtbGViZTFiY2FsZHFrMyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FtXL4FHPSnVJ0A%2Fgiphy.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2FtXL4FHPSnVJ0A%2Fgiphy.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="287" frameborder="0" scrolling="no"><a href="https://medium.com/media/3c475c31dfeea3976e1892183f83ed59/href">https://medium.com/media/3c475c31dfeea3976e1892183f83ed59/href</a></iframe><h3>Using the brick</h3><blockquote>Note: I am assuming you already have setup Widgetbook in your project. If not check out the official documentation <a href="https://docs.widgetbook.io/">https://docs.widgetbook.io/</a></blockquote><ol><li>Follow the installation steps for <a href="https://github.com/felangel/mason">mason</a> if you haven’t by running <br>dart pub global activate mason_cli<br>mason init</li><li>Add the widgetbook_base_components brick using <br>mason add widgetbook_base_components</li><li>&quot;Make&quot; the brick using mason make widgetbook_base_components</li><li>Run dart run build_runner build (ignore this if you are using watch )</li><li>Check your Widgetbook for the new Base Components 🎉</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zNw7mieMAMIoiefMypnnmQ.png" /><figcaption>Chips created by widgetbook_base_components on <a href="https://abausg.github.io/bricks/widgetbook_base_components/web/">https://abausg.github.io/bricks/widgetbook_base_components/web/</a></figcaption></figure><h3>Next Steps</h3><p>I think seeing the different Variants side by side offers a great way of grasping how your App might look for the provided ThemeData</p><p>I am well aware that this is only a small subset of Material Widgets that are supported but I think it is a great start. If you are missing a specific Widget feel free to <a href="https://github.com/ABausG/bricks">open a PR</a>.</p><p>For thoughts or any more questions reach out to me on <a href="https://twitter.com/abausg">X/Twitter</a></p><h3>Thanks</h3><p>Many thanks to <a href="https://twitter.com/roaakdm">Roaa</a>, <a href="https://twitter.com/LucasJosefiak">Lucas</a> and <a href="https://twitter.com/t_lavrk">Tim</a> from Widgetbook for the Feedback on the Brick.</p><p>Also big thanks to Mike! His work for everything Material is phenomenal and his <a href="https://rydmike.com/flexcolorscheme/themesplayground-v5/#/">Theme Playground</a> was a huge inspiration for this. You should follow him on X/Twitter to always get the latest news about Material and Flutter!</p><p><a href="https://twitter.com/RydMike">JavaScript is not available.</a></p><p>Code for the Brick:</p><p><a href="https://github.com/ABausG/bricks/tree/main/bricks/widgetbook_base_components">https://github.com/ABausG/bricks/tree/main/bricks/widgetbook_base_components</a></p><p>Example of the Widgetbook:</p><p><a href="https://abausg.github.io/bricks/widgetbook_base_components/web/">https://abausg.github.io/bricks/widgetbook_base_components/web/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c0e1f874b14b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/widgetbook/getting-material-base-components-into-widgetbook-c0e1f874b14b">Getting Material Base Components into Widgetbook</a> was originally published in <a href="https://medium.com/widgetbook">Widgetbook</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[iOS Lockscreen Widgets with Flutter and home_widget]]></title>
            <link>https://medium.com/@ABausG/ios-lockscreen-widgets-with-flutter-and-home-widget-0dfecc18cfa0?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/0dfecc18cfa0</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[widget]]></category>
            <category><![CDATA[lock-screen]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Sat, 14 Oct 2023 12:12:06 GMT</pubDate>
            <atom:updated>2023-10-14T12:12:06.358Z</atom:updated>
            <content:encoded><![CDATA[<p>Recently I started learning Italian with Duolingo and noticed that the iOS App has a Lockscreen Widget with their mascot bird Duo and the current streak.</p><p>This sparked the idea of checking if it is possible to use my package <a href="https://pub.dev/packages/home_widget">home_widget</a> to create a Lockscreen widget with <a href="https://docs.flutter.dev/dash">the best bird mascot there is</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*B5dO8kycH5QL68n9fg8O5g.jpeg" /></figure><p>Turns out it is super easy!</p><blockquote>Note the following steps build upon the Counter App/Widget created in <a href="https://medium.com/@ABausG/interactive-homescreen-widgets-with-flutter-using-home-widget-83cb0706a417">this Article for interactive widgets</a></blockquote><h3>Creating the Widget Image</h3><p>To create the Widget I am using the renderFlutterWidget method of home_widget to save a Flutter Widget as an image to use in the native widget.</p><p>For this I used one of the Dash illustrations by <a href="https://twitter.com/AlexBradt">Alex Bradt</a> created for the <a href="https://f3.events/">F3 event</a> and added the current counter value on top (with a bit of eyeballed transformation).</p><pre>Stack(<br>  children: [<br>    Positioned.fill(<br>      child: Padding(<br>        padding: const EdgeInsets.all(30.0),<br>        child: Image.asset(&#39;assets/dash_sign_small.png&#39;),<br>      ),<br>    ),<br>    Transform.rotate(<br>      angle: 5 * pi / 180,<br>      child: Transform.translate(<br>        offset: const Offset(58, 29),<br>        child: SizedBox(<br>          height: 100,<br>          width: 134,<br>          child: Padding(<br>            padding: const EdgeInsets.all(16.0),<br>            child: FittedBox(<br>              fit: BoxFit.contain,<br>              child: Center(<br>                child: Text(<br>                  count.toString(),<br>                  style: const TextStyle(<br>                    fontWeight: FontWeight.bold,<br>                    fontSize: 70,<br>                    color: Colors.white,<br>                  ),<br>                  textAlign: TextAlign.center,<br>                ),<br>              ),<br>            ),<br>          ),<br>        ),<br>      ),<br>    ),<br>  ],<br>)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*JLSmYkM62-KUI7laNMh3cg.png" /></figure><p>When updating the value I save a rendering of that Widget using renderFlutterWidget</p><pre>await HomeWidget.renderFlutterWidget(<br>  DashWithSign(count: value ?? 0),<br>  key: &#39;dash_counter&#39;,<br>  logicalSize: const Size(100, 100),<br>);</pre><p>Overall I think especially for smaller Widgets like this the renderFlutterWidget method is ideal for Flutter developers as I would assume most of us are way more efficient to write intricate widgets like this in Flutter than building things like this in SwiftUI.</p><p>When using this make sure to keep the following points in mind:</p><ol><li>You should check how your Widget looks after iOS applies the tinting on the Lockscreen as it will adjust it to a uniform color that might also be influenced by the wallpaper of the user.</li><li>Remember that for the some configurations iOS will clip the widget to a circle so make sure that you add adequate padding to your widget</li></ol><h3>The native part</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2F3ov9jEOp8zeaprTK3S%2Ftwitter%2Fiframe&amp;display_name=Giphy&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2F3ov9jEOp8zeaprTK3S%2Fgiphy.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2F3ov9jEOp8zeaprTK3S%2Fgiphy.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="214" frameborder="0" scrolling="no"><a href="https://medium.com/media/d635399de18f9d35e6dcde8649d122d8/href">https://medium.com/media/d635399de18f9d35e6dcde8649d122d8/href</a></iframe><p>Writing native iOS code can be scary but if you managed to get through the example for the counter app you should nail this! Especially as the Widget itself is super easy when using a rendered Flutter Widget</p><h4>Add the supported family</h4><p>In iOS Lockscreen Widgets are called AccessoryWidgets and can be added as a supportedFamily to your WidgetConfiguration</p><pre>var body: some WidgetConfiguration {<br>  ...<br>  .description(&quot;Count the Number Up&quot;)<br>  .supportedFamilies([<br>      .systemSmall,<br>      .systemMedium,<br>      .accessoryCircular<br>  ])</pre><p>The important family here is .acessoryCircular Here you can also define which sizes you want to support in general. For a list of all possible families check <a href="https://developer.apple.com/design/human-interface-guidelines/widgets#Interface-design">this page by Apple</a>.</p><h4>Adjust the Layout for the Widget</h4><p>When building the Widget we need to detect which widgetFamily we are building and in that case return our pre rendered Image.</p><pre>struct CounterWidgetEntryView: View {<br>  var entry: Provider.Entry<br><br>  // Detect the current Family<br>  @Environment(\.widgetFamily) var family<br>    <br>  var body: some View {<br>    if family == .accessoryCircular {<br>      Image(<br>        uiImage: UIImage(<br>          contentsOfFile: UserDefaults(suiteName: &quot;YOUR_APP_GROUP&quot;)?.string(<br>            forKey: &quot;dash_counter&quot;) ?? &quot;&quot;)!<br>      ).resizable()<br>        .frame(width: 76, height: 76)<br>        .scaledToFill()<br>    } else {<br>     // Build Widget for other families<br>    }<br>  }<br>}</pre><p>Thats already it. Now if you run the App you should be able to add the widgets to your HomeScreen.</p><p>You could of course also create a new Widget Target for the widgets if you want to add more</p><h3>Conclusion</h3><p>All in all extending your App to the iOS lockscreen is super quick (especially if you are already using home_widget) and allows you to give users access to your Apps even more.</p><h4>Links</h4><p>The full repository of this example is here:</p><p><a href="https://github.com/ABausG/home_widget_counter">GitHub - ABausG/home_widget_counter: An example App for home_widget rebuilding the Flutter Counter App as a HomeScreen Widget</a></p><p><a href="https://github.com/ABausG/home_widget_counter/commit/dbbe723f3d2420a92d91e49eaf7c7b7c3e903418">With the changes made for this article in this commit.</a></p><p>If you have any questions regarding this feel free to reach out to me on Twitter/X</p><p><a href="https://twitter.com/abausg"></a></p><p>Check out/Like home_widget on pub.dev</p><p><a href="https://pub.dev/packages/home_widget">home_widget | Flutter package</a></p><p>Check out/Star Open PRs/Issues for home_widget on github</p><p><a href="https://github.com/abausg/home_widget">GitHub - ABausG/home_widget: Flutter Package for Easier Creation of Home Screen Widgets</a></p><p>Check out Alex Bradt who did the amazing Dash Illustration</p><p><a href="https://twitter.com/AlexBradt">JavaScript is not available.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0dfecc18cfa0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Flutter Dev Tools with ApptiveGrid — Error Reporting]]></title>
            <link>https://medium.com/apptivegrid/building-flutter-dev-tools-with-apptivegrid-error-reporting-9ce1da89bec4?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/9ce1da89bec4</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[app-development]]></category>
            <category><![CDATA[no-code]]></category>
            <category><![CDATA[error-reporting]]></category>
            <category><![CDATA[apptivegrid]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Wed, 11 Oct 2023 08:28:23 GMT</pubDate>
            <atom:updated>2023-10-11T08:28:23.362Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building Flutter Dev Tools with ApptiveGrid — Error Reporting</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2oOU8gaY1543bcVkAgUxhQ.png" /></figure><p>While building several Flutter Apps in the past that use ApptiveGrid as a Data Source we realized that we could use the power of ApptiveGrid and our suite of ApptiveGrid Flutter Packages to build tools for development and quality insurance of Flutter Apps.</p><p>We are already using several of these tools in the apps we are building and we are excited to announce that we are open sourcing the first package now. <a href="https://pub.dev/packages/apptive_grid_error_reporting">apptive_grid_error_reporting</a> is a little tool to easily report errors to an ApptiveGrid Space</p><h3>Why another error reporting tool?</h3><p>Of course there are lots of options in the space of error reporting that all have their place. However we noticed that for our apps — often time for small customers — these solutions and associated costs are not always a great fit.</p><p>Another point is obviously that we want to use ApptiveGrid more ourselves. So while exploring options on how we can incorporate ApptiveGrid in more of our day to day work we quickly realized that we can build a lightweight and customizable solution for basic error reporting that is also GDPR compliant.</p><h3>Setting it up</h3><ol><li>Create a new Space to your ApptiveGrid Account using <a href="https://app.apptivegrid.de/#/template/65005ff33e85a232b5c17517">this template</a>. (If you don’t have an ApptiveGrid Account yet you can create one in the process. It’s free)</li><li>Go to the reporting form and copy the link from the share men</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*Mf-WsSIBJ-TUfcY6" /></figure><p>3. Add apptive_grid_error_reporting to you pubspec dependencies and create a reporting Instance using the form link</p><pre>final reporting = ApptiveGridErrorReporting(<br>  reportingForm: Uri.parse(&#39;FORM_LINK&#39;),<br>  project: &#39;myProject&#39;,<br>);</pre><p>That’s already it all in terms of setting everything up.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TxzxhCp4L4Z2guh3tHmi6Q.png" /></figure><h3>Reporting Errors</h3><p>To report errors simply call</p><pre>reporting.report(error);</pre><p>Optionally you can provide more Information like a StackTrace or a custom message to better figure out where the error occurred.</p><pre>reporting.report(<br>  error,<br>  stackTrace: StackTrace.current,<br>  message: &#39;Error when doing a demo&#39;,<br>);</pre><p>If you want to track down what a user might have done leading up to the error you can simply add some log entries.</p><pre>reporting.log(‘Something was clicked’);</pre><p>These logs will be added to the reports inside ApptiveGrid as a .txt file for your error analysis</p><h3>Creating custom Views</h3><p>In ApptiveGrid you can view your data not only as a Grid but also create custom views based on your needs. In this next example I can show you how quick it is to create a simple Kanban view for all the issues.</p><ol><li>Create a Kanban View</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MHdGjfx51brtl53V" /></figure><p>2. Select the “Status” Field as the default field</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CZfay-rG43c678Me" /></figure><p>3. Enable the column for ungrouped entries</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/642/0*uxh0BShHuGEIAxFo" /></figure><p>You now have a Kanban Board right inside of ApptiveGrid for all reported errors where you can move cards just like with any other view. You can customize these views by playing around with custom filters or sorting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SeHzHmn8Asad9f-W" /></figure><h3>Using ApptiveGrid Workflows</h3><p>ApptiveGrid offers custom workflows that can be used to get more out of your error reports. In this next example I’ll show you how quickly you can set up custom Slack Notifications whenever there is a new error reported</p><ol><li>Go to the Workflows Tab inside your ApptiveGrid and create a new flow</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*IVGmWyuhporwKJcQ" /></figure><p>2. Select “New Entry” as your trigger. Configure to use the Reporting Grid as the source</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*llrpWSZX0wwDM1Hj" /></figure><p>3. Add a “Slack” action</p><ul><li>If you “test the action” the fields panel will be populated with placeholder tokens that you can then use to create custom slack messages that report new errors</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*spfx3N4BJoVR1gu0" /></figure><ul><li>Add your <a href="https://api.slack.com/messaging/webhooks">Slack Webhook Url</a></li></ul><h3>Further customisation</h3><p>In this article I have only shown how to add custom views and use a simple workflow to report errors on Slack. However you can customize the whole reporting even further. For example you could add an “Assigned” Field to your grid and automatically send an email to the assigned developer using a workflow when you change the value or anything in the field by playing around with the trigger conditions inside the flow.</p><p>Also using the Http Call Node you can integrate with a lot of other services/products that you are already using for example to create tickets inside Jira or Github. If you have a need for a more specialized Workflow Node</p><h3>Closing</h3><p>If you want a simple and lightweight error reporting tool we encourage you to try out apptive_grid_error_reporting. Also if you want to use ApptiveGrid for the backend of your Apps check out our <a href="https://github.com/ApptiveGrid/ApptiveGrid-flutter/">other ApptiveGrid Flutter Packages</a>.</p><h3>Resources</h3><p>Package on pub.dev:</p><p><a href="https://pub.dev/packages/apptive_grid_error_reporting">apptive_grid_error_reporting | Flutter Package</a></p><p>ApptiveGrid on LinkedIn:</p><p><a href="https://www.linkedin.com/company/apptivegrid/">ApptiveGrid | LinkedIn</a></p><p>ApptiveGrid on X/Twitter:</p><p><a href="https://twitter.com/apptivegrid">JavaScript is not available.</a></p><p>Anton on X/Twitter:</p><p><a href="https://twitter.com/abausg"></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9ce1da89bec4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/apptivegrid/building-flutter-dev-tools-with-apptivegrid-error-reporting-9ce1da89bec4">Building Flutter Dev Tools with ApptiveGrid — Error Reporting</a> was originally published in <a href="https://medium.com/apptivegrid">apptivegrid</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Interactive HomeScreen Widgets with Flutter using home_widget]]></title>
            <link>https://medium.com/@ABausG/interactive-homescreen-widgets-with-flutter-using-home-widget-83cb0706a417?source=rss-3158ce720fe3------2</link>
            <guid isPermaLink="false">https://medium.com/p/83cb0706a417</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[homescreen]]></category>
            <category><![CDATA[interactive]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Anton Borries]]></dc:creator>
            <pubDate>Tue, 12 Sep 2023 16:15:55 GMT</pubDate>
            <atom:updated>2023-09-12T16:15:55.490Z</atom:updated>
            <content:encoded><![CDATA[<p>With the introduction of iOS 17 Apple now also supports adding interactivity to HomeScreen Widgets meaning we can build interactive HomeScreen Widgets for both iOS and Android that call Dart code. In this Article we will recreate the classic Flutter Counter App as HomeScreen Widgets for both platforms</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*05rXlxeR_hIBScsvMEe-ZA.gif" /><figcaption>HomeScreen Widget Counter Apps on iOS and Android</figcaption></figure><h3>Prerequisites</h3><p>This Article assumes that you know the basics of adding HomeScreen Widgets with home_widget. If you want more details on how to set things up initially please check out the <a href="https://github.com/ABausG/home_widget/blob/dev/README.md">README</a> as well as have a look at the <a href="https://codelabs.developers.google.com/flutter-home-screen-widgets#0">Codelab</a></p><h3>Dart Setup</h3><blockquote>Please note that as of writing this article iOS Interactivity is only available as a pre-release version of home_widget <em>0.4.0-alpha.0</em> This will change once Apple officially releases iOS 17 and XCode 15. Once that is fully released I will update this Article accordingly</blockquote><blockquote>While this is in pre-release add this to your pubspec.yaml file</blockquote><blockquote>home_widget: 0.4.0-alpha.0</blockquote><h4>Building the Counter App</h4><p>We only are doing slight modifications to the generated counter app. Mainly we will use static functions to save/update/restore the current count, as this will make our work to use in the interactive callback a lot easier. You should obviously not move all of your business logic into static functions in your real apps but for this example it will be fine.</p><p>We will use HomeWidget.getWidgetData for getting the current value. HomeWidget.saveData to save the new value and HomeWidget.updateWidget</p><p>In the context it looks like this</p><pre>const _countKey = &#39;counter&#39;;<br><br>/// Gets the currently stored Value<br>Future&lt;int&gt; get _value async {<br>  final value = await HomeWidget.getWidgetData&lt;int&gt;(_countKey, defaultValue: 0);<br>  return value!;<br>}<br><br>/// Retrieves the current stored value<br>/// Increments it by one<br>/// Saves that new value<br>/// @returns the new saved value<br>Future&lt;int&gt; _increment() async {<br>  final oldValue = await _value;<br>  final newValue = oldValue + 1;<br>  await _sendAndUpdate(newValue);<br>  return newValue;<br>}<br><br>/// Clears the saved Counter Value<br>Future&lt;void&gt; _clear() async {<br>  await _sendAndUpdate(null);<br>}<br><br>/// Stores [value] in the Widget Configuration<br>Future&lt;void&gt; _sendAndUpdate([int? value]) async {<br>  await HomeWidget.saveWidgetData(_countKey, value);<br>  await HomeWidget.updateWidget(<br>    iOSName: &#39;CounterWidget&#39;,<br>    androidName: &#39;CounterWidgetProvider&#39;,<br>  );<br>}</pre><h4>Adding an Interactivity callback</h4><p>To get interactivity working in the dart part we need to create and register an interactivity callback. This callback has 2 requirements. It needs to be static as well as public and needs to be annotated with @pragma(‘vm:entry-point’) . It will be called with an Uri to detect what element triggered the callback. In our example the callback looks like this</p><pre>@pragma(&#39;vm:entry-point&#39;)<br>Future&lt;void&gt; interactiveCallback(Uri? uri) async {<br>  // We check the host of the uri to determine which action should be triggered.<br>  if (uri?.host == &#39;increment&#39;) {<br>    await _increment();<br>  } else if (uri?.host == &#39;clear&#39;) {<br>    await _clear();<br>  }<br>}</pre><p>Lastly we need to register that callback. Please note that you should call this after you have setup the AppGroupId for iOS. In our case we just do it in the main method.</p><pre>await HomeWidget.registerInteractivityCallback(interactiveCallback);</pre><p>With this our main File should look something like this:</p><pre>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:home_widget/home_widget.dart&#39;;<br><br>Future&lt;void&gt; main() async {<br>  WidgetsFlutterBinding.ensureInitialized();<br>  // Set AppGroup Id. This is needed for iOS Apps to talk to their WidgetExtensions<br>  await HomeWidget.setAppGroupId(&#39;YOUR_GROUP_ID&#39;);<br>  // Register an Interactivity Callback. It is necessary that this method is static and public<br>  await HomeWidget.registerInteractivityCallback(interactiveCallback);<br>  runApp(const MyApp());<br>}<br><br>/// Callback invoked by HomeWidget Plugin when performing interactive actions<br>/// The @pragma(&#39;vm:entry-point&#39;) Notification is required so that the Plugin can find it<br>@pragma(&#39;vm:entry-point&#39;)<br>Future&lt;void&gt; interactiveCallback(Uri? uri) async {<br>  // We check the host of the uri to determine which action should be triggered.<br>  if (uri?.host == &#39;increment&#39;) {<br>    await _increment();<br>  } else if (uri?.host == &#39;clear&#39;) {<br>    await _clear();<br>  }<br>}<br><br>const _countKey = &#39;counter&#39;;<br><br>/// Gets the currently stored Value<br>Future&lt;int&gt; get _value async {<br>  final value = await HomeWidget.getWidgetData&lt;int&gt;(_countKey, defaultValue: 0);<br>  return value!;<br>}<br><br>/// Retrieves the current stored value<br>/// Increments it by one<br>/// Saves that new value<br>/// @returns the new saved value<br>Future&lt;int&gt; _increment() async {<br>  final oldValue = await _value;<br>  final newValue = oldValue + 1;<br>  await _sendAndUpdate(newValue);<br>  return newValue;<br>}<br><br>/// Clears the saved Counter Value<br>Future&lt;void&gt; _clear() async {<br>  await _sendAndUpdate(null);<br>}<br><br>/// Stores [value] in the Widget Configuration<br>Future&lt;void&gt; _sendAndUpdate([int? value]) async {<br>  await HomeWidget.saveWidgetData(_countKey, value);<br>  await HomeWidget.updateWidget(<br>    iOSName: &#39;CounterWidget&#39;,<br>    androidName: &#39;CounterWidgetProvider&#39;,<br>  );<br>}<br><br>class MyApp extends StatelessWidget {<br>  const MyApp({super.key});<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Flutter Demo&#39;,<br>      theme: ThemeData.light(<br>        useMaterial3: false,<br>      ),<br>      home: const MyHomePage(title: &#39;Flutter Demo Home Page&#39;),<br>    );<br>  }<br>}<br><br>class MyHomePage extends StatefulWidget {<br>  const MyHomePage({super.key, required this.title});<br><br>  final String title;<br><br>  @override<br>  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();<br>}<br><br>class _MyHomePageState extends State&lt;MyHomePage&gt; with WidgetsBindingObserver {<br>  @override<br>  void initState() {<br>    super.initState();<br>    WidgetsBinding.instance.addObserver(this);<br>  }<br><br>  Future&lt;void&gt; _incrementCounter() async {<br>    await _increment();<br>    setState(() {});<br>  }<br><br>  @override<br>  void didChangeAppLifecycleState(AppLifecycleState state) {<br>    super.didChangeAppLifecycleState(state);<br>    if (state == AppLifecycleState.resumed) {<br>      setState(() {});<br>    }<br>  }<br><br>  @override<br>  void dispose() {<br>    WidgetsBinding.instance.removeObserver(this);<br>    super.dispose();<br>  }<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(title: Text(widget.title)),<br>      body: Center(<br>        child: Column(<br>          mainAxisAlignment: MainAxisAlignment.center,<br>          children: &lt;Widget&gt;[<br>            const Text(<br>              &#39;You have pushed the button this many times:&#39;,<br>            ),<br>            FutureBuilder&lt;int&gt;(<br>              future: _value,<br>              builder: (_, snapshot) =&gt; Text(<br>                (snapshot.data ?? 0).toString(),<br>                style: Theme.of(context).textTheme.headlineMedium,<br>              ),<br>            ),<br>            TextButton(<br>              onPressed: () async {<br>                await _clear();<br>                setState(() {});<br>              },<br>              child: const Text(&#39;Clear&#39;),<br>            ),<br>          ],<br>        ),<br>      ),<br>      floatingActionButton: FloatingActionButton(<br>        onPressed: _incrementCounter,<br>        tooltip: &#39;Increment&#39;,<br>        child: const Icon(Icons.add),<br>      ),<br>    );<br>  }<br>}</pre><h3>iOS Widget</h3><blockquote>Note: As of writing this you will required XCode 15 for this</blockquote><p>Like mentioned above I assume that you already know how to build HomeWidgets. If not please check out the <a href="https://github.com/ABausG/home_widget/blob/dev/README.md">README</a> as well as have a look at the <a href="https://codelabs.developers.google.com/flutter-home-screen-widgets#0">Codelab</a>. Before adding interactivity you should have performed at least these steps:</p><ol><li>Add an AppIdentifier and GroupIdentifier on the AppleDeveloper Console</li><li>Create a WidgetExtension in Xcode. File &gt; New &gt; Target &gt; Widget Extension In this example I’ve named the extension CounterWidget</li><li>Add your GroupId to <strong>both</strong> your App Target and Widget Extension Target</li></ol><h4>Adjust your podfile</h4><p>We need to adjust the Podfile for your iOS app to make sure your WidgetExtension has access to the home_widget plugin. For that we’ll add the following code snippet</p><pre>target &#39;CounterWidgetExtension&#39; do<br>  inherit! :search_paths<br>end</pre><p>To your Runner’s target section in the Podfile so the whole section will look like this:</p><pre>target &#39;Runner&#39; do<br>  use_frameworks!<br>  use_modular_headers!<br><br>  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))<br>  target &#39;RunnerTests&#39; do<br>    inherit! :search_paths<br>  end<br>  target &#39;CounterWidgetExtension&#39; do<br>    inherit! :search_paths<br>  end<br>end</pre><h4>Creating an AppIntent</h4><p>In your Runners target you want to create a new swift file where we will define an AppIntent which we will use to trigger Actions from the Widget. When creating the AppIntent it is important that you select both your Runner and your WidgetExtension as targets (you can also set this in the inspector later)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/522/1*8Yx1SVKrYTHy9lrf4RWFZQ.png" /><figcaption>Make sure the Membership is set for the Runner and the Extension</figcaption></figure><p>In the AppIntent’s perform function call HomeWidgetBackgroundWorker.run this needs to be called with your AppGroupId and an Uri which will be passed into the interactivity callback in dart</p><pre>import AppIntents<br>import Foundation<br>import home_widget<br><br>@available(iOS 17, *)<br>public struct BackgroundIntent: AppIntent {<br>  static public var title: LocalizedStringResource = &quot;Increment Counter&quot;<br><br>  @Parameter(title: &quot;Method&quot;)<br>  var method: String<br><br>  public init() {<br>    method = &quot;increment&quot;<br>  }<br><br>  public init(method: String) {<br>    self.method = method<br>  }<br><br>  public func perform() async throws -&gt; some IntentResult {<br>    await HomeWidgetBackgroundWorker.run(<br>      url: URL(string: &quot;homeWidgetCounter://\(method)&quot;),<br>      appGroup: &quot;YOUR_GROUP_ID&quot;)<br><br>    return .result()<br>  }<br>}</pre><h4>Adding the Button to the Widget</h4><p>As a last step we will need to add a Button to our Widget that will use the BackgroundIntent we just created. So we are now back in our CounterWidget.swift and can simply add interactive buttons to the Widget like this</p><pre>Button(intent: BackgroundIntent(method: &quot;increment&quot;)) {<br>  Image(systemName: &quot;plus&quot;).font(.system(size: 16))<br>}</pre><p>The full CounterWidget.swift should look something like this. There you can also see how to load the current count into a TimelineEntry</p><pre>import SwiftUI<br>import WidgetKit<br><br>struct Provider: TimelineProvider {<br>  func placeholder(in context: Context) -&gt; CounterEntry {<br>    CounterEntry(date: Date(), count: 0)<br>  }<br><br>  func getSnapshot(in context: Context, completion: @escaping (CounterEntry) -&gt; Void) {<br>    // Get the UserDefaults for the AppGroup<br>    let prefs = UserDefaults(suiteName: &quot;YOUR_GROUP_ID&quot;)<br>    // Load the current Count<br>    let entry = CounterEntry(date: Date(), count: prefs?.integer(forKey: &quot;counter&quot;) ?? 0)<br>    completion(entry)<br>  }<br><br>  func getTimeline(in context: Context, completion: @escaping (Timeline&lt;Entry&gt;) -&gt; Void) {<br>    getSnapshot(in: context) { (entry) in<br>      let timeline = Timeline(entries: [entry], policy: .atEnd)<br>      completion(timeline)<br>    }<br>  }<br>}<br><br>struct CounterEntry: TimelineEntry {<br>  let date: Date<br>  let count: Int<br>}<br><br>struct CounterWidgetEntryView: View {<br>  var entry: Provider.Entry<br><br>  var body: some View {<br>    VStack {<br>      Text(&quot;You have pushed the button this many times:&quot;).font(.caption2).frame(<br>        maxWidth: .infinity, alignment: .center)<br>      Spacer()<br>      Text(entry.count.description).font(.title).frame(maxWidth: .infinity, alignment: .center)<br>      Spacer()<br>      HStack {<br>        // This button is for clearing<br>        Button(intent: BackgroundIntent(method: &quot;clear&quot;)) {<br>          Image(systemName: &quot;xmark&quot;).font(.system(size: 16)).foregroundColor(.red).frame(<br>            width: 24, height: 24)<br>        }.buttonStyle(.plain).frame(alignment: .leading)<br>        Spacer()<br>        // This button is for incrementing<br>        Button(intent: BackgroundIntent(method: &quot;increment&quot;)) {<br>          Image(systemName: &quot;plus&quot;).font(.system(size: 16)).foregroundColor(.white)<br><br>        }.frame(width: 24, height: 24)<br>          .background(.blue)<br>          .cornerRadius(12).frame(alignment: .trailing)<br>      }<br>    }<br>  }<br>}<br><br>struct CounterWidget: Widget {<br>  let kind: String = &quot;CounterWidget&quot;<br><br>  var body: some WidgetConfiguration {<br>    StaticConfiguration(kind: kind, provider: Provider()) { entry in<br>      if #available(iOS 17.0, *) {<br>        CounterWidgetEntryView(entry: entry)<br>          .containerBackground(.fill.tertiary, for: .widget)<br>      } else {<br>        CounterWidgetEntryView(entry: entry)<br>          .padding()<br>          .background()<br>      }<br>    }<br>    .configurationDisplayName(&quot;Counter Widget&quot;)<br>    .description(&quot;Count the Number Up&quot;)<br>  }<br>}<br><br>#Preview(as: .systemSmall){<br>  CounterWidget()<br>} timeline: {<br>  CounterEntry(date: .now, count: 0)<br>}</pre><p>Now run the App and start counting right from the HomeScreen</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*3vr6UbTfBMXiYgQZugLH1g.gif" /></figure><h3>Android Widget</h3><p>Like mentioned above I assume that you already know how to build HomeWidgets. If not please check out the <a href="https://github.com/ABausG/home_widget/blob/dev/README.md">README</a> as well as have a look at the <a href="https://codelabs.developers.google.com/flutter-home-screen-widgets#0">Codelab</a>. Before adding interactivity you should have performed at least these steps:</p><ol><li>Create a WidgetProvider (In our case CounterWidgetProvider)</li><li>Create your <a href="https://github.com/ABausG/home_widget_counter/blob/main/android/app/src/main/res/layout/counter_widget.xml">WidgetLayout</a> and xml <a href="https://github.com/ABausG/home_widget_counter/blob/main/android/app/src/main/res/xml/counter_widget.xml">appwidget-provider</a></li><li>Add your Widget to the AndroidManifest</li></ol><h4>Adding Interactivity</h4><p>To add interactivity simply assign a `HomeWidgetBackgroundIntent` to your Remote View.</p><pre>val incrementIntent = HomeWidgetBackgroundIntent.getBroadcast(<br>  context,<br>  Uri.parse(&quot;homeWidgetCounter://increment&quot;)<br>)<br>setOnClickPendingIntent(R.id.button_increment, incrementIntent)</pre><p>The full CounterWidgetProvider.kt should look something like this</p><pre>package es.antonborri.home_widget_counter<br><br>import android.appwidget.AppWidgetManager<br>import android.content.Context<br>import android.content.SharedPreferences<br>import android.net.Uri<br>import android.widget.RemoteViews<br>import es.antonborri.home_widget.HomeWidgetBackgroundIntent<br>import es.antonborri.home_widget.HomeWidgetProvider<br><br>class CounterWidgetProvider : HomeWidgetProvider() {<br>    override fun onUpdate(<br>            context: Context,<br>            appWidgetManager: AppWidgetManager,<br>            appWidgetIds: IntArray,<br>            widgetData: SharedPreferences) {<br>        appWidgetIds.forEach { widgetId -&gt;<br>            val views = RemoteViews(context.packageName, R.layout.counter_widget).apply {<br>                val count = widgetData.getInt(&quot;counter&quot;, 0)<br>                setTextViewText(R.id.text_counter, count.toString())<br><br>                val incrementIntent = HomeWidgetBackgroundIntent.getBroadcast(<br>                        context,<br>                        Uri.parse(&quot;homeWidgetCounter://increment&quot;)<br>                )<br>                val clearIntent = HomeWidgetBackgroundIntent.getBroadcast(<br>                        context,<br>                        Uri.parse(&quot;homeWidgetCounter://clear&quot;)<br>                )<br><br>                setOnClickPendingIntent(R.id.button_increment, incrementIntent)<br>                setOnClickPendingIntent(R.id.button_clear, clearIntent)<br>            }<br>            appWidgetManager.updateAppWidget(widgetId, views)<br>        }<br>    }<br>}</pre><p>And with that your interactive HomeScreenWidget is done. and should look like this.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*lUwzVcvcmwmD9tUWHvxYdw.gif" /></figure><h3>Wrapping it up</h3><p>Adding interactivity is not hard and can make your HomeScreenWidgets much more useful for your users.</p><p>The full code for the example app can be found here</p><p><a href="https://github.com/ABausG/home_widget_counter">GitHub - ABausG/home_widget_counter: An example App for home_widget rebuilding the Flutter Counter App as a HomeScreen Widget</a></p><p>Please check out and like home_widget on pub.dev</p><p><a href="https://pub.dev/packages/home_widget">home_widget | Flutter package</a></p><p>Do you have an issue with home_widget please create an issue on github</p><p><a href="https://github.com/ABausG/home_widget/tree/main">GitHub - ABausG/home_widget: Flutter Package for Easier Creation of Home Screen Widgets</a></p><p>Building something cool with home_widget ? Let me know over on Twitter/X</p><p><a href="https://twitter.com/abausg"></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=83cb0706a417" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>