Introduction of RecyclerView is the best thing ever happen to android world. You can create stunningly beautiful lists and grids using the RecyclerView. Lot of us are very familiar in rendering the basic lists when the UI is very simple. But it comes to more complex lists that contains multiple UI elements along with animations, not everybody can achieve the final output they are looking for.

This article aims to improvise your knowledge on RecyclerView by taking an example of Gmail style inbox that contains the complex list design with interactive animations.

android-gmail-like-inbox-tutorial-1

1. Overview

The desired output like Gmail app can’t be achieved just with the RecyclerView alone. It needs combination of other few android concepts. Overall we gonna use the below mentioned components to get the finest appearance and functionality.

> RecyclerView
The basic component required for this app is RecyclerView as our primary task is to display the data in list fashion. The appearance of the list is customized just like Gmail app displaying a thumbnail icon, three line message, timestamp and a star icon to mark the message as important.

> SwipeRefreshLayout
In order to refresh the inbox, SwipeRefreshLayout is wrapped around the RecyclerView. This article doesn’t explains the persistence of the data. So the inbox will be reset to initial state up on refresh.

> ActionMode
ActionMode is used to display the contextual toolbar when a row is long pressed in the list. This enables us to provide set of alternative toolbar icons when the recycler view is in multiple choice mode. Here we provide delete option to delete the selected messages.

> Object Animators
Object Animators allows us to animate a target element. In this we use the object animators to perform the Flip Animation of list thumbnail icon when a row is long pressed.

> Retrofit
In a production app, all the inbox messages are dynamic i.e they are fetched from a REST API. To demonstrate that, I have used a JSON url to list the messages. We use Retrofit library to fetch and deserialize the JSON.

demo-gif-compressed

2. Sample JSON for Inbox Messages

I have created an endpoint which serves the inbox messages in a JSON format. The JSON contains the information like profile picture, from, subject, message, timestamp and other details necessary to render the list. In realtime this json should be generated from a database using any server side language.

https://api.androidhive.info/json/inbox.json

[
  {
	"id": 1,
	"isImportant": false,
	"picture": "https://api.androidhive.info/json/google.png",
	"from": "Google Alerts",
	"subject": "Google Alert - android",
	"message": "Android N update is released to Nexus Family!",
	"timestamp": "10:30 AM",
	"isRead": false
  },
  .
  .
  .
]

3. Creating New Project

We’ll start by creating new project in Android Studio and do the basic setup required. Below is the final project structure I have planned for this article. This post seems to be lengthy but trust me this will enhance your knowledge and you will see surprising results when you reach to the bottom.

android-gmail-project-structure

1. Create a new project in Android Studio from File β‡’ New Project and fill the project details. While creating the project, I have selected the Basic Activity as default activity to get the Toolbar, FAB and other elements.

2. Open build.gradle located under app module and add RecyclerView, Retrofit and Glide dependencies and Sync the project.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha7'
    compile 'com.android.support:design:24.2.1'
    testCompile 'junit:junit:4.12'

    // RecyclerView
    compile 'com.android.support:recyclerview-v7:24.2.1'

    // retrofit, gson
    compile 'com.google.code.gson:gson:2.6.2'
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    compile 'com.squareup.retrofit2:converter-gson:2.0.2'

    // glide
    compile 'com.github.bumptech.glide:glide:3.7.0'
}

3. Download this res folder and paste the content in your project’s res folder. This folder contains all the necessary icons required for the RecyclerView and Toolbar.

4. Add the below colors, strings and dimens to respective files.
colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#db4437</color>
    <color name="colorPrimaryDark">#b93221</color>
    <color name="colorAccent">#FFFFFF</color>
    <color name="from">#000000</color>
    <color name="subject">#111111</color>
    <color name="timestamp">#4285f4</color>
    <color name="message">#7a7a7a</color>
    <color name="icon_tint_normal">#7a7a7a</color>
    <color name="icon_tint_selected">#fed776</color>
    <color name="row_activated">#e0e0e0</color>
    <color name="bg_action_mode">#757575</color>
    <color name="bg_circle_default">#666666</color>
</resources>

dimens.xml

<resources>
    <dimen name="fab_margin">16dp</dimen>
    <dimen name="padding_list_row">16dp</dimen>
    <dimen name="messages_padding_left">72dp</dimen>
    <dimen name="icon_width_height">40dp</dimen>
    <dimen name="msg_text_primary">16sp</dimen>
    <dimen name="msg_text_secondary">14sp</dimen>
    <dimen name="icon_star">25dp</dimen>
    <dimen name="icon_text">22dp</dimen>
    <dimen name="timestamp">12dp</dimen>
</resources>

strings.xml

<resources>
    <string name="app_name">Gmail</string>
    <string name="action_settings">Settings</string>
    <string name="action_search">Search</string>
    <string name="action_delete">Delete</string>
</resources>

5. Open styles.xml and add the below styles. Here windowActionModeOverlay is added to overlap the ActionMode onto Toolbar.

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="windowActionModeOverlay">true</item>
        <item name="android:actionModeBackground">@color/bg_action_mode</item>
    </style>

    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

6. As we are going to make network calls we need INTERNET permission in the manifest file. Open AndroidManifest.xml and add the permission.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.androidhive.gmail">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".activity.MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

7. Create five packages named activity, adapter, helper, model and network. We use these packages to keep the project organized. Once the packages are created, move your MainActivity to activity package.

4. Adding Retrofit – Fetching JSON

Now our project have the basic resources ready. Let’s add the network layer by using the Retrofit library. If you are new to Retrofit, I strongly suggest you go through my previous article about Retrofit.

8. Under model package, create a class named Message.java. This POJO class is used to deserialize the json while parsing.

package info.androidhive.gmail.model;

public class Message {
    private int id;
    private String from;
    private String subject;
    private String message;
    private String timestamp;
    private String picture;
    private boolean isImportant;
    private boolean isRead;
    private int color = -1;

