Conectando o app Android

Nessa prática vamos conhecer as ferramentas básicas do Android que possibilitarão conectar o app Countries ao serviço GeoNames que apresentamos anteriormente. Ao final, o app baixará a listagem completa de países a partir do serviço e apresentará suas respectivas bandeiras tanto na listagem como na tela de detalhes.

Configurando as permissões do app

O Android trabalha com um modelo de permissões bastante estrito, no qual é necessário registrar no arquivo de configuração de seus apps quais serviços e funcionalidades do sistema operacional serão utilizadas.

Para trabalhar com conectividade em rede, utilizadas nesse exercício, vamos precisar solicitar duas permissões ao sistema operacional:

  • android.permission.INTERNET: é usada quando nosso app precisa acessar a rede local ou a internet através de conexões Wi-FI ou da rede celular (3G, 4G, etc.).
  • android.permission.ACCESS_NETWORK_STATE: é usada para que nosso app seja capaz de determinar a disponibilidade da rede. Isso nos permite saber se há uma conexão disponível e qual o seu tipo (Wi-Fi ou celular).

No arquivo AndroidManifest.xml inclua as declarações de uso dessas permissões logo abaixo da abertura da tag raiz (manifest):

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

A Biblioteca Volley

Apesar de o Android oferecer suporte nativo à comunicação HTTP, já desde sua primeira versão, através da classe HttpURLConnection, recomenda-se o uso de uma das diversas bibliotecas open-source disponíveis para Android, por serem muito mais simples de usar. Para essa prática escolhemos a biblioteca Volley, criada e distribuída pelo próprio Google.

Usando o Gradle podemos instalar ela facilmente incluindo a linha abaixo, no item dependencies do arquivo build.gradle (o arquivo do módulo e não o do projeto):

dependencies {
        // itens anteriores ...
        compile 'com.android.volley:volley:1.0.0'
}

Feito isso, clique no botão Sync Now que aparecerá na barra de aviso acima do editor, conforme ilustrado abaixo.

O Gradle irá baixar a biblioteca e inclui-la no projeto, tornando disponível para nossa utilização.

Uma das questões fundamentais quando trabalhamos com comunicações em rede, através de nossos apps, é a de que devemos sempre considerar as operações que acessam a internet como “bloqueantes”, o que significa que a execução na thread onde a comunicação está sendo feita fica bloqueada até que ela termine. Executar uma operação como essas na thread principal causaria o efeito de congelar seu App durante esse processamento, o que é péssimo para a experiência do usuário, que terá a sensação de que o App travou completamente.

A thread principal onde a maior parte do nosso código executa por padrão é chamada de main ou UI thread. Isso porque todas as operações referentes a interface devem acontecer exclusivamente nela.

Uma das principais facilidades que o Volley nos traz é com relação à execução dos chamados em segundo plano. A própria biblioteca gerencia a execução dos chamados HTTP em threads separadas, funcionando de maneira totalmente assíncrona. Ao concluir a operação, o Volley devolve o resultado já na thread principal, usando um método de callback. O mesmo acontece caso ocorra um erro no chamado onde outro método é chamado com informações sobre a falha.

Adptando o modelo do app

Nesse exercício vamos adaptar o modelo para incluir a comunicação com o serviço GeoNames, usando a biblioteca Volley. Toda a comunicação com o serviço será gerenciada pelo modelo e vamos apenas oferecer um método que poderá ser chamado pela interface para dar inicio a sincronização dos dados. Esse método será usado posteriormente pela interface através do menu principal.

Conforme vimos na seção anterior, o Volley trabalha de maneira totalmente assíncrona. Assim, ao agendar a execução de uma requisição (request), toda a execução é feita em uma thread separada. O Volley nos informa a respeito da conclusão da operação usando um método de callback, uma função que passamos para ele executar passando o resultado da requisição.

No caso do nosso App toda essa lógica estará centralizada no modelo, portanto o Controller (Activity) em si sequer terá contato ou conhecimento sobre a existência do Volley, assim precisamos de uma maneira para informar-lo sobre quando a listagem dos países foi recarregada com os dados do serviço. Para isso, vamos usar o padrão Handler, no qual um objeto é passado para receber chamadas quando um determinado evento acontece.

