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
- 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 Adapter
In the src folder create an abstract class with name BaseCursorAdapter.java, which extends the T type View of RecyclerView.ViewHolder. It helps to deal with cursor and RecyclerView Adapter implementation. I will extend 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 an XML layout with named is item_friend_list.xml 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 an Adapter class for holder item_friend_list views
Create a java class with name FriendsCursorAdapter which extends base class name is BaseCursorAdapter. onCreateViewHolder() method will inflate the item_friend_list view and onBindViewHolder() will bind the data with a component like a 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, they will fetch all contact using LoaderManager and swap the cursor with an adapter.
7.1 Initialized Loader Manager
Initialized the Loader Manager using these methods
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); } }
1 Comment
Thank you!
but, I wonder when called ‘getItem(int position)’ method
It’s not used in my project…