MENU It's me: Mário!

Aula 8

Aula 8: Como conectar meu Aplicativo à nuvem?

Curso de Android 06 de agosto de 2015 22 comentários
Curso de Android
  • 06 - 08 - 15
  •        
  • 22

Veja todas as aulas aqui.

E ae pessoal. Tudo tranquilo?
Espero que sim! Desse lado aqui também está tudo tranquilo (finalmente).

Antes de começar nossa aula de hoje (e sim! Habemus Código \o/) queria pedir desculpas por estar há um tempinho sem escrever nossas aulas. O que houve é que meu companheiro de guerra (notebook) parou de funcionar :/ pois é… mas 250 dilmas e 20 dias na assistência técnica depois está tudo funcionando de novo. Além disso, saí do meu antigo emprego (morrendo de saudade) o que significa que terei tempo para escrever mais e trabalhar nos meus próprios projetos.

Então é isso! Vamos deixar de enrolação, peguem um lanche (porque essa aula será um pouquinho longa) e como prometido na aula passada: abram o Android Studio e vamos estudar!!!

Atualizando

Antes de dar uma relembrada sobre o nosso projeto, o Reader, acabei de atualizar o meu Android Studio para a versão 1.3 e tive alguns probleminhas. Nada complicado demais, basta ir seguindo as instruções da IDE, pois algumas coisas precisaram ser atualizadas no projeto também.

Por exemplo, aqui apareceu o seguinte erro após atualizar: Gradle DSL method not found.
Eu segui essas instruções aqui (do stackoverflow) e deu tudo certo!

Se você tiver algum problema e não conseguir resolver, recomendo salvar os arquivos que você já editou (a Activity e os resources XML) e criar o projeto novamente com a IDE atualizada.

Recapitulando o Reader

O Reader é o App que estamos desenvolvendo nesse curso. A lógica dele será bem simples, porém abrangerá muita coisa: ele irá ler uma API, que servirá os dados das notícias, irá guardar esses dados no banco de dados interno (para evitar se conectar na internet toda vez), além de atualizar esse banco quando necessário.

Você pode conferir o projeto no Github. Lembrando que os arquivos mais atualizados estão no branch master e toda aula com código terá um branch correspondente para que você possa revisar o que foi feito.

Agora vamos falar da nossa API.

Reader API

Para a aula de hoje eu precisei criar uma mini-API com tudo o que vamos utilizar: dados de categorias e posts fictícios. Os posts não possuem conteúdo, apenas id, título, descrição, data e estão em alguma categoria. Estas possuem id, nome e quantidade de posts.

Eu utilizei um outro projeto meu chamado Avant (ainda vou falar mais dele outro dia). Ele é um mini-framework, então só precisei criar a lógica da API mesmo, pois o framework abstraiu as outras funcionalidades como URL amigável, autoloaders, etc… Mas enfim, back-end web não é o assunto da aula, então vamos adiante: dê uma olhada em como ficou nossa API:

Agora eu recomendo tomar um tempinho se familiarizando e lendo a documentação da API (mesmo sendo pouco complexa e possuindo apenas dados fictícios, gastei um bom tempo criando uma documentação para que vocês aprendam a desenvolver como o mundo lá fora).

Além disso, ela não precisa de autenticação, então só colocar a URL no navegador já nos permite visualizar o resultado do request.

O que é JSON?

Se você mexeu com a nossa API ou já tem experiência com outras APIs, provavelmente já se deparou com o formato JSON ou tem uma ideia do que seja.

O JSON, ou JavaScript Object Notation, é um formato leve de troca de informações (dados). O media-type oficial do JSON é application/json e a extensão é .json. Confira mais informações na Wikipédia ou no site oficial.

Ah! Recomendo instalar a extensão JSONView no Chrome, pois ela facilita a visualização do JSON no navegador.

Como buscar informações através do aplicativo?

Ler a API no navegador é fácil: basta ir na URL e pronto! Para buscar esses dados através do nosso Aplicativo, precisamos de um pouco mais de trabalho:

  1. Fazer um “HTTP Request” (pedido HTTP)
  2. Ler a resposta do “input stream” (fluxo de entrada)
  3. Finalizamos a conexão, limpamos o input stream e registramos quaisquer erros possíveis

Você pode consultar o “training” da documentação antes de continuar, caso queira.

Como fazer o HTTP Request?

Quando formos escrever o código, você irá perceber que criamos uma conexão HTTP. Ela é necessária para receber e enviar dados através da rede e há dois clients que podemos usar no Android: a classe HttpURLConnection e a classe HttpClient. Ambas suportam Https, upload e download, configurações de esgotamento de tempo, IPv6 e pooling de conexão.

Iremos utilizar a primeira: HttpURLConnection, pois é a mais otimizada para aplicativos Android em geral.

