Tutorial Flutter: Como criar seu primeiro aplicativo Flutter
Publicados: 2022-03-11O que é Flutter?
O Flutter é o SDK de desenvolvimento de aplicativos móveis do Google que permite que seu produto seja direcionado às plataformas Android e iOS simultaneamente, sem a necessidade de manter duas bases de código separadas. Além disso, os aplicativos que usam o Flutter também podem ser compilados para direcionar o próximo sistema operacional Fuchsia do Google.
Flutter recentemente atingiu um marco importante - versão estável 1.0. O lançamento aconteceu em Londres, dia 5 de dezembro de 2018, no evento Flutter Live. Embora ainda possa ser considerado um empreendimento de software inicial e em evolução, este artigo se concentrará em um conceito já comprovado e demonstrará como desenvolver um aplicativo de mensagens totalmente funcional voltado para as principais plataformas móveis usando Flutter 1.2 e Firebase.
Como pode ser visto no gráfico abaixo, o Flutter vem ganhando muitos usuários nos últimos meses. Em 2018, a participação de mercado do Flutter dobrou e está a caminho de superar o React Native em termos de consultas de pesquisa, daí nossa decisão de criar um novo tutorial do Flutter.
Nota: Este artigo se concentra apenas em alguns bits da implementação. A referência completa do código-fonte do projeto pode ser encontrada neste repositório do GitHub.
Pré-requisitos
Embora tenham sido feitos esforços para permitir que os leitores sigam e realizem este projeto, mesmo que seja sua primeira tentativa de desenvolvimento móvel, muitos conceitos básicos de desenvolvimento móvel que não são específicos do Flutter são mencionados e usados sem explicação detalhada.
Isso foi realizado para a brevidade do artigo, pois um de seus objetivos é que o leitor conclua o projeto de uma só vez. Por fim, o artigo pressupõe que você já tenha seu ambiente de desenvolvimento configurado, incluindo os plug-ins necessários do Android Studio e o Flutter SDK.
Configuração do Firebase
Configurar o Firebase é a única coisa que temos que fazer de forma independente para cada plataforma. Antes de tudo, certifique-se de criar um novo projeto no Firebase Dashboard e adicionar aplicativos Android e iOS no workspace recém-gerado. A plataforma produzirá dois arquivos de configuração que você precisa baixar: google-services.json
para Android e GoogleService-Info.plist
para iOS. Antes de fechar o painel, certifique-se de ativar os provedores de autenticação do Firebase e do Google, pois os usaremos para identificação do usuário. Para fazer isso, escolha o item Autenticação no menu e selecione a guia Método de login.
Agora você pode fechar o painel enquanto o restante da configuração ocorre em nossa base de código. Antes de tudo, precisamos colocar os arquivos que baixamos em nosso projeto. O arquivo google-services.json
deve ser colocado na pasta $(FLUTTER_PROJECT_ROOT)/android/app
e GoogleService-Info.plist
deve ser colocado no $(FLUTTER_PROJECT_ROOT)/ios/Runner
. Em seguida, precisamos configurar as bibliotecas do Firebase que vamos usar no projeto e conectá-las aos arquivos de configuração. Isso é feito especificando os pacotes Dart (bibliotecas) que usaremos no arquivo pubspec.yaml
do nosso projeto. Na seção de dependências do arquivo, cole o seguinte snippet:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:
Os dois primeiros não estão relacionados ao Firebase, mas serão usados com frequência no projeto. Os dois últimos são, espero, auto-explicativos.
Por fim, precisamos definir as configurações de projeto específicas da plataforma que permitirão que nosso fluxo de autenticação seja concluído com êxito. No lado do Android, precisamos adicionar o plug-in google-services Gradle à nossa configuração do Gradle no nível do projeto. Em outras palavras, precisamos adicionar o seguinte item à lista de dependências no arquivo $(FLUTTER_PROJECT_ROOT)/android/build.gradle
:
classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version
Então precisamos aplicar esse plugin adicionando esta linha ao final de $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle
:
apply plugin: 'com.google.gms.google-services'
A última coisa para esta plataforma é listar os parâmetros do seu aplicativo do Facebook. O que estamos procurando aqui é editar esses dois arquivos - $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml
e $(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>
Agora é a vez do iOS. Felizmente, só precisamos alterar um arquivo neste caso. Adicione os seguintes valores (observe que o item CFBundleURLTypes
já pode existir na lista; nesse caso, você precisa adicionar esses itens ao array existente em vez de declará-lo novamente) para $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist
Arquivo:
<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>
Uma palavra sobre arquitetura BLoC
Esse padrão de arquitetura foi descrito em um de nossos artigos anteriores, demonstrando o uso do BLoC para compartilhamento de código em Flutter e AngularDart, portanto, não o explicaremos em detalhes aqui.
A ideia básica por trás da ideia principal é que cada tela tenha as seguintes classes: - view - que é responsável por exibir o estado atual e delegar a entrada do usuário como eventos ao bloco. - estado - que representa dados “vivos” com os quais o usuário interage usando a visualização atual. - bloc - que responde a eventos e atualiza o estado de acordo, opcionalmente solicitando dados de um ou vários repositórios locais ou remotos. - evento - que é um resultado de ação definido que pode ou não alterar o estado atual.
Como representação gráfica, pode-se pensar assim:
Além disso, temos um diretório de modelo que contém classes de dados e repositórios que produzem instâncias dessas classes.
Desenvolvimento de IU
A criação da interface do usuário usando o Flutter é feita completamente no Dart, ao contrário do desenvolvimento de aplicativos nativos no Android e iOS, onde a interface do usuário é construída usando o esquema XML e é completamente separada da base de código da lógica de negócios. Vamos usar composições de elementos de interface do usuário relativamente simples com diferentes componentes baseados no estado atual (por exemplo, parâmetros isLoading, isEmpty). A interface do usuário no Flutter gira em torno de widgets, ou melhor, da árvore de widgets. Os widgets podem ser sem estado ou com estado. Quando se trata de stateful, é importante ressaltar que, quando setState()
é chamado em um widget específico que está sendo exibido no momento (chamá-lo no construtor ou após ele ser descartado resulta em um erro de execução), um passo de construção e desenho é programado para ser executado no próximo ciclo de desenho.
Por brevidade, mostraremos apenas uma das classes de UI (view) aqui:
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); } }
O restante das classes de interface do usuário seguem os mesmos padrões, mas talvez tenham ações diferentes e possam apresentar uma árvore de widget de estado vazia além do estado de carregamento.