O primeiro passo é criar uma interface representando os eventos que podem acontecer em um objeto CountryList do modelo. Essa será uma interface aninhada à própria classe e vai declarar um único onRefreshCompleted(Boolean), que recebe uma flag indicando quando a operação de recarga foi concluída. Inclua o código abaixo no final do arquivo CountryList.java:

//
// INTERFACES

public interface RefreshHandler {
    void onRefreshCompleted(Boolean success);
}

Agora vamos declarar algumas constantes. COUNTRY_INFO_URL contém a URL para o serviço do GeoNames que retorna a lista de países do mundo em formato JSON. A constante TAG será usada para passar a identificação de contexto ao LogCat. Ainda no arquivo CountryList.java, inclua antes das declarações anteriores o seguinte código:

//
// CONSTANTS

private final static String COUNTRY_INFO_URL =  "http://api.geonames.org/countryInfoJSON?username=[nomeDoUsuario]";
private final static String TAG = "Countries.CountryList";

Não se esqueça de substituir o parâmetro [nomeDoUsuario] pelo nome de usuário que você criou anteriormente para o GeoNames. Agora vamos criar dois campos static para armazenar os seguintes objetos do Volley:

  • RequestQueue: todas os Requests feitos a partir do Volley são passados para uma fila (Queue) de requisições, que é responsável pelo seu processamento.
  • ImageLoader: esse é um objeto complementar do Volley que é usado para gerenciar o carregamento e cache de imagens. Vamos precisar dele quando formos incluir as bandeiras dos países.

Ao declarar esses campos como static, optamos por manter uma única instância desses objetos, de forma que mesmo que outras instâncias da classe CountryList sejam criadas, quaisquer requisições feitas para o Volley usarão as mesmas instâncias para processamento. Essa é uma boa prática já que uma fila de requisições do Volley processa essas operações sequencialmente, sem sobrecarregar o dispositivo.

No arquivo CountryList.java inclua o código abaixo na sequência da declaração dos campos (Fields):

static RequestQueue sRequestQueue;
static ImageLoader sImageLoader;

Vamos implementar o acesso a esses campos como propriedades usando o padrão de “inicialização preguiçosa” (Lazy Initialization), o que significa que as instâncias só serão criadas, de fato, quando forem utilizadas pela primeira vez. Inclua o seguinte código ao final da seção “PRIVATE API”:

private RequestQueue getRequestQueue() {
    if (sRequestQueue == null) {
        sRequestQueue = Volley.newRequestQueue(mContext);
    }
    return sRequestQueue;
}

private ImageLoader getImageLoader() {
    if (sImageLoader == null) {
        sImageLoader = new ImageLoader(getRequestQueue(), new ImageLoader.ImageCache() {
            private final LruCache<String, Bitmap>
                    cache = new LruCache<>(20);

            @Override
            public Bitmap getBitmap(String url) {
                return cache.get(url);
            }

            @Override
            public void putBitmap(String url, Bitmap bitmap) {
                cache.put(url, bitmap);
            }
        });
    }

    return sImageLoader;
}

Ao inserir esse código, o Android Studio deve apresentar um erro para a classe LruCache (Cannot Resolve symbol ‘LruCache’). Isso acontece pois essa classe existe em 2 packages diferentes (android.util e android.support.v4.util) e, portanto, o Android Studio não consegue resolver automaticamente qual delas deveria usar. Trata-se exatamente da mesma classe, porém ao usar a versão do segundo package ela é retro-compatível até o API Level 4 do Android.

Posicione o cursor sobre o item com a marcação vermelha de erro e clique no botão de lâmpada na barra, à esquerda do editor, ou utilize o atalho de teclado ⌥ + ENTER. Isso irá exibir o menu de contexto com as possíveis resoluções:

Selecione a opção Import Class e na nova janela de diálogo que irá aparecer selecione a segunda opção:

Note que nos dois getters das propriedades nós verificamos sempre se o campo static é nulo e, nesse caso, criamos as instâncias dos objetos. Nas próximas vezes que essas propriedades forem acessadas, a condição de nulidade será falsa e ele simplesmente retornará o objeto já existente.

Vamos incluir mais um método à classe CountryList para disparar o request ao serviço GeoNames a fim de obter a listagem dos países do mundo. Na seção “PUBLIC API” do arquivo CountryList.java, inclua o código abaixo:

/**
 * Carrega a lista de países de maneira assíncrona.
 * @param handler Um handler que será notificado nos eventos de recarga.
 */
public void refreshListFromGeonamesService(final RefreshHandler handler) {
    // 1.
    RequestQueue queue = getRequestQueue();

    // 2.
    JsonObjectRequest countriesRequest = new JsonObjectRequest(COUNTRY_INFO_URL,
            null,

            // 3.
            new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            try {
                // 4.
                mCountries.clear();

                // 5.
                JSONArray countryList = response.getJSONArray("geonames");
                for (int i = 0; i < countryList.length(); i++) {
                    JSONObject o = countryList.getJSONObject(i);
                    Country c = new Country();
                    c.setName(o.getString("countryName"));
                    c.setCode(o.getString("countryCode"));
                    c.setCapital(o.getString("capital"));
                    c.setContinent(o.getString("continent"));
                    c.setPopulation(o.getInt("population"));
                    c.setArea(o.optDouble("areaInSqKm"));
                    mCountries.add(c);
                }

                // 6.
                Log.i(TAG, "Country list refreshed.");
                handler.onRefreshCompleted(true);
            } catch (JSONException e) {
                // 7.
                Log.e(TAG, "Can' refresh the country list.", e);
                handler.onRefreshCompleted(false);
            }
        }

    // 8.
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            Log.e(TAG, "Can' refresh the country list.", error);
            handler.onRefreshCompleted(false);
        }
    });
    queue.add(countriesRequest);
}

Esse método é o coração de toda a lógica de comunicação com o serviço do GeoNames, portanto vamos entender melhor cada parte numerada:

  1. Obtemos o objeto RequestQueue que vamos usar para agendar a requisição.
  2. Criamos um objeto do tipo JsonObjectRequest que representa uma chamada feita para o serviço esperando receber como corpo do Response um objeto do tipo JSON. O construtor do objeto recebe quatro parâmetros:
    1. url (do tipo String): a URL completa para onde o request será feito. Nesse caso passamos a constante COUNTRY_INFO_URL que declaramos anteriormente.
    2. jsonRequest (do tipo JSONObject): esse parâmetro nos permite passar um objeto JSON como corpo do request. É útil quando vamos fazer um POST, PATCH ou PUT no qual em geral estamos enviando informações para o serviço HTTP.
    3. listener: aqui passamos um objeto que implemente a interface Response.Listener<JSONObject> que declara o método onResponse chamado pelo Volley quando o request é finalizado. Vamos explorá-lo a seguir.
    4. errorListener: passamos um objeto que implemente a interface Response.ErrorListener que declara o método onErrorResponse, chamado quando alguma falha acontece no request.
  3. Aqui criamos um Anonnymous Object do Java implementando a interface Response.Listener<JSONObject>. Seu código vai processar o retorno enviado pelo servidor através do Response.
  4. O primeiro passo quando recebemos uma nova lista de resultados é limpar todos os registros de países carregados atualmente em memória.
  5. O serviço de listagem de países do GeoNames retorna em um Array JSON na propriedade geonames. Usamos o método getJSONArray da classe JSONObject para extrair essa propriedade. A seguir navegamos por seus elementos um a um, extraindo o objeto em questão e carregando suas informações em um novo objeto do tipo Country, que é adicionado ao campo mCountries, nossa lista em memória.
  6. Ao final do processamento nós registramos o evento no LogCat e chamamos o método onRefreshCompleted no objeto handler para informar a quem chamou o método refreshListFromGeonamesService que a recarga finalizou com sucesso.
  7. Todo o bloco anterior estava inserido dentro de um try…catch, isso é necessário pois ao tentar interpretar o objeto JSON retornado pelo servidor, pode existir algum erro em sua formatação o que ocasionaria uma JSONException. Quando isso acontece, registramos a informação no LogCat e chamamos o handler informando que não foi possível recarregar a lista.
  8. Nessa última parte, criamos um outro anonnymous object, dessa vez implementando a interface Response.ErrorListener. O método onErrorResponse será chamado pelo Volley sempre que acontecer uma falha qualquer durante o processamento do Request, tal como uma indisponibilidade de rede, quebra de conectividade ou erro do servidor.