Criando um Log no Android

Antes de continuar, lembra que no passo 3 precisaríamos registrar os erros que ocorriam? Pois é!

Chamamos os registros de erros de Logs e no Android é muito fácil “escrever um log” como também lê-lo. Se você é front-end, provavelmente já usou console.log(“Alguma coisa”) no seu código JS, né? Aqui no Android é quase isso!

Você usa a classe Log e um dos métodos abaixo, de acordo com o tipo de log:

A primeira string do método serve de identificador. Geralmente colocamos o nome da classe ou algo que nos lembre onde estará o erro registrado. A segunda string é a informação do erro. Por exemplo, a linha de log abaixo:

Log.i("MainActivity", "A variável i está com o valor: " + Integer.toString(i));

Iria ser escrita assim:

I/MainActivity(1557): A variável i está com o valor: 2

Você pode ler a documentação completa, por enquanto vamos aprender meio que na prática: vá no Android Studio, na sua classe PlaceholderFragment (no arquivo MainActivity.java) e logo depois da abertura da class “{” insira o código abaixo:

// Aprendendo a escrever um LOG...
private final String LOG_TAG = PlaceholderFragment.class.getSimpleName();

Essa é a declaração da nossa LOG_TAG, ou o identificador que vamos usar no nosso Log mais abaixo. Basicamente estamos armazenando o nome da classe, evitando retrabalho se precisarmos mudá-la.

Obs: Não é obrigatório criarmos dessa forma, nem eu costumo fazer nos meus estudos, mas é uma boa prática e quando estivermos desenvolvendo algo complexo (ou para um cliente) é ótimo deixar tudo organizado, então vamos aprender assim, né? :D

Mais abaixo, colocamos as seguintes linhas de código, logo no início do método onCreateView:

// Aprendendo a escrever um LOG...
Log.e(LOG_TAG, "Não se preocupe, não é um erro mesmo, apenas um teste!");

Dúvidas? Dê uma olhada em como ficou o arquivo (linhas 57, 58, 67 e 68).

Agora teste seu aplicativo em Run app (Shift + F10). Nada mudou, certo? Errado. Vá na aba 4: Run logo no canto inferior esquerdo do Android Studio (ela já deve estar aberta) e selecione “logcat” (como na imagem):

logcat

Você deve estar vendo um monte de linhas, né? Pois é… (quase) todo log no mundo é confuso de primeira, mas uma dica é você selecionar o tipo de log na opção Log level ou digitar sua LOG_TAG na pesquisa, para filtrar as linhas do log. Como estamos buscando por uma linha de erro, eu selecionei a opção Error no Log level:

PlaceholderFragmentError

Claro que agora você vai apagar isso que fizemos (pode manter a definição da LOG_TAG), pois uma regra que você tem que ter em mente é: evite o SPAM de log. Sério… Como você viu, o Log pode ser confuso e ainda mais se for um “erro de verdade” como um NullPointException ou outro erro mais grave que impede nosso App de continuar executando.
E sim! Você vai encontrar muitos durante sua vida de desenvolvedor…

It’s time for code!

Agora sim! Vamos escrever código!

Vá até sua Activity e dentro da classe PlaceholderFragment no método onCreatedView() insira o código (que você vai encontrar na Wiki do projeto) logo antes do return rootView, que é a última linha de código desse método.

Clique aqui para ver o código e depois rode o seu App para testar.

Mas deu problema!

Vish… se o seu código está como o meu, ao tentar executar o App ele parou de funcionar:

ReaderException

Procurando o erro

Se você leu e entendeu o código que copiou (afinal estava todo comentado :D), vai lembrar que adicionamos código para escrever o Log dos possíveis erros (exceptions) e como você é um padawan esperto já sabe que como deu problema, o primeiro lugar para procurar é no log e você já sabe fazer isso, pois vimos no começo da aula!

Procurando no log (lembre-se de filtrar por “error”) achamos a causa. E é justamente por isso que fiz vocês copiarem o código no lugar errado: vocês precisam aprender como achar erros… não tem graça só copiar e colar código, né? :D

YouMustFeelTheForce

Como o bloco de erro é grande demais para copiar aqui, vou resumir e dizer para vocês que o problema foi causado por uma NetworkOnMainThreadException. Como ter certeza disso? Nosso bloco de erro (todo escrito em vermelho) começa mais ou menos assim:

08-06 02:30:54.210 13972-13972/? E/AndroidRuntime﹕ FATAL EXCEPTION: main

e após algumas linhas encontramos a causa:

Caused by: android.os.NetworkOnMainThreadException

No meio disso tudo (e depois da linha Caused by …) há um monte de linha de log começando com at. Basicamente esse monte de log serve para indicar onde a exceção foi pega. Como você deve ver, algumas referências (no final de algumas dessas linhas) devem vir “clicáveis”, como links. Se você clicar nela, irá abrir diretamente no arquivo e na linha que houve o problema (algumas vezes podem ser mais de uma linha).

No nosso caso, a linha é a 116 da MainActivity.java, ou seja, a linha abaixo:

urlConnection.connect();

Isso acontece porque não podemos iniciar uma conexão na Thread principal (ou Main Thread).

Main Thread vs Background Thread

O padrão dos aplicativos Android é rodar na Main Thread (ou Thread Principal).

Essa thread é também conhecida como UI Thread, pois é nela que se processa todas as entradas e saídas do usuário, ou seja a User Interface em si. É por isso que devemos evitar qualquer operação longa nela.

Pense comigo: solicitamos uma conexão com a internet, aí o Android tem que abrir a conexão, enviar o request, esperar pela resposta e só depois continuar o processamento. Caramba! Imagina se a tela tivesse que ficar travada durante esse processo… não dá, né? (Há outros exemplos de atividades longas que não devem estar na Main Thread como decodificação de bitmaps e conexões com o banco de dados).

É para esses casos que temos a Thread de Background! Sendo assim, temos que tirar o código que criamos para receber os dados da API e temos que colocá-lo na Background Thread, mas como? Usando uma tarefa assíncrona!

O que é uma AsyncTask?

Como eu sempre digo: o Android é uma mãe!

Ao dividir o processamento entre as Threads, ocorre isso aqui: você vai estar na Main Thread aí precisa fazer algo longo. Então decide jogar essa tarefa para a Background Thread, mas precisa de feedbacks (o status e principalmente um aviso de quando a tarefa estiver finalizada). E quando estiver tudo OK, precisa enviar o resultado de volta para a Main Thread para mudar sua UI e fazer o que mais precisar.

Puxa! Imagina fazer tudo isso… Mas é por isso que existe uma maneira bem simples de gerenciar os dados entre as Threads: a classe AsyncTask.

Usando a AsyncTask

Usar uma AsyncTask é bastante simples: basta criar sua classe (estendendo a classe AsynTask, claro) e quando for necessário iniciar essa tarefa, você executa o método execute().

Ao criar sua classe, você pode sobrescrever vários métodos de acordo com o que você precisa, mas obrigatoriamente, você deve sobrescrever o método doInBackground() e geralmente irá sobrescrever o método onPostExecute() também. Eles são respectivamente o que a tarefa deve fazer e o que fazer ao terminar a tarefa.

Outro método interessante é o publishProgress(). Você pode usá-lo dentro do doInBackground() para enviar informações de atualização do status para a sua UI.

Em código (segundo o exemplo da documentação), teremos isso aqui:

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected Long doInBackground(URL... urls) {
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             if (isCancelled()) break;
         }
         return totalSize;
     }

     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }

     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
 }

