Учебное пособие по Flutter: как создать свое первое приложение Flutter
Опубликовано: 2022-03-11Что такое флаттер?
Flutter — это SDK для разработки мобильных приложений Google, который позволяет вашему продукту одновременно ориентироваться на платформы Android и iOS без необходимости поддерживать две отдельные базы кода. Кроме того, приложения, использующие Flutter, также могут быть скомпилированы для будущей операционной системы Google Fuchsia.
Flutter недавно достиг важной вехи — стабильной версии 1.0. Релиз состоялся в Лондоне 5 декабря 2018 года на мероприятии Flutter Live. Хотя его все еще можно рассматривать как раннее и развивающееся программное обеспечение, в этой статье основное внимание будет уделено уже проверенной концепции и показано, как разработать полнофункциональное приложение для обмена сообщениями, предназначенное для обеих основных мобильных платформ с использованием Flutter 1.2 и Firebase.
Как видно из диаграммы ниже, в последние месяцы у Flutter появилось много пользователей. В 2018 году рыночная доля Flutter удвоилась, и он находится на пути к тому, чтобы превзойти React Native с точки зрения поисковых запросов, поэтому мы решили создать новый учебник по Flutter.
Примечание. Эта статья посвящена только некоторым частям реализации. Полную ссылку на исходный код проекта можно найти в этом репозитории GitHub.
Предпосылки
Несмотря на то, что были предприняты усилия, чтобы позволить читателям следить за этим проектом и выполнять его, даже если это их первая попытка мобильной разработки, многие основные концепции мобильной разработки, не относящиеся к Flutter, упоминаются и используются без подробного объяснения.
Это было сделано для краткости статьи, поскольку одна из ее целей состоит в том, чтобы читатель выполнил проект за один присест. Наконец, в статье предполагается, что у вас уже настроена среда разработки, включая необходимые плагины Android Studio и Flutter SDK.
Настройка Firebase
Настройка Firebase — это единственное, что нам нужно сделать независимо для каждой платформы. Прежде всего, убедитесь, что вы создали новый проект в панели инструментов Firebase и добавили приложения Android и iOS во вновь созданную рабочую область. Платформа создаст два файла конфигурации, которые необходимо загрузить: google-services.json
для Android и GoogleService-Info.plist
для iOS. Прежде чем закрыть панель управления, обязательно включите поставщиков аутентификации Firebase и Google, так как мы будем использовать их для идентификации пользователей. Для этого выберите в меню пункт «Аутентификация», а затем перейдите на вкладку «Метод входа».
Теперь вы можете закрыть панель инструментов, так как остальная часть настройки выполняется в нашей кодовой базе. Прежде всего, нам нужно поместить загруженные файлы в наш проект. Файл google-services.json
следует поместить в папку $(FLUTTER_PROJECT_ROOT)/android/app
а GoogleService-Info.plist
— в папку $(FLUTTER_PROJECT_ROOT)/ios/Runner
. Далее нам нужно настроить библиотеки Firebase, которые мы собираемся использовать в проекте, и подключить их к файлам конфигурации. Это делается путем указания пакетов (библиотек) Dart, которые мы будем использовать, в файле pubspec.yaml
нашего проекта. В разделе зависимостей файла вставьте следующий фрагмент:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:
Первые два не связаны с Firebase, но будут часто использоваться в проекте. Последние два, надеюсь, говорят сами за себя.
Наконец, нам нужно настроить параметры проекта для конкретной платформы, которые позволят успешно завершить наш поток аутентификации. На стороне Android нам нужно добавить плагин google-services Gradle в нашу конфигурацию Gradle на уровне проекта. Другими словами, нам нужно добавить следующий элемент в список зависимостей в $(FLUTTER_PROJECT_ROOT)/android/build.gradle
:
classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version
Затем нам нужно применить этот плагин, добавив эту строку в конец $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle
:
apply plugin: 'com.google.gms.google-services'
Последнее, что нужно сделать для этой платформы, — это указать параметры вашего приложения Facebook. Здесь нам нужно отредактировать эти два файла — $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml
и $(FLUTTER_PROJECT_ROOT)/android/app/src/main/res/values/strings.xml
:
<!-- AndroidManifest.xml --> <manifest xmlns:androcom.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> <activity android:name="com.facebook.FacebookActivity" android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:label="@string/app_name" /> <activity android:name="com.facebook.CustomTabActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="@string/fb_login_protocol_scheme" /> </intent-filter> </activity> <!-- … --> </application> </manifest> <!-- strings.xml --> <resources> <string name="app_name">Toptal Chat</string> <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string> <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string> </resources>
Теперь пришло время iOS. К счастью, в этом случае нам нужно изменить только один файл. Добавьте следующие значения (обратите внимание, что элемент CFBundleURLTypes
может уже существовать в списке; в этом случае вам нужно добавить эти элементы в существующий массив, а не объявлять его снова) в $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist
файл:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_FACEBOOK_URL}</string> </array> </dict> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_REVERSED_GOOGLE_WEB_CLIENT_ID}</string> </array> </dict> </array> <key>FacebookAppID</key> <string>${YOUR_FACEBOOK_APP_ID}</string> <key>FacebookDisplayName</key> <string>${YOUR_FACEBOOK_APP_NAME}</string> <key>LSApplicationQueriesSchemes</key> <array> <string>fbapi</string> <string>fb-messenger-share-api</string> <string>fbauth2</string> <string>fbshareextension</string> </array>
Несколько слов об архитектуре BLoC
Этот стандарт архитектуры был описан в одной из наших предыдущих статей, демонстрируя использование BLoC для совместного использования кода во Flutter и AngularDart, поэтому мы не будем подробно объяснять его здесь.
Основная идея, лежащая в основе основной идеи, заключается в том, что каждый экран имеет следующие классы: - view - который отвечает за отображение текущего состояния и делегирование пользовательского ввода в виде событий в блок. — состояние — которое представляет «живые» данные, с которыми взаимодействует пользователь, используя текущее представление. - блок - который реагирует на события и соответствующим образом обновляет состояние, опционально запрашивая данные из одного или нескольких локальных или удаленных репозиториев. - событие - это определенный результат действия, который может изменить или не изменить текущее состояние.
В графическом виде это можно представить так:
Кроме того, у нас есть каталог моделей, который содержит классы данных и репозитории, создающие экземпляры этих классов.
Разработка пользовательского интерфейса
Создание пользовательского интерфейса с помощью Flutter полностью выполняется в Dart, в отличие от разработки нативных приложений для Android и iOS, где пользовательский интерфейс создается с использованием схемы XML и полностью отделен от базы кода бизнес-логики. Мы собираемся использовать относительно простые композиции элементов пользовательского интерфейса с различными компонентами в зависимости от текущего состояния (например, параметры isLoading, isEmpty). Пользовательский интерфейс во Flutter вращается вокруг виджетов, а точнее дерева виджетов. Виджеты могут быть без состояния или с состоянием. Когда речь идет о виджетах с состоянием, важно подчеркнуть, что когда setState()
вызывается для определенного виджета, который отображается в данный момент (вызов его в конструкторе или после его удаления приводит к ошибке времени выполнения), проход сборки и отрисовки выполняется. планируется выполнить в следующем цикле рисования.
Для краткости мы покажем здесь только один из классов пользовательского интерфейса (представления):
class LoginScreen extends StatefulWidget { LoginScreen({Key key}) : super(key: key); @override State<StatefulWidget> createState() => _LoginState(); } class _LoginState extends State<LoginScreen> { final _bloc = LoginBloc(); @override Widget build(BuildContext context) { return BlocProvider<LoginBloc>( bloc: _bloc, child: LoginWidget(widget: widget, widgetState: this) ); } @override void dispose() { _bloc.dispose(); super.dispose(); } } class LoginWidget extends StatelessWidget { const LoginWidget({Key key, @required this.widget, @required this.widgetState}) : super(key: key); final LoginScreen widget; final _LoginState widgetState; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Login"), ), body: BlocBuilder( bloc: BlocProvider.of<LoginBloc>(context), builder: (context, LoginState state) { if (state.loading) { return Center( child: CircularProgressIndicator(strokeWidth: 4.0) ); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginGoogle(this), child: Text( "Login with Google", style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ), ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginFacebook(this), child: Text( "Login with Facebook", style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ), ), ], ), ); } }), ); } void navigateToMain() { NavigationHelper.navigateToMain(widgetState.context); } }
Остальные классы пользовательского интерфейса следуют тем же шаблонам, но, возможно, имеют другие действия и могут иметь дерево виджетов пустого состояния в дополнение к состоянию загрузки.

Аутентификация
Как вы уже догадались, мы будем использовать библиотеки google_sign_in
и flutter_facebook_login
для аутентификации пользователя, опираясь на его профиль в социальной сети. Прежде всего, обязательно импортируйте эти пакеты в файл, который будет обрабатывать логику запроса на вход:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';
Теперь у нас будет две независимые части, которые позаботятся о нашем потоке аутентификации. Первый инициирует запрос на вход в Facebook или Google:
void onLoginGoogle(LoginWidget view) async { dispatch(LoginEventInProgress()); final googleSignInRepo = GoogleSignIn(signInOption: SignInOption.standard, scopes: ["profile", "email"]); final account = await googleSignInRepo.signIn(); if (account != null) { LoginRepo.getInstance().signInWithGoogle(account); } else { dispatch(LogoutEvent()); } } void onLoginFacebook(LoginWidget view) async { dispatch(LoginEventInProgress()); final facebookSignInRepo = FacebookLogin(); final signInResult = await facebookSignInRepo.logInWithReadPermissions(["email"]); if (signInResult.status == FacebookLoginStatus.loggedIn) { LoginRepo.getInstance().signInWithFacebook(signInResult); } else if (signInResult.status == FacebookLoginStatus.cancelledByUser) { dispatch(LogoutEvent()); } else { dispatch(LoginErrorEvent(signInResult.errorMessage)); } }
Второй будет вызываться, когда мы получим данные профиля от любого провайдера. Мы собираемся добиться этого, поручив нашему обработчику входа прослушивать поток firebase_auth onAuthStateChange
:
void _setupAuthStateListener(LoginWidget view) { if (_authStateListener == null) { _authStateListener = FirebaseAuth.instance.onAuthStateChanged.listen((user) { if (user != null) { final loginProvider = user.providerId; UserRepo.getInstance().setCurrentUser(User.fromFirebaseUser(user)); if (loginProvider == "google") { // TODO analytics call for google login provider } else { // TODO analytics call for facebook login provider } view.navigateToMain(); } else { dispatch(LogoutEvent()); } }, onError: (error) { dispatch(LoginErrorEvent(error)); }); } }
Реализация UserRepo
и LoginRepo
не будет здесь публиковаться, но не стесняйтесь заглянуть в репозиторий GitHub для полного ознакомления.
Учебное пособие по Flutter: как создать приложение для обмена мгновенными сообщениями
Наконец, мы добрались до интересной части. Как следует из названия, обмен сообщениями должен осуществляться как можно быстрее, в идеале это должно быть мгновенным . К счастью, cloud_firestore
позволяет нам взаимодействовать с экземпляром Firestore, и мы можем использовать его функцию snapshots()
, чтобы открыть поток данных, который будет предоставлять нам обновления в режиме реального времени. На мой взгляд, весь код chat_repo
довольно прост, за исключением метода startChatroomForUsers
. Он отвечает за создание нового чата для двух пользователей, если нет существующего чата, содержащего обоих пользователей (поскольку мы не хотим иметь несколько экземпляров одной и той же пары пользователей), и в этом случае он возвращает существующий чат.
Однако из-за дизайна Firestore в настоящее время он не поддерживает вложенные запросы array-contains
. Таким образом, мы не можем получить соответствующий поток данных, но должны выполнить дополнительную фильтрацию на нашей стороне. Это решение состоит в получении всех чатов для вошедшего в систему пользователя и последующем поиске того, который также содержит выбранного пользователя:
Future<SelectedChatroom> startChatroomForUsers(List<User> users) async { DocumentReference userRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[1].uid); QuerySnapshot queryResults = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .where("participants", arrayContains: userRef) .getDocuments(); DocumentReference otherUserRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[0].uid); DocumentSnapshot roomSnapshot = queryResults.documents.firstWhere((room) { return room.data["participants"].contains(otherUserRef); }, orElse: () => null); if (roomSnapshot != null) { return SelectedChatroom(roomSnapshot.documentID, users[0].displayName); } else { Map<String, dynamic> chatroomMap = Map<String, dynamic>(); chatroomMap["messages"] = List<String>(0); List<DocumentReference> participants = List<DocumentReference>(2); participants[0] = otherUserRef; participants[1] = userRef; chatroomMap["participants"] = participants; DocumentReference reference = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .add(chatroomMap); DocumentSnapshot chatroomSnapshot = await reference.get(); return SelectedChatroom(chatroomSnapshot.documentID, users[0].displayName); } }
Кроме того, из-за аналогичных ограничений дизайна Firebase в настоящее время не поддерживает обновления массива (вставка нового элемента в существующее значение поля массива) со специальным FieldValue.serverTimestamp()
.
Это значение указывает платформе, что поле, содержащее это вместо фактического значения, должно быть заполнено фактической отметкой времени на сервере в момент выполнения транзакции. Вместо этого мы используем DateTime.now()
в тот момент, когда мы создаем наш новый сериализованный объект сообщения и вставляем этот объект в коллекцию сообщений чата.
Future<bool> sendMessageToChatroom(String chatroomId, User user, String message) async { try { DocumentReference authorRef = _firestore.collection(FirestorePaths.USERS_COLLECTION).document(user.uid); DocumentReference chatroomRef = _firestore.collection(FirestorePaths.CHATROOMS_COLLECTION).document(chatroomId); Map<String, dynamic> serializedMessage = { "author" : authorRef, "timestamp" : DateTime.now(), "value" : message }; chatroomRef.updateData({ "messages" : FieldValue.arrayUnion([serializedMessage]) }); return true; } catch (e) { print(e.toString()); return false; } }
Подведение итогов
Очевидно, что разработанное нами приложение для обмена мгновенными сообщениями Flutter является скорее проверкой концепции, чем готовым к продаже приложением для обмена мгновенными сообщениями. В качестве идей для дальнейшего развития можно рассмотреть внедрение сквозного шифрования или расширенного контента (групповые чаты, вложения мультимедиа, анализ URL). Но перед всем этим следует реализовать push-уведомления, поскольку они являются практически обязательной функцией для приложения для обмена мгновенными сообщениями, и мы убрали ее за рамки этой статьи для краткости. Кроме того, в Firestore по-прежнему отсутствует несколько функций, чтобы иметь более простые и точные запросы, подобные вложенным array-contains
данные.
Как упоминалось в начале статьи, Flutter только недавно превратился в стабильную версию 1.0 и будет продолжать расти, не только когда речь идет о функциях и возможностях фреймворка, но и когда речь идет о сообществе разработчиков и сторонних библиотеках. Ресурсы. Имеет смысл потратить свое время на знакомство с разработкой приложений Flutter сейчас, так как он явно здесь, чтобы остаться и ускорить процесс разработки мобильных приложений.
Без дополнительных затрат разработчики Flutter также будут готовы ориентироваться на новую ОС Google — Fuchsia.