Chuyển đến nội dung chính

Migrate UIScene: thay đổi iOS sẽ làm app Flutter chưa chuyển bị crash

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 → 15macOS 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:

  • AppDelegate giờ xử lý sự kiện tiến trình và vòng đời tổng thể của ứng dụng
  • UISceneDelegate xử lý vòng đời UI — foreground, background, active, resign

Hai hệ quả đi kèm, và cả hai đều làm hỏng code:

  1. Đăng ký plugin dời chỗ. Đăng ký trong didInitializeImplicitFlutterEngine, không phải application:didFinishLaunchingWithOptions:.
  2. Launch options trở thành nil trong application:didFinishLaunchingWithOptions: sau khi migrate. Chúng được chuyển tới scene: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 AppDelegateTương đương ở scene delegate
applicationDidBecomeActivesceneDidBecomeActive
applicationWillResignActivesceneWillResignActive
applicationWillEnterForegroundsceneWillEnterForeground
applicationDidEnterBackgroundsceneDidEnterBackground
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ị deprecateThay bằng
UIScreen.mainUIWindowScene.screen
UIApplication.shared.delegate.windowregistrar.viewController.view.window
UIApplication.shared.keyWindowUIWindowScene.keyWindow (iOS 15+)
UIApplication.shared.windowsUIWindowScene.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

  1. Build trên 3.47 và để migrate tự động chạy nếu AppDelegate của bạn còn nguyên bản.
  2. Thêm UIApplicationSceneManifest vào Info.plist nếu chưa có.
  3. Chuyển GeneratedPluginRegistrant vào didInitializeImplicitFlutterEngine, cùng với method channel và platform view factory.
  4. 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.
  5. Grep các singleton bị deprecateUIScreen.main, keyWindow, UIApplication.shared.windows — trong code của bạn và trong plugin.
  6. Nâng deployment target lên iOS 15 và macOS 12, rồi chạy flutter build ios --config-only.
  7. 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 AppDelegate của bạn.
  8. Đừng dùng enable-uiscene-migration: false như 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

Bài đăng phổ biến từ blog này

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Announcing Flutter 2

  Phụ lục: Flutter on the web Flutter 2 on desktops, foldables, and embedded devices The growing Flutter ecosystem Dart: The secret sauce behind Flutter Flutter 2: Available now Hôm nay, chúng tôi sẽ công bố Flutter 2: một bản nâng cấp lớn cho Flutter cho phép các nhà phát triển tạo các ứng dụng đẹp, nhanh chóng và di động cho bất kỳ nền tảng nào. Với Flutter 2, bạn có thể sử dụng cùng một cơ sở mã để gửi các ứng dụng gốc cho năm hệ điều hành: IOS, Android, Windows, macOS và Linux; cũng như trải nghiệm web nhắm mục tiêu các trình duyệt như Chrome, Firefox, Safari hoặc Edge. Flutter thậm chí có thể được nhúng vào ô tô, TV và thiết bị gia dụng thông minh, mang đến trải nghiệm di động và lan tỏa nhất cho thế giới điện toán xung quanh. Mục tiêu của chúng tôi là thay đổi cơ bản cách các nhà phát triển nghĩ về việc xây dựng ứng dụng, bắt đầu không phải với nền tảng bạn đang nhắm mục tiêu mà là với trải nghiệm bạn muốn tạo. Flutter cho phép bạn tạo ra những trải nghiệm tuyệt đẹp trong đó ...