Note essa linha aqui:

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {

Ao estender a AsyncTask, precisamos informar 3 tipos, que no caso foram URL, Integer, e Long. Sendo que esses três tipos são:

  • Params: o tipo do parâmetro que será enviado a tarefa. Recebido pelo método doInBackground.
  • Progress: o tipo do parâmetro usado para indicar o progresso. Usado no método publishProgress e recebido pelo método onProgressUpdate.
  • Result: é o tipo do resultado. Esse é o tipo do parâmetro que é retornado pelo método doInBackground e recebido pelo método onPostExecute.

Caso não vá usar algum desses parâmetros, coloque o tipo Void, mas nunca deixe de indicar algum dos três. Por exemplo:

private class MyAsyncTaskWithoutParams extends AsyncTask<Void, Void, Void> { ... }

E com a classe pronta, basta executá-la assim:

new DownloadFilesTask().execute(url1, url2, url3);

Como cancelar uma AsyncTask?

As tarefas assíncronas, servem justamente para gerenciar tarefas muito longas, certo? E se em algum momento não for mais necessário o retorno de uma tarefa ou então se eu quiser cancelar por algum motivo?

Basta usar o método cancel(). Esse método tentará cancelar a Tarefa, contudo ele pode não conseguir cancelá-la, se a tarefa não foi iniciada ou se já foi cancela ou concluída (ou algum outro problema). Além disso, no caso de a tarefa já ter sido iniciada o método cancel() deve receber o parâmetro true, senão as tarefas que já foram iniciadas não são canceladas e podem terminar de executar normalmente.

Esse método retorna false se não conseguiu cancelar a tarefa ou true, no caso inverso.

Além disso, se conseguir cancelar a tarefa, o método onCancelled() será chamado depois do retorno de doInBackground() e o método onPostExecute() (que é o método de retorno ao fim da tarefa) nunca será chamado.

Se você pretende usar esse método ou quiser permitir que a tarefa seja finalizada o mais rápido possível, você pode checar se isCancelled() periodicamente, como na linha 9 do nosso exemplo acima.

Use sempre o isCancelled em laços de repetição, evitando processamento inútil, se a tarefa for interrompida.

Algumas regras sobre o uso da AsyncTask

  • A classe AsyncTask deve ser carregada na UI Thread. (Mas é feito automaticamente desde o JELLY_BEAN)
  • A instância da tarefa deve ser criada na UI Thread.
  • O método execute() também deve ser chamado na UI Thread.
  • Não chame os métodos onPreExecute(), onPostExecute(), doInBackground() e onProgressUpdate() manualmente.
  • A tarefa só pode ser executada uma vez (executá-la novamente irá gerar uma exceção).

Quero caféeee

Caramba! Até agora já vimos um monte de coisa nova, mas é assim mesmo!
Esse conteúdo é bem vasto, por outro lado é relativamente simples e como é prático fica mais legal. Então se quiser, vá beber uma água (que no caso, é café) e volte para que possamos fazer nossa conexão do jeito certo dessa vez.

Quero-Cafeeeeeee

Criando nossa tarefa assíncrona

Antes de realmente criar nossa tarefa assíncrona, vamos dar uma refatorada no código para ficar mais organizado.

Primeiro crie uma nova classe java:

Creating-a-new-class

No campo Name: preencha como CategoriesListFragment.

Agora você pega tudo o que estava dentro do PlaceholderFragment e coloca dentro do CategoriesListFragment. Não esqueça de trocar “PlaceholderFragment” por “CategoriesListFragment” onde for necessário (dê uma olhada nesse commit para saber onde trocar) e de estender o nosso novo fragmento:

public class CategoriesListFragment extends Fragment {
    private final String LOG_TAG = CategoriesListFragment.class.getSimpleName();

    public CategoriesListFragment() {
    }

    (...)

Dessa forma, nossa MainActivity ficou bem menor (não esqueça de apagar a antiga classe e agora vazia “PlaceholderFragment”).

O segundo passo é definir uma nova classe dentro da CategoriesListFragment. Essa será nossa classe responsável pela conexão, logo ela deve estender a AsyncTask. Basta ir logo antes da última chave “}” e digitar:

public class FetchReaderAPITask extends AsyncTask<Void, Void, Void> {
    private final String LOG_TAG = FetchReaderAPITask.class.getSimpleName();

}

Agora vamos sobrescrever o método doInBackground(): coloque o cursor do teclado na linha em branco dentro da classe e pressione Ctrl + O (ou vá na barra de ferramentas em Code > Override Methods). Na janela que irá abrir, selecione o método desejado e clique em OK.

Agora temos isso aqui:

public class FetchReaderAPITask extends AsyncTask<Void, Void, Void> {
    private final String LOG_TAG = FetchReaderAPITask.class.getSimpleName();

    @Override
    protected Void doInBackground(Void... voids) {
        return null;
    }
}

Pronto! Já pode mover o código da conexão para dentro desse método (antes do return null claro)!

Testando novamente o App

Rode novamente o aplicativo e você vai ver que… nada mudou! Exatamente, mas pelo menos não está mais quebrando, certo?

Implementando a “FetchReaderAPITask”

Agora que está tudo organizado, vamos botar pra funcionar esse código!

Passo 1 – Alterando o “responseJson = null”

Volte à FetchReaderAPITask e altere as linha 95, 101 e 109: troque responseJson = null; por return null;.

Obs: quando eu citar o número de uma linha, tenha em mente que estou usando o arquivo que está no GitHub como base, então talvez o seu seja um pouco diferente.

Passo 2 – Criando o menu “Refresh”

Antes que alguém jogue uma pedra em mim, o que iremos fazer (criar um menu “Atualizar”) será feito única e exclusivamente para propósitos de estudo e debug (teste), pois precisamos “acionar” a atualização dos dados para ver como fica.

Para adicionar uma opção ao menu, iremos criar um novo arquivo de menu. Clique na pasta de resources menu/ com o botão direito do mouse e vá em New > Menu resource file. Digite o nome do arquivo, que será categories_list_fragment e clique em “OK”.

Agora você só precisa colocar o código abaixo:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainActivity" >

    <item android:id="@+id/action_refresh"
        android:title="@string/action_refresh"
        android:icon="@drawable/ic_action_refresh"
        app:showAsAction="ifRoom" />
</menu>

Agora precisamos criar os resources que usamos nesse código.
Para criar a string, basta ir no arquivo strings.xml, dentro da pasta values/ e adicionar uma linha, como aprendemos nas aulas anteriores:

<string name="action_refresh">Atualizar</string>

Para adicionar o ícone à pasta drawable/ basta arrastar os arquivos .png para lá.

Procure sempre usar os ícones oficiais, ou pelo menos tente criar os ícones nos tamanhos recomendados. Eu tenho pouco material sobre a parte de criação do design (e provavelmente está desatualizado, já que baixei esses ícones e essa tabela no meio do ano passado), por isso, se você tiver alguma informação a mais, deixe nos comentários, por favor:

MDPI (tamanho base) HDPI XHDPI XXHDPI XXXHDPI
Escala 1x 1.5x 2x 3x 4x
DPI 160 dpi 240 dpi 320 dpi 480 dpi 640 dpi
App Launcher 48px 72px 96px 144px 192px
Action Bar 32px (24px interno) 48px 64px 96px 128px
Tamanho pequeno/contextual 16px (12px interno) 24px 32px 48px 64px
Notificação 24px (22px interno) 36px 48px 72px 96px

Eu já tinha alguns ícones por aqui, então vou disponibilizar para download, basta clicar aqui (ou pode pegar através do repositório no GitHub).

// Para as versões mais novas do Android Studio:
Lembrando que se você for arrastar os arquivos para dentro da pasta res/drawable do Android Studio, você deve ter selecionado a opção Project Files da guia 1: Project. Geralmente uso a opção Android por ser mais fácil, então fica a dica, se não estiver conseguindo arrastar os arquivos para as pastas de resource.

Com os resources no lugar e nenhum erro de referência no código, temos que inflar esse menu.
Para isso vá até a CategoriesListFragment e adicione os métodos onCreate, onCreateOptionsMenu e onOptionsItemSelected. Basta pressionar Ctrl + O e escolher esses métodos na lista. Minha sugestão é colocá-los no início da classe, logo depois do construtor (o método de mesmo nome da classe).

Os dois primeiros métodos ficam assim (já falo do onOptionsItemSelected):

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Indica que esse fragmento possui um menu
    setHasOptionsMenu(true);
 }

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // Já que possui um menu "setHasOptionsMenu(true)", inflamos esse menu
    inflater.inflate(R.menu.categories_list_fragment, menu);
}

