1. Giới thiệu
Tiện ích là gì?
Đối với nhà phát triển Flutter, định nghĩa chung về tiện ích đề cập đến các thành phần giao diện người dùng được tạo bằng khung Flutter. Trong bối cảnh của lớp học lập trình này, tiện ích đề cập đến một phiên bản thu nhỏ của ứng dụng, cung cấp thông tin về ứng dụng mà không cần mở ứng dụng. Trên Android, tiện ích nằm trên màn hình chính. Trên iOS, bạn có thể thêm các tiện ích này vào màn hình chính, màn hình khoá hoặc chế độ xem hôm nay.

Tiện ích có thể phức tạp đến mức nào?
Hầu hết các tiện ích trên Màn hình chính đều đơn giản. Chúng có thể chứa văn bản cơ bản, đồ hoạ đơn giản hoặc các chế độ điều khiển cơ bản trên Android. Cả Android và iOS đều giới hạn những thành phần và tính năng giao diện người dùng mà bạn có thể sử dụng.

Tạo giao diện người dùng cho Tiện ích
Do những hạn chế này về giao diện người dùng, bạn không thể vẽ trực tiếp giao diện người dùng của một tiện ích Màn hình chính bằng khung Flutter. Thay vào đó, bạn có thể thêm các tiện ích được tạo bằng các khung nền tảng như Jetpack Compose hoặc SwiftUI vào ứng dụng Flutter. Lớp học lập trình này thảo luận về các ví dụ để chia sẻ tài nguyên giữa ứng dụng và các tiện ích nhằm tránh viết lại giao diện người dùng phức tạp.
Sản phẩm bạn sẽ tạo ra
Trong lớp học lập trình này, bạn sẽ tạo các tiện ích Màn hình chính trên cả Android và iOS cho một ứng dụng Flutter đơn giản bằng cách sử dụng gói home_widget. Ứng dụng này cho phép người dùng đọc các bài viết. Các tiện ích của bạn sẽ:
- Hiển thị dữ liệu từ ứng dụng Flutter của bạn.
- Hiển thị văn bản bằng các thành phần phông chữ được chia sẻ từ ứng dụng Flutter.
- Hiển thị hình ảnh của một tiện ích Flutter được kết xuất.

Ứng dụng Flutter này có 2 màn hình (hoặc tuyến đường):
- Màn hình đầu tiên hiển thị danh sách các bài viết tin tức có dòng tiêu đề và nội dung mô tả.
- Thứ hai hiển thị toàn bộ bài viết có biểu đồ được tạo bằng
CustomPaint.
.

