Flutter アプリにホーム画面ウィジェットを追加する

1. はじめに

ウィジェットとは何ですか?

Flutter デベロッパーにとって、ウィジェットの一般的な定義は、Flutter フレームワークを使用して作成された UI コンポーネントを指します。この Codelab では、ウィジェットとは、アプリを開くことなくアプリの情報を確認できるミニバージョンのアプリのことを指します。Android では、ウィジェットはホーム画面に表示されます。iOS では、ホーム画面、ロック画面、[今日] ビューに追加できます。

f0027e8a7d0237e0.png b991e79ea72c8b65.png

ウィジェットはどの程度複雑にできますか?

ほとんどのホーム画面ウィジェットはシンプルです。基本的なテキスト、シンプルなグラフィック、Android では基本的なコントロールなどを含めることができます。Android と iOS の両方で、使用できる UI コンポーネントと機能が制限されています。

819b9fffd700e571.png 92d62ccfd17d770d.png

ウィジェットの UI を作成する

こうした UI の制限により、Flutter フレームワークを使用してホーム画面ウィジェットの UI を直接描画することはできません。代わりに、Jetpack Compose や SwiftUI などのプラットフォーム フレームワークで作成したウィジェットを Flutter アプリに追加できます。この Codelab では、複雑な UI の書き換えを回避するためにアプリとウィジェットの間でリソースを共有する例について説明します。

作成するアプリの概要

この Codelab では、home_widget パッケージを使用して、Android と iOS の両方でシンプルな Flutter アプリ用のホーム画面ウィジェットを作成し、ユーザーが記事を閲覧できるようにします。ウィジェットの動作は次のとおりです。

  • Flutter アプリのデータを表示します。
  • Flutter アプリから共有されたフォント アセットを使用してテキストを表示します。
  • レンダリングされた Flutter ウィジェットの画像を表示します。

a36b7ba379151101.png

この Flutter アプリには、次の 2 つの画面(ルート)があります。

  • 最初の画面には、見出しと説明を含むニュース記事のリストが表示されます。
  • 2 行目では、CustomPaint を使用して作成されたグラフとともに記事全体を表示します。

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

学習内容

  • iOS と Android でホーム画面ウィジェットを作成する方法
  • home_widget パッケージを使用して、ホーム画面ウィジェットと Flutter アプリ間でデータを共有する方法。
  • 書き直す必要があるコードの量を減らす方法。
  • Flutter アプリからホーム画面ウィジェットを更新する方法

2. 開発環境を設定する

どちらのプラットフォームにも、Flutter SDKIDE が必要です。Flutter の操作には、お好みの IDE を使用できます。これには、Dart Code と Flutter の拡張機能がインストールされた Visual Studio Code、Flutter と Dart のプラグインがインストールされた Android Studio または IntelliJ があります。

iOS のホーム画面ウィジェットを作成するには:

  • この Codelab は、実際の iOS デバイスまたは iOS シミュレータで実行できます。
  • Xcode IDE で macOS システムを構成する必要があります。これにより、アプリの iOS 版をビルドするために必要なコンパイラがインストールされます。

Android のホーム画面ウィジェットを作成するには:

  • この Codelab は、実際の Android デバイスまたは Android Emulator で実行できます。
  • Android Studio を使用して開発システムを構成する必要があります。これにより、アプリの Android 版をビルドするために必要なコンパイラがインストールされます。

スターター コードを取得する

GitHub からプロジェクトの初期バージョンをダウンロードする

コマンドラインから、GitHub リポジトリのクローンを flutter-codelabs ディレクトリに作成します。

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

リポジトリのクローンを作成すると、flutter-codelabs/homescreen_codelab ディレクトリで、この Codelab のコードを確認できます。このディレクトリには、Codelab の各ステップの完成したプロジェクト コードが含まれています。

スターター アプリを開く

お好みの IDE で、flutter-codelabs/homescreen_codelab/step_03 ディレクトリを開きます。

パッケージをインストールする