Dúvidas? Dê uma olhada na documentação sobre “menus” ou deixe um comentário.

Passo 3 – Executando a tarefa

Você incluiu 3 métodos antes, mas só mostrei 2, né?
Pois é, o metódo onOptionsItemSelected é justamente onde colocaremos o código para executar nossa Tarefa e ficará assim:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Achamos o id do item selecionado
    int id = item.getItemId();

    // Se o id for "action_refresh", instanciamos nosssa tarefa assíncrona
    // Depois executamos essa tarefa
    if (id == R.id.action_refresh) {
        FetchReaderAPITask apiTask = new FetchReaderAPITask();
        apiTask.execute();
        return true;
    }
    return super.onOptionsItemSelected(item);
}

Adicionando a permissão necessária

Se você tentou rodar o App agora e tocou na opção de Atualizar, deve ter percebido que o App parou, ne? Isso foi causado por uma exceção de segurança:

Caused by: java.lang.SecurityException: Permission denied (missing INTERNET permission?)

Isso aconteceu, porque ainda não solicitamos a permissão de acesso à internet. Para isso, vamos lá no AndroidManifest.xml e adicionamos a permissão:

<!-- Permissão necessária para acessar a internet -->
<uses-permission android:name="android.permission.INTERNET" />

Lendo o JSON

