Flutter + нативные iOS виджеты: любовь с первого Method Channel

от автора

Почему эта статья появилась на свет

Привет! На связи мобильный Flutter разработчик. Если ты читаешь это, значит ты столкнулся с ситуацией когда необходимо подружить iOS виджеты с Flutter приложением.

Когда передо мной впервые встала эта задача, я черпал информацию из различных статей на английском, а теперь решил собрать все в одном (так еще и на русском)

Скажу сразу, разрабатывать будем без дополнительных библиотек + нужно будет иметь базовые знания в SwiftUI (нам как Flutter — разработчикам этот декларативный фреймворк не покажется сложным).

Реализация: пошаговый план действий

Теперь перейдем к делу. Весь процесс можно разбить на 2 основных этапа:

1. Создание Widget Target

Первым делом нам нужно создать новый target в iOS части нашего Flutter проекта. Widget Extension — это по сути отдельное мини-приложение, которое работает независимо от основного приложения.

Что делаем в Xcode:

  1. Открываем iOS проект (ios/Runner.xcworkspace)

  2. Добавляем новый target: File → New → Target → Widget Extension

  3. Даем имя виджету (например, MyAppWidget)

  4. В разделе Sign&Capabilities настраиваем App Groups для обмена данными между приложением и виджетом (Важно! Он должен быть идентичен настроенному в таргете основного приложения)

Теперь у нас сгенерировались основные файлы по умолчанию:

import SwiftUI  struct Provider: TimelineProvider {     func placeholder(in context: Context) -> SimpleEntry {         SimpleEntry(date: Date(), emoji: "😀")     }      func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {         let entry = SimpleEntry(date: Date(), emoji: "😀")         completion(entry)     }      func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {         var entries: [SimpleEntry] = []          // Generate a timeline consisting of five entries an hour apart, starting from the current date.         let currentDate = Date()         for hourOffset in 0 ..< 5 {             let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!             let entry = SimpleEntry(date: entryDate, emoji: "😀")             entries.append(entry)         }          let timeline = Timeline(entries: entries, policy: .atEnd)         completion(timeline)     }  //    func relevances() async -> WidgetRelevances<Void> { //        // Generate a list containing the contexts this widget is relevant in. //    } }  struct SimpleEntry: TimelineEntry {     let date: Date     let emoji: String }  struct MyAppWidgetEntryView : View {     var entry: Provider.Entry      var body: some View {         VStack {             Text("Time:")             Text(entry.date, style: .time)              Text("Emoji:")             Text(entry.emoji)         }     } }  struct MyAppWidget: Widget {     let kind: String = "MyAppWidget"      var body: some WidgetConfiguration {         StaticConfiguration(kind: kind, provider: Provider()) { entry in             if #available(iOS 17.0, *) {                 MyAppWidgetEntryView(entry: entry)                     .containerBackground(.fill.tertiary, for: .widget)             } else {                 MyAppWidgetEntryView(entry: entry)                     .padding()                     .background()             }         }         .configurationDisplayName("My Widget")         .description("This is an example widget.")     } }  #Preview(as: .systemSmall) {     MyAppWidget() } timeline: {     SimpleEntry(date: .now, emoji: "😀")     SimpleEntry(date: .now, emoji: "🤩") } 
  • MyAppWidget.swift

    Теперь рассмотрим подробнее структуру данного файла.

    Provider: TimelineProvider — это основная структура виджета описывающая его поведение.

    • функция placeholder — показывается пока виджет загружается впервые

    • функция getSnapshot — быстрая версия виджета для галереи

    • функция getTimeline — самый важный метод! Определяет когда и как часто обновлять.

    Стратегии обновления:
    .atEnd — обновить после последней записи в timeline
    .after(date) — обновить в конкретное время (примечательно что система говорит что обновит его в указанное время, но не гарантирует этого)
    .never — не обновлять автоматически

    Общение натива и Flutter

    В Flutter части я создал класс который позволяет записывать данные в платформу и извлекать их, а также вручную обновлять состояние iOS виджетов.

    class WidgetPreferencesChannel {   //! METHOD CHANNEL   static const _platformChannel =       MethodChannel('com.app.example/widgetPreferences');    Future<String?> setValue(String key, String value) async {     if (!Platform.isIOS) {       return null;     }     try {       //! PLATFORM KEY       final result = await _platformChannel.invokeMethod('saveWidgetData', {         'key': key,         'value': value,       });        return result as String?;     } catch (err) {       debugPrint('Error $err');       return null;     }   }    Future<String?> getValue(String key) async {     if (!Platform.isIOS) {       return null;     }     try {       //! PLATFORM KEY       final result = await _platformChannel.invokeMethod('getWidgetData', {         'key': key,       });        return result as String?;     } catch (err) {       debugPrint('Error $err');       return null;     }   }    Future<bool?> updateWidget(String kind) async {     if (!Platform.isIOS) {       return null;     }     try {       //! PLATFORM KEY       final result = await _platformChannel.invokeMethod('updateWidget', {         'kind': kind,       });        return result as bool?;     } catch (err) {       debugPrint('Error $err');       return false;     }   } } 

    Ну и соответственный класс в iOS части:

     public class StorageHelper {     static let storage = UserDefaults.init(suiteName: "group.com.app.example")      public static func setValue(key: String, value: Any) {         storage?.set(value, forKey: key)     }      public static func getString(key: String) -> String? {         return storage?.string(forKey: key)     } } 
    • Удобный хелпер для записи данных

    import WidgetKit import Foundation  // Для перехватывания events из flutter class WidgetPreferencesHandler {     static func register(with messenger: FlutterBinaryMessenger) { //        !!! PLATFORM KEY         let storageChannel = FlutterMethodChannel(             name: "com.app.example/widgetPreferences",             binaryMessenger: messenger         )                  storageChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in             // Аргументы запроса !!! PLATFORM KEY             guard let args = call.arguments as? [String: Any] else {                 result(FlutterError(                     code: "UNAVAILABLE",                     message: "Требуется передать аргументы",                     details: nil                 ))                 return             }             switch call.method {                              // Сохранение данных !!! PLATFORM KEY             case "saveWidgetData":                                  // Ключ значения                 guard let key = args["key"] as? String else {                     result(FlutterError(                         code: "UNAVAILABLE",                         message: "Требуется передать ключ (key)",                         details: nil                     ))                     return                 }                                  // Сохраняем в UserDefaults через ваш StorageHelper                 StorageHelper.setValue(key: key, value: args["value"] as Any)                                  // Возвращаем текущее сохранённое значение (или nil)                 let savedValue = StorageHelper.getString(key: key)                 result(savedValue)                  //          Получение данных !!! PLATFORM KEY             case "getWidgetData":                                  guard let key = args["key"] as? String else {                     result(FlutterError(                         code: "UNAVAILABLE",                         message: "Требуется передать ключ (key)",                         details: nil                     ))                     return                 }                                  // Получаем значение из UserDefaults                 let value = StorageHelper.getString(key: key)                 result(value)                  //          Обновление виджета !!! PLATFORM KEY             case "updateWidget":                                  guard let kind = args["kind"] as? String else {                     result(FlutterError(                         code: "UNAVAILABLE",                         message: "Требуется передать имя виджета (kind)",                         details: nil                     ))                     return                 }                 // Обновляем виджет, чей kind = переданному                 WidgetCenter.shared.reloadTimelines(ofKind: kind)                 result(true)                          default:                 return             }         }     } } 
    • класс для передачи и получения данных от Flutter приложения

    С помощью StorageHelper вы сможете получать данные непосредственно в getTimeline вашего виджета:

    func getTimeline(in context: Context, completion: @escaping (Timeline<SalavatEntry>) -> Void) {         let emojiValue = StorageHelper.getString("emoji")         let entry = SimpleEntry(date: Date(), emoji: emoji,)         let timeline = Timeline(entries: [entry], policy: .never)         completion(timeline)     } 

    Вот так это выглядит в нашем приложении

    Готово! Теперь вы знаете как синхронизировать данные между приложением и iOS виджетами. Можете подробнее изучить WidgetKit , а также возможности SwiftUI.


ссылка на оригинал статьи https://habr.com/ru/articles/940678/