1. Giới thiệu
Tiện ích là gì?
Đối với các nhà phát triển Flutter, định nghĩa phổ biến về tiện ích là 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 là một phiên bản thu nhỏ của một ứng dụng cung cấp khung hiển thị thông tin về ứng dụng mà không cần mở ứng dụng. Trên Android, các tiện ích nằm trên màn hình chính. Trên iOS, bạn có thể thêm ảnh và video vào màn hình chính, màn hình khoá hoặc chế độ xem hôm nay.
Một 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. Các ứng dụng này có thể chứa văn bản cơ bản, đồ hoạ đơn giản hoặc trên Android, các chế độ điều khiển cơ bản. 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 các hạn chế về giao diện người dùng này, bạn không thể trực tiếp vẽ giao diện người dùng của tiện ích trên 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 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 các ví dụ về cách chia sẻ tài nguyên giữa ứng dụng và tiện ích để 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 trên 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. Đây là gói 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 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 bao gồm hai màn hình (hoặc tuyến):
- Phần đầu tiên cho thấy danh sách các tin bài đi kèm với tiêu đề và nội dung mô tả.
- Cửa sổ thứ hai hiển thị toàn bộ bài viết với một 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ã cần viết lại.
- Cách cập nhật tiện ích Màn hình chính từ ứ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à IDE. Bạn có thể dùng IDE ưa thích để làm việc với Flutter. Đó có thể là Visual Studio Code với các tiện ích của Flutter Code và Flutter, hoặc Android Studio hay IntelliJ khi đã cài đặt các trình bổ trợ Flutter và Dart.
Cách tạo tiện ích Màn hình chính của 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 IDE Xcode. 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 trên 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 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 đoạn 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 thành 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 trên 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 tiện ích cơ bản trên Màn hình chính của iOS
Việc thêm tiện ích ứng dụng vào ứng dụng Flutter dành cho iOS cũng tương tự như việc thêm tiện ích ứng dụng vào một ứng dụng SwiftUI hoặc UIKit:
- Chạy
open ios/Runner.xcworkspace
trong 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 → New → Target (Tệp → Mới) trên trình đơn. Thao tác này sẽ thêm mục tiêu mới vào dự án.
- Một danh sách mẫu sẽ xuất hiện. Chọn Tiện ích mở rộng.
- Nhập "NewsWidgets" vào hộp Product Name (Tên sản phẩm) cho tiện ích này. Bỏ đánh dấu cả hộp kiểm Bao gồm Hoạt động trực tiếp và Bao gồm ý định cấu hình.
Kiểm tra mã mẫu
Khi bạn thêm mục tiêu mới, Xcode sẽ tạo mã mẫu dựa trên mẫu bạn đã chọn. Để biết thêm thông tin về mã đã 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 Trình chạy để hiển thị danh sách mục tiêu. Chọn mục tiêu tiện ích mà bạn vừa tạo, NewsWidgets rồi nhấp vào Run (Chạy). Chạy mục tiêu tiện ích từ Xcode khi bạn thay đổi mã tiện ích iOS.
- Trình mô phỏng hoặc màn hình thiết bị sẽ hiển thị tiện ích cơ bản trên Màn hình chính. Nếu không thấy, bạn có thể thêm vào màn hình. Nhấp và giữ màn hình chính rồi nhấp vào dấu + ở góc trên cùng bên trái.
- Tìm tên ứng dụng. Đối với lớp học lập trình này, hãy tìm "Tiện ích trên 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 tiện ích Android cơ bản
- Để thêm 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 tạo xong, 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ới trên Màn hình chính vào thư mục này. Nhấp chuột phải vào thư mục đó, chọn New (Mới) -> Tiện ích -> Tiện ích ứng dụng.
- Android Studio hiện một biểu mẫu mới. Thêm thông tin cơ bản về tiện ích trên Màn hình chính, bao gồm tên lớp, vị trí, kích thước và ngôn ngữ nguồn
Đối với lớp học lập trình này, hãy thiết lập các giá trị sau:
- Hộp Class Name (Tên lớp) thành NewsWidget
- Trình đơn thả xuống Chiều rộng tối thiểu (ô) thành 3
- Trình đơn thả xuống 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 này, Android Studio sẽ tạo và cập nhật một số tệp. Những 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 trên Màn hình chính. Bạn có thể điều chỉnh kích thước hoặc tên tiện ích trong tệp này. |
Tạo |
| Chứa mã Kotlin để thêm chức năng vào tiện ích trên Màn hình chính. |
Bạn có thể tìm thêm thông tin về các tệp này trong 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 của bạn và xem tiện ích Màn hình chính. Sau khi tạo ứng dụng, hãy chuyển đến màn hình lựa 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.
Trình mô phỏng hoặc thiết bị Android hiển thị tiện ích mặc định trên Màn hình chính cho Android.
4. Gửi dữ liệu từ ứng dụng Flutter đến tiện ích trên 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à mình đã tạo. Cập nhật tiện ích Màn hình chính để hiển thị tiêu đề và phần 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ị dòng tiêu đề và nội dung tóm tắt.
Để chuyển dữ liệu giữa ứng dụng và tiện ích trên Màn hình chính, bạn cần viết mã gốc Dart và mã gốc. Phần này chia quy trình này thành ba 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 iOS gốc
- 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 ứng dụng mẹ trên iOS và một tiện ích 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 mục tiêu của bạn. Trong phần Ký và Chức năng, hãy kiểm tra để đảm bảo bạn đã đặt mã nhận dạng 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 + Chức năng -> Nhóm ứng dụng và thêm một Nhóm ứng dụng mới. Lặp lại cho cả mục tiêu Trình chạy (ứ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 khác nhau.Để giao tiếp với những ứng dụng này, hãy tận dụng cửa hàng key/value
cục bộ của thiết bị. iOS gọi cửa hàng này là UserDefaults
, còn Android gọi cửa hàng 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, đồng thời cho phép các tiện ích trên Màn hình chính lấy dữ liệu 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à 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 trên 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 của Nhóm ứng dụng.
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 giá trị/khoá vào bộ nhớ cục bộ của thiết bị. Khoá headline_title
chứa giá trị của newHeadline.title
. Khoá headline_description
lưu 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à hiển thị dữ liệu mới cho các tiện ích trên Màn hình chính.
Sửa đổifloatActionButton
Gọi hàm updateHeadline
khi nhấn 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 dòng tiêu đề trên một trang bài viết, thông tin chi tiết về tiện ích trên 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 sử 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 đoạn 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 cầ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 có tham số ngày.Để tìm hiểu thêm về giao thức TimelineEntry
, hãy xem tài liệu Dòng thời gian 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ị tiêu đề và nội dung mô tả của tin bài thay vì hiển thị ngày. Để hiện văn bản trong SwiftUI, hãy dùng thành phần 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ế thành phần hiển thị NewsWidgetEntryView
đã tạo bằng 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 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 tại bằng mã sau. Sau đó, hãy thay thế mã nhận dạng nhóm ứng dụng bằng <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 phù hợp với TimelineProvider
. Provider
có 3 phương thức:
- Phương thức
placeholder
tạo một mục 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ừ 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
getTimeline
trả về các mục nhập trong dòng thời gian. Điều này sẽ hữu ích khi bạn có thể dự đoán được thời điểm cần 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.atEnd
yê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.
Đưa ra nhận xét về 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 chi tiết về cách xem trước tiện ích trên Màn hình chính SwiftUI, hãy xem Tài liệu của Apple về tiện ích gỡ lỗi.
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 trên 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.
- Hãy 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 cho 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 Mã ở góc trên cùng bên phải rồi 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" />
</RelativeLayout>
XML này xác định hai chế độ xem văn bản, một cho tiêu đề bài viết và một cho nội dung mô tả bài viết. Những thành phần 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 NewsWidget
Mở tệp mã nguồn Kotlin NewsWidget.kt
. 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 cấp cao của lớp này. Bạn cần sửa đổi phương thức onUpdate
. Android gọi phương thức này cho các tiện ích theo những 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)
}
}
}
Hiện tại, khi onUpdate
được gọi, Android sẽ nhận 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ử bản cập nhật
Kiểm thử ứng dụng để đảm bảo các 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 tính năng Cập nhật Màn hình chính FloatingActionButton
trên các trang bài viết. Tiện ích trên Màn hình chính phải cập nhật theo tiêu đề bài viết.
5. Dùng phông chữ tuỳ chỉnh của ứng dụng Flutter trong tiện ích trên Màn hình chính của iOS
Đến đây, 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ể dùng phông chữ tuỳ chỉnh trong tiện ích trên Màn hình chính của iOS. Bạn không thể sử dụng phông chữ tuỳ chỉnh trong các tiện ích trên 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 tài sản trong gói này từ mã tiện ích trên 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 tới 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 chế độ xem Văn bản của dòng 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)
}
}
...
}
Giờ đây, khi bạn chạy tiện ích trên Màn hình chính, tiện ích này sẽ sử dụng phông chữ tuỳ chỉnh cho dòng tiêu đề như trong hình sau:
6. Kết xuất 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 đồ trên ứng dụng Flutter dưới dạng tiện ích trên Màn hình chính.
Tiện ích này đưa ra nhiều thách thức hơn so với văn bản mà bạn thấy 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 thay vì 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.
Viết mã cho tiện ích trên Màn hình chính để kết xuất biểu đồ Flutter dưới dạng tệp PNG. Tiện ích trên 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, tên tệp và khoá. Phương thức này sẽ trả về một hình ảnh của tiện ích Flutter và lưu 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
lưu đường dẫn tệp đầy đủ dưới dạng 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 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
biểu thị biểu đồ. Phương thức xây dựng của hàm 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 GlobalKey
GlobalKey
nhận ngữ cảnh của tiện ích cụ thể, cần thiết để có được 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 để kết xuất
_globalKey
chứa tiện ích Flutter được kết xuất vào hình ảnh này. Trong trường hợp này, tiện ích Flutter là Trung tâm 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 sẽ lưu tệp PNG thu được 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ớ của 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ã này để 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 cấu trúc 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
chứa 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ấy từ 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 sẽ đặt biến filename
thành giá trị filename
trong bộ nhớ userDefaults
khi tiện ích trên 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
Khung hiển thị ChartImage
sẽ tạo một hình ảnh từ nội dung của tệp được tạo ở phía Dart. Tại đây, bạn đặt kích thước là 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 phần nội dung của NewsWidgetsEntryView
Thêm chế độ xem ChartImage vào phần 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
Để thử nghiệm các thay đổi, hãy chạy lại cả mục tiêu ứng dụng Flutter (Trình chạy) và mục tiêu tiện ích của bạn trên 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 nút đó để cập nhật tiện ích Màn hình chính.
Cập nhật mã Android
Mã Android hoạt động giống như mã iOS.
- Mở tệp
android/app/res/layout/news_widget.xml
. Ứng dụng này chứa các thành phần trên giao diện người dùng của tiện ích trên 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>
Mã mới này sẽ thêm một hình ảnh vào tiện ích Màn hình chính, tiện ích này (hiện tại) hiển thị biểu tượng dấu sao chung. Thay thế biểu tượng dấu 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 sẽ lưu ảnh chụp màn hình vào bộ nhớ cục bộ bằng khoá filename
. Phương thức này cũng nhận đường dẫn đầy đủ của hình ảnh và tạo một đối tượng File
từ đó. 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 và 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 trên Màn hình chính cho các ứng dụng Flutter dành cho iOS và Android của mình!
Liên kết đến nội dung trong ứng dụng Flutter
Bạn nên đưa người dùng đến một trang cụ thể trong ứng dụng, tuỳ thuộc vào nơi người dùng nhấp vào. Ví dụ: trong ứng dụng tin tức ở 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 đề xuất hiện.
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ể xem ví dụ về cách sử dụng một luồng mà gói home_Widget cung cấp để xác định các lần chạy ứng dụng từ tiện ích trên Màn hình chính và gửi tin nhắn từ tiện ích trên Màn hình chính thông qua URL đó. Để tìm hiểu thêm, hãy xem tài liệu về đường liên kết sâu trên docs.flutter.dev.
Cập nhật tiện ích ở chế độ nền
Trong lớp học lập trình này, bạn đã kích hoạt bản 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ý khi thử nghiệm, nhưng trong mã phát hành chính thức, 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ể sử dụng trình bổ trợ Workmanager để tạo tác vụ trong nền nhằm cập nhật 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 ở chế độ nền trong gói home_Widget.
Đối với iOS, bạn cũng có thể thiết lập tiện ích Màn hình chính gửi 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 phần "Cập nhật tiện ích" của Apple tài liệu.