Adding a Home Screen widget to your Flutter App

1. Introduction

What are Widgets?

For Flutter developers, the common definition of widget refers to UI components created using the Flutter framework. In the context of this codelab, a widget refers to a mini version of an app that provides a view into the information of the app without opening the app. On Android, widgets live on the home screen. On iOS, they can be added to the home screen, lock screen, or the today view.

f0027e8a7d0237e0.png b991e79ea72c8b65.png

How complex can a Widget be?

Most Home Screen widgets are simple. They might contain basic text, simple graphics or, on Android, basic controls. Both Android and iOS limit what UI components and features you can use.

819b9fffd700e571.png 92d62ccfd17d770d.png

Create the UI for Widgets

Because of these UI limitations, you can't directly draw the UI of a Home Screen widget using the Flutter framework. Instead, you can add widgets created with platform frameworks like Jetpack Compose or SwiftUI to your Flutter app. This codelab discusses examples for sharing resources between your app and the widgets to avoid rewriting complex UI.

What you'll build

In this codelab, you'll build Home Screen widgets on both Android and iOS for a simple Flutter app, using the home_widget package, that allows users to read articles. Your widgets will:

  • Show data from your Flutter app.
  • Display text using font assets shared from the Flutter app.
  • Display an image of a rendered Flutter widget.

a36b7ba379151101.png

This Flutter app includes two screens (or routes):

  • The first displays a list of news articles with headlines and descriptions.
  • The second displays the full article with a chart created using CustomPaint.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

What you will learn

  • How to create Home Screen widgets on iOS and Android.
  • How to use the home_widget package to share data between your Home Screen widget and your Flutter app.
  • How to reduce the amount of code you need to re-write.
  • How to update your Home Screen widget from your Flutter app.

2. Set up your development environment

For both platforms, you need the Flutter SDK and an IDE. You can use your preferred IDE for working with Flutter. This could be Visual Studio Code with the Dart Code and Flutter extensions, or Android Studio or IntelliJ with the Flutter and Dart plugins installed.

To create the iOS Home Screen widget:

  • You can run this codelab on a physical iOS device or the iOS simulator.
  • You must configure a macOS system with the Xcode IDE. This installs the compiler needed to build the iOS version of your app.

To create the Android Home Screen widget:

  • You can run this codelab on a physical Android device or the Android emulator.
  • You must configure your development system with Android Studio. This installs the compiler needed to build the Android version of your app.

Get the starter code

Download the initial version of your project from GitHub

From the command line, clone the GitHub repository into a flutter-codelabs directory:

$ git clone https://github.com/flutter/codelabs.git flutter-codelabs

After cloning the repo, you can find the code for this codelab in the flutter-codelabs/homescreen_codelab directory. This directory contains completed project code for each step in the codelab.

Open the starter app

Open the flutter-codelabs/homescreen_codelab/step_03 directory into your preferred IDE.

Install packages

All required packages were added to the project's pubspec.yaml file. To retrieve the project dependencies, run the following command:

$ flutter pub get

3. Add a basic Home Screen widget

First, add the Home Screen widget using the native platform tooling.

Creating a basic iOS Home Screen widget

Adding an app extension to your Flutter iOS app is similar to adding an app extension to a SwiftUI or UIKit app:

  1. Run open ios/Runner.xcworkspace in a terminal window from your Flutter project directory. Alternatively, right click on the ios folder from VSCode and select Open in Xcode. This opens the default Xcode workspace in your Flutter project.
  2. Select File → New → Target from the menu. This adds a new target to the project.
  3. A list of templates appears. Select Widget Extension.
  4. Type "NewsWidgets" into the Product Name box for this widget. Clear both the Include Live Activity and Include Configuration Intent check boxes.

Inspect the sample code

When you add a new target, Xcode generates sample code based on the template you selected. For more on the generated code and WidgetKit, see Apple's app extension documentation.

Debug and test your sample widget

  1. First, update your Flutter app's configuration. You must do this when you add new packages in your Flutter app and plan to run a target in the project from Xcode. To update your app's configuration, run the following command in your Flutter app directory:
$ flutter build ios --config-only
  1. Click Runner to bring up a list of targets. Select the widget target that you just created, NewsWidgets, and click Run. Run the widget target from Xcode when you change the iOS widget code.

bbb519df1782881d.png

  1. The simulator or device screen should display a basic Home Screen widget. If you don't see it, you can add it to the screen. Click and hold on the home screen then click the + in the top left corner.

18eff1cae152014d.png

  1. Search for the name of the app. For this codelab, search for "Homescreen Widgets"

a0c00df87615493e.png

  1. Once you add the Home Screen widget, it should display simple text giving the time.

Creating a Basic Android Widget

  1. To add a Home Screen widget in Android, open the project's build file in Android Studio. You can find this file at android/build.gradle. Alternatively, right click on the android folder from VSCode and select Open in Android Studio.
  2. After the project builds, locate the app directory in the top-left corner. Add your new Home Screen widget to this directory. Right click the directory, select New -> Widget -> App Widget.

f19d8b7f95ab884e.png

  1. Android Studio displays a new form. Add basic information about your Home Screen widget including its class name, placement, size, and source language

For this codelab, set the following values:

  • Class Name box to NewsWidget
  • Minimum Width (cells) dropdown to 3
  • Minimum Height (cells) dropdown to 3

Inspect the sample code

When you submit the form, Android Studio creates and updates several files. The changes relevant for this codelab are listed in the table below

Action

Target File

Change

Update

AndroidManifest.xml

Adds a new receiver which registers the NewsWidget.

Create

res/layout/news_widget.xml

Defines Home Screen widget UI.

Create

res/xml/news_widget_info.xml

Defines your Home Screen widget configuration. You can adjust the dimensions or name of your widget in this file.

Create

java/com/example/homescreen_widgets/NewsWidget.kt

Contains your Kotlin code to add functionality to your Home Screen widget.

You can find more detail on these files throughout this codelab.

Debug and test your sample widget

Now, run your application and see the Home Screen widget. Once you build the app, navigate to the application selection screen of your Android device, and long-press the icon for this Flutter project. Select Widgets from the popup menu.

dff7c9f9f85ef1c7.png

The Android device or emulator displays your default Home Screen widget for Android.

4. Send data from your Flutter app to your Home Screen widget

You can customize the basic Home Screen widget that you created. Update the Home Screen widget to display a headline and summary for a news article. The following screenshot displays an example of the Home Screen widget displaying a headline and summary.

acb90343a3e51b6d.png

To pass data between your app and Home Screen widget, you need to write Dart and native code. This section divides this process into three parts:

  1. Writing Dart code in your Flutter app that both Android and iOS can use
  2. Adding native iOS functionality
  3. Adding native Android functionality

Using iOS App Groups

To share data between an iOS parent app and a widget extension, both targets must belong to the same app group. To learn more about app groups, see Apple's app group documentation.

Update your bundle identifier:

In Xcode, go to your target's settings. In the Signing & Capabilities tab, check that your team and bundle identifier are set.

Add the App Group to both the Runner target and the NewsWidgetExtension target in Xcode:

Select + Capability -> App Groups and add a new App Group. Repeat for both the Runner (parent app) target and the widget target.

135e1a8c4652dac.png

Add the Dart code

Both iOS and Android apps can share data with a Flutter app in a few different ways.To communicate with these apps, leverage the device's local key/value store. iOS calls this store UserDefaults, and Android calls this store SharedPreferences. The home_widget package wraps these APIs to simplify saving data to either platform and enables the Home Screen widgets to pull updated data.

707ae86f6650ac55.png

The headline and description data come from the news_data.dart file. This file contains mock data and a NewsArticle data class.

lib/news_data.dart

class NewsArticle {
  final String title;
  final String description;
  final String? articleText;

  NewsArticle({
    required this.title,
    required this.description,
    this.articleText = loremIpsum,
  });
}

Update the headline and description values

