Phần lớn các cuộc migrate trong Flutter là tuỳ chọn cho tới khi chúng gây phiền. Cái này khác: khi Apple bắt đầu cưỡng chế, app chưa áp dụng vòng đời UIScene sẽ crash ngay lúc khởi động. Không phải giảm chất lượng. Là crash.
Apple yêu cầu app UIKit build bằng SDK mới nhất phải dùng vòng đời UIScene kể từ bản phát hành sau iOS 26. Apple chưa công bố ngày cưỡng chế chính xác. Flutter đã hỗ trợ migrate từ 3.38, và 3.47 làm các thay đổi nền tảng xung quanh trở nên cụ thể bằng cách nâng sàn: iOS 13 → 15 và macOS 10.15 → 12, để hỗ trợ Xcode 27.
UIScene thực sự thay đổi điều gì
Sự dịch chuyển về khái niệm là việc tách trách nhiệm vốn nằm trong một đối tượng:
AppDelegategiờ xử lý sự kiện tiến trình và vòng đời tổng thể của ứng dụngUISceneDelegatexử lý vòng đời UI — foreground, background, active, resign
Hai hệ quả đi kèm, và cả hai đều làm hỏng code:
- Đăng ký plugin dời chỗ. Đăng ký trong
didInitializeImplicitFlutterEngine, không phảiapplication:didFinishLaunchingWithOptions:. - Launch options trở thành
niltrongapplication:didFinishLaunchingWithOptions:sau khi migrate. Chúng được chuyển tớiscene:willConnectToSession:options:.
Cái thứ hai mới là sát thủ thầm lặng. Deep link, payload push notification, và shortcut item vốn đến qua launch options đơn giản là ngừng đến, không có lỗi biên dịch nào.
Tin tốt: thường là tự động
Từ Flutter 3.41, nếu AppDelegate của bạn chưa bị tuỳ biến, Flutter CLI tự migrate app khi bạn chạy flutter run hoặc flutter build ios. Một phần lớn app đã xong mà không hề hay biết.
Bạn có việc phải làm nếu bạn đã tuỳ biến AppDelegate, ship một tích hợp add-to-app, hoặc duy trì một plugin.
Info.plist
Cuộc migrate thêm một Application Scene Manifest:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
Một mẹo debug hữu ích: thêm dấu gạch dưới vào trước UIApplicationSceneManifest để tạm tắt hỗ trợ UIScene, và bỏ dấu gạch dưới để bật lại. Điều đó cho bạn một phép so sánh A/B nhanh khi có gì đó hỏng.
AppDelegate
Chuyển việc đăng ký plugin ra khỏi didFinishLaunchingWithOptions và vào callback mới:
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// GeneratedPluginRegistrant không còn thuộc về đây
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
let batteryChannel = FlutterMethodChannel(
name: "samples.flutter.dev/battery",
binaryMessenger: engineBridge.applicationRegistrar.messenger()
)
}
}
Method channel và platform view factory đều cần messenger từ engineBridge.applicationRegistrar, không phải cái ở cấp application cũ.
Với add-to-app, hãy thêm một scene delegate — thường chỉ một dòng:
import UIKit
import Flutter
class SceneDelegate: FlutterSceneDelegate {}
Nếu app chủ của bạn không kế thừa được FlutterSceneDelegate, hãy cài đặt FlutterSceneLifeCycleProvider và chuyển tiếp từng callback scene tới một instance FlutterPluginSceneLifeCycleDelegate.
Nếu bạn duy trì một plugin
Tác giả plugin gánh phần nặng nhất, vì mọi app phụ thuộc vào bạn đều thừa hưởng trạng thái migrate của bạn.
Nâng ràng buộc, rồi áp dụng protocol và đăng ký nhận callback scene:
environment:
sdk: ^3.10.0
flutter: ">=3.38.0"
public final class MyPlugin: NSObject, FlutterPlugin, FlutterSceneLifeCycleDelegate {
public static func register(with registrar: FlutterPluginRegistrar) {
registrar.addApplicationDelegate(instance)
registrar.addSceneDelegate(instance)
}
}
Rồi ánh xạ các callback cũ:
| Method của AppDelegate | Tương đương ở scene delegate |
|---|---|
applicationDidBecomeActive | sceneDidBecomeActive |
applicationWillResignActive | sceneWillResignActive |
applicationWillEnterForeground | sceneWillEnterForeground |
applicationDidEnterBackground | sceneDidEnterBackground |
application:openURL:options: | scene:openURLContexts: |
application:continueUserActivity: | scene:continueUserActivity: |
application:didFinishLaunchingWithOptions: | scene:willConnectToSession:options: |
Những API ngừng hoạt động
Một loạt singleton UIKit lâu đời bị deprecate dưới mô hình scene. Mỗi cái có bản thay thế theo phạm vi scene:
| Bị deprecate | Thay bằng |
|---|---|
UIScreen.main | UIWindowScene.screen |
UIApplication.shared.delegate.window | registrar.viewController.view.window |
UIApplication.shared.keyWindow | UIWindowScene.keyWindow (iOS 15+) |
UIApplication.shared.windows | UIWindowScene.windows |
Lưu ý UIWindowScene.keyWindow cần iOS 15 — đúng bằng cái sàn Flutter 3.47 vừa nâng bạn lên. Hai thay đổi này liên quan tới nhau, không phải trùng hợp.
Trường hợp thật sự khó: khởi tạo sớm
Một số API của Apple phải được cấu hình trước khi application:didFinishLaunchingWithOptions: trả về — BGTaskScheduler, UNUserNotificationCenterDelegate, HKHealthStore. Dưới mô hình scene, việc đăng ký plugin xảy ra muộn hơn thời điểm đó.
Không có cách nào để plugin tự giải quyết một mình. Mẫu được tài liệu hoá là plugin phơi ra một method public để lập trình viên app gọi từ AppDelegate của chính họ:
class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
BGTaskPlugin.shared.registerBackgroundHandler(identifier: "com.example.task")
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Nếu bạn dùng background task, dữ liệu sức khoẻ, hay notification delegate, hãy kiểm tra tài liệu plugin đúng theo mẫu này. Đây là bước migrate dễ bị bỏ sót nhất và ít có khả năng bị test bắt được nhất.
Checklist migrate của bạn
- Build trên 3.47 và để migrate tự động chạy nếu
AppDelegatecủa bạn còn nguyên bản. - Thêm
UIApplicationSceneManifestvàoInfo.plistnếu chưa có. - Chuyển
GeneratedPluginRegistrantvàodidInitializeImplicitFlutterEngine, cùng với method channel và platform view factory. - Test lại mọi lối vào: deep link, universal link, chạm vào push notification, quick action ở màn hình chính. Launch options giờ là
nil. - Grep các singleton bị deprecate —
UIScreen.main,keyWindow,UIApplication.shared.windows— trong code của bạn và trong plugin. - Nâng deployment target lên iOS 15 và macOS 12, rồi chạy
flutter build ios --config-only. - Kiểm tra các plugin cần khởi tạo sớm và thêm lời gọi bắt buộc trong
AppDelegatecủa bạn. - Đừng dùng
enable-uiscene-migration: falsenhư gì khác ngoài một cách gỡ kẹt ngắn hạn — nó giấu cảnh báo, không giấu cú crash sẽ tới.
Kết luận
Đây là cuộc migrate duy nhất trong Flutter 3.47 có chế độ hỏng dứt khoát. Các phần cơ học được tài liệu hoá tốt và phần lớn đã tự động, nên hầu hết app sẽ qua chỉ bằng một lần build lại. Thứ thực sự cắn bạn là những đường chưa được test: một deep link không còn mang theo payload, một plugin đăng ký background task quá muộn, một lời gọi keyWindow chôn trong dependency. Hãy build lại ngay hôm nay, rồi bỏ một giờ mở app của bạn từ mọi lối vào bên ngoài mà bạn hỗ trợ. Một giờ đó rẻ hơn nhiều so với một báo cáo crash vào ngày phát hành.
Bài viết gốc đăng tại FlutterCook. Bản trên đó là bản được cập nhật mới nhất.
Nhận xét
Đăng nhận xét