Kiến thức bạn sẽ học được
- Cách tạo tiện ích trên màn hình chính trên iOS và Android.
- Cách sử dụng gói home_widget để chia sẻ dữ liệu giữa tiện ích trên Màn hình chính và ứng dụng Flutter.
- Cách giảm lượng mã bạn cần viết lại.
- Cách cập nhật tiện ích Màn hình chính trong ứng dụng Flutter.
2. Thiết lập môi trường phát triển
Đối với cả hai nền tảng, bạn cần có Flutter SDK và một IDE. Bạn có thể sử dụng IDE mà mình muốn để làm việc với Flutter. Đó có thể là Visual Studio Code có các tiện ích Dart Code và Flutter, hoặc Android Studio hoặc IntelliJ có các trình bổ trợ Flutter và Dart đã cài đặt.
Cách tạo tiện ích Màn hình chính trên iOS:
- Bạn có thể chạy lớp học lập trình này trên một thiết bị iOS thực hoặc trình mô phỏng iOS.
- Bạn phải định cấu hình hệ thống macOS bằng Xcode IDE. Thao tác này sẽ cài đặt trình biên dịch cần thiết để tạo phiên bản iOS của ứng dụng.
Cách tạo tiện ích Màn hình chính của Android:
- Bạn có thể chạy lớp học lập trình này trên một thiết bị Android thực hoặc trình mô phỏng Android.
- Bạn phải định cấu hình hệ thống phát triển bằng Android Studio. Thao tác này sẽ cài đặt trình biên dịch cần thiết để tạo phiên bản Android của ứng dụng.
Tải mã nguồn ban đầu
Tải phiên bản ban đầu của dự án xuống từ GitHub
Từ dòng lệnh, hãy sao chép kho lưu trữ GitHub vào một thư mục flutter-codelabs:
$ git clone https://github.com/flutter/codelabs.git flutter-codelabs
Sau khi sao chép kho lưu trữ, bạn có thể tìm thấy mã cho lớp học lập trình này trong thư mục flutter-codelabs/homescreen_codelab. Thư mục này chứa mã dự án đã hoàn tất cho từng bước trong lớp học lập trình.
Mở ứng dụng khởi đầu
Mở thư mục flutter-codelabs/homescreen_codelab/step_03 trong IDE mà bạn muốn.
Cài đặt gói
Tất cả các gói bắt buộc đã được thêm vào tệp pubspec.yaml của dự án. Để truy xuất các phần phụ thuộc của dự án, hãy chạy lệnh sau:
$ flutter pub get
3. Thêm tiện ích cơ bản vào Màn hình chính
Trước tiên, hãy thêm tiện ích Màn hình chính bằng công cụ nền tảng gốc.
Tạo một tiện ích cơ bản cho Màn hình chính trên iOS
Việc thêm một tiện ích ứng dụng vào ứng dụng iOS Flutter cũng tương tự như việc thêm một tiện ích ứng dụng vào ứng dụng SwiftUI hoặc UIKit:
- Chạy
open ios/Runner.xcworkspacetrong cửa sổ dòng lệnh từ thư mục dự án Flutter. Ngoài ra, bạn có thể nhấp chuột phải vào thư mục ios trong VSCode rồi chọn Open in Xcode (Mở trong Xcode). Thao tác này sẽ mở không gian làm việc Xcode mặc định trong dự án Flutter của bạn. - Chọn File (Tệp) → New (Mới) → Target (Mục tiêu) trong trình đơn. Thao tác này sẽ thêm một mục tiêu mới vào dự án.
- Một danh sách các mẫu sẽ xuất hiện. Chọn Tiện ích Widget.
- Nhập "NewsWidgets" vào hộp Product Name (Tên sản phẩm) cho tiện ích này. Xoá cả hộp đánh dấu Bao gồm Hoạt động trực tiếp và Bao gồm Ý định về cấu hình.
Kiểm tra mã mẫu
Khi bạn thêm một mục tiêu mới, Xcode sẽ tạo mã mẫu dựa trên mẫu mà bạn đã chọn. Để biết thêm thông tin về mã được tạo và WidgetKit, hãy xem tài liệu về tiện ích ứng dụng của Apple.
Gỡ lỗi và kiểm thử tiện ích mẫu
- Trước tiên, hãy cập nhật cấu hình của ứng dụng Flutter. Bạn phải thực hiện việc này khi thêm các gói mới vào ứng dụng Flutter và dự định chạy một mục tiêu trong dự án từ Xcode. Để cập nhật cấu hình của ứng dụng, hãy chạy lệnh sau trong thư mục ứng dụng Flutter:
$ flutter build ios --config-only
- Nhấp vào Runner (Người chạy) để xem danh sách các mục tiêu. Chọn mục tiêu của tiện ích mà bạn vừa tạo (NewsWidgets) rồi nhấp vào Run (Chạy). Chạy đích đến của tiện ích từ Xcode khi bạn thay đổi mã tiện ích iOS.

- Màn hình trình mô phỏng hoặc thiết bị phải hiển thị một tiện ích Màn hình chính cơ bản. Nếu không thấy, bạn có thể thêm tiện ích này vào màn hình. Nhấp và giữ trên màn hình chính, sau đó nhấp vào biểu tượng + ở góc trên cùng bên trái.

- Tìm tên của ứng dụng. Trong lớp học lập trình này, hãy tìm "Homescreen Widgets" (Tiện ích màn hình chính)

- Sau khi bạn thêm tiện ích Màn hình chính, tiện ích này sẽ hiển thị văn bản đơn giản cho biết thời gian.
Tạo một tiện ích Android cơ bản
- Để thêm một tiện ích Màn hình chính trong Android, hãy mở tệp bản dựng của dự án trong Android Studio. Bạn có thể tìm thấy tệp này tại android/build.gradle. Ngoài ra, bạn có thể nhấp chuột phải vào thư mục android trong VSCode rồi chọn Open in Android Studio (Mở trong Android Studio).
- Sau khi dự án được tạo, hãy tìm thư mục ứng dụng ở góc trên cùng bên trái. Thêm tiện ích Màn hình chính mới vào thư mục này. Nhấp chuột phải vào thư mục, chọn New -> Widget -> App Widget (Mới -> Tiện ích -> Tiện ích ứng dụng).