Pronto! Os dados já estão chegando com segurança até o nosso aplicativo e temos todas as permissões necessárias.
Só falta agora exibir esses dados.

Faremos isso usando o JSONObject. Como a aula está ficando longa e todos vocês já possuem conhecimento em programação, não vou explicar essa classe, porém o código vai estar bem comentado :D

Criaremos o método getCategoriesDataFromJson (usaremos throws JSONException afinal essa é uma exceção passível no trabalho com JSONObject) dentro da classe FetchReaderAPITask:

private String[] getCategoriesDataFromJson(String readerApiJsonStr)
        throws JSONException {

    // Lista de "nós" do JSON que vamos ler
    // Status:
    final String API_STATUS = "status";
    // Se o status for ERRO, temos um "erro" com o código e a mensagem do erro:
    final String API_ERROR = "erro";
    final String API_ERROR_CODE = "code";
    final String API_ERROR_MESSAGE = "message";
    // Se o status for OK, temos uma "response" com o endpoint usado
    final String API_RESPONSE = "response";
    final String API_RESPONSE_COUNT = "count";

    // Por enquanto só estamos requisitando o endpoint "categorias"
    final String API_ENDPOINT_CATEGORIAS = "categorias";

    // Cada categoria possui id, nome e count, mas ignoraremos o ID por enquanto
    final String API_ENDPOINT_CATEGORIAS_NOME = "nome";
    final String API_ENDPOINT_CATEGORIAS_COUNT = "count";

    // Instanciamos o JSONObject
    JSONObject readerApiJson = new JSONObject(readerApiJsonStr);

    // Se o status for "ERROR"
    if (readerApiJson.getString(API_STATUS).equals("ERROR")) {
        String erroCode = readerApiJson.getJSONObject(API_ERROR).getString(API_ERROR_CODE);
        String erroMessage = readerApiJson.getJSONObject(API_ERROR).getString(API_ERROR_MESSAGE);

        // Escreve um log com o formato "(999) Mensagem"
        // Veja a lista de erros em: http://api.jangal.com.br/reader/
        Log.e(LOG_TAG, "Erro na API: (" + erroCode + ") " + erroMessage);
        return null;
    }

    // Se o status for "OK"
    if (readerApiJson.getString(API_STATUS).equals("OK")) {
        // Pegamos o Objeto "response"
        JSONObject response = readerApiJson.getJSONObject(API_RESPONSE);

        // Do Objeto response, pegamos o int count e a array de itens
        int countCategories = response.getInt(API_RESPONSE_COUNT);
        JSONArray categoriesArray = response.getJSONArray(API_ENDPOINT_CATEGORIAS);

        // Criamos uma String[] para armazenar cada linha que iremos passar para a View
        String[] result = new String[countCategories];

        // Fazemos um laço para percorrer os itens da Array
        for(int i = 0; i < categoriesArray.length(); i++) {
            // Pegamos o Obj
            JSONObject categoria = categoriesArray.getJSONObject(i);

            // Lemos o nome e o count dessa categoria
            String nome = categoria.getString(API_ENDPOINT_CATEGORIAS_NOME);
            String count = categoria.getString(API_ENDPOINT_CATEGORIAS_COUNT);

            // Como ´s tudo string, concatenamos no formado "Categoria (99)"
            result[i] = nome + " (" + count + ")";
        }

        return result;
    }

    // Se por algum motivo não tínhamos nem um status "OK", nem um "ERROR"
    // Então escrevemos o Log e retornamos null
    Log.e(LOG_TAG, "Erro ao ler o status da resposta");
    return null;
}