Autenticação
Como você deve ter adivinhado, usaremos as bibliotecas google_sign_in
e flutter_facebook_login
para autenticar o usuário com base em seu perfil de rede social. Antes de tudo, certifique-se de importar esses pacotes para o arquivo que irá lidar com a lógica de solicitação de login:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';
Agora, teremos duas partes independentes que cuidarão do nosso fluxo de autenticação. O primeiro vai iniciar uma solicitação de login do Facebook ou do 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)); } }
O segundo será chamado quando obtivermos os dados do perfil de um dos provedores. Vamos fazer isso instruindo nosso manipulador de login a ouvir o stream 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)); }); } }
A implementação do UserRepo
e LoginRepo
não será postada aqui, mas fique à vontade para dar uma olhada no repositório do GitHub para referência completa.
Tutorial Flutter: Como criar um aplicativo de mensagens instantâneas
Por fim, chegamos à parte interessante. Como o nome indica, as mensagens devem ser trocadas o mais rápido possível, idealmente, isso deve ser instantâneo . Felizmente, cloud_firestore
nos permite interagir com a instância do Firestore e podemos usar seu recurso snapshots()
para abrir um fluxo de dados que nos fornecerá atualizações em tempo real. Na minha opinião, todo o código chat_repo
é bastante direto, com exceção do método startChatroomForUsers
. Ele é responsável por criar uma nova sala de bate-papo para dois usuários, a menos que haja uma existente que contenha os dois usuários (já que não queremos ter várias instâncias do mesmo par de usuários), caso em que ele retorna a sala de bate-papo existente.
No entanto, devido ao design do Firestore, ele atualmente não oferece suporte a consultas array-contains
aninhadas. Portanto, não podemos recuperar o fluxo de dados apropriado, mas precisamos realizar uma filtragem adicional do nosso lado. Essa solução consiste em recuperar todas as salas de chat do usuário logado e depois procurar aquela que também contém o usuário selecionado:
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); } }
Além disso, devido a restrições de design semelhantes, o Firebase atualmente não oferece suporte a atualizações de matriz (inserção de novo elemento em um valor de campo de matriz existente) com valor especial FieldValue.serverTimestamp()
.
Esse valor indica à plataforma que o campo que contém isso em vez de um valor real deve ser preenchido com o timestamp real no servidor no momento em que a transação ocorre. Em vez disso, estamos usando DateTime.now()
no momento em que estamos criando nosso novo objeto serializado de mensagem e inserindo esse objeto na coleção de mensagens da sala de bate-papo.
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; } }
Empacotando
Obviamente, o aplicativo de mensagens Flutter que desenvolvemos é mais uma prova de conceito do que um aplicativo de mensagens instantâneas pronto para o mercado. Como ideias para desenvolvimento adicional, pode-se considerar a introdução de criptografia de ponta a ponta ou conteúdo rico (bate-papos em grupo, anexos de mídia, análise de URL). Mas antes de tudo isso, deve-se implementar as notificações push, pois elas são praticamente um recurso obrigatório para um aplicativo de mensagens instantâneas, e nós o movemos para fora do escopo deste artigo por uma questão de brevidade. Além disso, o Firestore ainda não possui alguns recursos para ter consultas mais simples e precisas, como dados, como array-contains
consultas.
Como mencionado no início do artigo, o Flutter amadureceu recentemente para a versão 1.0 estável e continuará crescendo, não apenas quando se trata de recursos e capacidades da estrutura, mas também quando se trata da comunidade de desenvolvimento e bibliotecas de terceiros e Recursos. Faz sentido investir seu tempo para se familiarizar com o desenvolvimento de aplicativos Flutter agora, pois está claramente aqui para ficar e acelerar seu processo de desenvolvimento móvel.
Sem despesas adicionais, os desenvolvedores do Flutter também estarão prontos para atingir o emergente sistema operacional do Google – Fuchsia.