- Android Studio sẽ hiển thị một biểu mẫu mới. Thêm thông tin cơ bản về tiện ích Màn hình chính, bao gồm tên lớp, vị trí, kích thước và ngôn ngữ gốc
Đối với lớp học lập trình này, hãy đặt các giá trị sau:
- hộp Class Name (Tên lớp) thành NewsWidget
- Chiều rộng tối thiểu (ô) thành 3
- Chiều cao tối thiểu (ô) thành 3
Kiểm tra mã mẫu
Khi bạn gửi biểu mẫu, Android Studio sẽ tạo và cập nhật một số tệp. Các thay đổi liên quan đến lớp học lập trình này được liệt kê trong bảng dưới đây
Hành động | Tệp đích | Chuyển |
Cập nhật |
| Thêm một receiver mới để đăng ký NewsWidget. |
Tạo |
| Xác định giao diện người dùng của tiện ích trên Màn hình chính. |
Tạo |
| Xác định cấu hình tiện ích Màn hình chính. Bạn có thể điều chỉnh kích thước hoặc tên của tiện ích trong tệp này. |
Tạo |
| Chứa mã Kotlin của bạn để thêm chức năng vào tiện ích Màn hình chính. |
Bạn có thể tìm thêm thông tin chi tiết về các tệp này trong suốt lớp học lập trình này.
Gỡ lỗi và kiểm thử tiện ích mẫu
Bây giờ, hãy chạy ứng dụng và xem tiện ích Màn hình chính. Sau khi bạn tạo ứng dụng, hãy chuyển đến màn hình chọn ứng dụng của thiết bị Android rồi nhấn và giữ biểu tượng của dự án Flutter này. Chọn Tiện ích trong trình đơn bật lên.

Thiết bị Android hoặc trình mô phỏng sẽ hiển thị tiện ích Màn hình chính mặc định cho Android.
4. Gửi dữ liệu từ ứng dụng Flutter đến tiện ích Màn hình chính
Bạn có thể tuỳ chỉnh tiện ích cơ bản trên Màn hình chính mà bạn đã tạo. Cập nhật tiện ích Màn hình chính để hiển thị tiêu đề và nội dung tóm tắt cho một tin bài. Ảnh chụp màn hình sau đây cho thấy ví dụ về tiện ích Màn hình chính hiển thị tiêu đề và nội dung tóm tắt.

Để truyền dữ liệu giữa ứng dụng và tiện ích Màn hình chính, bạn cần viết mã Dart và mã gốc. Phần này chia quy trình này thành 3 phần:
- Viết mã Dart trong ứng dụng Flutter mà cả Android và iOS đều có thể sử dụng
- Thêm chức năng gốc của iOS
- Thêm chức năng Android gốc
Sử dụng Nhóm ứng dụng iOS
Để chia sẻ dữ liệu giữa một ứng dụng gốc iOS và một tiện ích, cả hai mục tiêu phải thuộc cùng một nhóm ứng dụng. Để tìm hiểu thêm về nhóm ứng dụng, hãy xem tài liệu về nhóm ứng dụng của Apple.
Cập nhật mã nhận dạng gói:
Trong Xcode, hãy chuyển đến phần cài đặt của mục tiêu. Trong thẻ Signing & Capabilities (Ký và các chức năng), hãy kiểm tra để đảm bảo rằng bạn đã thiết lập nhóm và mã nhận dạng gói.
Thêm Nhóm ứng dụng vào cả mục tiêu Runner và mục tiêu NewsWidgetExtension trong Xcode:
Chọn + Capability -> App Groups (Khả năng > Nhóm ứng dụng) rồi thêm một Nhóm ứng dụng mới. Lặp lại cho cả mục tiêu Runner (ứng dụng mẹ) và mục tiêu tiện ích.

Thêm mã Dart
Cả ứng dụng iOS và Android đều có thể chia sẻ dữ liệu với ứng dụng Flutter theo một số cách.Để giao tiếp với các ứng dụng này, hãy tận dụng bộ nhớ key/value cục bộ của thiết bị. iOS gọi bộ nhớ này là UserDefaults và Android gọi bộ nhớ này là SharedPreferences. Gói home_widget bao bọc các API này để đơn giản hoá việc lưu dữ liệu vào một trong hai nền tảng và cho phép các tiện ích trên Màn hình chính kéo dữ liệu mới cập nhật.

