Crash Reporting, Firebase Android - Parte 11

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Crash Reporting, Firebase Android - Parte 11

Crash Reporting, Firebase Android - Parte 11

Vinícius Thiengo
(2993) (7)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição1ª
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Opa, blz?

Nesse post continuamos com a série Firebase Android, mais precisamente estaremos abordando o Crash Reporting ou Dashboard de Relatório de Erros de sua APP Android no dashboard Firebase.

O Crash Reporting vai nos permitir visualizar os problemas que estão ocorrendo com nossas APPs quando já em produção. O relatório de erros nos informa até mesmo a operadora do device que teve a Exception gerada. Somente os dados que identificam o usuário do APP é que não temos acesso, não de forma explícita no relatório de erros.

Antes de prosseguir, o projeto completo está disponível no GitHub: https://github.com/viniciusthiengo/nosso-chate

Depois desse "bla bla bla" você deve estar pensando: Ok, interessante saber dos erros, mas para mim um bloco try...catch vazio é mais que o suficiente.

Não é bem simples assim. Na verdade devemos olhar para o Crash Reporting como um dos principais caminhos para evolução da APP. Tendo em mente que nos primeiros releases dela serão necessários muitos feedbacks dos usuários e identificação de erros. Os usuários poderiam fazer esse último também, porém de maneira muito menos eficiente que um tracking de erros como o Crash Reporting do Firebase.

Lembrando que blocos try...catch não capturam erros fatais, uma funcionalidade do Crash Reporting, capturar erros fatais automaticamente, sem adição de linhas de código no projeto.

Note que a parte "evoluir a APP" citada acima deveria ser mais que o suficiente para lhe convencer sobre a necessidade de uso de um Crash Reporting. Evoluir sua APP tende a ser o melhor investimento em seu projeto, pois as chances de ele dar certo aumentam em muito a cada evolução que responde a feedbacks de usuários e a realtórios de erros.

Enfim, até aqui você já deve ter entendido a importância do Crash Reporting, logo vamos seguir com a atualização de nosso algoritmo para que seja possível utilizar o Crash Reporting.

Nesse post nossa proposta é colocar o FirebaseCrash (essa é a forma de referenciar o Firebase Crash Reporting no código) em todos os locais que têm, ou deveriam ter, métodos de error, failure e cancel. Isso na activity LogincActivity.

Vamos começar atualizando nosso gradle APP level, ou build.gradle (Module:app), adicionando a referência a 'com.google.firebase:firebase-crash:9.0.2':

...
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'com.google.firebase:firebase-database:9.0.2'
compile 'com.google.firebase:firebase-auth:9.0.2'
compile 'com.google.firebase:firebase-crash:9.0.2' /* ADICIONE ESSA LINHA */
compile 'com.firebaseui:firebase-ui:0.4.0'
compile 'com.facebook.android:facebook-android-sdk:[4,5)'
compile 'com.google.android.gms:play-services-auth:9.0.2'
compile('com.twitter.sdk.android:twitter:1.13.1@aar') {
transitive = true;
}
compile 'com.github.alorma:github-sdk:3.2.5'
}
...

 

Logo depois podemos começar a codificação em LoginActivity. Devemos estudar o código e então ir colocando a chamada a FirebaseCrash.report() nos locais propostos (métodos de erro, failure e cancel).

Logo no onCreate() temos o código de inicialização do Facebook, o método registerCallback() com uma implementação anônima de FacebookCallback, onde temos de fornecer uma implementação concreta para o método onError(). Esse é o que receberá nosso código de crash:

...
LoginManager.getInstance().registerCallback(callbackManager, new FacebookCallback<LoginResult>() {
@Override
public void onSuccess(LoginResult loginResult) {
accessFacebookLoginData( loginResult.getAccessToken() );
}

@Override
public void onCancel() {}

@Override
public void onError(FacebookException error) {
FirebaseCrash.report( error ); /* ADICIONE ESSA LINHA */
showSnackbar( error.getMessage() );
}
});
...

 