Esse método pode ser considerado um template para qualquer chamada HTTP que você precise fazer usando o Volley. Revise sempre os passos apresentados e você não deverá ter dificuldade em adaptá-los às necessidades específicas de seus serviços.

O último método que vamos adicionar à classe CountryList servirá para auxiliar na carga das imagens das bandeiras. O Volley conta com um componente chamado NetworkImageView. Trata-se de uma versão estendida do ImageView nativo Android, incluindo a capacidade de obter a imagem a ser exibida a partir da rede. Isso facilitará bastante o nosso trabalho, já que o próprio Volley irá tratar de baixar e exibir a imagem. Do contrário, teríamos de fazer todas essas operações manualmente, baixar a imagem a partir do servidor, salvar no armazenamento do dispositivo e passar a imagem para o ImageView.

No arquivo CountryList.java inclua o seguinte código abaixo da declaração anterior:

/**
 * Carrega a imagem da bandeira no componente NetworkImageView.
 * @param country O objeto Country representando o país do qual deseja obter a bandeira.
 * @param imageView O componente NetworkImageView que irá receber a imagem.
 */
public void loadFlagIntoView(Country country, NetworkImageView imageView) {
    imageView.setDefaultImageResId(R.drawable.unknown_flag);
    imageView.setImageUrl(country.getFlagUrl(), getImageLoader());
}

Esse método recebe os parâmetros country e imageView. O primeiro é um objeto representando o país do qual desejamos obter a bandeira. Lembrando que no capítulo no qual falamos do serviço GeoNames, a informação que precisamos para conseguir baixar uma bandeira é o código de duas letras do país, informação disponível no primeiro parâmetro. O segundo é um objeto do tipo NetworkImageView para o qual vamos passar a referência para onde ele deve passar a imagem, bem como o objeto ImageLoader que ele usa internamente para baixar essa informação.

Nesse momento o Android Studio deve estar apontando um erro por conta da ausência do método getFlagUrl() da classe Country. Abra o arquivo Country.java e inclua sua declaração conforme abaixo:

public String getFlagUrl() {
    return "http://www.geonames.org/flags/x/" + mCode.toLowerCase() + ".gif";
}

Esse método basicamente concatena a URL que tem as imagens das bandeiras com o código do país.

Solicitando que as informações de países sejam baixadas

No exercício anterior fizemos todas as alterações necessários no modelo para que ele seja capaz de obter tanto a listagem dos países, quanto suas bandeiras. Nos próximos passos vamos adaptar a interface para lidar com essas modificações no modelo.

Primeiro, vamos incluir a funcionalidade do Floating Action Button na tela principal, responsável por fazer com que a listagem seja carregada a partir do serviço GeoNames. Vamos declarar um campo do tipo ProgressDialog que será usado para exibir uma caixa de diálogo com uma animação, durante a execução da sincronização da lista de países que acontecerá de maneira assíncrona por parte do modelo. No arquivo MainActivity.java inclua o seguinte campo abaixo dos já declarados na seção “FIELDS”:

ProgressDialog mRefreshProgressDialog;

Ainda no mesmo arquivo, no final do método onCreate(), inclua o código abaixo para configurar o toque no botão de recarga:

// Configura o botão para obter a listagem de países a partir do GeoNames
FloatingActionButton refreshButton = (FloatingActionButton)findViewById(R.id.refresh_button);
refreshButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // Verifica se é necessário criar uma instância do ProgressDialog
        if (mRefreshProgressDialog == null) {
            // Cria e configura o objeto ProgressDialog
            mRefreshProgressDialog = new ProgressDialog(MainActivity.this);
            mRefreshProgressDialog.setTitle("Countries");
            mRefreshProgressDialog.setMessage("Downloading the Country list from the Geonames service...");
            mRefreshProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        }

        // Exibe o diálogo de progresso antes de iniciar a operação de recarregar os países.
        mRefreshProgressDialog.show();
        mCountryList.refreshListFromGeonamesService(MainActivity.this);
    }
});