    public Message() {
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(String timestamp) {
        this.timestamp = timestamp;
    }

    public boolean isImportant() {
        return isImportant;
    }

    public void setImportant(boolean important) {
        isImportant = important;
    }

    public String getPicture() {
        return picture;
    }

    public void setPicture(String picture) {
        this.picture = picture;
    }

    public boolean isRead() {
        return isRead;
    }

    public void setRead(boolean read) {
        isRead = read;
    }

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }
}

9. Under network package, create a new class named ApiClient.java. This class creates the static retrofit instance.

package info.androidhive.gmail.network;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;


public class ApiClient {
    public static final String BASE_URL = "https://api.androidhive.info/json/";
    private static Retrofit retrofit = null;
    
    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

10. Under network package, create a new class named ApiInterface.java. This class contains the rest api endpoints and the type of response it is expecting. In our case we have only one endpoint i.e inbox.json

package info.androidhive.gmail.network;

import java.util.List;

import info.androidhive.gmail.model.Message;
import retrofit2.Call;
import retrofit2.http.GET;

public interface ApiInterface {

    @GET("inbox.json")
    Call<List<Message>> getInbox();

}

This completes the retrofit integration. Now let’s add few helper classes those helps in rendering the list.

11. Under helper package, create a class named CircleTransform.java. This class is used to display the thumbnail image in circular shape using the Glide library.

package info.androidhive.gmail.helper;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;

import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;

public class CircleTransform  extends BitmapTransformation {
    public CircleTransform(Context context) {
        super(context);
    }

    @Override protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        return circleCrop(pool, toTransform);
    }

    private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
        if (source == null) return null;

        int size = Math.min(source.getWidth(), source.getHeight());
        int x = (source.getWidth() - size) / 2;
        int y = (source.getHeight() - size) / 2;

        // TODO this could be acquired from the pool too
        Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);

        Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
        if (result == null) {
            result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
        paint.setAntiAlias(true);
        float r = size / 2f;
        canvas.drawCircle(r, r, r, paint);
        return result;
    }

    @Override public String getId() {
        return getClass().getName();
    }
}

12. Under helper package, create another class named DividerItemDecoration.java. This helps in adding divider lines in recycler view.

package info.androidhive.gmail.helper;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

/**
 * Created by Ravi Tamada on 21/02/17.
 * www.androidhive.info
 */
public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    private Drawable mDivider;
    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

5. Generating Random Material Color

Another exciting thing you would come across here is assigning a random background color to each row icon. To achieve this we need to predefine set of material colors in an array and choose a random color while RecyclerView is prepared. Thanks to daniellevass for providing such an useful color codes.

13. Create an xml named array.xml under res β‡’ values. This xml contains few material colors thouse would be loaded randomly in list.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="mdcolor_400">
        <item name="red_400" type="color">#e84e40</item>
        <item name="pink_400" type="color">#ec407a</item>
        <item name="purple_400" type="color">#ab47bc</item>
        <item name="deep_purple_400" type="color">#7e57c2</item>
        <item name="indigo_400" type="color">#5c6bc0</item>
        <item name="blue_400" type="color">#738ffe</item>
        <item name="light_blue_400" type="color">#29b6f6</item>
        <item name="cyan_400" type="color">#26c6da</item>
        <item name="teal_400" type="color">#26a69a</item>
        <item name="green_400" type="color">#2baf2b</item>
        <item name="light_green_400" type="color">#9ccc65</item>
        <item name="lime_400" type="color">#d4e157</item>
        <item name="yellow_400" type="color">#ffee58</item>
        <item name="orange_400" type="color">#ffa726</item>
        <item name="deep_orange_400" type="color">#ff7043</item>
        <item name="brown_400" type="color">#8d6e63</item>
        <item name="grey_400" type="color">#bdbdbd</item>
        <item name="blue_grey_400" type="color">#78909c</item>
    </array>
    <array name="mdcolor_500">
        <item name="red_500" type="color">#e51c23</item>
        <item name="pink_500" type="color">#e91e63</item>
        <item name="purple_500" type="color">#9c27b0</item>
        <item name="deep_purple_500" type="color">#673ab7</item>
        <item name="indigo_500" type="color">#3f51b5</item>
        <item name="blue_500" type="color">#5677fc</item>
        <item name="light_blue_500" type="color">#03a9f4</item>
        <item name="cyan_500" type="color">#00bcd4</item>
        <item name="teal_500" type="color">#009688</item>
        <item name="green_500" type="color">#259b24</item>
        <item name="light_green_500" type="color">#8bc34a</item>
        <item name="lime_500" type="color">#cddc39</item>
        <item name="yellow_500" type="color">#ffeb3b</item>
        <item name="orange_500" type="color">#ff9800</item>
        <item name="deep_orange_500" type="color">#ff5722</item>
        <item name="brown_500" type="color">#795548</item>
        <item name="grey_500" type="color">#9e9e9e</item>
        <item name="blue_grey_500" type="color">#607d8b</item>
    </array>
</resources>

To load these colors randomly, the following function can be used. You will see how to use this function shortly.

    private int getRandomMaterialColor(String typeColor) {
        int returnColor = Color.GRAY;
        int arrayId = getResources().getIdentifier("mdcolor_" + typeColor, "array", getPackageName());

        if (arrayId != 0) {
            TypedArray colors = getResources().obtainTypedArray(arrayId);
            int index = (int) (Math.random() * colors.length());
            returnColor = colors.getColor(index, Color.GRAY);
            colors.recycle();
        }
        return returnColor;
    }

6. Flip Animation using Object Animators

If you observe the gmail app, when you long press and selects a row, the thumbnail icon will be animated in a flip motion showing other side of the icon. We can do the same using the ObjectAnimator concepts. Carefully create the below mentioned files in your project.

14. Under res β‡’ values, create an xml named integer.xml. Here we define the animation durations.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="card_flip_time_full">500</integer>
    <integer name="card_flip_time_half">200</integer>
</resources>

15. Create a folder named animator under res folder. In this directory we place all the xml resources related to animations.

16. Under animator, create four xml files named card_flip_left_in.xml, card_flip_left_out.xml, card_flip_right_in.xml and card_flip_right_out.xml.