To add the functionality to update your Home Screen widget from your Flutter app, navigate to the lib/home_screen.dart file. Replace the contents of the file with the following code. Then, replace <YOUR APP GROUP> with the identifier for your App Group.

lib/home_screen.dart

import 'package:flutter/material.dart';
import 'package:home_widget/home_widget.dart';             // Add this import

import 'article_screen.dart';
import 'news_data.dart';

// TODO: Replace with your App Group ID
const String appGroupId = '<YOUR APP GROUP>';              // Add from here
const String iOSWidgetName = 'NewsWidgets';
const String androidWidgetName = 'NewsWidget';             // To here.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

void updateHeadline(NewsArticle newHeadline) {             // Add from here
  // Save the headline data to the widget
  HomeWidget.saveWidgetData<String>('headline_title', newHeadline.title);
  HomeWidget.saveWidgetData<String>(
      'headline_description', newHeadline.description);
  HomeWidget.updateWidget(
    iOSName: iOSWidgetName,
    androidName: androidWidgetName,
  );
}                                                          // To here.

class _MyHomePageState extends State<MyHomePage> {

  @override                                                // Add from here
  void initState() {
    super.initState();

    HomeWidget.setAppGroupId(appGroupId);

    // Mock read in some data and update the headline
    final newHeadline = getNewsStories()[0];
    updateHeadline(newHeadline);
  }                                                        // To here.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: const Text('Top Stories'),
            centerTitle: false,
            titleTextStyle: const TextStyle(
                fontSize: 30,
                fontWeight: FontWeight.bold,
                color: Colors.black)),
        body: ListView.separated(
          separatorBuilder: (context, idx) {
            return const Divider();
          },
          itemCount: getNewsStories().length,
          itemBuilder: (context, idx) {
            final article = getNewsStories()[idx];
            return ListTile(
              key: Key('$idx ${article.hashCode}'),
              title: Text(article.title!),
              subtitle: Text(article.description!),
              onTap: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) {
                      return ArticleScreen(article: article);
                    },
                  ),
                );
              },
            );
          },
        ));
  }
}

The updateHeadline function saves the key/value pairs to your device's local storage. The headline_title key holds the value of newHeadline.title. The headline_description key holds the value of the newHeadline.description. The function also notifies the native platform that new data for the Home Screen widgets can be retrieved and rendered.

Modify the floatingActionButton

Call the updateHeadline function when the floatingActionButton is pressed as shown:

lib/article_screen.dart

// New: import the updateHeadline function
import 'home_screen.dart';

...

floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
            content: Text('Updating home screen widget...'),
          ));
          // New: call updateHeadline
          updateHeadline(widget.article);
        },
        label: const Text('Update Homescreen'),
      ),
...

With this change, when a user presses the Update Headline button from an article page, the Home Screen widget details are updated.

Update the iOS code to display the article data

To update the Home Screen widget for iOS, use Xcode.

Open the NewsWidgets.swift file in Xcode:

Configure the TimelineEntry.

Replace the SimpleEntry struct with the following code:

ios/NewsWidgets/NewsWidgets.swift

// The date and any data you want to pass into your app must conform to TimelineEntry
struct NewsArticleEntry: TimelineEntry {
    let date: Date
    let title: String
    let description:String
}

This NewsArticleEntry struct defines the incoming data to pass into the Home Screen widget when updated. The TimelineEntry type requires a date parameter.To learn more about the TimelineEntry protocol, check out Apple's TimelineEntry documentation.

Edit the View that displays the content

Modify your Home Screen widget to display the news article headline and description instead of the date. To display text in SwiftUI, use the Text view. To stack views on top of each other in SwiftUI, use the VStack view.

Replace the generated NewsWidgetEntryView view with the following code:

ios/NewsWidgets/NewsWidgets.swift

//View that holds the contents of the widget
struct NewsWidgetsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
      VStack {
        Text(entry.title)
        Text(entry.description)
      }
    }
}

Edit the provider to tell the Home Screen widget when and how to update