Tá tá… Eu não consigo ficar sem comentar algo… Mas só uma dica: se no JSON você visualizar chaves {} o método a ser usado é o getJSONObject, caso esteja vendo colchetes [] use getJSONArray e se não for nem um, nem o outro, provavelmente será uma string ou um inteiro e você usará getString e getInt, respectivamente.

Usando o método na tarefa assíncrona

Agora que já temos um método para ler o JSON, iremos realmente lê-lo.

Seu arquivo CategoriesListFragment deve estar assim. Nesse caso, vá até o seu método doInBackground e no final (antes do return) acrescente o seguinte código:

//Já que "getCategoriesDataFromJson" é throws, usaremos um try/catch
try {
    return getCategoriesDataFromJson(responseJson);
} catch (JSONException e) {
    Log.e(LOG_TAG, e.getMessage(), e);
    e.printStackTrace();
}

Como agora temos um retorno do tipo String[], vá na declaração da classe e altere o terceiro tipo:

public class FetchReaderAPITask extends AsyncTask<Void, Void, String[]> {

Além disso, acerte o tipo de retorno do método:

protected String[] doInBackground(Void... voids) {

Pronto! O arquivo deve estar assim e estamos quase lá!

Implementação do onPostExecute

Como está tudo OK, precisamos agora escrever os dados na User Interface e o método que fará isso é o onPostExecute, pois ele é executado na UI Thread e logo após o fim da tarefa.

@Override
protected void onPostExecute(String[] result) {
    // Se o retonorno não for nulo
    if (result != null) {
        mListOfCategoriesAdapter.clear();
        for (String categoryStr : result) {
            mListOfCategoriesAdapter.add(categoryStr);
        }
    }
}

Agora você precisa declarar o ArrayAdapter logo no começo do Fragment, ou seja, abaixo da declaração da LOG_TAG:

private ArrayAdapter<String> mListOfCategoriesAdapter;

Mas já tínhamos um ArrayAdapter, né? Sim! Só que eu decidi mudar o nome para fazer sentido, então vamos lá no onCreateView e trocaremos “listOfLastPostsAdapter” por “mListOfCategoriesAdapter” onde for necessário.

Lembrando que não precisaremos declará-lo novamente então a linha:

ArrayAdapter<String> listOfLastPostsAdapter = new ArrayAdapter<String>(

Vira:

mListOfCategoriesAdapter= new ArrayAdapter<String>(

E se você está acompanhando pelo Github, vai precisar corrigir um pequeno erro no código do arquivo que eu commitei (se você copiou o código daqui mesmo, já está corrigido). Logo depois da linha:

reader = new BufferedReader(new InputStreamReader(inputStream));

Eu esqueci de acrescentar o código:

String line;
while ((line = reader.readLine()) != null) {
    buffer.append(line);
}

Sem ele não vai funcionar, desculpem :D

Para finalizar, vamos alterar a String[] data um pouco acima de onde estamos agora:

// Novos dados iniciais
String[] data = {
    "Atualize os dados..."
};

Pronto! Agora sim!

Conclusão

E é isso galera!

Essa foi de longe a maior aula que já tivemos. Espero que tenha me redimido pelo tempo que passei longe (mas a culpa nem foi minha) e que tenham gostado!

Como vocês sabem, eu vou desenvolvendo, versionando e escrevendo a aula ao mesmo tempo. Então como o foco no começo da lição não era o código funcionar, mas apresentar o erro (Main Thread Exception) que apresentei a vocês (para irmos puxando o assunto), acabei não me ligando que esqueci umas linhas de código e commitei o arquivo desse jeito mesmo… Acontece. Então se você acompanhou a aula por aqui, está tudo OK, mas se pegou algum código de um commit do Github dá uma verificada se está como na versão final, que é o branch com o nome da aula.

Além disso, lembrem-se que devemos usar o método equals() na comparação das Strings e não o “==”. Acho que algum commit pode ter saído errado, mas novamente nos arquivos finais (e aqui na aula) está tudo OK.

Como virei a noite escrevendo essa aula e já passamos de 4400 palavras, vocês vão ter que me perdoar… Usem sempre o método equals() e que a Força esteja com vocês. =P

Maaaaaaaaas como eu sei que vocês não estão aqui apenas para copiar e colar código, mas sim para acompanhar as aulas todas, acabou que no final deu tudo certo e todos devem estar com o Reader rodando perfeito nos aparelhos ou emuladores de vocês.

Vocês podem conferir o código final no branch da aula.

Espero que tenham gostado, comentem e compartilhem para fazer um DEV feliz e espalhar a palavra, além de ensinar Android para mais humanos (os gatos continuam estudando, hein… não fique para trás).

Abraços e até a próxima!

Por favor, considere desativar o AdBlock

Não perca nenhuma novidade do nosso Curso!

Não se preoculpe, não enviaremos muitos e-mails, nem mostraremos seu e-mail para ninguém. Dúvidas?


Deixe seu comentário! Dúvida sobre como comentar
ou vai postar código? Leia antes.

  • lieberson xavier dos santos

    Na versão 1.3.1 o Android Studio monta duas classes(MainActivity e MainActivityFragment).
    O Placeholder está na classe MainActivityFragment, então tem que ser feita a alteração na declaração da constante “private final String LOG_TAG = PlaceholderFragment.class.getSimpleName();”.
    No lugar de “…PlaceholderFragment.class.getSimpleName();”, coloca-se o nome da classe MainActivityFragment.class.getSimpleName();

    correto?

    • Exatamente!

      Como a nova versão do Android Studio está criando a Activity e o Fragment separados (o que é melhor mesmo, afinal nós separamos os arquivos nessa aula, né?) e isso implica em nomes diferentes, você tem que fazer essa adaptação, se não estiver usando os arquivos do repositório.

  • Primeiro de tudo parabéns pelas aulas, estou vendo Android agora na Faculdade, apesar de ser no ultimo semestre (fazer o que né… kkkkk). Vamos a duvida. Estava comparando o código gerado pelo meu Android Studio com a Branch Final. No MainActivity do branch contem esse trecho que eu não tinha percebido durante a leitura:

    if (savedInstanceState == null) {
    getSupportFragmentManager().beginTransaction()
    .add(R.id.container, new MainActivityFragment())
    .commit();
    }

    Digitei ele no meu, mas está dando erro no .add(R.id.container, new MainActivityFragment()), onde não encontra o container. Pode me dizer o que está faltando?

    • Eu troquei o container por “action_bar_container” como sugestão do android Studio só para ver o que acontecia :P e executei o app, apenas apareceu mais um ícone de atualizar na barra. Comentei o trecho e tá rodando perfeitamente… :)

      • Esse “container”, é o id da View “FrameLayout” que eu coloquei no arquivo de layout que está sendo chamado (activity_main.xml).

    • Olá. O que esse código faz é adicionar o MainActivityFragment() através do FragmentManager. Se você não criou o projeto na versão anterior (como eu), talvez o seu código faça isso de outra forma…
      Se não colocar isso, funciona?

    • João Eduardo Fayad Milken

      Também estou na versão que separa em dois arquivos… Minha dúvida esta no fato de termos criado a class CategoriesListFragment e a aplicação entende que é pra executar essa classe?

      • Olá João. Tudo bem?

        Essa class estende a classe Fragment, certo? Logo ela é um Fragmento.

        Um Fragmento deve ser chamado dentro de uma Atividade (Activity), que no caso é startada (iniciada) junto com a Aplicação.

        Seguindo esse fluxo, o que acontece ao iniciarmos o App é que a atividade MainActivity é lançada e ela possui um método chamado onCreate. Nele nós iremos atribuir um arquivo de layout à Atividade usando o setContentView e o getSupportFragmentManager, como você pode ver lá no meu código.

        De qualquer forma, a minha dica para todos que estão tendo problema com o Android Studio criando os arquivos de uma maneira diferente é que copiem os arquivos atuais do repositório, pois agora na Aula 8 nós separamos a classe Fragmento da classe Atividade em dois arquivos. Então tudo igual agora (só os nomes que devem estar diferentes, mas aconselho mudar pois imagina um projeto com 100 classes… precisamos usar nomes que tenham a ver).

        Espero ter ajudado, abraços!

  • John Marlen

    Muito bom esse material. :)
    Terminei as aulas sem grandes problemas para alguém que nunca tinha pego nada de programação para android kkkk
    Parabéns mesmo. Só senti falta da parte de transição de páginas e clicar na categoria e ir para os posts. Mas vou ver se consigo fazer aqui.

    Valeu! :D

    • Olá, John!
      Agradeço os elogios. Essa é a nossa oitava aula e ela fala “apenas” sobre como conectar o App à nuvem. Ainda há muito o que fazer e a transição entre “páginas” (activities) é o tema da nossa 10ª aula, então veremos sim isso :D

      Abração!

      • John Marlen

        Blz então. Pensei que essa era a última aula :)
        Na expectativa aqui pela continuação!