card_flip_left_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />

    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="-180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_left_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_right_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />

    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_right_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="-180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

17. Under helper package, create a class named FlipAnimator.java. This class has a static method flipView() which performs the flip animation.

package info.androidhive.gmail.helper;

import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.content.Context;
import android.view.View;

import info.androidhive.gmail.R;

public class FlipAnimator {
    private static String TAG = FlipAnimator.class.getSimpleName();
    private static AnimatorSet leftIn, rightOut, leftOut, rightIn;

    /**
     * Performs flip animation on two views
     */
    public static void flipView(Context context, final View back, final View front, boolean showFront) {
        leftIn = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_left_in);
        rightOut = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_right_out);
        leftOut = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_left_out);
        rightIn = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_right_in);

        final AnimatorSet showFrontAnim = new AnimatorSet();
        final AnimatorSet showBackAnim = new AnimatorSet();

        leftIn.setTarget(back);
        rightOut.setTarget(front);
        showFrontAnim.playTogether(leftIn, rightOut);

        leftOut.setTarget(back);
        rightIn.setTarget(front);
        showBackAnim.playTogether(rightIn, leftOut);

        if (showFront) {
            showFrontAnim.start();
        } else {
            showBackAnim.start();
        }
    }
}

7. Finally Rendering the Inbox in RecyclerView

Oooh, finally we have reached to core part of the article i.e rendering the actual list data. Have some more patience πŸ™‚ and follow along with me.

Now let’s create the few remaining files required for the RecyclerView. All we need is xml layouts for main activity, list row, background drawables and an adapter class.

18. Under res β‡’ drawable, create two drawable resources named bg_circle.xml and bg_list_row.xml.

bg_circle.xml (This provides solid circular background color to thumbnail icon)

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid
        android:color="@color/bg_circle_default"/>

    <size
        android:width="120dp"
        android:height="120dp"/>
</shape>

bg_list_row.xml (Provides background color to list item with normal and active states)

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/row_activated" android:state_activated="true" />
    <item android:drawable="@android:color/transparent" />
</selector>

19. Open the layout file of your main activity (content_main.xml) and add the RecyclerView element.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:mContext="info.androidhive.gmail.activity.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="@color/colorPrimary"
        app:srcCompat="@drawable/ic_edit_white_24dp" />

</android.support.design.widget.CoordinatorLayout>

content_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:mContext="info.androidhive.gmail.activity.MainActivity"
    tools:showIn="@layout/activity_main">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical" />

    </android.support.v4.widget.SwipeRefreshLayout>

</android.support.constraint.ConstraintLayout>

20. Under res β‡’ layout, create an xml layout named message_list_row.xml with below code. This layout will renders each row in recycler view. The actual customization of recycler view row happens here.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_list_row"
    android:clickable="true"
    android:focusable="true"
    android:orientation="vertical"
    android:paddingBottom="@dimen/padding_list_row"
    android:paddingLeft="?listPreferredItemPaddingLeft"
    android:paddingRight="?listPreferredItemPaddingRight"
    android:paddingTop="@dimen/padding_list_row">

    <LinearLayout
        android:id="@+id/message_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:orientation="vertical"
        android:paddingLeft="72dp"
        android:paddingRight="@dimen/padding_list_row">

        <TextView
            android:id="@+id/from"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:lines="1"
            android:textColor="@color/from"
            android:textSize="@dimen/msg_text_primary"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/txt_primary"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:lines="1"
            android:textColor="@color/subject"
            android:textSize="@dimen/msg_text_secondary"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/txt_secondary"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:lines="1"
            android:textColor="@color/message"
            android:textSize="@dimen/msg_text_secondary" />

    </LinearLayout>

    <RelativeLayout
        android:id="@+id/icon_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/icon_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

            <ImageView
                android:layout_width="@dimen/icon_width_height"
                android:layout_height="@dimen/icon_width_height"
                android:src="@drawable/bg_circle" />

            <ImageView
                android:layout_width="25dp"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:src="@drawable/ic_done_white_24dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/icon_front"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/icon_profile"
                android:layout_width="@dimen/icon_width_height"
                android:layout_height="@dimen/icon_width_height" />

            <TextView
                android:id="@+id/icon_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:textColor="@android:color/white"
                android:textSize="@dimen/icon_text" />
        </RelativeLayout>

    </RelativeLayout>

    <TextView
        android:id="@+id/timestamp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:textColor="@color/timestamp"
        android:textSize="@dimen/timestamp"
        android:textStyle="bold" />

    <ImageView
        android:id="@+id/icon_star"
        android:layout_width="@dimen/icon_star"
        android:layout_height="@dimen/icon_star"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:tint="@color/icon_tint_normal" />

</RelativeLayout>

We also need two menu files to render the toolbar icons. One is to display the icons when Toolbar is in normal mode. The other is to display the icons when ActionMode is enabled.

21. Under res β‡’ menu folder, create two menu files named menu_main.xml and menu_action_mode.xml.

menu_main.xml

menu_action_mode.xml

One more important class you will have to take care is the adapter class. The functionality of the RecyclerView completely depends on how efficiently you are managing the adapter class.

22. Under adapter package, create a class named MessagesAdapter.java and paste the below code. This class is very important, take your own time to explore and understand the code. All the magic happens in onBindViewHolder() method.

> applyReadStatus() method make the text bold depending on read status. If the message is unread, it will be kept in bold.

> applyImportant() – Displays the star icon in yellow color if the message is marked as important.

> applyIconAnimation() – Method performs the flip animation on thumbnail icon.

> applyProfilePicture() – Displays the thumbnail icon profile picture / background in circular fashion.

package info.androidhive.gmail.adapter;

import android.content.Context;
import android.graphics.Typeface;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;

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

import info.androidhive.gmail.R;
import info.androidhive.gmail.helper.CircleTransform;
import info.androidhive.gmail.helper.FlipAnimator;
import info.androidhive.gmail.model.Message;

public class MessagesAdapter extends RecyclerView.Adapter<MessagesAdapter.MyViewHolder> {
    private Context mContext;
    private List<Message> messages;
    private MessageAdapterListener listener;
    private SparseBooleanArray selectedItems;

