3

I read the documentation about making an network request activity independent by making a singleton class and passing the application context to it. I implemented it similarly, however I still find that on rotation the app waits for the call again to complete before displaying any data. So what am I doing wrong and how to set it up properly so that the call lasts the lifetime of the application so that it doesn't call every time on orientation change as per the documentation. I know it can be done using loaders or retrofit or okhttp but I wanna know how to achieve it using volley

MainActivity.java

package com.example.imnobody.photosearch;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private ImageGridAdapter imageGridAdapter;
    private List<String> imageList;

    public static final String URL = "API_HERE";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageList = new ArrayList<>();
        //imageList = QueryUtils.extractImages(SAMPLE_JSON_RESPONSE);


        GridView gridView = (GridView) findViewById(R.id.gridview);
        final TextView emptyTextView = (TextView)findViewById(android.R.id.empty);
        gridView.setEmptyView(emptyTextView);

        imageGridAdapter = new ImageGridAdapter(MainActivity.this,imageList);

        gridView.setAdapter(imageGridAdapter);

        gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {

                Intent intent = new Intent(MainActivity.this,ImageActivity.class);
                intent.putExtra("imageuri",imageList.get(position));
                startActivity(intent);

            }
        });

        StringRequest stringRequest = new StringRequest(Request.Method.GET, URL,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {

                        imageList = QueryUtils.extractImages(response); //extract needed things from json
                        imageGridAdapter.clear();
                        imageGridAdapter.addAll(imageList);

                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {

                emptyTextView.setText("Unknown error occured");
            }
        });

        VolleySingleton.getInstance(this.getApplicationContext()).addToRequestQueue(stringRequest);


    }
}

VolleySingleton.java

package com.example.imnobody.photosearch;

import android.content.Context;
import android.graphics.Bitmap;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;

import android.support.v4.util.LruCache;

/**
 * Created by imnobody on 7/8/17.
 */

public class VolleySingleton {

    private static VolleySingleton mInstance;
    private RequestQueue mRequestQueue;
    private ImageLoader mImageLoader;
    private static Context mCtx;

    private VolleySingleton(Context context) {
        mCtx = context;
        mRequestQueue = getRequestQueue();

        mImageLoader = new ImageLoader(mRequestQueue,
                new ImageLoader.ImageCache() {
                    private final LruCache<String, Bitmap>
                            cache = new LruCache<String, Bitmap>(20);

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

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

    public static synchronized VolleySingleton getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new VolleySingleton(context);
        }
        return mInstance;
    }

    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {

            mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext());
        }
        return mRequestQueue;
    }

    public <T> void addToRequestQueue(Request<T> req) {
        getRequestQueue().add(req);
    }

    public ImageLoader getImageLoader() {
        return mImageLoader;
    }
}
Nobody
  • 397
  • 1
  • 5
  • 25

1 Answers1

1

Well, a couple of points:

You're making a request every time your activity is recreated, in onCreate. In theory, this isn't necessarily bad if you really need to refresh your data whenever the activity is opened, since it seems that Volley automatically sets a DiskBasedCache for itself when creating a RequestQueue with newRequestQueue (see here).

This means that even though you're making a new request after each orientation change, Volley would fetch you a cached response, instead of hitting the network. By enabling verbose logging you should see when Volley uses the network or the cache to serve a request.

To enable verbose logging, see here: https://stackoverflow.com/a/23654407/2220337

However, the default cache is still just a Disk cache, which is slower than an in-memory cache. If you need your cache to be faster, you can implement your own in-memory cache by implementing the Cache interface, and then by creating your RequestQueue as described here, and providing your own custom, in-memory cache in the constructor.

An even better way would be to not make a request at all after an orientation change, and instead rely on onSaveInstanceState/onRestoreInstanceState to persist and then restore your data. This way, if a request was already completed, you won't fire a new one when the activity gets recreated.

Instead, you just display the data you saved in onSaveInstanceState.

public class MainActivity extends AppCompatActivity {

public static final String URL = "API_HERE";
private static final String SAVED_RESPONSE = "SAVED_RESPONSE";
private ImageGridAdapter imageGridAdapter;
private List<String> imageList;
private GridView gridView;
private String savedResponse;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    imageList = new ArrayList<>();
    gridView = (GridView) findViewById(R.id.gridview);
    final TextView emptyTextView = (TextView) findViewById(android.R.id.empty);
    gridView.setEmptyView(emptyTextView);
    imageGridAdapter = new ImageGridAdapter(MainActivity.this, imageList);
    gridView.setAdapter(imageGridAdapter);

    gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {

            Intent intent = new Intent(MainActivity.this, ImageActivity.class);
            intent.putExtra("imageuri", imageList.get(position));
            startActivity(intent);
        }
    });

    if (savedInstanceState == null) {
        //activity was created for the first time, fetch images
        getImages();
    } else {
        //everything in this else branch can be moved in onRestoreInstanceState, this is just a matter of preference
        savedResponse = savedInstanceState.getString(SAVED_RESPONSE);
        if (savedResponse != null) {
            refreshImages(savedResponse);
        } else {
            //an error occurred when the request was fired previously
            ((TextView) gridView.getEmptyView()).setText("Unknown error occured");
        }
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString(SAVED_RESPONSE, savedResponse);
}

private void getImages() {
    StringRequest stringRequest = new StringRequest(Request.Method.GET, URL,
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    savedResponse = response;
                    refreshImages(response);
                }
            }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            savedResponse = null;
            ((TextView) gridView.getEmptyView()).setText("Unknown error occured");
        }
    });

    VolleySingleton.getInstance(this.getApplicationContext()).addToRequestQueue(stringRequest);
}

private void refreshImages(String response) {
    imageList = QueryUtils.extractImages(response); //extract needed things from json
    imageGridAdapter.clear();
    imageGridAdapter.addAll(imageList);
}

Please also be mindful regarding the following points:

  • If you start a request and an orientation change occurs before it completes, you will have memory leaks, and your Activity won't be garbage collected. This is because your stringRequest is an anonymous inner class instance that will reference MainActivity implicitly.

    To circumvent this, I had good results in the past having my Volley requests & responses being managed by an Android service, with the responses being forwarded to the UI through sticky broadcasts. An event bus also works for this purpose.

    The broadcasts needed to be sticky in order to not lose a response if it completed while the activity was being recreated, since while being recreated it wasn't registered as a broadcast receiver. By sending sticky broadcasts though, they would persist and allow me to read the data after Android has finished recreating my Activity.

  • The second approach I mentioned should be fine if your response string is a not-very-large JSON which points to some online images that will later be downloaded. However, if it contains BASE64 enconded images instead, then maybe the default DiskBasedCache from Volley is more appropriate for caching its data.

Hope this helps!

Community
  • 1
  • 1
cjurjiu
  • 1,758
  • 13
  • 21
  • So just one question, so this will also lead to a memory leak on orientation change so why would one would want a separate singleton class for volley? Or what it's purpose instead of simply having something [like this](https://developer.android.com/training/volley/simple.html) – Nobody Aug 16 '17 at 19:55
  • just to clarify, with " **this** will also lead to a memory leak..", does **this** refer to the code fragment from my answer, or to the approach with the service, mentioned after the code fragment? – cjurjiu Aug 17 '17 at 08:31
  • No I meant the [singleton approach](https://developer.android.com/training/volley/requestqueue.html) mentioned in the documentation versus the normal approach mentioned in the [former page](https://developer.android.com/training/volley/simple.html) of the documentation. I just want to know in what way having a singleton class approach is better compared to this one. – Nobody Aug 17 '17 at 09:50
  • ah, I see. Well, whether you use the singleton approach or the "other version", the general idea is to use the same RequestQueue for multiple requests. Even if you use the method described [here](https://developer.android.com/training/volley/simple.html), you would still want to save your RequestQueue object and use it again for other requests. Whether your RequestQueue is actually a singleton, or just a normal object passed around wherever you need it, doesn't really matter. Generally, you want to use the same RequestQueue because this object knows how to deal with paralell requests, – cjurjiu Aug 17 '17 at 11:56
  • contains your caches, and knows how to prioritize requests based on their priority. Creating a new RequestQueue for every request new means you have to persist the cache yourself (if you want to cache), and provide each new RequestQueue with the same cache...parallel requests won't be serviced as efficiently and you would waste memory basically with duplicate objects with the same purpose. Think of RequestQueue as Conductor, and the Requests as members of the orchestra. You could have two conductors (or more), but you only really need one, and the orchestra works best with just one. – cjurjiu Aug 17 '17 at 12:01