Nosso próximo método de atualização é o accessLoginData(), nele temos uma chamada para addOnCompleteListener(). Vamos encadear uma chamada a addOnFailureListener() e então dentro do método onFailure() da implementação anônima de OnFailureListener teremos o código de crash:

...
mAuth.signInWithCredential(credential)
.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {

if( !task.isSuccessful() ){
showSnackbar("Login social falhou");
}
}
})
.addOnFailureListener(new OnFailureListener() { /* ADICIONE TODO O CÓDIGO DAQUI PARA BAIXO */
@Override
public void onFailure(@NonNull Exception e) {
FirebaseCrash.report( e );
}
});
...

 

Agora em todos os métodos listeners de clique para login, vamos adicionar uma chamada a FirebaseCrash.log(). Por qual motivo?

Dessa vez o objetivo é realizar o tracking da interação do usuário com a APP, isso para que caso ocorra algum erro com o usuário na APP possamos simular exatamente o que o ele fez até o problema ocorrer. Segue códigos atualizados dos métodos vinculados aos listeners de clique. Começando por sendLoginData():

...
public void sendLoginData( View view ){
/* ADICIONE A LINHA ABAIXO */
FirebaseCrash.log("LoginActivity:clickListener:button:sendLoginData()");

openProgressBar();
initUser();
verifyLogin();
}
...

 

Note a mensagem utilizada: "LoginActivity:clickListener:button:sendLoginData()". Você é quem defini qual será a mensagem, porém busque escolher uma que facilmente informará no dashboard de crash do Firebase o significado dela no tracking.

Antes de prosseguir abaixo um print de como esse tracking obtido com o log() apareceria no Firebase Crash Dashboard caso algum erro ocorra depois do tracking:

O que é esse: "LoginActivity:verifyLogin()" e esse "Crash"?

O "LoginActivity:verifyLogin()" será utilizado em uma nova chamada a FirebaseCrash.log() que teremos em outro método que será abordado ainda no post. O "Crash" é o indicador de quando houve o erro.

Ok, mas por que essa linha está marcando o time de acontecimento antes mesmo que as linhas de tracking que teoricamente deveriam vir primeiro no dashboard?

Excelente questão. No tempo desse post o Firebase Crash Reporting ainda estava em fase beta, e realmente não faz sentido esse crash ter ocorrido antes das chamadas a log() realizando o tracking. Logo, a princípio, o que temos é um bug. Note que nesse crash o Crash time foi apresentado antes dos dois tracking, porém é comum ele ser a penúltima linha de tracking a ser apresentada, algo que também não faz sentido.

Porém não há problemas no entendimento, pois é somente a linha de crash que tem essa inconsistência quando o tracking de erro é apresentado no dashboard, todas as outras linhas respeitam a ordem definida no algoritmo do projeto. Logo assuma sempre que a linha com o termo "Crash" é sempre a última.

Vale ressaltar que também temos o método estático FirebaseCrash.logcat(), onde além de realizar o tracking até o acontecimento do erro ele imprime esse tracking também nos logs do AndroidStudio.

Em nosso projeto não houve a necessidade do logcat(). Podemos prosseguir com a atualização de todos os outros métodos listeners de clique para login, digo, colocando nosso tracking. Agora no método sendLoginFacebookData():

...
public void sendLoginFacebookData( View view ){
/* ADICIONE A LINHA ABAIXO */
FirebaseCrash.log("LoginActivity:clickListener:button:sendLoginFacebookData()");

LoginManager
.getInstance()
.logInWithReadPermissions(
this,
Arrays.asList("public_profile", "user_friends", "email")
);
}
...

 

Note que sempre colocamos na mensagem de tracking o nome original do método, para o tracking ser exato quanto ao entendimento do developer no dashboard Fireabse. Agora seguimos com o método sendLoginGoogleData():