    // array used to perform multiple animation at once
    private SparseBooleanArray animationItemsIndex;
    private boolean reverseAllAnimations = false;

    // index is used to animate only the selected row
    // dirty fix, find a better solution
    private static int currentSelectedIndex = -1;

    public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener {
        public TextView from, subject, message, iconText, timestamp;
        public ImageView iconImp, imgProfile;
        public LinearLayout messageContainer;
        public RelativeLayout iconContainer, iconBack, iconFront;

        public MyViewHolder(View view) {
            super(view);
            from = (TextView) view.findViewById(R.id.from);
            subject = (TextView) view.findViewById(R.id.txt_primary);
            message = (TextView) view.findViewById(R.id.txt_secondary);
            iconText = (TextView) view.findViewById(R.id.icon_text);
            timestamp = (TextView) view.findViewById(R.id.timestamp);
            iconBack = (RelativeLayout) view.findViewById(R.id.icon_back);
            iconFront = (RelativeLayout) view.findViewById(R.id.icon_front);
            iconImp = (ImageView) view.findViewById(R.id.icon_star);
            imgProfile = (ImageView) view.findViewById(R.id.icon_profile);
            messageContainer = (LinearLayout) view.findViewById(R.id.message_container);
            iconContainer = (RelativeLayout) view.findViewById(R.id.icon_container);
            view.setOnLongClickListener(this);
        }

        @Override
        public boolean onLongClick(View view) {
            listener.onRowLongClicked(getAdapterPosition());
            view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
            return true;
        }
    }


    public MessagesAdapter(Context mContext, List<Message> messages, MessageAdapterListener listener) {
        this.mContext = mContext;
        this.messages = messages;
        this.listener = listener;
        selectedItems = new SparseBooleanArray();
        animationItemsIndex = new SparseBooleanArray();
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.message_list_row, parent, false);

        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        Message message = messages.get(position);

        // displaying text view data
        holder.from.setText(message.getFrom());
        holder.subject.setText(message.getSubject());
        holder.message.setText(message.getMessage());
        holder.timestamp.setText(message.getTimestamp());

        // displaying the first letter of From in icon text
        holder.iconText.setText(message.getFrom().substring(0, 1));

        // change the row state to activated
        holder.itemView.setActivated(selectedItems.get(position, false));

        // change the font style depending on message read status
        applyReadStatus(holder, message);

        // handle message star
        applyImportant(holder, message);

        // handle icon animation
        applyIconAnimation(holder, position);

        // display profile image
        applyProfilePicture(holder, message);

        // apply click events
        applyClickEvents(holder, position);
    }

    private void applyClickEvents(MyViewHolder holder, final int position) {
        holder.iconContainer.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onIconClicked(position);
            }
        });

        holder.iconImp.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onIconImportantClicked(position);
            }
        });

        holder.messageContainer.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onMessageRowClicked(position);
            }
        });

        holder.messageContainer.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                listener.onRowLongClicked(position);
                view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
                return true;
            }
        });
    }

    private void applyProfilePicture(MyViewHolder holder, Message message) {
        if (!TextUtils.isEmpty(message.getPicture())) {
            Glide.with(mContext).load(message.getPicture())
                    .thumbnail(0.5f)
                    .crossFade()
                    .transform(new CircleTransform(mContext))
                    .diskCacheStrategy(DiskCacheStrategy.ALL)
                    .into(holder.imgProfile);
            holder.imgProfile.setColorFilter(null);
            holder.iconText.setVisibility(View.GONE);
        } else {
            holder.imgProfile.setImageResource(R.drawable.bg_circle);
            holder.imgProfile.setColorFilter(message.getColor());
            holder.iconText.setVisibility(View.VISIBLE);
        }
    }

    private void applyIconAnimation(MyViewHolder holder, int position) {
        if (selectedItems.get(position, false)) {
            holder.iconFront.setVisibility(View.GONE);
            resetIconYAxis(holder.iconBack);
            holder.iconBack.setVisibility(View.VISIBLE);
            holder.iconBack.setAlpha(1);
            if (currentSelectedIndex == position) {
                FlipAnimator.flipView(mContext, holder.iconBack, holder.iconFront, true);
                resetCurrentIndex();
            }
        } else {
            holder.iconBack.setVisibility(View.GONE);
            resetIconYAxis(holder.iconFront);
            holder.iconFront.setVisibility(View.VISIBLE);
            holder.iconFront.setAlpha(1);
            if ((reverseAllAnimations && animationItemsIndex.get(position, false)) || currentSelectedIndex == position) {
                FlipAnimator.flipView(mContext, holder.iconBack, holder.iconFront, false);
                resetCurrentIndex();
            }
        }
    }


    // As the views will be reused, sometimes the icon appears as
    // flipped because older view is reused. Reset the Y-axis to 0
    private void resetIconYAxis(View view) {
        if (view.getRotationY() != 0) {
            view.setRotationY(0);
        }
    }

    public void resetAnimationIndex() {
        reverseAllAnimations = false;
        animationItemsIndex.clear();
    }

    @Override
    public long getItemId(int position) {
        return messages.get(position).getId();
    }

    private void applyImportant(MyViewHolder holder, Message message) {
        if (message.isImportant()) {
            holder.iconImp.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_star_black_24dp));
            holder.iconImp.setColorFilter(ContextCompat.getColor(mContext, R.color.icon_tint_selected));
        } else {
            holder.iconImp.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_star_border_black_24dp));
            holder.iconImp.setColorFilter(ContextCompat.getColor(mContext, R.color.icon_tint_normal));
        }
    }

    private void applyReadStatus(MyViewHolder holder, Message message) {
        if (message.isRead()) {
            holder.from.setTypeface(null, Typeface.NORMAL);
            holder.subject.setTypeface(null, Typeface.NORMAL);
            holder.from.setTextColor(ContextCompat.getColor(mContext, R.color.subject));
            holder.subject.setTextColor(ContextCompat.getColor(mContext, R.color.message));
        } else {
            holder.from.setTypeface(null, Typeface.BOLD);
            holder.subject.setTypeface(null, Typeface.BOLD);
            holder.from.setTextColor(ContextCompat.getColor(mContext, R.color.from));
            holder.subject.setTextColor(ContextCompat.getColor(mContext, R.color.subject));
        }
    }

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

    public void toggleSelection(int pos) {
        currentSelectedIndex = pos;
        if (selectedItems.get(pos, false)) {
            selectedItems.delete(pos);
            animationItemsIndex.delete(pos);
        } else {
            selectedItems.put(pos, true);
            animationItemsIndex.put(pos, true);
        }
        notifyItemChanged(pos);
    }

    public void clearSelections() {
        reverseAllAnimations = true;
        selectedItems.clear();
        notifyDataSetChanged();
    }

    public int getSelectedItemCount() {
        return selectedItems.size();
    }

    public List<Integer> getSelectedItems() {
        List<Integer> items =
                new ArrayList<>(selectedItems.size());
        for (int i = 0; i < selectedItems.size(); i++) {
            items.add(selectedItems.keyAt(i));
        }
        return items;
    }

    public void removeData(int position) {
        messages.remove(position);
        resetCurrentIndex();
    }

    private void resetCurrentIndex() {
        currentSelectedIndex = -1;
    }

    public interface MessageAdapterListener {
        void onIconClicked(int position);

        void onIconImportantClicked(int position);

        void onMessageRowClicked(int position);

        void onRowLongClicked(int position);
    }
}