必要なすべてのパッケージがプロジェクトの pubspec.yaml ファイルに追加されました。プロジェクトの依存関係を取得するには、次のコマンドを実行します。

$ flutter pub get

3. 基本的なホーム画面ウィジェットを追加する

まず、ネイティブ プラットフォーム ツールを使用してホーム画面ウィジェットを追加します。

iOS で基本的なホーム画面ウィジェットを作成する

Flutter iOS アプリにアプリリンク表示オプションを追加する方法は、SwiftUI アプリや UIKit アプリにアプリ拡張機能を追加する場合と同様です。

  1. Flutter プロジェクト ディレクトリからターミナル ウィンドウで open ios/Runner.xcworkspace を実行します。または、VSCode で ios フォルダを右クリックし、[Xcode で開く] を選択します。これにより、Flutter プロジェクトのデフォルトの Xcode ワークスペースが開きます。
  2. メニューから [File] > [New] > [Target] を選択します。これにより、プロジェクトに新しいターゲットが追加されます。
  3. テンプレートのリストが表示されます。[Widget Extension] を選択します。
  4. 「NewsWidgets」と入力します。」をこのウィジェットの [Product Name] ボックスに入力します。[Include Live Activity] と [Include Configuration Intent] の両方のチェックボックスをオフにします。

サンプルコードを調べる

新しいターゲットを追加すると、Xcode は選択したテンプレートに基づいてサンプルコードを生成します。生成されたコードと WidgetKit について詳しくは、Apple の App Extensions に関するドキュメント

サンプル ウィジェットのデバッグとテスト

  1. まず、Flutter アプリの構成を更新します。Flutter アプリに新しいパッケージを追加し、Xcode からプロジェクトでターゲットを実行する場合は、これを行う必要があります。アプリの構成を更新するには、Flutter アプリのディレクトリで次のコマンドを実行します。
$ flutter build ios --config-only
  1. [Runner] をクリックしてターゲットのリストを表示します。作成したウィジェット ターゲットである NewsWidgets を選択し、[実行] をクリックします。iOS ウィジェット コードを変更したら、Xcode からウィジェット ターゲットを実行します。

bbb519df1782881d.png

  1. シミュレータまたはデバイスの画面には、基本的なホーム画面ウィジェットが表示されます。表示されない場合は、画面に追加できます。ホーム画面を長押しして、左上の [+] をクリックします。

18eff1cae152014d.png

  1. アプリの名前を検索します。この Codelab では、「ホーム画面ウィジェット」を検索します。

a0c00df87615493e.png

  1. ホーム画面ウィジェットを追加すると、時刻を示すシンプルなテキストが表示されます。

基本的な Android ウィジェットを作成する

  1. Android でホーム画面ウィジェットを追加するには、Android Studio でプロジェクトのビルドファイルを開きます。このファイルは android/build.gradle にあります。または、VSCode で android フォルダを右クリックして [Open in Android Studio] を選択します。
  2. プロジェクトがビルドされたら、左上隅でアプリのディレクトリを見つけます。新しいホーム画面ウィジェットをこのディレクトリに追加します。ディレクトリを右クリックして、[New] -> [New] を選択します。ウィジェット ->アプリ ウィジェット

f19d8b7f95ab884e.png

  1. Android Studio に新しいフォームが表示されます。クラス名、配置、サイズ、原文の言語など、ホーム画面ウィジェットに関する基本情報を追加します

この Codelab では、次の値を設定します。

  • [Class Name] ボックスを NewsWidget
  • [最小幅(セル数)] プルダウンを 3 に設定
  • [最小の高さ(セル数)] プルダウンを 3 に設定

サンプルコードを調べる

フォームを送信すると、Android Studio によって複数のファイルが作成され、更新されます。この Codelab に関連する変更は以下の表のとおりです。

アクション

ターゲット ファイル

変更

更新

AndroidManifest.xml

NewsWidget を登録する新しいレシーバを追加します。

作成

res/layout/news_widget.xml

ホーム画面ウィジェット UI を定義します。

作成

res/xml/news_widget_info.xml