Dữ liệu về dòng tiêu đề và nội dung mô tả được lấy từ tệp news_data.dart. Tệp này chứa dữ liệu mô phỏng và một lớp dữ liệu 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,
});
}
Cập nhật các giá trị của dòng tiêu đề và nội dung mô tả
Để thêm chức năng cập nhật tiện ích Màn hình chính từ ứng dụng Flutter, hãy chuyển đến tệp lib/home_screen.dart. Thay thế nội dung của tệp bằng đoạn mã sau. Sau đó, hãy thay thế <YOUR APP GROUP> bằng giá trị nhận dạng cho Nhóm ứng dụng của bạn.
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);
},
),
);
},
);
},
));
}
}
Hàm updateHeadline lưu các cặp khoá/giá trị vào bộ nhớ cục bộ của thiết bị. Khoá headline_title giữ giá trị của newHeadline.title. Khoá headline_description giữ giá trị của newHeadline.description. Hàm này cũng thông báo cho nền tảng gốc rằng có thể truy xuất và kết xuất dữ liệu mới cho các tiện ích trên Màn hình chính.
Sửa đổi floatingActionButton
Gọi hàm updateHeadline khi người dùng nhấn vào floatingActionButton như minh hoạ:
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'),
),
...
Với thay đổi này, khi người dùng nhấn vào nút Cập nhật tiêu đề trên trang bài viết, thông tin chi tiết về tiện ích Màn hình chính sẽ được cập nhật.
Cập nhật mã iOS để hiển thị dữ liệu bài viết
Để cập nhật tiện ích Màn hình chính cho iOS, hãy dùng Xcode.
Mở tệp NewsWidgets.swift trong Xcode:
Định cấu hình TimelineEntry.
Thay thế cấu trúc SimpleEntry bằng mã sau:
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
}
Cấu trúc NewsArticleEntry này xác định dữ liệu đến để truyền vào tiện ích Màn hình chính khi được cập nhật. Loại TimelineEntry yêu cầu phải có tham số ngày.Để tìm hiểu thêm về giao thức TimelineEntry, hãy xem tài liệu TimelineEntry của Apple.
Chỉnh sửa View hiển thị nội dung
Sửa đổi tiện ích Màn hình chính để hiển thị dòng tiêu đề và nội dung mô tả của tin bài thay vì ngày. Để hiển thị văn bản trong SwiftUI, hãy dùng khung hiển thị Text. Để xếp chồng các khung hiển thị lên nhau trong SwiftUI, hãy sử dụng khung hiển thị VStack.
Thay thế khung hiển thị NewsWidgetEntryView đã tạo bằng đoạn mã sau:
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)
}
}
}
Chỉnh sửa trình cung cấp để cho tiện ích Màn hình chính biết thời điểm và cách cập nhật
Thay thế Provider hiện có bằng mã sau. Sau đó, thay thế mã nhận dạng nhóm ứng dụng của bạn cho <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)
}
}
}
Provider trong mã trước tuân theo TimelineProvider. Provider có 3 phương thức:
- Phương thức
placeholdersẽ tạo một mục nhập giữ chỗ khi người dùng xem trước tiện ích Màn hình chính lần đầu tiên.