23. Finally open your main activity (MainActivity.java) and modify the code as below.

> SwipeRefreshLayout is added to fetch the json messages on refresh.

> getInbox() Method fetches and parses the JSON appending it to array list.

> Adapter instance is created and attached to RecyclerView.

> ActionMode is enabled when the list item is long pressed.

package info.androidhive.gmail.activity;

import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

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

import info.androidhive.gmail.R;
import info.androidhive.gmail.adapter.MessagesAdapter;
import info.androidhive.gmail.helper.DividerItemDecoration;
import info.androidhive.gmail.model.Message;
import info.androidhive.gmail.network.ApiClient;
import info.androidhive.gmail.network.ApiInterface;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener, MessagesAdapter.MessageAdapterListener {
    private List<Message> messages = new ArrayList<>();
    private RecyclerView recyclerView;
    private MessagesAdapter mAdapter;
    private SwipeRefreshLayout swipeRefreshLayout;
    private ActionModeCallback actionModeCallback;
    private ActionMode actionMode;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });

        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
        swipeRefreshLayout.setOnRefreshListener(this);

        mAdapter = new MessagesAdapter(this, messages, this);
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
        recyclerView.setAdapter(mAdapter);

        actionModeCallback = new ActionModeCallback();

        // show loader and fetch messages
        swipeRefreshLayout.post(
                new Runnable() {
                    @Override
                    public void run() {
                        getInbox();
                    }
                }
        );
    }

    /**
     * Fetches mail messages by making HTTP request
     * url: https://api.androidhive.info/json/inbox.json
     */
    private void getInbox() {
        swipeRefreshLayout.setRefreshing(true);

        ApiInterface apiService =
                ApiClient.getClient().create(ApiInterface.class);

        Call<List<Message>> call = apiService.getInbox();
        call.enqueue(new Callback<List<Message>>() {
            @Override
            public void onResponse(Call<List<Message>> call, Response<List<Message>> response) {
                // clear the inbox
                messages.clear();

                // add all the messages
                // messages.addAll(response.body());

                // TODO - avoid looping
                // the loop was performed to add colors to each message
                for (Message message : response.body()) {
                    // generate a random color
                    message.setColor(getRandomMaterialColor("400"));
                    messages.add(message);
                }

                mAdapter.notifyDataSetChanged();
                swipeRefreshLayout.setRefreshing(false);
            }

            @Override
            public void onFailure(Call<List<Message>> call, Throwable t) {
                Toast.makeText(getApplicationContext(), "Unable to fetch json: " + t.getMessage(), Toast.LENGTH_LONG).show();
                swipeRefreshLayout.setRefreshing(false);
            }
        });
    }

    /**
     * chooses a random color from array.xml
     */
    private int getRandomMaterialColor(String typeColor) {
        int returnColor = Color.GRAY;
        int arrayId = getResources().getIdentifier("mdcolor_" + typeColor, "array", getPackageName());

        if (arrayId != 0) {
            TypedArray colors = getResources().obtainTypedArray(arrayId);
            int index = (int) (Math.random() * colors.length());
            returnColor = colors.getColor(index, Color.GRAY);
            colors.recycle();
        }
        return returnColor;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_search) {
            Toast.makeText(getApplicationContext(), "Search...", Toast.LENGTH_SHORT).show();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onRefresh() {
        // swipe refresh is performed, fetch the messages again
        getInbox();
    }

    @Override
    public void onIconClicked(int position) {
        if (actionMode == null) {
            actionMode = startSupportActionMode(actionModeCallback);
        }

        toggleSelection(position);
    }

    @Override
    public void onIconImportantClicked(int position) {
        // Star icon is clicked,
        // mark the message as important
        Message message = messages.get(position);
        message.setImportant(!message.isImportant());
        messages.set(position, message);
        mAdapter.notifyDataSetChanged();
    }

    @Override
    public void onMessageRowClicked(int position) {
        // verify whether action mode is enabled or not
        // if enabled, change the row state to activated
        if (mAdapter.getSelectedItemCount() > 0) {
            enableActionMode(position);
        } else {
            // read the message which removes bold from the row
            Message message = messages.get(position);
            message.setRead(true);
            messages.set(position, message);
            mAdapter.notifyDataSetChanged();

            Toast.makeText(getApplicationContext(), "Read: " + message.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onRowLongClicked(int position) {
        // long press is performed, enable action mode
        enableActionMode(position);
    }

    private void enableActionMode(int position) {
        if (actionMode == null) {
            actionMode = startSupportActionMode(actionModeCallback);
        }
        toggleSelection(position);
    }

    private void toggleSelection(int position) {
        mAdapter.toggleSelection(position);
        int count = mAdapter.getSelectedItemCount();

        if (count == 0) {
            actionMode.finish();
        } else {
            actionMode.setTitle(String.valueOf(count));
            actionMode.invalidate();
        }
    }


    private class ActionModeCallback implements ActionMode.Callback {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            mode.getMenuInflater().inflate(R.menu.menu_action_mode, menu);

            // disable swipe refresh if action mode is enabled
            swipeRefreshLayout.setEnabled(false);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {
                case R.id.action_delete:
                    // delete all the selected messages
                    deleteMessages();
                    mode.finish();
                    return true;

                default:
                    return false;
            }
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mAdapter.clearSelections();
            swipeRefreshLayout.setEnabled(true);
            actionMode = null;
            recyclerView.post(new Runnable() {
                @Override
                public void run() {
                    mAdapter.resetAnimationIndex();
                    // mAdapter.notifyDataSetChanged();
                }
            });
        }
    }

    // deleting the messages from recycler view
    private void deleteMessages() {
        mAdapter.resetAnimationIndex();
        List<Integer> selectedItemPositions =
                mAdapter.getSelectedItems();
        for (int i = selectedItemPositions.size() - 1; i >= 0; i--) {
            mAdapter.removeData(selectedItemPositions.get(i));
        }
        mAdapter.notifyDataSetChanged();
    }
}

Run the project and see the output in action. Make sure that your device is having good internet connection. If you have any queries, feel free to ask them in the comment section below.

Happy Coding πŸ˜€

android-gmail-like-inbox-recycler-view-tutorial

Whats Next?

This article covers everything but one thing is missing i.e adding Swipe to delete and undo functionalities. But don’t worry, the next article explains adding Swipe Delete and Undo to RecyclerView.

recycler-view-swipe-delete-undo-example
Subscribe
Notify of
guest
233 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
monish monish
monish monish
3 years ago

hey Ravi many thanks for this tutorial after downloading your code it seems that the source was different from this tutorial .

Ravi Tamada
3 years ago
Reply to  monish monish

Yup! I forgot to update the code. Thanks for letting me know. Updating right away.

fight live online stream 2017
Reply to  monish monish

This article aims to improvise your knowledge on RecyclerView by taking an example of Gmail style inbox that contains the complex list design with interactive animations.

Ibrahim Samad
Ibrahim Samad
3 years ago

Ravi, please do you have a tutorial showing how to do one time data initialization from a remote source( mysql or any) before the app starts. I’m currently working on something like that. I do the network operation in the onCreate() of SQLiteOpenHelper class since I want it to run only once (during the app installation). I used volley. The problem I however have is that after the app finishes installing and saving the data to sqlite, it doesn’t fetch the data from sqlite before starting therefore presenting blank screen. When I close the app and start again then it fetches them from sqlite. I want that for the first time installation, after the network operation, it should make them available to the UI before starting the UI. Any tips will be appreciated.

Ravi Tamada
3 years ago
Reply to  Ibrahim Samad

May be you can add a splash screen and download the data before showing the UI to user. When user launches the app on second time and there is data in SQLite, you can ignore showing the splash screen.
http://www.androidhive.info/2013/07/how-to-implement-android-splash-screen-2/

Ibrahim Samad
Ibrahim Samad
3 years ago
Reply to  Ravi Tamada

Thanks very much. But that will mean each time the user starts the app, I will have to check SQLite to see if there is data? I was thinking there could be a way to make sure that UI waits for the http request to finish before it starts.

Ravi Tamada
3 years ago
Reply to  Ibrahim Samad

Yes, you have to check for data in SQLite and start appropriate activity. UI won’t wait for the network calls response. If you want to show the data after http response received, have the UI in a layout and hide it initially. Show a loader while the data is downloading, once received show the actual UI.

Ibrahim Samad
Ibrahim Samad
3 years ago
Reply to  Ravi Tamada

Wow! I appreciate it thanks. I think I will go for the splash screen method for now. So In that case can I use volley. It is quite easier for me when processing jsonObjects. But I fear since it is asynchronous, the UI might start again b4 it finishes the network call. Just thinking.

Ravi Tamada
3 years ago
Reply to  Ibrahim Samad

Call startActivity() on response received method.

Ibrahim Samad
Ibrahim Samad
3 years ago
Reply to  Ravi Tamada

It is now working as expected thanks. I used Okhttp though.

Ravi Tamada
3 years ago
Reply to  Ibrahim Samad

Great.

Ziigic
Ziigic
3 years ago

Hello Ravi,
Great work with the tutorial.

Ravi Tamada
3 years ago
Reply to  Ziigic

Thanks Ziigic πŸ™‚

newDevL
newDevL
3 years ago

Hello buddy. Good tutorial. But how to implement endless loading (endless adapter). thanks

Ravi Tamada
3 years ago
Reply to  newDevL

You can detect the scroll bottom and send a request get the next page. You can follow the below article to do the same with listview.
http://www.androidhive.info/2012/03/android-listview-with-load-more-button/

sebastian cipolat
sebastian cipolat
3 years ago

Hi, great tutorial.
How do you made the video? do you use any tool? thnks

Ravi Tamada
3 years ago

I use Camtasia Studio.

arul raj
arul raj
3 years ago

Hi,

Thanks for an informative tutorial which helped me to learn about ActionMode and HapticFeedback.

I have an question, how can I implement swipe in an item. In Gmail app swiping a message to right brings delete option.

Could you please help me.

Arvind Kumar
Arvind Kumar
3 years ago

Hi Ravi ,This tutorial is Awesome. but how we can implement swipe delete item like GMAIL

Ravi Tamada
3 years ago
Reply to  Arvind Kumar

You can do that by adding swipe listener on recycler view. Try the first answer
http://stackoverflow.com/questions/32227482/recyclerview-swipe-with-a-view-below-it

Arvind Kumar
Arvind Kumar
3 years ago
Reply to  Ravi Tamada

Thanks Ravi

Ravi Tamada
3 years ago
Reply to  Arvind Kumar

πŸ™‚

Sahil Garg
Sahil Garg
3 years ago

Hi Ravi,

I have been following your tutorials since first day of the android code and they are very helpful. I just want one favor from you. Could you please make an tutorial on making a download manager like UC browser have or a kind of that one. Please it will be very helpful.

Thanks for all your tutorials.

Ravi Tamada
3 years ago
Reply to  Sahil Garg

Hi Sahil

These download managers are quite complex. I can’t cover this in a single article. Search if there are any libraries available.

Sahil Garg
Sahil Garg
3 years ago
Reply to  Ravi Tamada

Okay sure, Thank you for replying… Keep making these types of useful tutorials…

Ravi Tamada
3 years ago
Reply to  Sahil Garg

Yup!

Jojo
Jojo
3 years ago

Hi Ravi, I always find your tutorial useful, Thanks you for the awesome tutorials you make.
Could you make a tutorial about
1. RXJava ( or Alternative for async task)
2. Reson of using Retrofit instead of OKHTTP.

I hope this will help other developers to be clear about this arguments.
I stuggle a lot with async task.

thanks you again for this tutorial.

Sudesh Kumar
Sudesh Kumar
3 years ago

My live wallpaper set on Screen but it is not run can u give me any solution i used it with service please help me to solve this

Nasser Sawadi
Nasser Sawadi
3 years ago

Hi Ravi Tamada,
When I change the language of the device to the arabic elements overlap on each other, is there a solution to this problem, such as the app gmail …

Ravi Tamada
3 years ago
Reply to  Nasser Sawadi

Can u post the screenshot of it.

Nasser Sawadi
Nasser Sawadi
3 years ago
Reply to  Ravi Tamada

Thanks for the quick replycomment image

Ravi Tamada
3 years ago
Reply to  Nasser Sawadi

Got it. You need to create same layout in rt folder and give padding-right to messages container.

Nasser Sawadi
Nasser Sawadi
3 years ago
Reply to  Ravi Tamada

Thanks I will try

Jeffrey Nyauke
Jeffrey Nyauke
3 years ago
Reply to  Nasser Sawadi

Simply add an android:layout_alignParentStart=”true” to every instance of android:layout_alignParentLeft=”true”

Nasser Sawadi
Nasser Sawadi
3 years ago
Reply to  Jeffrey Nyauke

Thank you for helping me

divya prajapati
divya prajapati
3 years ago
Reply to  Ravi Tamada

comment image
It is not perfact look plz help me
last sem project plzzzzz help me

divya prajapati
divya prajapati
3 years ago

comment image
plz help me

shashi patil
shashi patil
3 years ago

means?

Olayinka Peter Oluwafemi
Olayinka Peter Oluwafemi
3 years ago

Awesome tutorial, Ravi.
Thanks.

Ravi Tamada
3 years ago

πŸ˜€

Ravi Tamada
3 years ago

I am glad you liked it πŸ™‚

Daniel
3 years ago

Awesome Bro….thanks a lot!!! πŸ˜€

Nihat YΔ±ldΔ±z
Nihat YΔ±ldΔ±z
3 years ago

Thanks
epic source πŸ˜€

Ravi Tamada
3 years ago

πŸ˜€

sharon
sharon
3 years ago

sir pls make firebase realtime chat app

deaspo
deaspo
3 years ago
Reply to  sharon

Hey there, I have made one, in the process of completing it but the chat functionality is already there. Here is the Gitpage https://github.com/deaspo/developers-chat . Holla if you want to be one of the testers.

Okock Polycarp Omondi
Okock Polycarp Omondi
3 years ago
Reply to  sharon

Hi Sharon, am in the process of making realtime chat app using FCM, the chat already working just remaining the polishing. You can check the project on my Github page – https://github.com/deaspo/developers-chat

Okock Polycarp Omondi
Okock Polycarp Omondi
3 years ago
Reply to  sharon

Hi Sharo, I have made such an app, feel free to checkout my Giithub Repo @deaspo

Okock Polycarp Omondi
Okock Polycarp Omondi
3 years ago

Thanks Ravi, your projects have helped me so far in building my own real time chat application using FCM, checkout the Github project – https://github.com/deaspo/developers-chat

License Sheriff
License Sheriff
3 years ago

I see licenses violation DividerItemDecoration code is from AOSP under Apache License, Version 2.0

Prasanth S
Prasanth S
3 years ago

Suprb Real time example Thanks Ravi.

Ravi Tamada
3 years ago
Reply to  Prasanth S

You are welcome πŸ™‚

Ankit Agarwal
Ankit Agarwal
3 years ago
Reply to  Ravi Tamada

Hi Ravi,

I know I am writing out of context here but I am trying to create an app which will sign in my google account automatically after which I will use my googlesheet as a user credential database to verify the userid and password for different users.

I could find multiple samples to use GoogleSignInOptions and GoogleApiClient but none of them could be insight of how to I pass username and password explicitly to authenticate instead of using com.google.android.gms.common.SignInButton

Can you help?

radamante falcao
radamante falcao
3 years ago

Hi Ravi Tamada i am from CAMEROON and i always love learn your tutorials and find it useful
but when do you write a tutorial about:
HOW TO CONNECT ANDROID WITH PHP AND MONGODB?

I think you never write about MONGODB with android WHY?

Lord
Lord
3 years ago

why actionmode not show in toolbar,it show in actionbar. So when I longclick, it show both actionbar and toolbar?

Ravi Tamada
3 years ago
Reply to  Lord

Verify your styles.xml code with the article’s code.

dblackmask
dblackmask
3 years ago

Would you create an infinite scroll recyclerview tutorial,,
Thanks

Prasoon
Prasoon
3 years ago

how can i create a inbox which has a pre provided email account! like when i will open the app i will be seeing the mails of the account which is inbuilt in the app!

Ravi
Ravi
3 years ago
Reply to  Prasoon

Enthoooo???? Enganeee??????

ImRan ALi ShAh
ImRan ALi ShAh
3 years ago

Hi Ravi,
Thanks for such a nice tutorial.I encountered a problem the code always show message
” Unable to fetch json:null”.Thanks in advance

Ravi Tamada
3 years ago
Reply to  ImRan ALi ShAh

Do you have internet connected?

Sandy
Sandy
3 years ago
Reply to  Ravi Tamada

Load more in expandable listview ….any idea

ImRan ALi ShAh
ImRan ALi ShAh
3 years ago
Reply to  Ravi Tamada

yes even i install your apk file the problem remain same

ImRan ALi ShAh
ImRan ALi ShAh
3 years ago
Reply to  Ravi Tamada

Thanks Ravi I uninstall the application and install again now its worked .

Rajasekher Arekuti
Rajasekher Arekuti
3 years ago

Hi, Thanks for tutorial. I have a doubt. if recyclerview is in fragment then how to interact with activity toolbar. thanks in advance

Rajasekher Arekuti
Rajasekher Arekuti
3 years ago
V Abhishek
V Abhishek
3 years ago

Caused by: android.view.InflateException: Binary XML file line #24: Binary XML file line #2: Error inflating class android.support.constraint.ConstraintLayout
at android.view.LayoutInflater.inflate(LayoutInflater.java:539)
at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
at android.view.LayoutInflater.inflate(LayoutInflater.java:374)
at android.support.v7.app.AppCompatDelegateImplV9.setContentView(AppCompatDelegateImplV9.java:288)
at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:143)
at com.example.innovista.recyclerviewgmailinbox.MainActivity.onCreate(MainActivity.java:41)
at

getting this error at setContentView(R.layout.activity_main)

can you please help me out………..
thanks in advance

Osama Inam
Osama Inam
3 years ago

Response{protocol=http/1.1, code=404, message=Not Found, url=http://api.androidhive.info/json/indox.json}

Ravi Tamada
3 years ago
Reply to  Osama Inam

You are looking for indox.json not inbox.json

http://api.androidhive.info/json/inbox.json

Osama Inam
Osama Inam
3 years ago
Reply to  Ravi Tamada

Thanks

Love Songs
Love Songs
3 years ago

Hi, Ravi, thanks a lot for very useful tutorials. How can I go to next activity by clicking the rows.

Ravi Tamada
3 years ago
Reply to  Love Songs

You need to attach click event listener and launch the activity. Check the below article
http://www.androidhive.info/2016/01/android-working-with-recycler-view/

LIc. Juan Armando Trujillo
LIc. Juan Armando Trujillo
3 years ago

Hi, Ravi, Thanks.

Ravi Tamada
3 years ago

You are welcome πŸ™‚

Abhishek Abhi
Abhishek Abhi
3 years ago

Hi ravi i want to know how did you created this file “http://api.androidhive.info/json/inbox.json” can you help me

Ravi Tamada
3 years ago
Reply to  Abhishek Abhi

Basically it has to be generated from database. But I manually created this file using http://www.json-generator.com/

Adel Jaffan
Adel Jaffan
3 years ago

from Syria …. thank you sir πŸ™‚

Ravi Tamada
3 years ago
Reply to  Adel Jaffan

You are welcome πŸ™‚

Ravinder singh
Ravinder singh
3 years ago

Sir The download code can’t be run the error is Gradle:Casue:unsupproted version 52.0 how to solve this problem.

Nandha Krishnan
Nandha Krishnan
3 years ago

hai Ravi,i am import ur project but Showing Error:SSL peer shut down incorrectly like this.how to solve it.help me

ashish kudale
ashish kudale
3 years ago

which blogging tool you use. and which plugins you use to highlight code.

Kevin Cando
Kevin Cando
3 years ago

Men, I love your posts! they are awesome!!, How can I handle left and right drag of messages? If I want to add that in this project what I should do??

Rony smile
Rony smile
3 years ago

Sir can you please upload tutorials on Reactive Programming in Android/Java(Rx-Java/Android),Inversion of Control,Dagger2, Geofencing because I got a new project where I need to include these new concepts.

Ablaikhan Bekdullayev
Ablaikhan Bekdullayev
3 years ago

Thank you, Master Ravi! Hi from Kazakhstan

Ravi Tamada
3 years ago

You are welcome.

shashi patil
shashi patil
3 years ago

hi ravi, it would be great if u write an example on POST request with retrofit

WinGoku
3 years ago

Hey Ravi, Kindly make a Retrofit caching & OkHttp interceptors tutorial.

bhavin makwana
bhavin makwana
3 years ago

how to json file generated from a database using any server side language ???
help me if u have any example …

Asbath Sama
Asbath Sama
3 years ago

Thanks Ravi. Great tutorial.
I am using Android 2.1. Can I follow this tutorial because of the use of Constraint layout

Wajid Khan
Wajid Khan
3 years ago

Very Nice

imo
imo
3 years ago

any plans on kotlin tutorials

Ravi Tamada
3 years ago
Reply to  imo

Preparing already.

dayakar vbe
dayakar vbe
3 years ago

Hiii Ravi sir can you upload a tutorial for Material SearchView like whatsapp?

Ruslan Osmanov
Ruslan Osmanov
3 years ago

Instead of flip animation it performs some sort of fade animation:
comment image

Android 7.0 (API 24). By the way, the stuff works correctly in the original Gmail app. Any ideas?

Thanks.

Rohit Bansal
Rohit Bansal
3 years ago

Awesome

Abhishek EH
3 years ago

Liked the post. Before going through this tutorial I thought Creating Inbox like Google is complex. Thanks for the nice tutorial.

Nicholas Ocholla
3 years ago

Great tutorial Ravi …. Thanks!

Ravi Tamada
3 years ago

You are welcome πŸ™‚

ehab
ehab
3 years ago

when changing language to Arabic flipView don’t work fine

Marcos Garcia
Marcos Garcia
3 years ago

Thanks Ravi. I’d like this tutorial. It’s fantastic!!!

Ravi Tamada
3 years ago
Reply to  Marcos Garcia

You are welcome πŸ™‚

Thulani Samuel Nkosi
Thulani Samuel Nkosi
3 years ago

Thanks Ravi. How would I enable offline cache.The app works only when I have internet connection. Great Tutorial.

233
0
Would love your thoughts, please comment.x
()
x