ホーム画面ウィジェットの構成を定義します。このファイルで、ウィジェットのサイズや名前を調整できます。

作成

java/com/example/homescreen_widgets/NewsWidget.kt

ホーム画面ウィジェットに機能を追加するための Kotlin コードが含まれています。

これらのファイルについては、この Codelab 全体で詳しく説明します。

サンプル ウィジェットのデバッグとテスト

アプリを実行すると、ホーム画面ウィジェットが表示されます。アプリを作成したら、Android デバイスのアプリ選択画面に移動し、この Flutter プロジェクトのアイコンを長押しします。ポップアップ メニューから [ウィジェット] を選択します。

dff7c9f9f85ef1c7.png

Android デバイスまたはエミュレータには、Android のデフォルトのホーム画面ウィジェットが表示されます。

4. Flutter アプリからホーム画面ウィジェットにデータを送信する

作成した基本的なホーム画面ウィジェットをカスタマイズできます。ニュース記事の見出しと概要を表示するようにホーム画面ウィジェットを更新します。次のスクリーンショットは、見出しと概要を表示するホーム画面ウィジェットの例です。

acb90343a3e51b6d.png

アプリとホーム画面ウィジェットの間でデータを渡すには、Dart とネイティブ コードを記述する必要があります。このセクションでは、このプロセスを 3 つのパートに分けます。

  1. Flutter アプリに Dart コードを記述して、Android と iOS の両方で使用できるようにする
  2. ネイティブ iOS 機能の追加
  3. ネイティブ Android 機能の追加

iOS アプリ グループを使用する

iOS の親アプリとウィジェット拡張機能の間でデータを共有するには、両方のターゲットが同じアプリグループに属している必要があります。アプリグループについて詳しくは、Apple のアプリグループに関するドキュメントをご覧ください。

バンドル ID を更新します。

Xcode で、ターゲットの設定に移動します。[署名と機能] タブで、チームとバンドル ID が設定されていることを確認します。

Xcode で、Runner ターゲットと NewsWidgetExtension ターゲットの両方にアプリグループを追加します。

[+ Capability ->] を選択します。アプリ グループ] をクリックし、新しいアプリグループを追加します。Runner(親アプリ)ターゲットとウィジェット ターゲットの両方に繰り返します。

135e1a8c4652dac.png

Dart コードを追加する

iOS アプリと Android アプリは、いくつかの方法で Flutter アプリとデータを共有できます。これらのアプリと通信するには、デバイスのローカル key/value ストアを利用します。iOS の場合は「UserDefaults」、Android の場合は「SharedPreferences」と呼ばれます。home_widget package はこれらの API をラップして、いずれかのプラットフォームへのデータの保存を簡素化し、ホーム画面ウィジェットが更新されたデータを pull できるようにします。

707ae86f6650ac55.png

広告見出しと説明文のデータは、news_data.dart ファイルから取得されます。このファイルには、モックデータと NewsArticle データクラスが含まれています。

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,
  });
}

広告見出しと説明文の値を更新する

Flutter アプリからホーム画面ウィジェットを更新する機能を追加するには、lib/home_screen.dart ファイルに移動します。ファイルの内容を次のコードに置き換えます。次に、<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);
                    },
                  ),
                );
              },
            );
          },
        ));
  }
}

updateHeadline 関数は、Key-Value ペアをデバイスのローカル ストレージに保存します。headline_title キーは newHeadline.title の値を保持します。headline_description キーは newHeadline.description の値を保持します。この関数は、ホーム画面ウィジェットの新しいデータを取得してレンダリングできることもネイティブ プラットフォームに通知します。

floatingActionButton を変更する

次に示すように、floatingActionButton が押されたときに updateHeadline 関数を呼び出します。

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'),
      ),
...

この変更により、ユーザーが記事ページから [Update Headline] ボタンを押すと、ホーム画面ウィジェットの詳細が更新されます。

記事データを表示するように iOS コードを更新する

iOS のホーム画面ウィジェットを更新するには、Xcode を使用します。

Xcode で NewsWidgets.swift ファイルを開きます。