public void sendLoginGoogleData( View view ){
/* ADICIONE A LINHA ABAIXO */
FirebaseCrash.log("LoginActivity:clickListener:button:sendLoginGoogleData()");

Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
startActivityForResult(signInIntent, RC_SIGN_IN_GOOGLE);
}

 

Então o método sendLoginTiwtterData(). Nsse temos ainda um método failure() da instanciação anônima de Callback<TwitterSession>. Nesse método colocaremos também um código de crash:

...
public void sendLoginTwitterData( View view ){
/* ADICIONE A LINHA ABAIXO */
FirebaseCrash.log("LoginActivity:clickListener:button:sendLoginTwitterData()");

twitterAuthClient.authorize(
this,
new Callback<TwitterSession>() {
@Override
public void success(Result<TwitterSession> result) {
TwitterSession session = result.data;
accessTwitterLoginData(
session.getAuthToken().token,
session.getAuthToken().secret,
String.valueOf( session.getUserId() )
);
}
@Override
public void failure(TwitterException exception) {
FirebaseCrash.report( exception ); /* ADICIONE ESSA LINHA */
showSnackbar( exception.getMessage() );
}
}
);
}
...

 

E enfim o último listener de clique nos botões de login, o sendLoginGithubData():

...
public void sendLoginGithubData( View view ){
/* ADICIONE A LINHA ABAIXO */
FirebaseCrash.log("LoginActivity:clickListener:button:sendLoginGithubData()");
...
}
...

 

Depois desses métodos de listeners, coninuando o debug no código, chegamos ao método requestGitHubUserAccessToken() que também tem um método de erro, mais precisamente o onError() na instanciação anônima de Observer<Token>, logo colocamos nele também o nosso código de crash:

...
requestTokenClient
.observable()
.subscribe(new Observer<Token>() {
@Override
public void onCompleted() {}

@Override
public void onError(Throwable e) {
FirebaseCrash.report( e ); /* ADICIONE ESSA LINHA */
showSnackbar( e.getMessage() );
}

@Override
public void onNext(Token token) {
if( token.access_token != null ){
requestGitHubUserData( token.access_token );
}
}
});
...

 

Ainda em um método vinculado ao GitHub login, dessa vez o requestGitHubUserData(), temos novament o onError(), segue código atualizado:

...
getAuthUserClient
.observable()
.subscribe(new Observer<com.alorma.github.sdk.bean.dto.response.User>() {
@Override
public void onCompleted() {}

@Override
public void onError(Throwable e) {
FirebaseCrash.report( e ); /* ADICIONE ESSA LINHA */
}

@Override
public void onNext(com.alorma.github.sdk.bean.dto.response.User user) {
LoginActivity.this.user.setName( user.name );
LoginActivity.this.user.setEmail( user.email );

accessGithubLoginData( accessToken );
}
});
...

 

Depois dessas atualizações temos o método verifyLogin(), o que é invocado no método sendLoginData(). Além de colocarmos nosso código de tracking (FirebaseCrash.log()) nesse método, teremos também a adição de um addOnFailureListener(). Segue código atualizado:

...
private void verifyLogin(){
/* ADICIONE A LINHA ABAIXO */
FirebaseCrash.log("LoginActivity:verifyLogin()");

user.saveProviderSP( LoginActivity.this, "" );
mAuth.signInWithEmailAndPassword(
user.getEmail(),
user.getPassword()
)
.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if( !task.isSuccessful() ){
showSnackbar("Login falhou");
return;
}
}
})
.addOnFailureListener(new OnFailureListener() { /* ADICIONE TODO O CÓDIGO A PARTIR DESSA LINHA */
@Override
public void onFailure(@NonNull Exception e) {
FirebaseCrash.report( e );
}
});
}
...

 

