UI/UX

Implementation of RecyclerView with Cursor Adapter

In previous blog, I have explain best practises of implementing RecyclerView in Android. In this tutorials, I’m going to explain the implementation of LoaderManager with cursor adapter and render data over the RecyclerView. We are creating a sample app that populates data from data source. The data source can be Content Provider or SQLite or both, We will fetch data from these data source using LoaderManader and render the over RecyclerView with the help of RecyclerView.

The following to-do list for prepare sample app

  • Create a list item view with a custom layout such as profile picture and display name
  • Create RecyclerViewCursorAdapter which deal the cursor instance of the model class
  • Fetch the data from data source using LoaderManager and set the cursor to the adapter
Sample App (Demo)
1. Create a New Project

Open Android Studio, go to File menu and Select New Project and fill project details furthermore select EmptyActivity template. So Activity and XML layout file will automatically created.

2. Add Lib Dependency

Open the app build.gradle and add the dependency for RecyclerView and CardView and Dexter ( For runtime permission )

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.androidwave.sampleapp"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    /**
     * dependency to request the runtime permissions.
     */
    implementation 'com.karumi:dexter:4.2.0'
}
3. Write a Base Cursor Adaper

In src folder create abstract class with name BaseCursorAdapter.java, which extends the T type View of RecyclerView.ViewHolder. It help to deal with cursor and RecyclerView Adapter implementation. I will be extends same parent class in every CursorAdapter.

package com.androidwave.sampleapp.base;

import android.database.Cursor;

import androidx.recyclerview.widget.RecyclerView;

