редакции Выбор
Flutter. Учимся работать с платформозависимыми сервисами. Самостоятельно и с помощью сторонних библиотек
Перед тем как заняться кроссплатформенной разработкой, мы часто мучились вопросами: каким образом в таких проектах осуществляется работа с системными сервисами и компонентами? Возможна ли она вообще? Очень сложно найти приложение, которое не обращается к адресной книге, геолокации или хотя бы камере устройства. К счастью, у Flutter-сообщества есть ответы почти на все наши запросы.
Взаимодействие Dart с нативным кодом
Для начала немного теории. Flutter помогает организовать взаимодействие между Dart и нативным кодом приложения при помощи механизма PlatformChannel.
В самом простом случае на стороне Flutter создается экземпляр MethodChannel с уникальным идентификатором, после чего в этот канал отправляются сообщения с уникальными (в рамках канала) именами и при необходимости параметрами. В нативном коде регистрируются канал с аналогичным идентификатором (FlutterMethodChannel на iOS и MethodChannel на Android) и его обработчик. В обработчике нужно только прогнать имя сообщения через обычный switch и вызвать соответствующий метод, написанный на Swift/Kotlin. Результат работы метода (или nil для void функций) возвращается обратно в Dart с callback’ом.
const MethodChannel channel = MethodChannel("your-unique-identifier"); static FuturecallFirstMethod({String param}) async { String result = await channel.invokeMethod(firstMethod, { «param»: param }); return result; } public class SwiftServicePlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: «your-unique-identifier», binaryMessenger: registrar.messenger()) let instance = SwiftServicePlugin() registrar.addMethodCallDelegate(instance, channel: channel) channel.invokeMethod("your-method-name«, arguments: [«key»: «value»]) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) -> String { switch call.method { case «firstMethod»: let arguments = call.arguments as! [String:Any] result(firstMethod(param: (arguments["param"] as? String))) default: result(FlutterMethodNotImplemented) } } func firstMethod(param: String?) { ... } } public class ServicePlugin implements MethodCallHandler { public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), «your-unique-identifier»); channel.setMethodCallHandler(new ServicePlugin(registrar.context().getContentResolver())); } @Override public void onMethodCall(MethodCall call, Result result) { switch(call.method){ case «firstMethod»: { this.firstMethod((String)call.argument("param"), result); break; } default: { result.notImplemented(); break; } } } private void firstMethod(String param, Result result) { ... } } Есть возможность и обратного взаимодействия. Для этого в нативном коде вызываем, например: а в Dart файле прописываем: channel.setMethodCallHandler((MethodCall call) async { if(call.method == «your-method-name») { yourHandler(call.arguments); } }); ообщения и callback’и отправляются асинхронно и не блокируют UI, однако, вызывать методы обработчика рекомендуется все же в главном потоке. Подробнее о взаимодействии Flutter и нативного кода можно почитать тут. Как видите, имея такой удобный инструмент под рукой, не сложно реализовать обращение к системным сервисам самостоятельно. Ну, а для тех, кто не любит изобретать велосипеды, мы приготовили подборку удобных библиотек, которые помогут сэкономить потраченное на разработку время. Итак, первым делом нам понадобится запросить у пользователя разрешение на использование того или иного ресурса системы. Одной из наиболее удобных библиотек для этого является permission_handler. Нужно только не забыть указать user-friendly текст запроса в Info.plist файле iOS-приложения и прописать необходимое разрешение в файле AndroidManifest. После этого останется вызвать метод requestPermissions и обработать результат. Приятным бонусом станет возможность отправить пользователя в Настройки устройства, чтобы открыть приложению доступ к тому или иному системному сервису. Правда, и здесь iOS-разработчик может попасть впросак. Шутка заключается в том, что при первом запуске на iPhone библиотека выдаст статус unknown, а на Android-устройстве — сразу denied. И Android-разработчиков это не остановит. Они продолжат выпрашивать у пользователя разрешение, пока тот не пометит галочкой опцию «Не спрашивать больше» (проверить это можно при помощи метода библиотеки shouldShowRequestPermissionRationale). Поэтому, определяя, запрашивается ли разрешение впервые, придется полагаться на флаг, вручную добавленный в SharedPreferenses (Flutter-аналог «яблочного» UserDefaults). Future _getPermissions() async { PermissionStatus status = await PermissionHandler() .checkPermissionStatus(PermissionGroup.contacts); final bool shouldShow = await PermissionHandler() .shouldShowRequestPermissionRationale(PermissionGroup.contacts); final SharedPreferences preferences = await SharedPreferences.getInstance(); final bool hasAlreadyAsked = preferences .getBool(Constants.spContactsPermissionKey) ?? false; if (status == PermissionStatus.unknown || (status == PermissionStatus.denied && (shouldShow || !hasAlreadyAsked))) { final Map result = await PermissionHandler().requestPermissions([PermissionGroup.contacts]); status = result[PermissionGroup.contacts]; await preferences.setBool(Constants.spContactsPermissionKey, true); } return status == PermissionStatus.granted; } Итак, разрешение получено, пора за дело. Однажды нам понадобилось получить список контактов из адресной книги устройства. Здесь хорошим подспорьем стала библиотека contacts_service. Она позволяет получать контакты из адресной книги, добавлять, редактировать и удалять записи, отдельно вытаскивать аватарки и фильтровать контакты. Future> getContacts() async { final Iterable map = await ContactsService.getContacts(); return map.toList(); } Future addContact(Contact contact) async { return ContactsService.addContact(contact); } Future updateContact(Contact contact) async { return ContactsService.updateContact(contact); } Future deleteContact(Contact contact) async { return ContactsService.deleteContact(contact); } Future avatarOfContact(Contact contact) async { Uint8List avatar = await ContactsService.getAvatar(contact); return Image.memory(avatar, fit: BoxFit.cover); } Для каждого профиля доступны идентификатор, имя, данные о компании и должности, адрес, телефоны, email адреса, аватар. А если нужно поработать с системными календарями, на помощь придет device_calendar. Эта удобная библиотека умеет и разрешение у пользователя спросить, и со списком доступных календарей управиться, определив при этом, в какие из них допускается запись, а в каких — только чтение, и событие ваше сохранить, отредактировать или удалить. Для каждого события необходимо указать идентификатор календаря, название, дату начала и окончания. В нагрузку можно добавить описание, адрес проведения, организатора и участников, правило повторения и напоминание. При создании события библиотека отдает его идентификатор, используя который можно редактировать и удалять события. Для работы с камерой и галереей устройства рекомендуем библиотеку image_picker. Несмотря на то, что работа над ней еще в самом разгаре, текущая версия порадует пользователей привычным системным интерфейсом, а программистов — минимумом необходимых для интеграции усилий. По сути, после добавления в Info.plist необходимых разрешений понадобится только вызвать: или, соответственно: Стоит обратить внимание, что при выборе изображения мы получаем путь к файлу, использовать который в виджете можно следующим образом: Чтобы проиграть видеофайл, советуем обратиться к video_player. Библиотека «под капотом» опирается на AVPlayer для iOS и ExoPlayer для Android, но формат видео лучше выбрать популярный, чтобы обе платформы сумели его потянуть. Важно учесть, что плеер не разворачивается автоматически на весь экран, его придется руками добавлять в иерархию виджетов. А если понадобится повернуть видео в landscape, просто заверните плейер в виджет RotatedBox. Если появится желание открыть браузер или почтовый клиент, сделать звонок или отправить смс, обратите внимание на url_launcher. Эта простая библиотечка поможет проверить, возможна ли необходимая операция (сложно будет позвонить кому-то с планшета или отправить письмо с iOS-устройства, если системный почтовый клиент не настроен), а также выполнит ее. Написать письмо, указав адресата, тему и тело: launch("mailto:your@email.com?subject=Test&body=Test"); Отправить смс на номер: launch("sms:5554443322″); Или позвонить: launch("tel:5554443322″); А еще можно открыть веб-сайт: Итак, оказывается, работа с системными сервисами во Flutter пугает только на первый взгляд. В следующей статье мы посмотрим, как отправить ваши тестовые сборки с помощью сервиса Firebase, и какие подводные камни могут поджидать вас в этом, казалось бы, несложном деле.
Запрашиваем разрешение у пользователя
Надежные помощники. Адресная книга
Надежные помощники. Календарь
Надежные помощники. Камера и галерея
Надежные помощники. Видеоплеер
Добавляем плагин запуска URL
Что дальше?