How to use a RecyclerView to show images from storage

final

The issue at hand

The RecyclerView widget is a more advanced and flexible version of ListView. It manages and optimizes the view holder bindings according to the scrolling position, and recycles the views so that it uses only a small number of views for a large number of list items. Seeing as the RecyclerView sample app is outdated and doesn’t even compile, this tutorial aims to show a relatively quick way to add a RecyclerView to modern Android Studio projects, and use it to display a list of random images we’ll download to our device.

Creating a new project

Make a new project (or open an existing one). When creating the project, we’ll choose to add a scrolling activity for this example, but you can choose any layout you want.

Run it now for a small sanity check:

finished

Adding a list fragment

Right click on the project folder -> add -> fragment (list) -> finish

This creates a RecyclerView with lots of boilerplate code. Let’s go over the added classes:

MyItemRecyclerViewAdapter - Creates the view holder which, well, holds the views for items in the list and binds the data to the views inside the view holder.

ItemFragment - The fragment that holds and initializes the adapter.

Dummy/DummyContent - Dummy items for populating the list. We’ll replace those with our picture items.

fragment_item_list.xml - contains the RecyclerView widget.

fragment_item.xml - layout of each item in the list.

Now we need to add the fragment we created to our activity. In content_scrolling.xml replace the TextView with:

<fragment android:name="com.example.myapplication.ItemFragment"
    android:id="@+id/main_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

We’ll also make the activity implement our interaction listener interface:

implements ItemFragment.OnListFragmentInteractionListener

After adding this to the activity class, you’ll have to implement the onListFragmentInteraction method, you can do it automatically with the suggestion window. This is the auto-generated method that’s added:

@Override
public void onListFragmentInteraction(DummyContent.DummyItem item) {    
}

Run the project now to see that the list shows and scrolls:

finished

Replacing dummy content

In android studio, rename DummyContent.java to PictureContent.java (and the class name), and move it out of the dummy package. Delete the dummy package. We’ll also delete the DummyItem class, and create a POJO class PictureItem in a new file, containing the picture URI and creation date:

class PictureItem {
    public Uri uri;
    public String date;
}

In PictureContent replace the DummyItem creation with a PictureItem creation:

public class PictureContent {
    static final List<PictureItem> ITEMS = new ArrayList<>();

    public static void loadImage(File file) {
        PictureItem newItem = new PictureItem();
        newItem.uri = Uri.fromFile(file);
        newItem.date = getDateFromUri(newItem.uri);
        addItem(newItem);
    }

    private static void addItem(PictureItem item) {
        ITEMS.add(0, item);
    }
}

Now we’ll update fragment_item.xml to display our image item with an ImageView for the image and a TextView for the creation date:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/item_image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@string/app_name"
        android:scaleType="centerInside"
        android:padding="10dp" />

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:text="@string/created_at"
        android:textAppearance="?attr/textAppearanceListItem" />

    <TextView
        android:id="@+id/item_date_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:textAppearance="?attr/textAppearanceListItem" />
    </LinearLayout>
</LinearLayout>

Finally, in MyItemRecyclerViewAdapter, replace the content to bind our new data fields to our new views:

public class MyItemRecyclerViewAdapter extends RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder> {

    private final List<PictureItem> mValues;
    private final OnListFragmentInteractionListener mListener;

    public MyItemRecyclerViewAdapter(List<PictureItem> items, OnListFragmentInteractionListener listener) {
        mValues = items;
        mListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.fragment_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        holder.mItem = mValues.get(position);
        holder.mImageView.setImageURI(mValues.get(position).uri);
        holder.mDateView.setText(mValues.get(position).date);

        holder.mView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != mListener) {
                    // Notify the active callbacks interface (the activity, if the
                    // fragment is attached to one) that an item has been selected.
                    mListener.onListFragmentInteraction(holder.mItem);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return mValues.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        public final View mView;
        public final ImageView mImageView;
        public final TextView mDateView;
        public PictureItem mItem;

        public ViewHolder(View view) {
            super(view);
            mView = view;
            mImageView = view.findViewById(R.id.item_image_view);
            mDateView = view.findViewById(R.id.item_date_tv);
        }
    }
}

Load pictures

Now we’ll populate the list with images saved in the device storage. Add the images loading methods to PictureContent:

public static void loadSavedImages(File dir) {
    ITEMS.clear();
    if (dir.exists()) {
        File[] files = dir.listFiles();
        for (File file : files) {
            String absolutePath = file.getAbsolutePath();
            String extension = absolutePath.substring(absolutePath.lastIndexOf("."));
            if (extension.equals(".jpg")) {
                loadImage(file);
            }
        }
    }
}

private static String getDateFromUri(Uri uri){
    String[] split = uri.getPath().split("/");
    String fileName = split[split.length - 1];
    String fileNameNoExt = fileName.split("\\.")[0];
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = format.format(new Date(Long.parseLong(fileNameNoExt)));
    return dateString;
}

public static void loadImage(File file) {
    PictureItem newItem = new PictureItem();
    newItem.uri = Uri.fromFile(file);
    newItem.date = getDateFromUri(newItem.uri);
    addItem(newItem);
}

We’re going to call loadSavedImages from our activity ScrollingActivity, so we first need to get a reference to the recycler view. Add two fields:

private RecyclerView.Adapter recyclerViewAdapter;
private RecyclerView recyclerView;

Which will be lazy loaded in onCreate:

if (recyclerViewAdapter == null) {
    Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.main_fragment);
    recyclerView = (RecyclerView) currentFragment.getView();
    recyclerViewAdapter = ((RecyclerView) currentFragment.getView()).getAdapter();
}

And in onResume we’ll add a call to loadSavedImages:

@Override
protected void onResume() {
    super.onResume();

    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            loadSavedImages(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS));
            recyclerViewAdapter.notifyDataSetChanged();
        }
    });
}

Notice we’re loading the files from DIRECTORY_DOWNLOADS which is a convensional folder for storing downloaded files.

Downloading the pictures

We’ll download random pictures from Lorem Picsum whenever clicking the Floating Action Button, using the built in DownloadManager class.

Add the download method to PictureContent:

public static void downloadRandomImage(DownloadManager downloadmanager, Context context) {
    long ts = System.currentTimeMillis();
    Uri uri = Uri.parse(context.getString(R.string.image_download_url));

    DownloadManager.Request request = new DownloadManager.Request(uri);
    request.setTitle("My File");
    request.setDescription("Downloading");
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
    request.setVisibleInDownloadsUi(false);
    String fileName = ts + ".jpg";
    request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName);

    downloadmanager.enqueue(request);
}

This downloads the file to DIRECTORY_DOWNLOADS with the current timestamp as file name.

Set image_download_url in strings.xml:

<string name="image_download_url">https://picsum.photos/200/300/?random</string>

Don’t forget to add the INTERNET permission to the manifest

Now we need to handle the download complete event. Add the following to activity’s onCreate:

onComplete = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String filePath="";
        DownloadManager.Query q = new DownloadManager.Query();
        q.setFilterById(intent.getExtras().getLong(DownloadManager.EXTRA_DOWNLOAD_ID));
        Cursor c = downloadManager.query(q);

        if (c.moveToFirst()) {
            int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
            if (status == DownloadManager.STATUS_SUCCESSFUL) {
                String downloadFileLocalUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                filePath = Uri.parse(downloadFileLocalUri).getPath();
            }
        }
        c.close();
        PictureContent.loadImage(new File(filePath));
        recyclerViewAdapter.notifyItemInserted(0);
        progressBar.setVisibility(View.GONE);
        fab.setVisibility(View.VISIBLE);
    }
};

context.registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));

This is the (quite verbose) way of getting the downloaded file name when the download manager completes the download. After getting filePath we call PictureContent.loadImage, which adds it to our list.

Notice the call to recyclerViewAdapter.notifyItemInserted(0). This will cause the list to refresh with the new item we’ve inserted (at index 0)

Aside: creating a plus icon with Asset Studio

As the final touchup, we’ll update the FAB’s icon, using Android Studio’s Asset Studio, for creating a vector material icon. Right-click the res folder and select New > Vector Asset. Click the Button and search for the keyword add:

vector

This will give us the plus material icon. Change color to white, and save the xml in the drawable folder.

That’s it! Now we have a scrolling RecyclerView showing the downloaded pictures:

final

Full source can be found here


Jonathan Perry
Written by@Jonathan Perry
Fullstack dev - I like making products fast

GitHubMediumTwitter