Utilizando Image Loaders no desenvolvimento Android

Recentemente participei de um desafio na Fluid27, uma Startup que está desenvolvendo um app social para compartilhar experiências entre mães, o Mãeguru.

O desafio consistia em desenvolver uma solução mobile com capacidade de lidar com muitas imagens, visando uma melhor experiência do usuário. A tecnologia utilizada ficava a critério do desenvolvedor, então decidi utilizar android nativo. O Layout estava definido nos requisitos do desafio e não precisaria ser igual, mas chegar a algo próximo.

Mock_timeline_challenge

Antes de colocar a mão na massa (ou no código), fiz um checklist do que gostaria de entregar para o usuário:

  • Layout dos posts conforme layout, ou próximo da proposta.
  • Visualizar as imagens.
  • Receber os posts de uma API, para mostrar a aplicação do app mais próximo da realidade.
  • Atualizar a lista de posts quando houver um novo.

Criei um Adapter para o layout dos posts, como pode ser visto no código abaixo:

// Código omitido
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = activity.getLayoutInflater()
                                    .inflate(R.layout.post, parent, false);
		Post post = posts.get(position);

		TextView userName = (TextView) view.findViewById(R.id.post_user_name);
		userName.setText(post.getUserName());

		TextView content = (TextView) view.findViewById(R.id.post_content);
		content.setText(post.getContent());

		new ImageAsyncTask(view).execute(post.getAvatar());

		return view;
	}


No método getView() recupero o layout do post usando o método inflate(). Em seguida pego o post naquela posição, então se estou vendo o primeiro post a minha position é 0. Ao ir para o próximo post a position será 1 e assim sucessivamente.

No código recupero a minha view do tipo TextView para setar os valores do post, o nome do usuário e a mensagem. Nas linhas abaixo recupero a imagem do post (não tratei o avatar pois estava testando uma hipótese) assincronamente usando a classe ImageAsyncTask, filha de AsyncTask.

Abaixo é possível ver classe que busca os endereços dos meus posts assincronamente e transformava em um bitmap, para que eu pudesse incluir nas Views do tipo ImageView.


public class ImageAsyncTask extends AsyncTask<String,Void,Bitmap> { 
        //Código omitido

	@Override
	protected Bitmap doInBackground(String... urls) {
		Bitmap image = null;
		String url = urls[0];
		try {
			InputStream inputStream = new URL(url).openStream();
			image = BitmapFactory.decodeStream(inputStream);
		} catch (IOException e) {
			e.printStackTrace();
		}
		        return image;
	}

  @Override
  protected void onPostExecute(Bitmap bitmap) {
        ImageView imageView = (ImageView) view.findViewById(R.id.post_image);
        imageView.setImageBitmap(bitmap);
	this.cancel(true);
  }
}

Sendo filha de AsyncTask, o método doInBackground() recebe um Array de strings e no método pego a primeira posição. Transformo a URL recebida em um InputStream() para então decodificar em um Bitmap. O método onPostExecute() faz o set desse Bitmap que tenho em um Objeto ImageView, que possibilita o usuário visualizar a imagem. O método cancel é executado após o set para que a requisição não continue infinitamente.

Durante essa parte do desenvolvimento, usei um JSON estático no projeto pois receber os posts de uma API era outra parte da solução. Isso me ajudou a resolver esse requisito e pensar nas seguintes mais pra frente, quando aquilo se tornasse realmente uma necessidade.
Utilizando a classe filha de AsyncTask as imagens podiam ser vistas no app, então marquei os dois primeiros itens do meu checklist como pronto.

  • Layout dos posts conforme layout, ou próximo da proposta.
  • Visualizar as imagens
  • Receber os posts de uma API, para mostrar a aplicação do app mais próximo da realidade.
  • Atualizar os posts quando houverem novos.

Ao subir um pouco a lista eu percebi que as imagens desapareciam e uma nova requisição era feita quando eu subia a timeline.
Imagine a seguinte situação: Você está passando por sua timeline no Instagram, e decide voltar alguns posts anteriores e esse posts não estão mais no app, demora um certo período para você conseguir ver o post já que uma nova requisição é executada. Pessoalmente, desinstalaria o app e daria uma nota baixa no Google play.
O ciclo de vida de um adapter ocorre da seguinte forma: Um item é montado na visualização da tela, e ao fazer o scroll outros itens são montados para visualização. Os primeiros itens, ao saírem da visualização são recolhidos pelo garbage collector para que objetos sem uso não fiquem alocados na memória desnecessáriamente.

Incluí mais um item, que a principio não havia enxergado a necessidade: Manter as imagens por um tempo no app.

  • Manter as imagens por um tempo no app.
  • Receber os posts de uma API, para mostrar a aplicação do app mais próximo da realidade.
  • Atualizar os posts quando houver novos.

Como solucionar isso?

Precisaria deixar as imagens em cache para que a experiência do usuário não se tornasse frustrante. O Google recomenda criar uma classe filha de AsyncTask que implementa métodos que lidam com Cache, como pode ser visto na documentação.