TimelineEntry を構成します。

SimpleEntry 構造体を次のコードに置き換えます。

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
}

この NewsArticleEntry 構造体は、更新時にホーム画面ウィジェットに渡す受信データを定義します。TimelineEntry タイプには日付パラメータが必要です。TimelineEntry プロトコルの詳細については、Apple の TimelineEntry ドキュメントをご覧ください。

コンテンツを表示する View を編集します。

日付の代わりにニュース記事の見出しと説明を表示するようにホーム画面ウィジェットを変更します。SwiftUI でテキストを表示するには、Text ビューを使用します。SwiftUI でビューを重ねて表示するには、VStack ビューを使用します。

生成された NewsWidgetEntryView ビューを次のコードに置き換えます。

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)
      }
    }
}

プロバイダを編集して、更新のタイミングと方法をホーム画面ウィジェットに伝える

既存の Provider を次のコードに置き換えます。<YOUR APP GROUP> を実際のアプリグループ ID に置き換えます。

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)
              }
    }
}

前のコードの ProviderTimelineProvider に準拠しています。Provider には次の 3 つのメソッドがあります。

  1. placeholder メソッドは、ユーザーがホーム画面ウィジェットを初めてプレビューしたときにプレースホルダ エントリを生成します。

45a0f64240c12efe.png

  1. getSnapshot メソッドは、ユーザーのデフォルトからデータを読み取り、現在の時刻のエントリを生成します。
  2. getTimeline メソッドはタイムライン エントリを返します。これは、コンテンツを更新するタイミングが予測しやすい場合に役立ちます。この Codelab では、getSnapshot 関数を使用して現在の状態を取得します。.atEnd メソッドは、現在の時間が経過した後にデータを更新するようホーム画面ウィジェットに指示します。

NewsWidgets_Previews をコメントアウトします。

プレビューの使用はこの Codelab の対象外です。SwiftUI のホーム画面ウィジェットのプレビューについて詳しくは、デバッグ ウィジェットに関する Apple のドキュメントをご覧ください。

すべてのファイルを保存し、アプリとウィジェットのターゲットを再実行します。

ターゲットを再度実行して、アプリとホーム画面ウィジェットが機能することを確認します。

  1. Xcode でアプリスキーマを選択して、アプリ ターゲットを実行します。
  2. Xcode で拡張機能スキーマを選択して、拡張機能ターゲットを実行します。
  3. アプリの記事ページに移動します。
  4. ボタンをクリックすると見出しが更新されます。ホーム画面ウィジェットでも見出しが更新されます。

Android コードを更新する

ホーム画面ウィジェットの XML を追加します。

Android Studio で、前の手順で生成したファイルを更新し、res/layout/news_widget.xml ファイルを開きます。ホーム画面ウィジェットの構造とレイアウトを定義します。右上にある [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>

この XML では、2 つのテキストビュー(1 つは記事の見出し用、もう 1 つは記事の説明用)を定義しています。これらのテキストビューはスタイルも定義します。この Codelab 全体を通して、このファイルに戻ってきます。

NewsWidget の機能を更新する

NewsWidget.kt Kotlin ソースコード ファイルを開きます。このファイルには、AppWidgetProvider クラスを拡張する NewsWidget という名前の生成されたクラスが含まれています。

NewsWidget クラスには、そのスーパークラスの 3 つのメソッドが含まれています。onUpdate メソッドを変更します。Android では、ウィジェットに対して一定の間隔でこのメソッドを呼び出します。

NewsWidget.kt ファイルの内容を次のコードに置き換えます。

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)
        }
    }
}

これで、onUpdate が呼び出されると、Android は the widgetData.getString() メソッドを使用してローカル ストレージから最新の値を取得し、setTextViewText を呼び出してホーム画面ウィジェットに表示されるテキストを変更します。

更新をテストする

アプリをテストして、ホーム画面ウィジェットが新しいデータで更新されることを確認します。データを更新するには、記事ページの [ホーム画面を更新 ] FloatingActionButton を使用します。ホーム画面ウィジェットが記事のタイトルに更新されます。

