Flutter. Учимся работать с платформозависимыми сервисами. Самостоятельно и с помощью сторонних библиотек
В прошлой статье мы разобрали по косточкам архитектуру BLoC и, надеемся, научились сносно ее готовить. Сегодня предлагаем поговорить о внедрении в проект функционала, требующего взаимодействия с платформозависимыми сервисами.
Перед тем как заняться кроссплатформенной разработкой, мы часто мучились вопросами: каким образом в таких проектах осуществляется работа с системными сервисами и компонентами? Возможна ли она вообще? Очень сложно найти приложение, которое не обращается к адресной книге, геолокации или хотя бы камере устройства. К счастью, у 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 Future<String>callFirstMethod({String param}) async {
String result = await channel.invokeMethod(firstMethod, <String, dynamic>{ «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) {
…
}
}
Есть возможность и обратного взаимодействия. Для этого в нативном коде вызываем, например:
channel.invokeMethod("your-method-name", arguments: ["key": "value!"])
а в 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<bool> _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<List<Contact>> getContacts() async {
final Iterable<Contact> map = await ContactsService.getContacts();
return map.toList();
}
Future<void> addContact(Contact contact) async {
return ContactsService.addContact(contact);
}
Future<void> updateContact(Contact contact) async {
return ContactsService.updateContact(contact);
}
Future<void> deleteContact(Contact contact) async {
return ContactsService.deleteContact(contact);
}
Future<Image> avatarOfContact(Contact contact) async {
Uint8List avatar = await ContactsService.getAvatar(contact);
return Image.memory(avatar, fit: BoxFit.cover);
}
Для каждого профиля доступны идентификатор, имя, данные о компании и должности, адрес, телефоны, email адреса, аватар.
Надежные помощники. Календарь
А если нужно поработать с системными календарями, на помощь придет device_calendar. Эта удобная библиотека умеет и разрешение у пользователя спросить, и со списком доступных календарей управиться, определив при этом, в какие из них допускается запись, а в каких — только чтение, и событие ваше сохранить, отредактировать или удалить. Для каждого события необходимо указать идентификатор календаря, название, дату начала и окончания. В нагрузку можно добавить описание, адрес проведения, организатора и участников, правило повторения и напоминание. При создании события библиотека отдает его идентификатор, используя который можно редактировать и удалять события.
Надежные помощники. Камера и галерея
Для работы с камерой и галереей устройства рекомендуем библиотеку image_picker. Несмотря на то, что работа над ней еще в самом разгаре, текущая версия порадует пользователей привычным системным интерфейсом, а программистов — минимумом необходимых для интеграции усилий. По сути, после добавления в Info.plist необходимых разрешений понадобится только вызвать:
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
или, соответственно:
var image = await ImagePicker.pickImage(source: ImageSource.camera);
Стоит обратить внимание, что при выборе изображения мы получаем путь к файлу, использовать который в виджете можно следующим образом:
Image.file(image);
Надежные помощники. Видеоплеер
Чтобы проиграть видеофайл, советуем обратиться к video_player. Библиотека «под капотом» опирается на AVPlayer для iOS и ExoPlayer для Android, но формат видео лучше выбрать популярный, чтобы обе платформы сумели его потянуть. Важно учесть, что плеер не разворачивается автоматически на весь экран, его придется руками добавлять в иерархию виджетов. А если понадобится повернуть видео в landscape, просто заверните плейер в виджет RotatedBox.
Добавляем плагин запуска URL
Если появится желание открыть браузер или почтовый клиент, сделать звонок или отправить смс, обратите внимание на url_launcher. Эта простая библиотечка поможет проверить, возможна ли необходимая операция (сложно будет позвонить кому-то с планшета или отправить письмо с iOS-устройства, если системный почтовый клиент не настроен), а также выполнит ее.
Написать письмо, указав адресата, тему и тело:
launch("mailto:your@email.com?subject=Test&body=Test");
Отправить смс на номер:
launch("sms:5554443322");
Или позвонить:
launch("tel:5554443322");
А еще можно открыть веб-сайт:
launch("https://your/web/site", forceWebView: false, forceSafariVC: false);
Что дальше?
Итак, оказывается, работа с системными сервисами во Flutter пугает только на первый взгляд. В следующей статье мы посмотрим, как отправить ваши тестовые сборки с помощью сервиса Firebase, и какие подводные камни могут поджидать вас в этом, казалось бы, несложном деле.