- Phương thức
getSnapshotđọc dữ liệu từ các giá trị mặc định của người dùng và tạo mục nhập cho thời gian hiện tại. - Phương thức
getTimelinetrả về các mục trên dòng thời gian. Điều này sẽ hữu ích khi bạn có những thời điểm dự đoán được để cập nhật nội dung. Lớp học lập trình này sử dụng hàm getSnapshot để lấy trạng thái hiện tại. Phương thức.atEndyêu cầu tiện ích Màn hình chính làm mới dữ liệu sau khi thời gian hiện tại trôi qua.
Thêm dấu chú thích vào NewsWidgets_Previews
Việc sử dụng bản xem trước nằm ngoài phạm vi của lớp học lập trình này. Để biết thêm thông tin về cách xem trước các tiện ích SwiftUI trên Màn hình chính, hãy xem Tài liệu của Apple về gỡ lỗi tiện ích.
Lưu tất cả các tệp và chạy lại mục tiêu ứng dụng và tiện ích.
Chạy lại các mục tiêu để xác thực rằng ứng dụng và tiện ích Màn hình chính hoạt động.
- Chọn giản đồ ứng dụng trong Xcode để chạy mục tiêu ứng dụng.
- Chọn giản đồ tiện ích trong Xcode để chạy mục tiêu tiện ích.
- Chuyển đến một trang bài viết trong ứng dụng.
- Nhấp vào nút này để cập nhật tiêu đề. Tiện ích Màn hình chính cũng sẽ cập nhật dòng tiêu đề.
Cập nhật mã Android
Thêm XML tiện ích Màn hình chính.
Trong Android Studio, hãy cập nhật các tệp được tạo ở bước trước.Mở tệp res/layout/news_widget.xml. Tệp này xác định cấu trúc và bố cục của tiện ích trên màn hình chính. Chọn Code (Mã) ở góc trên cùng bên phải rồi thay thế nội dung của tệp đó bằng mã sau:
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 này xác định 2 khung hiển thị văn bản, một cho dòng tiêu đề bài viết và một cho nội dung mô tả bài viết. Các khung hiển thị văn bản này cũng xác định kiểu. Bạn sẽ quay lại tệp này trong suốt lớp học lập trình này.
Cập nhật chức năng của NewsWidget
Mở tệp mã nguồn NewsWidget.kt Kotlin. Tệp này chứa một lớp được tạo có tên là NewsWidget, lớp này mở rộng lớp AppWidgetProvider.
Lớp NewsWidget chứa 3 phương thức từ lớp cha. Bạn sẽ sửa đổi phương thức onUpdate. Android gọi phương thức này cho các tiện ích theo khoảng thời gian cố định.
Thay thế nội dung của tệp NewsWidget.kt bằng đoạn mã sau:
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)
}
}
}
Giờ đây, khi onUpdate được gọi, Android sẽ lấy các giá trị mới nhất từ bộ nhớ cục bộ bằng phương thức the widgetData.getString(), sau đó gọi setTextViewText để thay đổi văn bản hiển thị trên tiện ích Màn hình chính.
Kiểm thử các bản cập nhật
Kiểm thử ứng dụng để đảm bảo tiện ích trên Màn hình chính cập nhật dữ liệu mới. Để cập nhật dữ liệu, hãy sử dụng Cập nhật màn hình chính FloatingActionButton trên các trang bài viết. Tiện ích Màn hình chính sẽ cập nhật tiêu đề bài viết.

5. Sử dụng phông chữ tuỳ chỉnh của ứng dụng Flutter trong tiện ích Màn hình chính trên iOS
Cho đến nay, bạn đã định cấu hình tiện ích Màn hình chính để đọc dữ liệu mà ứng dụng Flutter cung cấp. Ứng dụng Flutter có một phông chữ tuỳ chỉnh mà bạn có thể muốn dùng trong tiện ích Màn hình chính. Bạn có thể sử dụng phông chữ tuỳ chỉnh trong tiện ích Màn hình chính trên iOS. Bạn không thể dùng phông chữ tuỳ chỉnh trong tiện ích Màn hình chính trên Android.
Cập nhật mã iOS
Flutter lưu trữ các thành phần của mình trong mainBundle của ứng dụng iOS. Bạn có thể truy cập vào các thành phần trong gói này từ mã tiện ích Màn hình chính.
Trong cấu trúc NewsWidgetsEntryView trong tệp NewsWidgets.swift, hãy thực hiện các thay đổi sau
Tạo một hàm trợ giúp để lấy đường dẫn đến thư mục thành phần 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
}
...
}
Đăng ký phông chữ bằng URL đến tệp phông chữ tuỳ chỉnh của bạn.
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)
}
...
}
Cập nhật view văn bản tiêu đề để sử dụng phông chữ tuỳ chỉnh của bạn.
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)
}
}
...
}
Khi bạn chạy tiện ích Màn hình chính, tiện ích này sẽ sử dụng phông chữ tuỳ chỉnh cho tiêu đề như trong hình ảnh sau:

6. Hiển thị các tiện ích Flutter dưới dạng hình ảnh
Trong phần này, bạn sẽ hiển thị một biểu đồ từ ứng dụng Flutter dưới dạng tiện ích trên Màn hình chính.
Tiện ích này mang đến một thử thách lớn hơn so với văn bản mà bạn đã hiển thị trên màn hình chính. Việc hiển thị biểu đồ Flutter dưới dạng hình ảnh sẽ dễ dàng hơn nhiều so với việc cố gắng tạo lại biểu đồ đó bằng các thành phần giao diện người dùng gốc.
Lập trình tiện ích Màn hình chính để kết xuất biểu đồ Flutter dưới dạng tệp PNG. Tiện ích Màn hình chính có thể hiển thị hình ảnh đó.
Viết mã Dart
Ở phía Dart, hãy thêm phương thức renderFlutterWidget từ gói home_widget. Phương thức này sử dụng một tiện ích, một tên tệp và một khoá. Phương thức này trả về hình ảnh của tiện ích Flutter và lưu hình ảnh đó vào một vùng chứa dùng chung. Cung cấp tên hình ảnh trong mã của bạn và đảm bảo tiện ích Màn hình chính có thể truy cập vào vùng chứa. key sẽ lưu đường dẫn đầy đủ của tệp dưới dạng một chuỗi trong bộ nhớ cục bộ của thiết bị. Điều này cho phép tiện ích Màn hình chính tìm thấy tệp nếu tên thay đổi trong mã Dart.
Đối với lớp học lập trình này, lớp LineChart trong tệp lib/article_screen.dart đại diện cho biểu đồ. Phương thức tạo của lớp này trả về một CustomPainter vẽ biểu đồ này lên màn hình.
Để triển khai tính năng này, hãy mở tệp lib/article_screen.dart. Nhập gói home_widget. Tiếp theo, hãy thay thế mã trong lớp _ArticleScreenState bằng mã sau:
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!),
],
),
);
}
}
Ví dụ này thực hiện 3 thay đổi đối với lớp _ArticleScreenState.
Tạo một GlobalKey
GlobalKey sẽ lấy ngữ cảnh của tiện ích cụ thể, cần thiết để lấy kích thước của tiện ích đó .
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
// New: add this GlobalKey
final _globalKey = GlobalKey();
...
}
Thêm imagePath
Thuộc tính imagePath lưu trữ vị trí của hình ảnh nơi tiện ích Flutter được kết xuất.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
// New: add this imagePath
String? imagePath;
...
}
Thêm khoá vào tiện ích để hiển thị
_globalKey chứa tiện ích Flutter được kết xuất thành hình ảnh. Trong trường hợp này, tiện ích Flutter là Center chứa LineChart.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
Center(
// New: Add this key
key: _globalKey,
child: const LineChart(),
),
...
}
- Lưu tiện ích dưới dạng hình ảnh
Phương thức renderFlutterWidget được gọi khi người dùng nhấp vào floatingActionButton. Phương thức này lưu tệp PNG kết quả dưới dạng "ảnh chụp màn hình" vào thư mục vùng chứa dùng chung. Phương thức này cũng lưu đường dẫn đầy đủ đến hình ảnh dưới dạng khoá tên tệp trong bộ nhớ trên thiết bị.
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);
},
...
}
Cập nhật mã iOS
Đối với iOS, hãy cập nhật mã để lấy đường dẫn tệp từ bộ nhớ và hiển thị tệp dưới dạng hình ảnh bằng SwiftUI.
Mở tệp NewsWidgets.swift để thực hiện các thay đổi sau:
Thêm filename và displaySize vào struct NewsArticleEntry
Thuộc tính filename chứa chuỗi đại diện cho đường dẫn đến tệp hình ảnh. Thuộc tính displaySize lưu giữ kích thước của tiện ích Màn hình chính trên thiết bị của người dùng. Kích thước của tiện ích trên Màn hình chính là kích thước của context.
ios/NewsWidgets/NewsWidgets.swift
struct NewsArticleEntry: TimelineEntry {
...
// New: add the filename and displaySize.
let filename: String
let displaySize: CGSize
}
Cập nhật hàm placeholder
Thêm phần giữ chỗ filename và 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)
}
Lấy tên tệp từ userDefaults trong getSnapshot
Thao tác này đặt biến filename thành giá trị filename trong bộ nhớ userDefaults khi tiện ích Màn hình chính cập nhật.
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"
...
)
Tạo ChartImage hiển thị hình ảnh từ một đường dẫn
Thành phần hiển thị ChartImage tạo một hình ảnh từ nội dung của tệp được tạo ở phía Dart. Ở đây, bạn đặt kích thước thành 50% khung hình.
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())
}
...
}
Sử dụng ChartImage trong nội dung của NewsWidgetsEntryView
Thêm khung hiển thị ChartImage vào nội dung của NewsWidgetsEntryView để hiển thị ChartImage trong tiện ích Màn hình chính.
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
}
Kiểm thử các thay đổi
Để kiểm thử các thay đổi, hãy chạy lại cả mục tiêu ứng dụng Flutter (Runner) và mục tiêu tiện ích của bạn từ Xcode. Để xem hình ảnh, hãy chuyển đến một trong các trang bài viết trong ứng dụng rồi nhấn vào nút để cập nhật tiện ích Màn hình chính.

