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

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 822
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1
Почему эта статья появилась на свет


Привет! На связи мобильный 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.
 
Сверху Снизу