/**
 * Created on : Jan 27, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public abstract class BaseCursorAdapter<V extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<V> {
    private Cursor mCursor;
    private boolean mDataValid;
    private int mRowIDColumn;

    public abstract void onBindViewHolder(V holder, Cursor cursor);

    public BaseCursorAdapter(Cursor c) {
        setHasStableIds(true);
        swapCursor(c);
    }

    @Override
    public void onBindViewHolder(V holder, int position) {

        if (!mDataValid) {
            throw new IllegalStateException("Cannot bind view holder when cursor is in invalid state.");
        }
        if (!mCursor.moveToPosition(position)) {
            throw new IllegalStateException("Could not move cursor to position " + position + " when trying to bind view holder");
        }

        onBindViewHolder(holder, mCursor);
    }

    @Override
    public int getItemCount() {
        if (mDataValid) {
            return mCursor.getCount();
        } else {
            return 0;
        }
    }

    @Override
    public long getItemId(int position) {
        if (!mDataValid) {
            throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state.");
        }
        if (!mCursor.moveToPosition(position)) {
            throw new IllegalStateException("Could not move cursor to position " + position + " when trying to get an item id");
        }

        return mCursor.getLong(mRowIDColumn);
    }

    public Cursor getItem(int position) {
        if (!mDataValid) {
            throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state.");
        }
        if (!mCursor.moveToPosition(position)) {
            throw new IllegalStateException("Could not move cursor to position " + position + " when trying to get an item id");
        }
        return mCursor;
    }

    public void swapCursor(Cursor newCursor) {
        if (newCursor == mCursor) {
            return;
        }

        if (newCursor != null) {
            mCursor = newCursor;
            mDataValid = true;
            // notify the observers about the new cursor
            notifyDataSetChanged();
        } else {
            notifyItemRangeRemoved(0, getItemCount());
            mCursor = null;
            mRowIDColumn = -1;
            mDataValid = false;
        }
    }
}

4. Create a layout for row item of RecyclerView

Create a XML layout with named is item_friend_list.xml for for showing display name and user profile picture.

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardCornerRadius="4dp"
    app:cardElevation="4dp"
    app:cardUseCompatPadding="true"
    android:background="#ffffff">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_profile_pic"
            android:contentDescription="@string/user_profile_pic" />

        <TextView
            android:id="@+id/textViewName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="8dp"
            android:textColor="@color/colorText"
            android:textSize="@dimen/_18sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/imageView"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_chainStyle="packed" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
5. Write a Adapter class for holder item_friend_list views

Create a java class with name FriendsCursorAdapter which extend base class name is BaseCursorAdapter. onCreateViewHolder() method will inflate the item_friend_list view and onBindViewHolder() will bind the data with a component like profile picture and display name

package com.androidwave.sampleapp;

import android.database.Cursor;
import android.provider.ContactsContract;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.androidwave.sampleapp.base.BaseCursorAdapter;

import androidx.recyclerview.widget.RecyclerView;

/**
 * Created on : Jan 27, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public class FriendsCursorAdapter extends BaseCursorAdapter<FriendsCursorAdapter.FriendViewHolder> {

    public FriendsCursorAdapter() {
        super(null);
    }

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

    @Override
    public void onBindViewHolder(FriendViewHolder holder, Cursor cursor) {
        int mColumnIndexName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
        String contactName = cursor.getString(mColumnIndexName);
        holder.nameTextView.setText(contactName);
    }

    @Override
    public void swapCursor(Cursor newCursor) {
        super.swapCursor(newCursor);
    }

    class FriendViewHolder extends RecyclerView.ViewHolder {

        TextView nameTextView;

        FriendViewHolder(View itemView) {
            super(itemView);
            nameTextView = itemView.findViewById(R.id.textViewName);
        }
    }
}
6. Set Read Contact Permission in AndroidManifest
   <uses-permission android:name="android.permission.READ_CONTACTS" />
7. Open the Activity class add below code

In Activity create method we will request read contact permission using dexter lib. When user will granted permission, will fetch all contact using LoaderManager and swap the cursor with adapter.

7.1 nitialised Loader Manager

Initialised the Loader Manager using these method

  LoaderManager.getInstance(MainActivity.this).initLoader(CONTACTS_LOADER_ID, null, MainActivity.this);
7.2 Loader Manager Callback methods

onCreateLoader() methods is return the cursor to onLoadFinished() method

    @NonNull
    @Override
    public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
        if (id == CONTACTS_LOADER_ID) {
            return contactsLoader();
        }
        return null;
    }

    @Override
    public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
        mAdapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(@NonNull Loader<Cursor> loader) {
        mAdapter.swapCursor(null);
    }
8. Finally MainActivity. Java looks like this
package com.androidwave.sampleapp;

import android.Manifest;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.Settings;
import android.widget.Toast;

import com.karumi.dexter.Dexter;
import com.karumi.dexter.MultiplePermissionsReport;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;

import java.util.List;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
    RecyclerView mRecyclerView;
    private static final int CONTACTS_LOADER_ID = 1;
    FriendsCursorAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // bind view
        mRecyclerView = findViewById(R.id.recyclerViewContact);
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
        // set layout manager
        mRecyclerView.setLayoutManager(mLayoutManager);
        //set default animator
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mAdapter = new FriendsCursorAdapter();
        mRecyclerView.setAdapter(mAdapter);
        /**
         * check contact permission if permission is enable the init loader
         */
        checkContactPermission();
    }

    @NonNull
    @Override
    public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
        if (id == CONTACTS_LOADER_ID) {
            return contactsLoader();
        }
        return null;
    }

    @Override
    public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
        mAdapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(@NonNull Loader<Cursor> loader) {
        mAdapter.swapCursor(null);
    }

    private Loader<Cursor> contactsLoader() {
        Uri contactsUri = ContactsContract.Contacts.CONTENT_URI; // The content URI of the phone contacts

        String[] projection = {                                  // The columns to return for each row
                ContactsContract.Contacts.DISPLAY_NAME
        } ;

        String selection = null;                                 //Selection criteria
        String[] selectionArgs = {};                             //Selection criteria
        String sortOrder = null;                                 //The sort order for the returned rows

        return new CursorLoader(
                getApplicationContext(),
                contactsUri,
                projection,
                selection,
                selectionArgs,
                sortOrder);
    }

    /**
     * Requesting multiple permissions (contact) at once
     * This uses multiple permission model from dexter
     * On permanent denial opens settings dialog
     */
    private void checkContactPermission() {
        Dexter.withActivity(this).withPermissions(Manifest.permission.READ_CONTACTS)
                .withListener(new MultiplePermissionsListener() {
                    @Override
                    public void onPermissionsChecked(MultiplePermissionsReport report) {
                        // check if all permissions are granted
                        if (report.areAllPermissionsGranted()) {
                            // Prepare the loader.
                            LoaderManager.getInstance(MainActivity.this).initLoader(CONTACTS_LOADER_ID, null, MainActivity.this);
                        }
                        // check for permanent denial of any permission
                        if (report.isAnyPermissionPermanentlyDenied()) {
                            // show alert dialog navigating to Settings
                            showSettingsDialog();
                        }
                    }

                    @Override
                    public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
                        token.continuePermissionRequest();
                    }
                }).withErrorListener(error -> Toast.makeText(getApplicationContext(), "Error occurred! ", Toast.LENGTH_SHORT).show())
                .onSameThread()
                .check();
    }


    /**
     * Showing Alert Dialog with Settings option
     * Navigates user to app settings
     * NOTE: Keep proper title and message depending on your app
     */
    private void showSettingsDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Need Permissions");
        builder.setMessage("This app needs permission to use this feature. You can grant them in app settings.");
        builder.setPositiveButton("GOTO SETTINGS", (dialog, which) -> {
            dialog.cancel();
            openSettings();
        });
        builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
        builder.show();

    }

    // navigating user to app settings
    private void openSettings() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", getPackageName(), null);
        intent.setData(uri);
        startActivityForResult(intent, 101);
    }
}

After following all above just RUN the project and use app, If you have any queries, feel free to ask them in the comment section below. Happy Coding 🙂

1
Leave a Reply

2000
Hyeon

Thank you!

but, I wonder when called ‘getItem(int position)’ method

It’s not used in my project…