5ce1c9914b43ad79.png

5. iOS のホーム画面ウィジェットで Flutter アプリのカスタム フォントを使用する

ここまでは、Flutter アプリから提供されるデータを読み取るようにホーム画面ウィジェットを構成しました。Flutter アプリには、ホーム画面ウィジェットで使用できるカスタム フォントが含まれています。iOS のホーム画面ウィジェットでカスタム フォントを使用できます。Android では、ホーム画面ウィジェットでカスタム フォントを使用することはできません。

iOS コードを更新する

Flutter では、iOS アプリの mainBundle にアセットが保存されます。このバンドルのアセットには、ホーム画面のウィジェット コードからアクセスできます。

NewsWidgets.swift ファイルの NewsWidgetsEntryView 構造体で、次の変更を行います。

Flutter アセット ディレクトリへのパスを取得するヘルパー関数を作成します。

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
       }
   ...
}

カスタム フォント ファイルの URL を使用してフォントを登録します。

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)
   }
   ...
}

見出しのテキストビューを更新して、カスタム フォントを使用する。

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)
    }
   }
   ...
}

ホーム画面ウィジェットを実行すると、次の画像のように見出しにカスタム フォントが使用されるようになりました。

93f8b9d767aacfb2.png

6. Flutter ウィジェットを画像としてレンダリングする

このセクションでは、Flutter アプリのグラフをホーム画面ウィジェットとして表示します。

このウィジェットは、ホーム画面に表示されているテキストよりも難しい部分です。ネイティブ UI コンポーネントを使用して再作成するよりも、Flutter のグラフを画像として表示する方がはるかに簡単です。

Flutter チャートを PNG ファイルとしてレンダリングするようにホーム画面ウィジェットをコーディングします。ホーム画面のウィジェットにその画像を表示できます。

Dart コードを記述する

Dart 側では、home_widget パッケージから renderFlutterWidget メソッドを追加します。このメソッドは、ウィジェット、ファイル名、およびキーを受け取ります。Flutter ウィジェットの画像を返し、共有コンテナに保存します。コード内に画像名を指定し、ホーム画面ウィジェットがコンテナにアクセスできるようにします。key は、ファイルのフルパスを文字列としてデバイスのローカル ストレージに保存します。これにより、Dart コード内で名前が変更された場合に、ホーム画面ウィジェットがファイルを見つけられます。

この Codelab では、lib/article_screen.dart ファイルの LineChart クラスがグラフを表します。その build メソッドは、このグラフを画面に描画する CustomPainter を返します。

この機能を実装するには、lib/article_screen.dart ファイルを開きます。home_widget パッケージをインポートします。次に、_ArticleScreenState クラスのコードを次のコードに置き換えます。

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!),
        ],
      ),
    );
  }
}

この例では、_ArticleScreenState クラスに 3 つの変更を加えます。

GlobalKey を作成します

GlobalKey は、ウィジェットのサイズを取得するために必要な特定のウィジェットのコンテキストを取得します。

lib/article_screen.dart

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

imagePath を追加する

imagePath プロパティには、Flutter ウィジェットがレンダリングされる画像の場所が格納されます。

lib/article_screen.dart

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

キーをウィジェットに追加してレンダリングする

_globalKey には、画像にレンダリングされる Flutter ウィジェットが含まれています。この場合、Flutter ウィジェットは LineChart を含む Center です。

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   Center(
      // New: Add this key
 key: _globalKey,
 child: const LineChart(),
   ),
   ...
}
  1. ウィジェットを画像として保存します

renderFlutterWidget メソッドは、ユーザーが floatingActionButton をクリックすると呼び出されます。このメソッドは、生成された PNG ファイルを「screenshot」として保存します。共有コンテナのディレクトリに移動しますまた、このメソッドは、イメージのフルパスをファイル名キーとしてデバイス ストレージに保存します。

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);
  },
   ...
}

iOS コードを更新する

iOS の場合は、ストレージからファイルパスを取得し、SwiftUI を使用してファイルを画像として表示するようにコードを更新します。

