1. はじめに
ウィジェットとは何ですか?
Flutter デベロッパーにとって、ウィジェットの一般的な定義は、Flutter フレームワークを使用して作成された UI コンポーネントを指します。この Codelab では、ウィジェットとは、アプリを開くことなくアプリの情報を確認できるミニバージョンのアプリのことを指します。Android では、ウィジェットはホーム画面に表示されます。iOS では、ホーム画面、ロック画面、[今日] ビューに追加できます。
ウィジェットはどの程度複雑にできますか?
ほとんどのホーム画面ウィジェットはシンプルです。基本的なテキスト、シンプルなグラフィック、Android では基本的なコントロールなどを含めることができます。Android と iOS の両方で、使用できる UI コンポーネントと機能が制限されています。
ウィジェットの UI を作成する
こうした UI の制限により、Flutter フレームワークを使用してホーム画面ウィジェットの UI を直接描画することはできません。代わりに、Jetpack Compose や SwiftUI などのプラットフォーム フレームワークで作成したウィジェットを Flutter アプリに追加できます。この Codelab では、複雑な UI の書き換えを回避するためにアプリとウィジェットの間でリソースを共有する例について説明します。
作成するアプリの概要
この Codelab では、home_widget パッケージを使用して、Android と iOS の両方でシンプルな Flutter アプリ用のホーム画面ウィジェットを作成し、ユーザーが記事を閲覧できるようにします。ウィジェットの動作は次のとおりです。
- Flutter アプリのデータを表示します。
- Flutter アプリから共有されたフォント アセットを使用してテキストを表示します。
- レンダリングされた Flutter ウィジェットの画像を表示します。
この Flutter アプリには、次の 2 つの画面(ルート)があります。
- 最初の画面には、見出しと説明を含むニュース記事のリストが表示されます。
- 2 行目では、
CustomPaint
を使用して作成されたグラフとともに記事全体を表示します。
.
学習内容
- iOS と Android でホーム画面ウィジェットを作成する方法
- home_widget パッケージを使用して、ホーム画面ウィジェットと Flutter アプリ間でデータを共有する方法。
- 書き直す必要があるコードの量を減らす方法。
- Flutter アプリからホーム画面ウィジェットを更新する方法
2. 開発環境を設定する
どちらのプラットフォームにも、Flutter SDK と IDE が必要です。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 アプリにアプリ拡張機能を追加する場合と同様です。
- Flutter プロジェクト ディレクトリからターミナル ウィンドウで
open ios/Runner.xcworkspace
を実行します。または、VSCode で ios フォルダを右クリックし、[Xcode で開く] を選択します。これにより、Flutter プロジェクトのデフォルトの Xcode ワークスペースが開きます。 - メニューから [File] > [New] > [Target] を選択します。これにより、プロジェクトに新しいターゲットが追加されます。
- テンプレートのリストが表示されます。[Widget Extension] を選択します。
- 「NewsWidgets」と入力します。」をこのウィジェットの [Product Name] ボックスに入力します。[Include Live Activity] と [Include Configuration Intent] の両方のチェックボックスをオフにします。
サンプルコードを調べる
新しいターゲットを追加すると、Xcode は選択したテンプレートに基づいてサンプルコードを生成します。生成されたコードと WidgetKit について詳しくは、Apple の App Extensions に関するドキュメント
サンプル ウィジェットのデバッグとテスト
- まず、Flutter アプリの構成を更新します。Flutter アプリに新しいパッケージを追加し、Xcode からプロジェクトでターゲットを実行する場合は、これを行う必要があります。アプリの構成を更新するには、Flutter アプリのディレクトリで次のコマンドを実行します。
$ flutter build ios --config-only
- [Runner] をクリックしてターゲットのリストを表示します。作成したウィジェット ターゲットである NewsWidgets を選択し、[実行] をクリックします。iOS ウィジェット コードを変更したら、Xcode からウィジェット ターゲットを実行します。
- シミュレータまたはデバイスの画面には、基本的なホーム画面ウィジェットが表示されます。表示されない場合は、画面に追加できます。ホーム画面を長押しして、左上の [+] をクリックします。
- アプリの名前を検索します。この Codelab では、「ホーム画面ウィジェット」を検索します。
- ホーム画面ウィジェットを追加すると、時刻を示すシンプルなテキストが表示されます。
基本的な Android ウィジェットを作成する
- Android でホーム画面ウィジェットを追加するには、Android Studio でプロジェクトのビルドファイルを開きます。このファイルは android/build.gradle にあります。または、VSCode で android フォルダを右クリックして [Open in Android Studio] を選択します。
- プロジェクトがビルドされたら、左上隅でアプリのディレクトリを見つけます。新しいホーム画面ウィジェットをこのディレクトリに追加します。ディレクトリを右クリックして、[New] -> [New] を選択します。ウィジェット ->アプリ ウィジェット。
- Android Studio に新しいフォームが表示されます。クラス名、配置、サイズ、原文の言語など、ホーム画面ウィジェットに関する基本情報を追加します
この Codelab では、次の値を設定します。
- [Class Name] ボックスを NewsWidget
- [最小幅(セル数)] プルダウンを 3 に設定
- [最小の高さ(セル数)] プルダウンを 3 に設定
サンプルコードを調べる
フォームを送信すると、Android Studio によって複数のファイルが作成され、更新されます。この Codelab に関連する変更は以下の表のとおりです。
アクション | ターゲット ファイル | 変更 |
更新 |
| NewsWidget を登録する新しいレシーバを追加します。 |
作成 |
| ホーム画面ウィジェット UI を定義します。 |
作成 |
| ホーム画面ウィジェットの構成を定義します。このファイルで、ウィジェットのサイズや名前を調整できます。 |
作成 |
| ホーム画面ウィジェットに機能を追加するための Kotlin コードが含まれています。 |
これらのファイルについては、この Codelab 全体で詳しく説明します。
サンプル ウィジェットのデバッグとテスト
アプリを実行すると、ホーム画面ウィジェットが表示されます。アプリを作成したら、Android デバイスのアプリ選択画面に移動し、この Flutter プロジェクトのアイコンを長押しします。ポップアップ メニューから [ウィジェット] を選択します。
Android デバイスまたはエミュレータには、Android のデフォルトのホーム画面ウィジェットが表示されます。
4. Flutter アプリからホーム画面ウィジェットにデータを送信する
作成した基本的なホーム画面ウィジェットをカスタマイズできます。ニュース記事の見出しと概要を表示するようにホーム画面ウィジェットを更新します。次のスクリーンショットは、見出しと概要を表示するホーム画面ウィジェットの例です。
アプリとホーム画面ウィジェットの間でデータを渡すには、Dart とネイティブ コードを記述する必要があります。このセクションでは、このプロセスを 3 つのパートに分けます。
- Flutter アプリに Dart コードを記述して、Android と iOS の両方で使用できるようにする
- ネイティブ iOS 機能の追加
- ネイティブ Android 機能の追加
iOS アプリ グループを使用する
iOS の親アプリとウィジェット拡張機能の間でデータを共有するには、両方のターゲットが同じアプリグループに属している必要があります。アプリグループについて詳しくは、Apple のアプリグループに関するドキュメントをご覧ください。
バンドル ID を更新します。
Xcode で、ターゲットの設定に移動します。[署名と機能] タブで、チームとバンドル ID が設定されていることを確認します。
Xcode で、Runner ターゲットと NewsWidgetExtension ターゲットの両方にアプリグループを追加します。
[+ Capability ->] を選択します。アプリ グループ] をクリックし、新しいアプリグループを追加します。Runner(親アプリ)ターゲットとウィジェット ターゲットの両方に繰り返します。
Dart コードを追加する
iOS アプリと Android アプリは、いくつかの方法で Flutter アプリとデータを共有できます。これらのアプリと通信するには、デバイスのローカル key/value
ストアを利用します。iOS の場合は「UserDefaults
」、Android の場合は「SharedPreferences
」と呼ばれます。home_widget package はこれらの API をラップして、いずれかのプラットフォームへのデータの保存を簡素化し、ホーム画面ウィジェットが更新されたデータを pull できるようにします。
広告見出しと説明文のデータは、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)
}
}
}
前のコードの Provider
は TimelineProvider
に準拠しています。Provider
には次の 3 つのメソッドがあります。
placeholder
メソッドは、ユーザーがホーム画面ウィジェットを初めてプレビューしたときにプレースホルダ エントリを生成します。
getSnapshot
メソッドは、ユーザーのデフォルトからデータを読み取り、現在の時刻のエントリを生成します。getTimeline
メソッドはタイムライン エントリを返します。これは、コンテンツを更新するタイミングが予測しやすい場合に役立ちます。この Codelab では、getSnapshot 関数を使用して現在の状態を取得します。.atEnd
メソッドは、現在の時間が経過した後にデータを更新するようホーム画面ウィジェットに指示します。
NewsWidgets_Previews
をコメントアウトします。
プレビューの使用はこの Codelab の対象外です。SwiftUI のホーム画面ウィジェットのプレビューについて詳しくは、デバッグ ウィジェットに関する Apple のドキュメントをご覧ください。
すべてのファイルを保存し、アプリとウィジェットのターゲットを再実行します。
ターゲットを再度実行して、アプリとホーム画面ウィジェットが機能することを確認します。
- Xcode でアプリスキーマを選択して、アプリ ターゲットを実行します。
- Xcode で拡張機能スキーマを選択して、拡張機能ターゲットを実行します。
- アプリの記事ページに移動します。
- ボタンをクリックすると見出しが更新されます。ホーム画面ウィジェットでも見出しが更新されます。
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
を使用します。ホーム画面ウィジェットが記事のタイトルに更新されます。
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)
}
}
...
}
ホーム画面ウィジェットを実行すると、次の画像のように見出しにカスタム フォントが使用されるようになりました。
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(),
),
...
}
- ウィジェットを画像として保存します
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
ファイルを開いて、次の変更を行います。
filename
と displaySize
を NewsArticleEntry
構造体に追加する
filename
プロパティは、画像ファイルのパスを表す文字列を保持します。displaySize
プロパティは、ユーザーのデバイスのホーム画面ウィジェットのサイズを保持します。ホーム画面ウィジェットのサイズは context
から取得されます。
ios/NewsWidgets/NewsWidgets.swift
struct NewsArticleEntry: TimelineEntry {
...
// New: add the filename and displaySize.
let filename: String
let displaySize: CGSize
}
placeholder
関数を更新する
プレースホルダ filename
と 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)
}
getSnapshot で userDefaults
からファイル名を取得します
これにより、ホーム画面ウィジェットの更新時に、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)ターゲットと拡張機能ターゲットの両方を再実行します。画像を表示するには、アプリの記事ページに移動し、ボタンを押してホーム画面ウィジェットを更新します。
Android コードを更新する
Android コードは iOS コードと同じように機能します。
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 コードで保存した画像に置き換えます。
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 コードによってホーム画面ウィジェットの画像が新しい画像に置き換えられます。
- アプリを再読み込みし、記事画面に移動します。[ホーム画面を更新] を押します。ホーム画面ウィジェットにグラフが表示されます。
7. 次のステップ
お疲れさまでした
おつかれさまでした。これで、Flutter iOS アプリと Android アプリ用のホーム画面ウィジェットを作成できました。
Flutter アプリのコンテンツにリンクする
ユーザーがクリックした場所に応じて、アプリ内の特定のページにユーザーを誘導したい場合があります。たとえば、この Codelab のニュースアプリでは、表示された見出しのニュース記事をユーザーに表示させることができます。
この機能は、この Codelab の対象外です。home_widget パッケージが提供するストリームを使用して、ホーム画面ウィジェットから起動されたアプリを識別し、URL を介してホーム画面ウィジェットからメッセージを送信する例を確認できます。詳しくは、docs.flutter.dev のディープリンクに関するドキュメント をご覧ください。
バックグラウンドでウィジェットを更新する
この Codelab では、ボタンを使ってホーム画面ウィジェットの更新をトリガーしました。これはテストには妥当ですが、本番環境のコードでは、アプリでホーム画面ウィジェットをバックグラウンドで更新することをおすすめします。Workmanager プラグインを使用してバックグラウンド タスクを作成し、ホーム画面ウィジェットが必要とするリソースを更新できます。詳しくは、home_widget パッケージの [Background update] セクションをご覧ください。
iOS の場合は、ホーム画面ウィジェットでネットワーク リクエストを行って UI を更新することもできます。リクエストの条件や頻度を管理するには、タイムラインを使用します。タイムラインの使い方について詳しくは、Apple の「ウィジェットを最新の状態に保つ」をご覧ください。ご覧ください