Após subir uma aplicação em produção, é bem provável que seus usuários encontrem alguns erros durante o uso de sua aplicação.
Sabendo disso, é importante que você desenvolva formas de capturar erros e informações relevantes que levarão a aplicação a falhar.
A ferramenta mais conhecida para captura de erros em aplicações mobile é o Crashlytics.
Vamos começar.
- Criando projeto no Firebase
- Instação no Android
- Instação no IOS
- Instalando pacotes
- Configurando Crashlytics no Android
- Configurando Crashlytics no IOS
- Iniciando Crashlytics
- Simulando um crash
- Enviando um erro
- Adicionando uma chave personalizada
- Adicionando logs
- Vinculando usuário
- Tramento de erros
- Tramento de erros
Criando projeto no Firebase
O primeiro passo para a instalação do Crashlytics é a criação de um projeto no Firebase.
Acesse o Firebase Console e clique em "Adicionar projeto".
Dê um nome para o seu projeto.
Desmarque a opção "Ativar o Google Analytics neste projeto".
Instalação no Android
Vamos iniciar com a configuração no Android.
Durante a criação o nome do pacote deve ser exatamente igual ao que está no arquivo * android/app/src/main/AndroidManifest.xml*.
Faça o download do google-services.json e cole na pasta android/app.
Adicione o plugin do google-service no arquivo android/build.gradle.
buildscript {
...
repositories {
...
}
dependencies {
...
classpath 'com.google.gms: google-services: 4.3.3'
}
}
Execute o plugin do google-service adicionando apply plugin: 'com.android.application' no arquivo /android/app/build.gradle.
...
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
...
No arquivo /android/app/build.gradle habilite o Multidex e o adicione nas dependecias.
android {
...
defaultConfig {
applicationId "com.example.crashlytics_demo"
minSdkVersion 16
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
...
}
...
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementação 'com.android.support:multidex:1.0.3'
}
Habilite o android:usesCleartextTraffic="true" para que você possa realizar os testes usando um emulador.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.crashlytics_demo">
<application
android:icon="@mipmap/ic_launcher"
android:label="crashlytics_demo"
android:usesCleartextTraffic="true">
Instalação no IOS
Agora vamos configurar o Firebase para IOS.
Selecione a opção "Adicionar app", após isso selecione "IOS".
Da mesma forma que fizemos no Android, devemos utilizar o nome do pacote igual ao que está no "Bundle identifier" do IOS.
Prossiga com a instalação e faça o download do arquivo "GoogleService-Info.plist".
Para adicionar esse arquivo em um projeto IOS você deve usar o Xcode.
Com o projeto aberto clique com o botão direito em "Runner" e selecione a opção "Add Files to Runner".
Encontre e selecione o arquivo "GoogleService-Info.plist" e verifique se "Copy items if needed" está selecionado.
Você ainda precisa ativar o Firebase para funcionar quando você estiver utilizando um emulador.
Para isso abra o arquivo "Info.plist" clicando com o botão direito em cima dele e selecionando a opção "Open as" > " Source Code".
E adicione essa chave:
...
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
Instalando pacotes
Instale as dependências do Crashlytics.
dependencies:
flutter:
sdk: flutter
firebase_core: "^1.3.0"
firebase_crashlytics: "^2.0.6"
Configurando Crashlytics no Android
dependencies {
...
classpath 'com.google.gms:google-services:4.3.5'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
}
...
android {
...
}
dependencies {
...
}
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
Configurando Crashlytics no IOS
Abra sua aplicação no Xcode. Clique em "Runner", selecione a opção "Build Phases" em seguida clique em "+" > "New Run Script Phase".
Adicione "${PODS_ROOT}/FirebaseCrashlytics/run" na caixa de texto.
Ativação do Crashlytics
Iniciando o Crashlytics
No seu arquivo main.dart faça a inicialização do Firebase.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: _initialization,
builder: (context, snapshot) {
if (snapshot.hasError) return Center(child: Text('Error'));
if (snapshot.connectionState == ConnectionState.done) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('Home'),
));
}
return Center(child: CircularProgressIndicator());
}));
}
}
Simulando um crash
Adicione um botão e simule um evento de crash na aplicação.
ElevatedButton(
child: Text('CRASH'),
onPressed: () {
FirebaseCrashlytics.instance.crash();
},
),
Rode sua aplicação e clique no botão Crash. Após isso esperamos que chegue um erro no Firebase Console.
Enviando um erro
Você pode enviar um erro manualmente para o Crashlytics utilizando o recordError
.
Parametros:
- error: dynamic (Obrigatorio) -> Nesse campo você deve enviar a Exception ou a string do erro.
- stackTrace: StackTrace (Obrigatorio) -> A pilha de informações de onde o erro ocorreu. (O Crashlytics utiliza esse campo para fazer o agrupamento dos erros).
- reason: String (Opcional) -> O valor enviado nesse campo será salvo na propriedade
flutter_error_reason
. Você poderia visualizar essa mensagem acessando os detalhes do erro no Console do Firebase. Você pode utilizar esse campo para realizar filtros e encontrar todos os erros que possuem e mesma propriedade. - fatal: boolean (Opcional) -> Boolean que indicará se o erro é do tipo fatal.
...
onPressed: () async {
try {
throw Exception('Teste');
} catch(error, stackTrace) {
await FirebaseCrashlytics.instance.recordError(
error,
stackTrace,
reason: 'a error test',
fatal: true,
);
}
},
...
Adicionando uma chave personalizada
Podemos adicionar chaves personalizadas ao erro do Crashlytics, para isso usamos o método setCustomKey
.
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
String _type = 'Padrao';
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: _initialization,
builder: (context, snapshot) {
if (snapshot.hasError) return Center(child: Text('Error'));
if (snapshot.connectionState == ConnectionState.done) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
ElevatedButton(
onPressed: () {
setState(() {
_type = 'Digital';
});
FirebaseCrashlytics.instance
.setCustomKey('type', _type);
},
child: Text('Digital')),
ElevatedButton(
onPressed: () {
setState(() {
_type = 'Analogico';
});
FirebaseCrashlytics.instance
.setCustomKey('type', _type);
},
child: Text('Analogico')),
ElevatedButton(
child: Text('CRASH'),
onPressed: () async {
try {
throw Exception('Teste');
} catch (error, stackTrace) {
await FirebaseCrashlytics.instance.recordError(
error,
stackTrace,
reason: 'a error test',
);
}
},
)
],
));
}
return Center(child: CircularProgressIndicator());
}));
}
}
Esse é o resultado no Console do Firebase:
Adicionando logs
Muitas vezes as informações que temos disponíveis no Crashlytics não são suficientes para entendermos o motivo de uma
falha. Com isso, podemos optar por enviar algumas mensagens de log usando o método log
.
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: _initialization,
builder: (context, snapshot) {
if (snapshot.hasError) return Center(child: Text('Error'));
if (snapshot.connectionState == ConnectionState.done) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
ElevatedButton(
onPressed: () {
FirebaseCrashlytics.instance.log('Tap on "A"');
},
child: Text('A')),
ElevatedButton(
onPressed: () {
FirebaseCrashlytics.instance.log('Tap on "B"');
},
child: Text('B')),
ElevatedButton(
child: Text('CRASH'),
onPressed: () async {
try {
throw Exception('Teste');
} catch (error, stackTrace) {
await FirebaseCrashlytics.instance.recordError(
error,
stackTrace,
);
}
},
)
],
));
}
return Center(child: CircularProgressIndicator());
}));
}
}
Resultado:
Vinculando usuário
Podemos vincular o usuário a um evento de erro usando o setUserIdentifier
.
Exemplo:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: _initialization,
builder: (context, snapshot) {
if (snapshot.hasError) return Center(child: Text('Error'));
if (snapshot.connectionState == ConnectionState.done) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
ElevatedButton(
child: Text('CRASH'),
onPressed: () async {
try {
throw Exception('Teste');
} catch (error, stackTrace) {
await FirebaseCrashlytics.instance
.setUserIdentifier('25');
await FirebaseCrashlytics.instance.recordError(
error,
stackTrace,
);
}
},
)
],
));
}
return Center(child: CircularProgressIndicator());
}));
}
}
Tratamento de erros
Faremos o tratamento para que nossa aplicação detecte erros e faça o envio do erro para o Crashlytics automaticamente.
Vamos trabalhar com a captura de dois tipos de erros:
FlutterError: Problemas lançados na estrutura do Flutter.
Ex:. Problemas de renderização e erros síncronos.
ZoneError: Problemas que não conseguem ser detectados pelo Flutter, mas são detectados via
runZonedGuarded
.
Ex:. Erros assíncronos.
Abaixo está um exemplo onde reproduzimos esses dois cenários.
import 'dart:async';
import 'package:flutter/material.dart';
Future<void> main() async {
runZonedGuarded(() {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (FlutterErrorDetails errorDetails) {
print('FlutterError');
};
runApp(MyApp());
}, (error, stackTrace) {
print('ZoneError');
});
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Column(
children: [
ElevatedButton(
child: Text('CRASH'),
onPressed: () {
throw Exception('Teste');
},
),
ElevatedButton(
onPressed: () {
Future.delayed(
Duration.zero, () => throw Exception('async error'));
},
child: Text('CRASH ASSINCRONO'),
),
],
)));
}
}
No nosso caso, enviaremos o erro capturado para o Crashlytics. Para isso vamos utilizar a função
recordFlutterError
quando for um erro do Flutter e recordError
quando for um erro na
Zones.
...
Future<void> main() async {
runZonedGuarded(() {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
runApp(MyApp());
}, FirebaseCrashlytics.instance.recordError);
}
...
Veja o exemplo completo em: https://github.com/gabrielferreir/demo_crashlytis
Qualquer dúvida ou sugestão, estou à disposição! Valeeeeu.