Mas esse problema já foi resolvido por alguns desenvolvedores, e evitando reinventar a roda utilizei a lib Picasso da Square que na sua descrição diz “A powerful image downloading and caching library for Android“. Problema resolvido, eu não precisava mais ter uma classe filha de AsyncTask que transformaria os links em Bitmaps e depois settar estes bitmaps nas ImageViews, o Picasso seria responsável por isso.
A documentação do Picasso é super simples, com exemplos de fácil entendimento. Alterei a minha classe filha de Adapter, responsável pelo Layout dos posts. O método getView() ficou assim:

public View getView(int position, View view, ViewGroup parent) {
	// Código omitido
	Picasso.with(context).load(post.getImageUrl()).into(holder.postImage);
	Picasso.with(context).load(post.getAvatarUrl()).into(holder.userAvatar);
	return view;
}

Esse código basicamente diz: Nesse contexto, faça o carregamento dessa imagem nesse endereço em um ImageView (holder.postImage e holder.userAvatar). O context nessa situação é a Activity onde a View será mostrada.

Para importar o Picasso no Android Studio, inclui a seguinte linha em app/build.gradle:
compile 'com.squareup.picasso:picasso:2.5.1'.
O resultado foi muito bom, consegui excluir uma classe que executava um trabalho pesado e usei uma dependência simples de ser entendida no código. Assim consegui atender a os requisitos do desafio, usando uma lib já consolidada no mercado. Usamos a biblioteca Picasso em nosso Curso Técnicas de Desenvolvimento Android avançado.

Satisfação garantida?

Após isso, dei continuidade à construção do App. Criei a API para receber um JSON nas requisições e transformar esse JSON em Objeto Java para incluir em uma ListView.

Em um dos testes, incluí um gif de um gato (gifs de gatos movem a internet) e no app ficou uma imagem estática. Esse comportamento não me deixou feliz. Queria ir além e mostrar gifs animados, como pode ser visto em Apps como o 9gag. Mas a lib não provê uma forma de lidar com Gifs então busquei outra alternativa.

Alternativas para o Picasso

Buscando alternativas para o Picasso, encontrei o Glide. O uso da biblioteca é muito semelhante a lib Picasso. A documentação também é bastante simples, como pode ser visto no exemplo abaixo retirado do github deles:

       ImageView imageView = (ImageView) findViewById(R.id.my_image_view);
       Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);

No app, utilizei da seguinte forma:

public View getView(int position, View view, ViewGroup parent) {
    //Código omitido
    Glide.with(context).load(post.getImageUrl())
         .diskCacheStrategy(RESULT)
         .into(holder.postImage);

    Glide.with(context).load(post.getAvatarUrl())
         .error(R.mipmap.fluid)
         .diskCacheStrategy(RESULT)
         .into(holder.userAvatar);

    return view;
}

Este código é bastante parecido com o anterior, usando o Picasso, com um destaque para os métodos diskCacheStrategy(RESULT) que faz o cache da imagem redimensionada e o método error() que recebe uma imagem como padrão em caso de algo dar errado no dowload.

Inclui no app da mesma forma, incluindo a dependencia no build.gradle: compile 'com.github.bumptech.glide:glide:3.5.2'

O Glide possui algumas vantagens sobre o Picasso, como consumo de memória mais baixo ao renderizar uma imagem. Porém essa vantagem está intrinsecamente ligada a uma desvantagem: a perda qualidade das imagens pois seu formato padrão é o RGB_565.
Há uma alternativa para sobrescrever o padrão, settando explicitamente que o DecodeFormat deve ser ARGB_8888.

public class GlideConfiguration implements GlideModule {
	@Override
	public void applyOptions(Context context, GlideBuilder builder) {
		// Apply options to the builder here.
		builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
	}
	// Código omitido
}

O Glide não mostra um delay na visualização da imagem, diferente do Picasso que por padrão possui um efeito de FadeIn. Isso dá a impressão ao usuário que o app é mais rápido, uma vez que o usuário não vê a imagem sendo “montada”. A imagem é apresentada o mais rápido possível – variando de acordo com a velocidade do device e a conexão. O cache é feito de uma forma mais esperta, sendo feito apenas após fazer resize da imagem, enquanto o Picasso faz o cache da imagem que recebeu. Então se uma imagem tiver 2560×1600, usando o Picasso este será o tamanho da imagem em cache enquanto usando Glide será o tamanho da ImageView, por exemplo (768×432 pixels).
Porém há um custo, a lib Glide é maior que a lib Picasso, possui 430kb enquanto o Picasso possui 118kb. Não vejo como um grande problema quanto a essa particularidade, uma vez que a necessidade que eu gostaria de atender – visualizar gifs – foi atendida.