NewsWidgets.swift ファイルを開いて、次の変更を行います。

filenamedisplaySizeNewsArticleEntry 構造体に追加する

filename プロパティは、画像ファイルのパスを表す文字列を保持します。displaySize プロパティは、ユーザーのデバイスのホーム画面ウィジェットのサイズを保持します。ホーム画面ウィジェットのサイズは context から取得されます。

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

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

placeholder 関数を更新する

プレースホルダ filenamedisplaySize を含めます。

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)
    }

getSnapshotuserDefaults からファイル名を取得します

これにより、ホーム画面ウィジェットの更新時に、filename 変数が userDefaults ストレージの filename 値に設定されます。

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"
   ...
)

パスからの画像を表示する ChartImage を作成する

ChartImage ビューは、Dart 側で生成されたファイルのコンテンツから画像を作成します。ここでは、サイズをフレームの 50% に設定します。

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())
    }
   ...
}

NewsWidgetsEntryView の本文で ChartImage を使用する

NewsWidgetsEntryView の本文に ChartImage ビューを追加して、ホーム画面ウィジェットに ChartImage を表示します。

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
}

変更をテストする

変更をテストするには、Xcode から Flutter アプリ(Runner)ターゲットと拡張機能ターゲットの両方を再実行します。画像を表示するには、アプリの記事ページに移動し、ボタンを押してホーム画面ウィジェットを更新します。

33bdfe2cce908c48.png

Android コードを更新する

Android コードは iOS コードと同じように機能します。

  1. android/app/res/layout/news_widget.xml ファイルを開きます。ホーム画面ウィジェットの UI 要素が含まれています。その内容を次のコードに置き換えます。

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>

この新しいコードにより、ホーム画面ウィジェットに画像が追加され、(現時点では)汎用のスターアイコンが表示されます。このスターアイコンを、Dart コードで保存した画像に置き換えます。

  1. NewsWidget.kt ファイルを開きます。その内容を次のコードに置き換えます。

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)
        }
    }
}

この Dart コードは、filename キーを使用してスクリーンショットをローカル ストレージに保存します。また、画像のフルパスを取得し、そこから File オブジェクトを作成します。画像が存在する場合、Dart コードによってホーム画面ウィジェットの画像が新しい画像に置き換えられます。

  1. アプリを再読み込みし、記事画面に移動します。[ホーム画面を更新] を押します。ホーム画面ウィジェットにグラフが表示されます。

7. 次のステップ

お疲れさまでした

おつかれさまでした。これで、Flutter iOS アプリと Android アプリ用のホーム画面ウィジェットを作成できました。

Flutter アプリのコンテンツにリンクする

ユーザーがクリックした場所に応じて、アプリ内の特定のページにユーザーを誘導したい場合があります。たとえば、この Codelab のニュースアプリでは、表示された見出しのニュース記事をユーザーに表示させることができます。

この機能は、この Codelab の対象外です。home_widget パッケージが提供するストリームを使用して、ホーム画面ウィジェットから起動されたアプリを識別し、URL を介してホーム画面ウィジェットからメッセージを送信する例を確認できます。詳しくは、docs.flutter.dev のディープリンクに関するドキュメント をご覧ください。

バックグラウンドでウィジェットを更新する

この Codelab では、ボタンを使ってホーム画面ウィジェットの更新をトリガーしました。これはテストには妥当ですが、本番環境のコードでは、アプリでホーム画面ウィジェットをバックグラウンドで更新することをおすすめします。Workmanager プラグインを使用してバックグラウンド タスクを作成し、ホーム画面ウィジェットが必要とするリソースを更新できます。詳しくは、home_widget パッケージの [Background update] セクションをご覧ください。

iOS の場合は、ホーム画面ウィジェットでネットワーク リクエストを行って UI を更新することもできます。リクエストの条件や頻度を管理するには、タイムラインを使用します。タイムラインの使い方について詳しくは、Apple の「ウィジェットを最新の状態に保つ」をご覧ください。ご覧ください

関連情報