Antes de prosseguir note que no método acima não tem a validação dos campos de email e senha (password), digo, eles podem ser enviados vazios, null. Esse será o trecho onde realizaremos o teste para gerar um crash no dashboard Firebase Crash Reporting.

Mas antes ainda temos de trabalhar o FirebaseCrash.report() no método listener de conexão falha, onConnectionFailed():

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
/* EXCETO A LINHA DO showSnackbar(), ADICIONE TODO O CÓDIGO */
FirebaseCrash
.report(
new Exception(
connectionResult.getErrorCode()+": "+connectionResult.getErrorMessage()
)
);
showSnackbar( connectionResult.getErrorMessage() );
}

 

ConnectionResult não é uma subclasse de Throwable, logo temos de instanciar um Exception e passar os dados de identificação do problema a ele.

Finalizada a atualizacão do código de LoginActivity podemos simular um erro, exception. Executando a APP e então tentando um login sem fornecer dados. Temos a tela a seguir:

O dado de crash pode levar até 20 minutos para ser plotado no dashboard do Firebase. Depois disso teremos o seguinte conteúdo:

A partir de Clusters é que temos os registros que nos interessam, os erros. Como eles são ordenados? Segundo a documentação é de acordo com a "severidade" do problema ante ao uso da APP pelo usuário.

Estudando o dashboard e realizando alguns testes foi constatado que a coluna Erros é a que tem mais peso na classificação dos dados, quanto maior o número de erros, mais acima o agrupamento fica na lista.

Os agrupamentos são feitos por stack trace (rastreamento de pilha), o mesmo conteúdo informado nos logs do AndroidStudio quando há algum erro. Stack traces similares permanecerão agrupadas na mesma linha.

Uma crítica que tenho é quanto a classificação do erros, digo, ordenação. Não ficou claro na documentação como é feita. E outra, não há possibilidade de classificação utilizando as colunas numéricas da lista de Clusters. Outra funcionalidade que senti falta foi a não possibilidade de marcar o erro como Solved ou algo parecido, até mesmo um Delete.

Voltando ao pensamento... Clicando na primeira linha de nosso exemplo, na lista de clusters, e logo depois clicando em VISUALIZAR DETALHES temos na primeira parte da página do cluster um resumo das Versões do aplicativo (a que definimos em versionCode no gradle), APIs level dos devices Android que geraram os crashs e a lista de dispositivos desses crashs:

Então, em Amostra de erros temos a parte mais importante, o stack trace, onde podemos literalmente tomar conhecimento da parte exata de nosso código que gerou o problema e então implementar a correção:

Veja que por página no cluster estamos acessando os erros ocorridos de forma individual por device. Para navegar em todos os erros vinculados ao cluster temos as setas azul e cinza na parte superior direita.

As outras duas partes dessa página de erro têm respectivamente a área de dados que identifica unicamente o dispositivo e logo depois o Registro referente ao possível tracking, se tivermos utilizado o mesmo em nosso código (nós utilizamos, lembra do log()?!):

O Firebase Crash Report está vinculado também ao Firebase Analytics, clicando no item Analytics no menu esquerdo e logo depois na aba EVENTOS temos esses números de crash em "app_exception":

Note que se você estiver utilizando o Proguard terá de enviar um arquivo de mapeamento para que seja possível a desobfuscação (existe essa palavra?) do conteúdo de crash para que ele fique "humanamente" entendível.

Há vários pontos onde você pode enviar esse arquivo. Na aba ARQUIVOS DE MAPEAMENTO do Crash Reporting. Na versão simplificada de apresentação do crash (como na imagem abaixo) e na área de cluster. Clique em enviar e selecione o arquivo de mapeamento:

Como informado na documentação, caso você tenha a necessidade de saber os dados do usuário que teve problemas de crash, isso para uma possível comunicação direta com ele (dados como email, por exemplo). Você deverá utilizar o Proguard, pois caso contrário, se colocar qualquer dado que identifique o usuário (em FirebaseCrash.log(), por exemplo) e esse dado não estiver obfuscado, os scripts do Google vão identificá-lo e então o omitir, você não terá acesso a ele.