        • Não :D
          A ideia como temos na Aula 1 é construir um Aplicativo completo.

  • João Eduardo Fayad Milken

    Mário, primeiro gostaria de lhe parabenizar pelo conteúdo. Muito bom. Eu desenvolvo em PHP com o ZendFramework, tenho bom conhecimento de consultas SQLs, em PostgreSQL, SQL Server e Mysql. No PHP eu consigo identificar, qual a lógica de chamada, seja de uma action, o que vai carregar na pagina inicial, etc.

    Aqui, como estou na versão mais atualizada, tive problemas ao levar o conteudo da MainActivityFragment para a classe que criamos na aula 8. Passou a carregar apenas o título. Se eu limpava completamente a MainActivityFragment, apresentar erro. Então comparando o código do seu repositório me deparei com as linhas que o Sidney citou. Mesmo apontando para a CategoriesListFragment ainda apresentava erro. Então fui aos poucos tentar identificar a sequência lógica para chamada.

    Na versão atual como na antiga temos o AndroidManifest.xml que informa a nossa Action principal, Main, como você explicou em uma das aulas. Segui para a MainActivity, e vi que ela infla o layout activity_main, e abrindo o mesmo, tanto no seu repositório tanto no meu projeto me deparei que la estava o apontamento para a MainActivityFragment, alterei para CategoriesListFragment e plim… mágica. Funcionou. Segue a linha alterada.