Ao clicar no botão, primeiro verificamos se o campo mRefreshProgressDialog já foi criado anteriormente. Caso ele não exista precisamos configurá-lo antes de poder utilizá-lo. Fazemos essa verificação já que esse objeto pode ser reaproveitado sempre que o usuário solicitar baixar a lista de países.

Para configurar um objeto do tipo ProgressDialog precisamos passar 3 propriedades fundamentais:

  • Title: o título exibido na caixa de diálogo.
  • Message: a mensagem explicativa apresentada na caixa de diálogo.
  • ProgressStyle: o ProgressDialog pode reportar o progresso de duas formas:
    • ProgressDialog.STYLE_SPINNER: usamos um Spinner quando desejamos informar ao usuário uma operação cujo progresso é indeterminado. Esse é o caso ao baixar a listagem dos países, pois não conseguimos determinar facilmente em que estágio do Request ela se encontra, dessa forma mostramos uma animação circular para o usuário dando a ele a noção de que algo esta acontecendo.
    • ProgressDialog.STYLE_HORIZONTAL: o estilo horizontal mostra uma barra de status que vai sendo preenchida a medida que a operação vai acontecendo. Usamos esse estilo quando conseguimos determinar o progresso enquanto a operação acontece.

A seguir, usamos o método show() do ProgressDialog para exibir a caixa de diálogo. Isso fará com que ela seja apresentada sobrepondo todo o conteúdo da página e inibindo o usuário de interagir com o App enquanto a operação acontece. Por último, chamamos o método refreshListFromGeonamesService do CountryList para disparar a operação de maneira assíncrona.