Replace the existing Provider with the following code. Then, substitute your app group identifier for <YOUR APP GROUP>:

ios/NewsWidgets/NewsWidgets.swift

struct Provider: TimelineProvider {

// Placeholder is used as a placeholder when the widget is first displayed
    func placeholder(in context: Context) -> NewsArticleEntry {
//      Add some placeholder title and description, and get the current date
      NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description")
    }

// Snapshot entry represents the current time and state
    func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
      let entry: NewsArticleEntry
      if context.isPreview{
        entry = placeholder(in: context)
      }
      else{
        //      Get the data from the user defaults to display
        let userDefaults = UserDefaults(suiteName: <YOUR APP GROUP>)
        let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
        let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
        entry = NewsArticleEntry(date: Date(), title: title, description: description)
      }
        completion(entry)
    }

//    getTimeline is called for the current and optionally future times to update the widget
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
//      This just uses the snapshot function you defined earlier
      getSnapshot(in: context) { (entry) in
// atEnd policy tells widgetkit to request a new entry after the date has passed
        let timeline = Timeline(entries: [entry], policy: .atEnd)
                  completion(timeline)
              }
    }
}

The Provider in the previous code conforms to a TimelineProvider. Provider has three different methods:

  1. The placeholder method generates a placeholder entry when the user first previews the Home Screen widget.

45a0f64240c12efe.png

  1. The getSnapshot method reads the data from user defaults and generates the entry for the current time.
  2. The getTimeline method returns timeline entries. This helps when you have predictable points in time to update your content. This codelab uses the getSnapshot function to get the current state. The.atEnd method tells the Home Screen widget to refresh the data after the current time passes.

Comment out the NewsWidgets_Previews

Using previews is out of scope for this codelab. For more details on previewing SwiftUI Home Screen widgets see Apple's Documentation on Debugging Widgets.

Save all files and re-run the app and widget target.

Run the targets again to validate that the app and Home Screen widget work.

  1. Select the app schema in Xcode to run the app target.
  2. Select the extension schema in Xcode to run the extension target.
  3. Navigate to an article page in the app.
  4. Click the button to update the headline. The Home Screen widget should also update the headline.

Update the Android code

Add the Home Screen widget XML.

In Android Studio, update the files generated in the previous step.Open the res/layout/news_widget.xml file. It defines the structure and layout of your home screen widget. Select Code in the top right hand corner and replace the contents of that file with the following code:

android/app/res/layout/news_widget.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/widget_container"
   style="@style/Widget.Android.AppWidget.Container"
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:background="@android:color/white"
   android:theme="@style/Theme.Android.AppWidgetContainer">
   
   <TextView
       android:id="@+id/headline_title"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="20sp"
       android:textStyle="bold" />

   <TextView
       android:id="@+id/headline_description"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_below="@+id/headline_title"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:layout_marginTop="4dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="16sp" />

</RelativeLayout>

This XML defines two text views, one for the article headline and the other for the article description. These text views also define styling. You will return to this file throughout this codelab.

Update NewsWidget functionality

Open the NewsWidget.kt Kotlin source code file. This file contains a generated class called NewsWidget that extends the AppWidgetProvider class.

The NewsWidget class contains three methods from its superclass. You'll modify the onUpdate method. Android calls this method for widgets at fixed intervals.

Replace the contents of the NewsWidget.kt file with the following code:

android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt

// Import will depend on App ID.
package com.mydomain.homescreen_widgets

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews

// New import.
import es.antonborri.home_widget.HomeWidgetPlugin


/**
 * Implementation of App Widget functionality.
 */