    Alterei de:
    android:name=”com.joaoeduardo.readerplus.MainActivityFragment”

    Para:
    android:name=”com.joaoeduardo.readerplus.CategoriesListFragment”

    Gostarei de uma explicação mais técnica, pra ver se meu entendimento foi correto!
    Abs.

    • Isso seria em que arquivo?

    • Em que arquivo isso? Pergunto, pois não achei.

    • João Eduardo Fayad Milken

      No activity_main.xml (pasta Layout)

    • João Eduardo Fayad Milken

      Minha activity_main.xml :

      • Por isso.

        Sua activity_main contem um elemento <fragment> que é usado justamente para inserir um Fragmento (lembra das aulas anteriores que eu falei sobre poder usar diversos Fragmentos na mesma Atividade ou em Atividades diferentes?).

        Agora você pode alterar esse arquivo para ficar igual ao nosso ou então pode manter. O Elemento <fragment> funciona desse jeito mesmo: você passa o parâmetro “name” com a classe Fragmento que deseja inserir. Diferente do <FrameLayout>, que eu usei para acondicionar o Fragmento através da programação.

        Particularmente prefiro fazer de outra forma: inserindo o Fragmento na programação, como é o caso do código que está no repositório.

        Abraços!

    • Kauly Bohm

      Acredito que como vc já tinha MainActivityFragment, não era necessário criar a CategoriesListFragment.

  • Exatamente.
    Eu uso o método onCreate da Atividade para jogar o Fragmento na View FrameLayout do arquivo activity_main.xml.

  • Josimar Souza

    Caraca não pensei que fossemos chegar tão longe… Muito ansioso pela aula 9. Thxs…