Utilizando o Proguard e logo depois o arquivo de mapeamento você conseguirá o acesso, pois devido ao código obfuscado o Google não consegue identificar os dados que seu script gravou e identificam o usuário.

Uma outra coisa que ainda temos de informar sobre o Crash Reporting é que ele trabalha em um novo Processo Linux quando o utilizamos em nossa APP. Um novo processo? Do que está falando?

Bom, toda vez que a APP é aberta no device, ao menos um Processo Linux é iniciado para conter todo o necessário de recurso para essa APP, inclusive os próprios fontes da APP. Isso também inclui a memória disponível para a APP no foreground (primeiro plano).

Ao menos um Processo Linux? Existe a possibilidade ter mais?

Sim, na verdade você developer é que escolhe. Esse tipo de comportamento é comum com APPs que consomem muita memória e podem rapidamente ter um OutOfMemoryException. APPs de MP3, por exemplo, se manterem todo o conteúdo em somente um processo o OutOfMemoryException é quase certo de ocorrer.

Porém se trabalharem também com outro processo, um somente para o processamento do MP3, por exemplo. Esse outro processo terá todo o conjunto de recursos disponíveis somente para ele, evitando assim o problema de vazamento de memória.

Já em 2015 o Spotify trabalhava dessa maneira, veja a imagem abaixo com dois processos do Spotify em execução:

Mas qual o problema que isso pode ocasionar? Digo, o uso de mais de um processo?

Em nosso contexto com o Crash Report, na verdade nada muito sério, a princípio. Caso seu APP esteja estendendo a classe Application, em nosso exemplo de APP de Chat nós estamos (CustomApplication extends Application), e como conteúdo dessa nova classe tenha parte da lógica de negócio de seu projeto, digo, lógica que altere de alguma forma uma entidade de acesso global em seu projeto (um SharedPreferences, por exemplo)... se essa situação é comum a sua APP, então você muito provavelmente terá problemas de inconsistência.

Ok, mas o que seria uma entidade de acesso global, somente a referência a SharedPreferences não disse muito a mim?

Seria uma entidade que independente da quantidade de processos ela seria única no projeto, ou seja, o acesso a ela seria o mesmo em todos os processos. Outro exemplo de entidade global no projeto é o SQLite, caso altere algo nele no onCreate() de sua CustomApplication, por exemplo. Terá problemas de conconrrência devido ao Crash Reporting trabalhar dentro do próprio Processo Linux dele.

Quando utilizando sua APP em ambiente de desenvolvimento, vá ao AndroidStudio e clique no ícone Attach Debug to Android process, terá a tela com ao menos dois processos:

Para um melhor entendimento sobre processos Linux no Android estude os conteúdos dos links indicados abaixo:

Application Fundamentals

Processes and Threads

Se testar em sua APP, mais precisamente em seu CustomApplication, o código abaixo, terá nos logs do AndroidStudio a incrementação ocorrendo duas vezes, uma para o processo de sua APP (o que tem a UIThread) e a outra para o processo do Firebase Crash Report:

public class CustomApplication extends Application {

@Override
public void onCreate() {
super.onCreate();

SharedPreferences sp = getSharedPreferences("SP_TEST", MODE_PRIVATE);
int value = sp.getInt("count", 0);

Log.i("LOG", "Value: "+value);

SharedPreferences.Editor editor = sp.edit();
editor.putInt("count", value + 1 );
editor.apply();
}
}

 

Caso for utilizar o código acima para testes não esqueça de atualizar o AndroidManifest.xml, a tag <application>:

...
<application xmlns:tools="http://schemas.android.com/tools"
android:name=".CustomApplication" <!-- REFERÊNCIA A NOSSA CUSTOM APPLICATION -->
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...

 