class NewsWidget : AppWidgetProvider() {
    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray,
    ) {
        for (appWidgetId in appWidgetIds) {
            // Get reference to SharedPreferences
            val widgetData = HomeWidgetPlugin.getData(context)
            val views = RemoteViews(context.packageName, R.layout.news_widget).apply {

                val title = widgetData.getString("headline_title", null)
                setTextViewText(R.id.headline_title, title ?: "No title set")

                val description = widgetData.getString("headline_description", null)
                setTextViewText(R.id.headline_description, description ?: "No description set")
            }

            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

Now, when onUpdate is called, Android gets the newest values from local storage using the widgetData.getString() method, and then calls setTextViewText to change the text displayed on the Home Screen widget.

Test the updates

Test the app to ensure your Home Screen widgets update with new data. To update the data, use the Update Home Screen FloatingActionButton on the article pages. Your Home Screen widget should update with the article title.

5ce1c9914b43ad79.png

5. Using Flutter app custom fonts in your iOS Home Screen widget

So far, you have configured the Home Screen widget to read data that the Flutter app provides. The Flutter app includes a custom font that you might want to use in the Home Screen widget. You can use the custom font in your iOS Home Screen widget. Using custom fonts in Home Screen widgets is not available on Android.

Update the iOS code

Flutter stores its assets in the mainBundle of iOS applications. You can access assets in this bundle from your Home Screen widget code.

In the NewsWidgetsEntryView struct in your NewsWidgets.swift file, make the following changes

Create a helper function to get the path to the Flutter asset directory:

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...

   // New: Add the helper function.
   var bundle: URL {
           let bundle = Bundle.main
           if bundle.bundleURL.pathExtension == "appex" {
               // Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
               var url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
               url.append(component: "Frameworks/App.framework/flutter_assets")
               return url
           }
           return bundle.bundleURL
       }
   ...
}

Register the font using the URL to your custom font file.

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...

   // New: Register the font.
   init(entry: Provider.Entry){
     self.entry = entry
     CTFontManagerRegisterFontsForURL(bundle.appending(path: "/fonts/Chewy-Regular.ttf") as CFURL, CTFontManagerScope.process, nil)
   }
   ...
}

Update the headline Text view to use your custom font.

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...


   var body: some View {
    VStack {
      // Update the following line.
      Text(entry.title).font(Font.custom("Chewy", size: 13))
      Text(entry.description)
    }
   }
   ...
}

When you run your Home Screen widget, it now uses the custom font for the headline as displayed in the following image:

93f8b9d767aacfb2.png

6. Rendering Flutter widgets as an image

In this section, you'll display a graph from your Flutter app as a Home Screen widget.

This widget provides a greater challenge than the text that you've displayed on the homescreen. It's far easier to display the Flutter chart as an image rather than try to recreate it using native UI components.

Code your Home Screen widget to render your Flutter chart as a PNG file. Your Home Screen widget can display that image.

Write the Dart code

On the Dart side, add the renderFlutterWidget method from the home_widget package. This method takes a widget, a filename, and a key. It returns an image of the Flutter widget and saves it to a shared container. Provide the image name in your code and ensure the Home Screen widget can access the container. The key saves the full file path as a string in the device's local storage. This allows the Home Screen widget to find the file if the name changes in the Dart code.

For this codelab, the LineChart class in the lib/article_screen.dart file represents the chart. Its build method returns a CustomPainter that paints this chart to the screen.

To implement this feature, open the lib/article_screen.dart file. Import the home_widget package. Next, replace the code in the _ArticleScreenState class with the following code:

lib/article_screen.dart

import 'package:flutter/material.dart';
// New: import the home_widget package.
import 'package:home_widget/home_widget.dart';

import 'home_screen.dart';
import 'news_data.dart';

...

class _ArticleScreenState extends State<ArticleScreen> {
  // New: add this GlobalKey
  final _globalKey = GlobalKey();
  String? imagePath;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.article.title!),
      ),
      // New: add this FloatingActionButton
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () async {
          if (_globalKey.currentContext != null) {
            var path = await HomeWidget.renderFlutterWidget(
              const LineChart(),
              fileName: 'screenshot',
              key: 'filename',
              logicalSize: _globalKey.currentContext!.size,
              pixelRatio:
                  MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
            );
            setState(() {
              imagePath = path as String?;
            });
          }
          updateHeadline(widget.article);
        },
        label: const Text('Update Homescreen'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16.0),
        children: [
          Text(
            widget.article.description!,
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 20.0),
          Text(widget.article.articleText!),
          const SizedBox(height: 20.0),
          Center(
            // New: Add this key
            key: _globalKey,
            child: const LineChart(),
          ),
          const SizedBox(height: 20.0),
          Text(widget.article.articleText!),
        ],
      ),
    );
  }
}

