What is the goal?
In the last post, I demonstrated how to create a RecyclerView and load data from the external API, particularly we used the Picasso
library to load and cache images for us. In this post, I will show how to manually do that without the use of any libraries. To not repeat myself, in this article I will just post the code for newly added classes and code where changes were made, the rest of the code is the same as previous post. The full code can be downloaded at the bottom of this page. We will learn how to create new threads in Java and how to cache bitmap for memory optimization.
Concurrency and Parallelism in Android
I will briefly explain the main purpose and methods here and in the next post, I will go in-depth about these two topics. Concurrent apps use multiple threads to simultaneously run computations that often interact with each other. Parallelism is similar to concurrency but tries to have little or no interaction between threads. Thread - is a single sequential flow of control within a program, it usually lives within a process and each of these threads is responsible for executing a specific task, hence the program can execute simultaneously multiple tasks without overloading the main thread. The main thread - in the android context usually refers to the UI thread, all computations and user interaction with UI by default happens in this thread, so it is important to not overload this thread with heavy tasks which can lead to unresponsiveness in your app. From Android developers documentation:
When your app performs intensive work in response to user interaction, this single thread model can yield poor performance unless you implement your application properly. Specifically, if everything is happening in the UI thread, performing long operations such as network access or database queries will block the whole UI. When the thread is blocked, no events can be dispatched, including drawing events. From the user’s perspective, the application appears to hang. Even worse, if the UI thread is blocked for more than a few seconds (about 5 seconds currently) the user is presented with the infamous “application not responding” (ANR) dialog. The user might then decide to quit your application and uninstall it if they are unhappy Additionally, the Android UI toolkit is not thread-safe. So, you must not manipulate your UI from a worker thread—you must do all manipulation to your user interface from the UI thread. Thus, there are simply two rules to Android’s single thread model:
- Do not block the UI thread
- Do not access the Android UI toolkit from outside the UI thread
In our case, making a network request is a heavy task and needs to be done on a new Thread
.
Caching in Android
Caching is an important concept in web development, it significantly improves user experience. In our case, after the first network request, when we get the images, we need to cache them inside the memory, so next time when we need to access them again, we can just load them from memory rather than making a new API call
. To store an image we need to convert it into a bitmap and there are two types of caching a bitmap in Android:
-
Using Memory Cache (LruCache)
LruCache
class is similar to theHashMap
data structure, in that it stores key-value pairs. The main advantage of it is that it’s faster to access this cache. The cons are, that it’s volatile memory, so every time cache gets destroyed when the app goes background or gets closed, and this cache takes up your application memory, which is limited so it’s not recommended to store huge data. -
Using Disk Cache (DiskLruCache)
DiskLruCache
is slower than the memory cache, but the main advantage is that we can store larger data and that its non-volatile memory means that we won’t lose any data if the application exits or when it goes to the background.
In our case, I’ve used Memory cache for caching the images.
Create a Memory Cache
All explanations provided in comments:
public class MainActivity extends AppCompatActivity {
RecyclerView recyclerView;
ArrayList<User> users;
private static final String TAG = "MainActivity";
private LruCache<String, Bitmap> memoryCache; // create LruCache to store bitmap values
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.mRecyclerView);
Context context = this;
initCache(); // initialize cache when activity gets created
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://dummyapi.io/data/v1/")
.addConverterFactory(GsonConverterFactory.create())
.build();
CallAPI callAPI = retrofit.create(CallAPI.class);
Call<UserWrapper> call = callAPI.getUsers();
// asynchronously makes network request
call.enqueue(new Callback<UserWrapper>() {
@Override
public void onResponse(Call<UserWrapper> call, Response<UserWrapper> response) {
if(!response.isSuccessful()) {
Toast.makeText(MainActivity.this, response.code(), Toast.LENGTH_SHORT).show();
return;
}
Log.d(TAG, String.valueOf(response.body().getUsers().get(0).getFirstName()));
// Get the data to display
users = response.body().getUsers();
// pass memoryCache to adapter
RecyclerViewAdapter recyclerViewAdapter = new RecyclerViewAdapter(context, users, memoryCache);
recyclerView.setAdapter(recyclerViewAdapter);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
}
@Override
public void onFailure(Call<UserWrapper> call, Throwable t) {
Toast.makeText(MainActivity.this, t + "", Toast.LENGTH_SHORT).show();
}
});
}
public void initCache() {
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
}
}
RecyclerViewAdapter.java
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder> {
Context context;
ArrayList<User> users = new ArrayList<>();
Bitmap userBitmap;
private LruCache<String, Bitmap> memoryCache;
public RecyclerViewAdapter(Context context, ArrayList<User> users, LruCache<String, Bitmap> memoryCache) {
this.context = context;
// This is array that contains the list of object,
// each object is type of User.
this.users = users;
this.memoryCache = memoryCache;
}
@NonNull
@Override
public RecyclerViewAdapter.MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// In onCreateViewHolder we inflate the layout and give a look to each of our rows
// as you can see, we referencing custom_row.xml, from the Step #1, in inflate method
View view = LayoutInflater.from(context).inflate(R.layout.custom_row, parent, false);
return new RecyclerViewAdapter.MyViewHolder(view);
}
@SuppressLint("SetTextI18n")
@Override
public void onBindViewHolder(@NonNull RecyclerViewAdapter.MyViewHolder holder, @SuppressLint("RecyclerView") int position) {
Log.d("onBindViewHolder", String.valueOf(Thread.currentThread().getName()));
ProcessImage processImageObject = new ProcessImage(users.get(position).getUserImage());
// For each item in RecyclerView, create a new Thread
// to separately make a network request
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
Log.d("myThread", String.valueOf(Thread.currentThread().getName()));
userBitmap = processImageObject.getBitmapFromURL(processImageObject.getImageUrl(), memoryCache, position);
}
});
newThread.start();
try {
Log.d("checkThread", String.valueOf(Looper.myLooper() == Looper.getMainLooper()));
/*
* It's synchronization mechanism, to ensure that we get bitmap first
* before setting it to ImageView. join()-method, when invoked, calling
* thread goes to waiting state(in this case main thread goes to waiting
* state). It remains in a waiting state until
* the referenced thread terminates.
*/
newThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
holder.userImage.setImageBitmap(userBitmap);
holder.userFullName.setText(users.get(position).getTitle() + ". " + users.get(position).getFirstName() + " " + users.get(position).getLastName()); // Setting text to TextView
holder.userId.setText("My ID: " + users.get(position).getUserId());
}
@Override
public int getItemCount() {
// RecyclerView wants to know the count of items in the list
return this.users.size();
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
// grabbing views from our custom_row layout file,
// similar what we normally do in onCreate method
ImageView userImage;
TextView userFullName;
TextView userId;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
userImage = itemView.findViewById(R.id.userImage);
userFullName = itemView.findViewById(R.id.fullNameText);
userId = itemView.findViewById(R.id.userId);
}
}
}
ProcessImage.java
// This class is responsible for processing an image,
// converting from url to bitmap and storing it to the local cache
public class ProcessImage {
String imageUrl;
int key;
private LruCache<String, Bitmap> memoryCache;
public ProcessImage(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getImageUrl() {
return imageUrl;
}
// This function takes image url and converts it to bitmap,
// after that it stores it to the cache in key-value pair format,
// with a key being the item position in recyclerView
public Bitmap getBitmapFromURL(String src, LruCache<String, Bitmap> memoryCache, int key) {
try {
CacheImages cacheImage = new CacheImages(memoryCache, key);
// Retrieves the cache by providing the key value.
Bitmap myBitmap = cacheImage.getBitmapFromMemCache(String.valueOf(cacheImage.getKey()));
if(myBitmap != null) {
Log.d("cached", "Got the cached content");
// if the cache is not null, return the cache content
return myBitmap;
} else {
// if the cache for that key is null, get the image from server,
// convert to bitmap and return it.
// Credits to @silentnuke: https://stackoverflow.com/a/8993175/7686275
URL url = new URL(src);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
myBitmap = BitmapFactory.decodeStream(input);
cacheImage.addBitmapToMemoryCache(String.valueOf(cacheImage.getKey()), myBitmap);
return myBitmap;
}
} catch (IOException e) {
// Log exception
return null;
}
}
}
CacheImages.java
// This class is responsible for caching
public class CacheImages {
private LruCache<String, Bitmap> memoryCache;
int key;
public LruCache<String, Bitmap> getMemoryCache() {
return memoryCache;
}
public int getKey() {
return key;
}
public CacheImages(LruCache<String, Bitmap> memoryCache, int key) {
this.memoryCache = memoryCache;
this.key = key;
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
// if bitmap doesn't exist in cache, store that bitmap to cache
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return memoryCache.get(key);
}
}