O parâmetro que esse último método recebe é um objeto que implemente a interface ``CountryList.RefreshHandler```. No código anterior nós passamos a própria Activity como handler, porém como ela não ainda implementa essa interface o Android Studio está apontando erro. Vamos resolver esse problema a seguir!

Primeiro vamos modificar a declaração da classe MainActivity para torná-la aderente à interface, conforme o código abaixo:

public class MainActivity 
        extends AppCompatActivity
        implements CountryList.RefreshHandler {

A seguir, vamos incluir a implementação do método onRefreshCompleted declarado na interface. Inclua o código abaixo no final da declaração da classe MainActivity:

//
// INTERFACE: CountryListHandler

@Override
public void onRefreshCompleted(Boolean success) {
    mRefreshProgressDialog.hide();
    if (success) {
        CountriesAdapter adapter = new CountriesAdapter(mCountryList.getCountries());
        mCountriesListView.setAdapter(adapter);
    } else {
        Toast.makeText(this, "Refresh failed.", Toast.LENGTH_LONG).show();
    }
}

Isso é tudo que precisamos para adequar o app às alterações que fizemos no Modelo. Podemos agora testar o App e verificar as alterações. Compile e execute num emulador ou em um dispositivo de testes.

Clique no Floating Action Button com o Mapa. Durante a execução do chamado repare no ProgressDialog sendo exibido:

Ao terminar a execução o ListView deverá ser recarregado para mostrar a listagem completa de países conforme ilustrado abaixo:

Exibindo a bandeira dos países na listagem

Para deixar nosso app mais completo vamos agora incluir a apresentação das bandeiras de cada país. Para isso, precisamos modificar o layout das células para incluir o componente NetworkImageView, que irá carregar as bandeiras a partir do GeoNames. No arquivo list_item_country.xml, inclua a declaração desse componente logo no início, antes da declaração do TextView:

<com.android.volley.toolbox.NetworkImageView
    android:id="@+id/country_flag_image_view"
    android:layout_width="80dp"
    android:layout_height="50dp"
    android:layout_marginRight="8dp"
    android:layout_marginEnd="8dp"
    android:src="@drawable/unknown_flag" />

Anteriormente nossos TextView’s estavam configurados alinhados à esquerda da célula. Agora precisamos ajustá-los para que eles fiquem à esquerda do NetworkImageView. Para isso precisamos incluir as seguintes propriedades:

android:layout_toRightOf="@id/country_flag_image_view"
android:layout_toEndOf="@id/country_flag_image_view"

Mas o que essas propriedades fazem? Essencialmente a mesma coisa. Estamos sinalizando ao RelativeLayout no qual esses TextView’s estão contidos desejamos que sejam posicionados à direita do componente identificado pelo nome country_flag_image_view, assim como no NetworkImageView que adicionamos anteriormente.

Mas qual a diferença de usar toRightOf e to toEndOf? O segundo foi adicionado a partir do API Level 17 para tornar o Android compatível com as línguas chamadas RTL (Right-to-Left), ou seja, aquelas onde escrevemos da direta para esquerda tais como árabe ou o coreano. Dessa forma, quando o app for usado em uma configuração com alguma dessas línguas, a interface automaticamente será ajustada para mostrar esses elementos alinhados à direita.

Precisamos fazer apenas um último ajuste no código para que a imagem seja carregada corretamente. Para isso abra o arquivo MainActivity.java e localize o método getView() dentro da classe aninhada CountriesAdapter. Modifique o método conforme a listagem abaixo:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // If we weren' given a view, inflate one
    if (convertView == null) {
        convertView = MainActivity.this.getLayoutInflater().inflate(R.layout.list_item_country, null);
    }

    // Get components
    TextView countryTextView = (TextView)convertView.findViewById(R.id.country_text_view);
    TextView capitalTextView = (TextView)convertView.findViewById(R.id.capital_text_view);
    NetworkImageView flagView = (NetworkImageView)convertView.findViewById(R.id.country_flag_image_view);

    // Configure the view for this Country
    Country c = getItem(position);
    countryTextView.setText(c.getName());
    capitalTextView.setText("Capital: " + c.getCapital());
    mCountryList.loadFlagIntoView(c, flagView);

    return convertView;
}

Com isso nós obtemos o elemento criado no Layout da célula e chamamos o método loadFlagIntoView criado no exercício anterior para carregar a imagem dentro do elemento. Com isso já podemos executar o app e verificar as alterações:

O efeito é imediato com os dados de exemplo, uma vez que a carga das bandeiras depende apenas do código do país que já esta disponível. Se carregarmos a listagem completa dos países agora será possível identificá-los com mais facilidade:

Exibindo a bandeira do país na tela de detalhes

Para finalizarmos nosso App, vamos incluir a bandeira também na tela de detalhes. Da mesma forma como fizemos no layout da célula, vamos incluir o componente NetworkImageView. Abra o arquivo activity_country_details.xml e abaixo da declaração do primeiro TextView insira a declaração abaixo:

<com.android.volley.toolbox.NetworkImageView
    android:id="@+id/country_flag_image_view"
    android:layout_width="100dp"
    android:layout_height="60dp"
    android:layout_alignParentRight="true"
    android:layout_alignParentEnd="true"
    android:src="@drawable/unknown_flag" />

Diferente do caso anterior, não será necessário modificar outros elementos do Layout. Note que fizemos o componente ligeiramente maior para aproveitar mais espaço disponível nessa tela. Tal como fizemos com as células do ListView também precisamos configurar o componente.

No arquivo CountryDetailsActivity.java declare um campo para fazer referência ao componente de imagem da tela, abaixo dos campos já existentes:

NetworkImageView mFlagImageView;

No final da seção “Load Components” dentro do método onCreate, inclua o código abaixo para carregar a referência do componente:

mFlagImageView = (NetworkImageView)findViewById(R.id.country_flag_image_view);

E, por último, no método loadCountryData inclua a seguinte linha no final:

countryList.loadFlagIntoView(country, mFlagImageView);

Isso é tudo para configurar a tela de detalhes para exibição da bandeira. Execute o App novamente e teste suas alterações:

results matching ""

    No results matching ""