This example makes three changes to the _ArticleScreenState class.

Creates a GlobalKey

The GlobalKey gets the context of the specific widget, which is needed to get the size of that widget .

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   // New: add this GlobalKey
   final _globalKey = GlobalKey();
   ...
}

Adds imagePath

The imagePath property stores the image's location where the Flutter widget is rendered.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   // New: add this imagePath
   String? imagePath;
   ...
}

Adds the key to the widget to render

The _globalKey contains the Flutter widget that is rendered to the image. In this case, the Flutter widget is the Center that contains the LineChart.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   Center(
      // New: Add this key
 key: _globalKey,
 child: const LineChart(),
   ),
   ...
}
  1. Saves the widget as an image

The renderFlutterWidget method is called when the user clicks the floatingActionButton. The method saves the resulting PNG file as "screenshot" to the shared container directory. The method also saves the full path to the image as the filename key in the device storage.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   floatingActionButton: FloatingActionButton.extended(
 onPressed: () async {
   if (_globalKey.currentContext != null) {
     var path = await HomeWidget.renderFlutterWidget(
       LineChart(),
       fileName: 'screenshot',
       key: 'filename',
       logicalSize: _globalKey.currentContext!.size,
       pixelRatio:
         MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
     );
     setState(() {
        imagePath = path as String?;
     });
    }
  updateHeadline(widget.article);
  },
   ...
}

Update the iOS code

For iOS, update the code to get the file path from storage and display the file as an image using SwiftUI.

Open the NewsWidgets.swift file to make the following changes:

Add filename and displaySize to the NewsArticleEntry struct

The filename property holds the string that represents the path to the image file. The displaySize property holds the size of the Home Screen widget on the user's device. The size of the Home Screen widget comes from the context.

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

   // New: add the filename and displaySize.
   let filename: String
   let displaySize: CGSize
}

Update the placeholder function

Include a placeholder filename and displaySize.

ios/NewsWidgets/NewsWidgets.swift

func placeholder(in context: Context) -> NewsArticleEntry {
      NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description", filename: "No screenshot available",  displaySize: context.displaySize)
    }

Get the filename from userDefaults in getSnapshot

This sets the filename variable to the filename value in the userDefaults storage when the Home Screen widget updates.

ios/NewsWidgets/NewsWidgets.swift

func getSnapshot(
   ...

   let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
   let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
   // New: get fileName from key/value store
   let filename = userDefaults?.string(forKey: "filename") ?? "No screenshot available"
   ...
)

Create ChartImage that displays the image from a path

The ChartImage View creates an image from the contents of the file generated on the Dart side. Here, you set the size to 50% of the frame.

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...

   // New: create the ChartImage view
   var ChartImage: some View {
        if let uiImage = UIImage(contentsOfFile: entry.filename) {
            let image = Image(uiImage: uiImage)
                .resizable()
                .frame(width: entry.displaySize.height*0.5, height: entry.displaySize.height*0.5, alignment: .center)
            return AnyView(image)
        }
        print("The image file could not be loaded")
        return AnyView(EmptyView())
    }
   ...
}

Use the ChartImage in the body of NewsWidgetsEntryView

Add the ChartImage view to the body of the NewsWidgetsEntryView to display the ChartImage in the Home Screen widget.

ios/NewsWidgets/NewsWidgets.swift

VStack {
   Text(entry.title).font(Font.custom("Chewy", size: 13))
   Text(entry.description).font(.system(size: 12)).padding(10)
   // New: add the ChartImage to the NewsWidgetEntryView
   ChartImage
}

Test the changes

To test the changes, re-run both your Flutter app (Runner) target and your extension target from Xcode. To see the image, navigate to one of the article pages in the app and press the button to update the Home Screen widget.