O uso de bibliotecas de terceiros é uma grande ajuda. Nessa situação me poupou bastante tempo. Analisando as duas Libs, não cheguei a uma conclusão de qual das duas é melhor. Usaria a lib Picasso em outra situação, onde nao houvesse necessidade de visualizar gifs e o tamanho de libs de terceiros fosse importante. Para esta necessidade o Glide foi escolhido por possuir features que o Picasso não atende. Existem inúmeras Libs que fazem o mesmo, como a Fresco do Facebook, DaVinci, e mais algumas que podem ser vistas aqui.
O código da app do desafio está disponível no github.

Como você lidaria com essa situação? Já teve que desenvolver algo assim? Qual foi a sua solução? Compartilhe conosco a sua experiência!

Tags:

8 Comentários

  1. Alberto 29/09/2015 at 09:51 #

    Uma outra que pode ser interessante é a lib Fresco(https://code.facebook.com/posts/366199913563917/introducing-fresco-a-new-image-library-for-android/). Ela foi criada pelo time do facebook para lidar com problemas de performance bem complicados, já que a app do facebook roda em lugares que a gente nem imagina.

  2. João Santana 29/09/2015 at 13:50 #

    Alberto,

    a lib Fresco foi uma opção enquanto pesquisava. Mas me incomodou usar uma View customizada (SimpleDraweeView) e achei o uso dela um pouco verboso. Mas vou testar nesse mesmo app pra validar se tenho algum ganho em performance.

  3. Fábio Amorim 09/10/2015 at 11:18 #

    João,

    você chegou a realmente validar essa questão? Estou começando a desenvolver para Android agora e esse tipo de informação é muito útil 🙂

  4. Fabiano França 28/02/2016 at 12:08 #

    Bom dia.
    Com essas lib, tem como eu carregar uma imagem da minha aplicação em uma conta picasso e depois recuperar com a url?

  5. João Santana 02/03/2016 at 08:30 #

    Fabiano,

    O upload de imagens para um serviço como o Picasa pode ser feito por alguma biblioteca como Okhttp ou retrofit, que são clients Http. Uma vez que você tem a url da imagem, ela pode ser usada com o Glide ou Picasso. Na própria aplicação, você também pode usar estas bibliotecas para fazer o carregamento de arquivos locais.

  6. Anselmo 22/10/2016 at 18:38 #

    Fabiano,

    ver se isso li ajuda.

    /*
    Metodo que compatilhar o evento.
    */
    @Override
    public boolean onLongPressClickListener(View view, int position) {
    int tamanhoPadraoCompartilhamento = 395; // opcional, utilize o valor que achar melhor
    String imgPath = DataUrl.getUrlCustom(mList.get(position).getUrlPhoto(), tamanhoPadraoCompartilhamento);
    Log.i(“log”, “Path img em Server: “+ imgPath );

    picassoDownloadImg(imgPath);
    return true;
    }

    private void picassoDownloadImg( String imgPath ){
    Picasso.with(getActivity())
    .load(imgPath)
    .into(new Target() {
    @Override
    public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
    try {
    String root = Environment.getExternalStorageDirectory().toString();
    File myDir = new File(root + “/partiuapp”);
    boolean success = true;

    // CRIANDO DIRETÓRIO CASO NÃO EXISTA
    if (!myDir.exists()) {
    success = myDir.mkdirs();
    }

    // CLÁUSULA DE GUARDA
    if (!success) {
    return;
    }

    String name = “shared_image_” + System.currentTimeMillis() + “.jpg”;
    myDir = new File(myDir, name);
    FileOutputStream out = new FileOutputStream(myDir);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);

    out.flush();
    out.close();
    shareEventImg(name); // CHAMA O CÓDIGO INTENT PARA COMPARTILHAR A IMG
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    @Override
    public void onBitmapFailed(Drawable errorDrawable) {
    }

    @Override
    public void onPrepareLoad(Drawable placeHolderDrawable) {
    }
    }
    );
    }

    private void shareEventImg( String imgName ){

    Intent shareIntent = new Intent(Intent.ACTION_SEND);
    shareIntent.setType(“image/jpg”);

    shareIntent.putExtra(Intent.EXTRA_TEXT, “Melhor Aplicativo de Eventos de Maceió”);
    String imagePath = Environment.getExternalStorageDirectory().toString()+”/partiuapp”;
    File photoFile = new File(imagePath, imgName);

    shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(photoFile));
    startActivity(Intent.createChooser(shareIntent, “Compartilhar imagem”));
    }

    ——–//———————

    compile ‘com.squareup.picasso:picasso:2.5.2’ /*ADD ESSA REFERENCIA */

    ———————//—————————–

    Permissões.

  7. Souza Penetra Raphael 16/04/2017 at 16:42 #

    Olá! Qual a extensão do arquivo q você usou no link http://fluid-challenge.herokuapp.com/posts/json

  8. João Santana 17/04/2017 at 09:17 #

    Raphael, é um json (javascript notation object) mesmo. Nos primeiros testes eu criei um arquivo .json localmente no projeto, depois fiz o app ler o retorno de um GET nesta página. A aplicação não entra muito neste post, mas fiz com rails. Você pode fazer com qualquer linguagem backend que tenha mais facilidade. Abraço!

Deixe uma resposta