Cập nhật mã Android
Mã Android có chức năng tương tự như mã iOS.
- Mở tệp
android/app/res/layout/news_widget.xml. Thư mục này chứa các phần tử giao diện người dùng của tiện ích Màn hình chính. Thay thế nội dung của tệp bằng đoạn mã sau:
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>
Đoạn mã mới này sẽ thêm một hình ảnh vào tiện ích Màn hình chính, (hiện tại) tiện ích này sẽ hiển thị một biểu tượng ngôi sao chung. Thay thế biểu tượng ngôi sao này bằng hình ảnh mà bạn đã lưu trong mã Dart.
- Mở tệp
NewsWidget.kt. Thay thế nội dung của tệp bằng đoạn mã sau:
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)
}
}
}
Mã Dart này lưu ảnh chụp màn hình vào bộ nhớ cục bộ bằng khoá filename. Thao tác này cũng lấy đường dẫn đầy đủ của hình ảnh và tạo một đối tượng File từ đường dẫn đó. Nếu hình ảnh đó tồn tại, mã Dart sẽ thay thế hình ảnh trong tiện ích Màn hình chính bằng hình ảnh mới.
- Tải lại ứng dụng rồi chuyển đến màn hình bài viết. Nhấn vào Cập nhật màn hình chính. Tiện ích Màn hình chính sẽ hiển thị biểu đồ.
7. Các bước tiếp theo
Xin chúc mừng!
Xin chúc mừng, bạn đã tạo thành công các tiện ích Màn hình chính cho ứng dụng Flutter iOS và Android!
Liên kết đến nội dung trong ứng dụng Flutter
Bạn có thể muốn đưa người dùng đến một trang cụ thể trong ứng dụng, tuỳ thuộc vào vị trí mà người dùng nhấp vào. Ví dụ: trong ứng dụng tin tức của lớp học lập trình này, bạn có thể muốn người dùng xem tin bài cho tiêu đề đã hiển thị.
Tính năng này nằm ngoài phạm vi của lớp học lập trình này. Bạn có thể tìm thấy các ví dụ về cách sử dụng luồng do gói home_widget cung cấp để xác định các lần khởi chạy ứng dụng từ tiện ích Màn hình chính và gửi thông báo từ tiện ích Màn hình chính thông qua URL. Để tìm hiểu thêm, hãy xem tài liệu về tính năng liên kết sâu trên docs.flutter.dev.
Cập nhật tiện ích trong nền
Trong lớp học lập trình này, bạn đã kích hoạt quá trình cập nhật tiện ích Màn hình chính bằng một nút. Mặc dù điều này là hợp lý cho việc kiểm thử, nhưng trong mã sản xuất, bạn có thể muốn ứng dụng của mình cập nhật tiện ích Màn hình chính ở chế độ nền. Bạn có thể dùng trình bổ trợ workmanager để tạo các tác vụ ở chế độ nền nhằm cập nhật những tài nguyên mà tiện ích Màn hình chính cần. Để tìm hiểu thêm, hãy xem phần Cập nhật trong nền trong gói home_widget.
Đối với iOS, bạn cũng có thể yêu cầu tiện ích Màn hình chính thực hiện một yêu cầu mạng để cập nhật giao diện người dùng. Để kiểm soát các điều kiện hoặc tần suất của yêu cầu đó, hãy sử dụng Dòng thời gian. Để tìm hiểu thêm về cách sử dụng Dòng thời gian, hãy xem tài liệu "Luôn cập nhật tiện ích" của Apple.