33bdfe2cce908c48.png

Update the Android code

The Android code functions the same as the iOS code.

  1. Open the android/app/res/layout/news_widget.xml file. It contains the UI elements of your Home Screen widget. Replace its contents with the following code:

android/app/res/layout/news_widget.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/widget_container"
   style="@style/Widget.Android.AppWidget.Container"
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:background="@android:color/white"
   android:theme="@style/Theme.Android.AppWidgetContainer">

   <TextView
       android:id="@+id/headline_title"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="20sp"
       android:textStyle="bold" />

   <TextView
       android:id="@+id/headline_description"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_below="@+id/headline_title"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:layout_marginTop="4dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="16sp" />
   
   <!--New: add this image view -->
   <ImageView
       android:id="@+id/widget_image"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:layout_below="@+id/headline_description"
       android:layout_alignBottom="@+id/headline_title"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:layout_marginTop="6dp"
       android:layout_marginBottom="-134dp"
       android:layout_weight="1"
       android:adjustViewBounds="true"
       android:background="@android:color/white"
       android:scaleType="fitCenter"
       android:src="@android:drawable/star_big_on"
       android:visibility="visible"
       tools:visibility="visible" />

</RelativeLayout>

This new code adds an image to the Home Screen widget, which (for now) displays a generic star icon. Replace this star icon with the image that you saved in the Dart code.

  1. Open the NewsWidget.kt file. Replace its content with the following code:

android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt

// Import will depend on App ID.
package com.mydomain.homescreen_widgets

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.RemoteViews
import java.io.File
import es.antonborri.home_widget.HomeWidgetPlugin


/**
 * Implementation of App Widget functionality.
 */
class NewsWidget : AppWidgetProvider() {
    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray,
    ) {
        for (appWidgetId in appWidgetIds) {
            val widgetData = HomeWidgetPlugin.getData(context)
            val views = RemoteViews(context.packageName, R.layout.news_widget).apply {

                val title = widgetData.getString("headline_title", null)
                setTextViewText(R.id.headline_title, title ?: "No title set")

                val description = widgetData.getString("headline_description", null)
                setTextViewText(R.id.headline_description, description ?: "No description set")

                // New: Add the section below
               // Get chart image and put it in the widget, if it exists
                val imageName = widgetData.getString("filename", null)
                val imageFile = File(imageName)
                val imageExists = imageFile.exists()
                if (imageExists) {
                    val myBitmap: Bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
                    setImageViewBitmap(R.id.widget_image, myBitmap)
                } else {
                    println("image not found!, looked @: ${imageName}")
                }
                // End new code
            }

            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

This Dart code saves a screenshot to local storage with the filename key. It also gets the image's full path and creates a File object from it. If the image exists, the Dart code replaces the image in the Home Screen widget with the new image.

  1. Reload your app and navigate to an article screen. Press Update Homescreen. The Home Screen widget displays the chart.

7. Next Steps

Congratulations!

Congratulations, you succeeded in creating Home Screen widgets for your Flutter iOS and Android apps!

Linking to content in your Flutter app

You might want to direct the user to a specific page in your app, depending on where the user clicks. For example, in the news app from this codelab, you might want the user to see the news article for the displayed headline.

This feature falls outside the scope of this codelab. You can find examples of using a stream that the home_widget package provides to identify app launches from Home Screen widgets and send messages from the Home Screen widget through the URL. To learn more, see the deep linking documentation on docs.flutter.dev.

Updating your widget in the background

In this codelab, you triggered an update of the Home Screen widget using a button. Though this is reasonable for testing, in production code you might want your app to update the Home Screen widget in the background. You can use the workmanager plugin to create background tasks to update resources the Home Screen widget needs. To learn more, check out the Background update section in the home_widget package.

For iOS, you could also have the Home Screen widget make a network request to update its UI. To control the conditions or frequency of that request, use the Timeline. To learn more about using the Timeline, see Apple's "Keeping a widget up to date" documentation.

Further reading