Com isso terminamos o conteúdo sobre o Firebase Crash Reporting. Para acessar o projeto completo entre no GitHub: https://github.com/viniciusthiengo/nosso-chate

Abaixo o vídeo com o passo a passo completo da implementação do Firebase Crash Reporting em nossa APP de Chat:

Segue os posts já liberados dessa série:

Múltiplos Links de Autenticação e Correção de Código, Firebase Android - Parte 10

GitHub Login, Firebase Android - Parte 9

Recuperação de Senha, Firebase Atualizado - Parte 8

Twitter Login (Fabric), Firebase Android - Parte 7

Google SignIn API, Firebase Android - Parte 6

Facebook Login, Firebase Android - Parte 5

Remoção de Conta e Dados de Login, Firebase Android - Parte 4

Atualização de Dados, Firebase Android - Parte 3

Eventos de Leitura e Firebase UI Android - Parte 2

Persistência Com Firebase Android - Parte 1

Fontes do conteúdo post:

Doc Firebase Crash Reporting

Referência classe FirebaseCrash

Going multiprocess on Android

Vlw.

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Refatoração de Código: Extrair ParâmetroRefatoração de Código: Extrair ParâmetroAndroid
Refatoração de Código: Substituir Código de Tipo Por ClasseRefatoração de Código: Substituir Código de Tipo Por ClasseAndroid
Padrão de Projeto: ObserverPadrão de Projeto: ObserverAndroid
Refatoração de Código: Substituir Notificações Hard-Coded Por ObserverRefatoração de Código: Substituir Notificações Hard-Coded Por ObserverAndroid

Compartilhar

Comentários Facebook

Comentários Blog (7)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
13/09/2016
Boa tarde Thiengo.

Eu posso utilizar essa ferramenta só para capturar Exception do meu app, ou eu preciso implementar alguma tecnologia que o Firebase me proporciona, para conseguir utilizá-la  ?
Responder
Vinícius Thiengo (0) (0)
17/09/2016
Alan, blz?
O Crash report tende a ser o suficiente, sem precisar de outra library. De qualquer forma vc ainda tem a PlayStore, ela lhe mostra problemas em sua APP, caso existam exceptions. Abraço
Responder
laube.vinicius (1) (0)
01/07/2016
Thiengo, este será o último vídeo da série??
E para aproveitar o comentário não estou conseguindo capturar o clique no RecicleView, fiz com os métodos dos vídeos do material designer e não foi daí refiz e agora quando clico pela primeira vez na lista ele abre 1 activity mas quando clico pela segunda vez ele abre 2 activitys e assim sucessivamente, como resolvo isto??
Responder
Vinícius Thiengo (1) (0)
02/07/2016
Fala Laube, blz?
Não, vão ter outros vídeos.
Quanto ao problema com clique, como um paliativo, crie o listener de clique em uma variável de instancia, depois coloque essa variável de instancia no método de listener do Recycler. No onStop() da Activity ou Fragment vc remove esse listener do RecyclerView. O que estou presumindo que está acontecendo é que seu código está colocando o listener novamente em um RecyclerView que já tinha um. Com a lógica acima o listener será removido. Abraço
Responder
30/06/2016
bom dia thiengo estou acompanhando as video aulas postadas notei que no arquivo dos códigos disponibilizados https://github.com/viniciusthiengo/nosso-chate não tem um pacote e algumas classes demonstradas no video
Responder
Vinícius Thiengo (1) (0)
01/07/2016
Fala Jasian, blz?
Dei uma olhada aqui e a princípio está ok, realizei mais um commit. Se possível informe quais arquivos e pacotes estão faltando. Abraço
Responder
02/07/2016
boa noite thiengo vc tem razão os arquivos estão certos e que eu estava acompanhando o video 6 e la tinha um pacote chamado listener mais agora que percebi que o linque do codigo e um so para todos os videos e acompanha a demostração da ultima postagem.
Responder