ZOFTINO.COM android and web dev tutorials

Android Notification & Data Messages From App Server Using Firebase Cloud Messaging

If your Android app relies on app servers for data and functionality, instead of android app contacting app server at a specified regular intervals, server can notify events or data changes to android using Firebase cloud messaging (FCM).

This way of app servers notifying client or android apps has advantages compared to client contacting app server at a regular intervals. It reduces the use of android device resources including computing power and consumption of network bandwidth as communication between server and client happens only when there is a change in data or an occurrence of an event at server.

Table of Contents

Firebase Cloud Messaging Message Types

Firebase Cloud Messaging supports two types of messages, notification message and data message. The difference between them is that with data message you can send your own data elements in the message where as with notification message you have to use predefined elements.

As names suggest, notification message type is used to send notifications which will be displayed as notifications in the notification bar. FCM automatically handles notification messages and your app can also process it and customize it. Data message type is used to send data to client. Your app has to process it and take further action. There is restriction of 4kb on the size of message that can be sent to client.

Notification message in JSON format

{"message":{"topic":"deals","notification":{"body":"View latest deals from top brands.","title":"Latest Deals"}}}

Data message in JSON format

{"message":{"topic":"deals","data":{"storeNAME":"Nordstorm","deal":"Get upto 50% off on Shoes","dealDesc":"Get upto 50% off on branded shoes.","expiry":"20180110","code":"NORDSH"}}}

Notification and data message in JSON format

{"message":{"topic":"deals","data":{"storeNAME":"Nordstorm","deal":"Get upto 50% off on Shoes","dealDesc":"Get upto 50% off on branded shoes.","expiry":"20180110","code":"NORDSH"},"notification":{"body":"View latest deals from top brands.","title":"Latest Deals"}}}

Note that in the data message, only name and value pairs are allowed under data element, meaning data element can’t have hierarchical data or JSON array.

App server can send a generic message to multiple clients using Firebase topic. Client apps which listen to the topic will get the message sent by the app server. In this post, the example shows how to send messages to a topic from server app and how client apps can register to a topic and handle the messages.

Using FCM, app server can send messages to a specific client or device using firebase generated registration key. To know how to send user or device specific message, please read sending device specific message from app server to FCM server and handling the message on android.

Handling FCM Messages in Android

Notification messages are handled automatically when receiving android app is in background and notification is displayed. On taping the notification, app’s launcher activity is started.

If notification needs to be displayed when receiving app is in foreground, the app needs to provide FirebaseMessagingService service implementing its callback onMessageReceived to process notification messages.

To handle the data messages in foreground and background, app needs to provide FirebaseMessagingService service implementing its callback onMessageReceived. Data can be retrieved from RemoteMessage object passed to onMessageReceived method.

If message contains both data and notification and app is in foreground, onMessageReceived callback is called. If message contains both data and notification and app is in background, notification is displayed in notification bar and data is passed as extras in the intent to launcher activity.

Android Project Set up - Client of FCM

To make your android app as client of firebase cloud messaging server to handle and process messages, you need to follow below steps.

  • Login to Firebase console.
  • Add project to Firebase by clicking add project link on main screen of Firebase console.
  • To install Firebase SDK, go to project overview section in Firebase console, click add-Firebase to android project and follow steps to generate google-services.json. Download the file and save it in your project under app folder.
  • Add below entry to bottom of app level gradle build file.
    plugin: 'com.google.gms.google-services'
  • Add below entry to project level gradle build file in dependencies element.
    classpath 'com.google.gms:google-services:3.1.0'
  • Add Firebase cloud messaging library to app level build file.
    implementation 'com.google.firebase:firebase-messaging:11.6.2'
  • Enable Firebase cloud messaging API in Google developers console.
  • Create android service extending FirebaseMessagingService and implementing onMessageReceived, see example below to know how to handle and process messages.
  • Add the service to mainfest file.
     <service
        android:name="zoftino.com.firebase.fcm.DealsMessagingService">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT"/>
        </intent-filter>
    </service> 

Sending Messages from App Server to Firebase Cloud Messaging Server

Firebase cloud messaging server delivers to client app the message sent from the app server. App server can send message to FCM server using various protocols. Below example app I created shows how to send messages to FCM server from app server using HTTP v1 protocol.

Authentication and authorization of the app server to use firebase service can be done using short-lived access token. In order for your app server code to get the access token, it needs private key associated with a service account which is either owner or editor.

To get private key in Firebase console, go to settings, then service accounts and click Generate New Private Key. The Json file contain key gets downloaded. You need to store it securely.

firebase service account private key

To get access token using the private key in your app server code, you need to use Google API Client library. Below are the maven and gradle dependencies which you need to add to your app server project.

<dependency>
     <groupId>com.google.api-client</groupId>
     <artifactId>google-api-client</artifactId>
     <version>1.23.0</version>
   </dependency>
implement 'com.google.api-client:google-api-client:1.23.0'

Getting access token

GoogleCredential can be used to get access token using private key downloaded from firebase console.

private static String SCOPE = 
      "https://www.googleapis.com/auth/firebase.messaging";

GoogleCredential googleCredential = GoogleCredential
.fromStream(new FileInputStream("firebase-admin-key.json"))
.createScoped(Arrays.asList(SCOPE));
googleCredential.refreshToken();
String token = googleCredential.getAccessToken();
return token;

Adding access token to http request header

You can use any http library to send message request to FCM server. Http request header should contain access token.

httpURLConnection.setRequestProperty("Authorization", "Bearer " + getAccessToken());
httpURLConnection.setRequestProperty("Content-Type", "application/json; UTF-8"); 

Creating and sending JSON message

I used gson library to create JSON message as shown below.

	JsonElement dealsJson = getDealInJsonFormat();

	JsonObject jsonObj = new JsonObject();
	jsonObj.addProperty("topic", "deals");
	jsonObj.add("data", dealsJson);

	JsonObject msgObj = new JsonObject();
	msgObj.add("message", jsonObj);

	
	httpConn.setRequestMethod("POST");
	
	DataOutputStream wr = new DataOutputStream(httpConn.getOutputStream()); 
	wr.writeBytes(msgObj.toString());
	wr.flush();
	wr.close();

Android Firebase Cloud Messaging Example

I’ll show the use of Firebase cloud messaging using an example app. The app displays coupons in recycler view retrieved from SQLite database using ROOM. When latest coupons are available, app server sends notification and data messages to FCM server. The android app receives and processes the messages, stores latest coupons data in SQLite database on the device and displays notifications. Since LiveData is used, as data changes in SQLite database, change will be reflected in UI.

Notification message

firebase cloud messaging notification message example

Data message

firebase cloud messaging data message example

Sending FCM Message from App Server Example

 import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;


public class MessagesClientFCMServer {
	
	private static final Logger log = Logger.getLogger(MessagesClientFCMServer.class
			.getName());

	private static String SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
	private static String FCM_DEALS_ENDPOINT 
	= "https://fcm.googleapis.com/v1/projects/your-project/messages:send";

	public static void main(String args[]) {
		MessagesClientFCMServer fcmClient = new MessagesClientFCMServer();
		fcmClient.sendNotification();
		fcmClient.sendData();
	}
	
	private void sendNotification(){
		String notificationTitle = "Latest Deals";
		String notificationBody = "View latest deals from top brands.";
		
		sendMessageToFcm(getFcmMessageJSONNotification(notificationTitle, notificationBody));
	}
	private void sendData(){
		sendMessageToFcm(getFcmMessageJSONData());
	}
	private void sendDataNotification(){
		String notificationTitle = "Latest Deals";
		String notificationBody = "View latest deals from top brands.";
		sendMessageToFcm(getFcmMessageJSONDataAndNotification(notificationTitle, notificationBody));
	}

	//Using HttpURLConnection it send http post request containing data to FCM server
	private void sendMessageToFcm(String postData) {
		try {

			HttpURLConnection httpConn = getConnection();
			httpConn.setDoOutput(true);
			httpConn.setUseCaches(false);
			httpConn.setRequestMethod("POST");
			
			DataOutputStream wr = new DataOutputStream(httpConn.getOutputStream()); 
			wr.writeBytes(postData);
			wr.flush();
			wr.close();

			BufferedReader in = new BufferedReader(
					new InputStreamReader(httpConn.getInputStream()));
			String inputLine;
			StringBuffer response = new StringBuffer();

			while ((inputLine = in.readLine()) != null) {
				response.append(inputLine);
			}
			in.close();

			log.info(response.toString());			

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static String getAccessToken() throws IOException {
		GoogleCredential googleCredential = GoogleCredential
				.fromStream(new FileInputStream("firebase-admin-key.json"))
				.createScoped(Arrays.asList(SCOPE));
		googleCredential.refreshToken();
		String token = googleCredential.getAccessToken();
		return token;
	}

	//create HttpURLConnection setting Authorization token
	//and Content-Type header
	private HttpURLConnection getConnection() throws Exception {
		URL url = new URL(FCM_DEALS_ENDPOINT);
		HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
		httpURLConnection.setRequestProperty("Authorization", "Bearer " + getAccessToken());
		httpURLConnection.setRequestProperty("Content-Type", "application/json; UTF-8");
		return httpURLConnection;
	}

	private JsonElement getDealInJsonFormat() {
		
		Deal dealList = prepareLatestDealData();
		Gson gson = new Gson();
		Type type = new TypeToken<Deal>(){}.getType();

		JsonElement jsonElement = gson.toJsonTree(dealList, type);
		return jsonElement;
	}

	private String getFcmMessageJSONData() {

		JsonElement dealsJson = getDealInJsonFormat();

		JsonObject jsonObj = new JsonObject();
		jsonObj.addProperty("topic", "deals");
		jsonObj.add("data", dealsJson);

		JsonObject msgObj = new JsonObject();
		msgObj.add("message", jsonObj);

		log.info("json  message "+msgObj.toString());

		return msgObj.toString();
	}
	
	private String getFcmMessageJSONNotification(String title, String msg) {
		JsonObject notifiDetails = new JsonObject();
		notifiDetails.addProperty("body", msg);
		notifiDetails.addProperty("title", title);

		JsonObject jsonObj = new JsonObject();
		jsonObj.addProperty("topic", "deals");
		jsonObj.add("notification", notifiDetails);

		JsonObject msgObj = new JsonObject();
		msgObj.add("message", jsonObj);

		log.info("json  message "+msgObj.toString());

		return msgObj.toString();
	}
	
	private String getFcmMessageJSONDataAndNotification(String title, String msg) {

		JsonElement dealsJson = getDealInJsonFormat();

		JsonObject notifiDetails = new JsonObject();
		notifiDetails.addProperty("body", msg);
		notifiDetails.addProperty("title", title);

		JsonObject jsonObj = new JsonObject();
		jsonObj.addProperty("topic", "deals");
		jsonObj.add("data", dealsJson);
		jsonObj.add("notification", notifiDetails);

		JsonObject msgObj = new JsonObject();
		msgObj.add("message", jsonObj);

		log.info("json  message "+msgObj.toString());

		return msgObj.toString();
	}
	private Deal prepareLatestDealData() {
		List<Deal> dealList = new ArrayList<Deal>();
		Deal deal = new Deal();
		deal.setStoreNAME("Bestbuy");
		deal.setDeal("Get upto 10% off on Laptops");
		deal.setDealDesc("Get upto 10% off on dell, hp and lenovo laptops.");
		deal.setExpiry("20180110");
		deal.setCode("NORDSH");
		dealList.add(deal);

		return deal;
	}
	
}

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="zoftino.com.firebase.fcm">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ShoppingDealsActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name=".DealsMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
        <service
            android:exported="false"
            android:name=".DealsJobService">
            <intent-filter>
                <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
            </intent-filter>
        </service>
    </application>
</manifest>

FirebaseMessagingService

public class DealsMessagingService extends FirebaseMessagingService {

    private static final String TAG = "DealsMessagingService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Log.d(TAG, "Received message " + remoteMessage.getFrom());
        
        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data " + remoteMessage.getData());
            scheduleJob(remoteMessage.getData());
        }
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification "
                    + remoteMessage.getNotification().getBody());
            sendNotification(remoteMessage.getNotification());
        }
    }

    private void scheduleJob(Map<String, String> data) {
        Bundle bundle = new Bundle();
        for (Map.Entry<String, String> entry : data.entrySet()) {
            bundle.putString(entry.getKey(), entry.getValue());
        }

        FirebaseJobDispatcher dispatcher =
                new FirebaseJobDispatcher(new GooglePlayDriver(this));
        Job myJob = dispatcher.newJobBuilder()
                .setService(DealsJobService.class)
                .setTag("deals-job")
                .setExtras(bundle)
                .build();
        dispatcher.schedule(myJob);
    }
    private void sendNotification(RemoteMessage.Notification notification) {
        Intent intent = new Intent(this, ShoppingDealsActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
                PendingIntent.FLAG_ONE_SHOT);

        NotificationCompat.Builder notificationBuilder =
                new NotificationCompat.Builder(this, "fcm-channel")
                        .setSmallIcon(R.drawable.zoftino)
                        .setContentTitle(notification.getTitle())
                        .setContentText(notification.getBody())
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.notify(0, notificationBuilder.build());
    }
}

JobService

Since log running task can’t be run in FirebaseMessagingService, firebase job scheduler is used to process FCM data messages.

public class DealsJobService extends JobService {

    private static final String TAG = "DealsJobService";

    private final Executor executor = Executors.newFixedThreadPool(2);
    private DealsDAO dealsDAO = Repository.getDealsDatabase(this).dealsDAO();

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        Log.d(TAG, "updating ROOM database with latest deals");

        addDealsDataToSQLiteDatabase(jobParameters.getExtras());
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters jobParameters) {
        return false;
    }

    //add data to sqlite database using room
    private void addDealsDataToSQLiteDatabase(Bundle bundle) {

        final Deal dealObj = getDealObjectFromBundle(bundle);
        executor.execute(new Runnable() {
            @Override
            public void run() {
              long rec =  dealsDAO.insertDeal(dealObj);
                Log.d(TAG, "added record to db "+rec);
            }
        });

    }
    private Deal getDealObjectFromBundle(Bundle bundle){
        Deal deal = new Deal();
        deal.setStoreNAME(bundle.getString("storeNAME"));
        deal.setDeal(bundle.getString("deal"));
        deal.setDealDesc(bundle.getString("dealDesc"));
        deal.setExpiry(bundle.getString("expiry"));
        deal.setCode(bundle.getString("code"));

        return deal;
    }
}

Activity

public class ShoppingDealsActivity extends AppCompatActivity {

    private static final String TAG = "ShoppingDealsActivity";
    private RecyclerView dealsRecyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.shopping_deals_layout);

        Toolbar tb = findViewById(R.id.toolbar);
        setSupportActionBar(tb);
        tb.setSubtitle("FCM");

        //on first run of the app, subscribe to FCM deals topic
        //to get latest deals from server as and when they are available
        subscribeToFcmDealsTopic();

        dealsRecyclerView = findViewById(R.id.deals_lst);

        LinearLayoutManager recyclerLayoutManager =
                new LinearLayoutManager(this.getApplicationContext());
        dealsRecyclerView.setLayoutManager(recyclerLayoutManager);

        DividerItemDecoration dividerItemDecoration =
                new DividerItemDecoration(dealsRecyclerView.getContext(),
                        recyclerLayoutManager.getOrientation());
        dealsRecyclerView.addItemDecoration(dividerItemDecoration);

        Repository.getDealsDatabase(this).dealsDAO().getDeals()
                .observe(this, new Observer<List<Deal>>() {
                    @Override
                    public void onChanged(@Nullable List<Deal> dealsList) {
                        if (dealsList == null) {
                            return;
                        }
                        DealsRecyclerViewAdapter recyclerViewAdapter = new
                                DealsRecyclerViewAdapter(dealsList, ShoppingDealsActivity.this);
                        dealsRecyclerView.setAdapter(recyclerViewAdapter);
                    }
        });
    }
    //subscribes to firebase cloud messaging topic one time
    private void subscribeToFcmDealsTopic(){
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        if (!prefs.getBoolean("firstRun", false)) {

            FirebaseMessaging.getInstance().subscribeToTopic("deals");

            SharedPreferences.Editor editor = prefs.edit();
            editor.putBoolean("firstRun", true);
            editor.commit();
        }
    }
}

Activity Layout

<?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"
    tools:context="zoftino.com.firebase.fcm.ShoppingDealsActivity">
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/deals_head"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:text="Shopping Deals"
        android:textAppearance="@style/TextAppearance.AppCompat.Headline"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"  />
    <android.support.v7.widget.RecyclerView
        android:id="@+id/deals_lst"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/deals_head"/>
</android.support.constraint.ConstraintLayout>

RecyclerView Adapter

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

    private List<Deal> dealsList;
    private Context context;

    public DealsRecyclerViewAdapter(List<Deal> list, Context ctx) {
        dealsList = list;
        context = ctx;
    }
    @Override
    public int getItemCount() {
        return dealsList.size();
    }

    @Override
    public DealsRecyclerViewAdapter.ViewHolder
    onCreateViewHolder(ViewGroup parent, int viewType) {

        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.deal_item_layout, parent, false);

        DealsRecyclerViewAdapter.ViewHolder viewHolder =
                new DealsRecyclerViewAdapter.ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(DealsRecyclerViewAdapter.ViewHolder holder, int position) {
        final int itemPos = position;
        final Deal deal = dealsList.get(position);
        holder.store.setText(deal.getStoreNAME());
        holder.deal.setText(deal.getDeal());
        holder.desc.setText(deal.getDealDesc());
        holder.expiry.setText(deal.getExpiry());
        holder.code.setText(deal.getCode());

        holder.shop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d("Deals Recyclerview", "Shop ....");
                Toast.makeText(context,
                        "Visit "+deal.getStoreNAME(),
                        Toast.LENGTH_SHORT).show();
            }
        });

    }
    public class ViewHolder extends RecyclerView.ViewHolder {
        public TextView store;
        public TextView deal;
        public TextView desc;
        public TextView expiry;
        public TextView code;
        public Button shop;

        public ViewHolder(View view) {
            super(view);
            store = (TextView) view.findViewById(R.id.store_name);
            deal = (TextView) view.findViewById(R.id.deal_name);
            desc = (TextView) view.findViewById(R.id.deal_description);
            expiry = (TextView) view.findViewById(R.id.deal_expiry);
            code = (TextView) view.findViewById(R.id.deal_code);
            shop = view.findViewById(R.id.shop_b);
        }
    }
}

RecyclerView Item Layout

<?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="wrap_content"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp"
    tools:context="zoftino.com.firebase.fcm.ShoppingDealsActivity">
    <TextView
        android:id="@+id/store_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Headline"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/deal_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/store_name" />
    <TextView
        android:id="@+id/deal_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/deal_name" />
    <TextView
        android:id="@+id/deal_code"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/deal_expiry"
        app:layout_constraintTop_toBottomOf="@+id/deal_description" />
    <TextView
        android:id="@+id/deal_expiry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        app:layout_constraintLeft_toRightOf="@+id/deal_code"
        app:layout_constraintRight_toLeftOf="@+id/shop_b"
        app:layout_constraintTop_toBottomOf="@+id/deal_description" />
    <Button  android:id="@+id/shop_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        style="@style/Widget.AppCompat.Button.Colored"
        android:minHeight="0dp"
        android:minWidth="0dp"
        android:text="Shop"
        app:layout_constraintLeft_toRightOf="@+id/deal_expiry"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/deal_description"/>
</android.support.constraint.ConstraintLayout>

Room Database

import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase;

@Database(entities = {Deal.class}, version = 1)
public abstract class DealsDatabase extends RoomDatabase {
    public abstract DealsDAO dealsDAO();
}

Room DAO

import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;

import java.util.List;

@Dao
public interface DealsDAO {
    @Query("SELECT * FROM deal")
    public LiveData<List<Deal>> getDeals();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public long insertDeal(Deal deal);

    @Query("DELETE FROM deal WHERE expiry < :expiryIn")
    public void deleteAllCoupons(String expiryIn);
}

Repository

public class Repository {

    private static DealsDatabase dealsDatabase;
    private DealsDAO dealsDAO;
    private static final Object LOCK = new Object();

    public synchronized static DealsDatabase getDealsDatabase(Context context){
        if(dealsDatabase == null) {
            synchronized (LOCK) {
                if (dealsDatabase == null) {
                    dealsDatabase = Room.databaseBuilder(context,
                            DealsDatabase.class, "deals db").build();
                }
            }
        }
        return dealsDatabase;
    }
    public LiveData<List<Deal>> getDeals(Context context) {
        if (dealsDAO == null) {
            dealsDAO = Repository.getDealsDatabase(context).dealsDAO();
        }
        return dealsDAO.getDeals();
    }
}

Data Model

@Entity
public class Deal {
    @PrimaryKey(autoGenerate = true)
    private int id;
    private String storeNAME;
    private String deal;
    private String dealDesc;
    private String expiry;
    private String code;

    public int getId() {
        return id;
    }

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

    public String getStoreNAME() {
        return storeNAME;
    }

    public void setStoreNAME(String storeNAME) {
        this.storeNAME = storeNAME;
    }

    public String getDeal() {
        return deal;
    }

    public void setDeal(String deal) {
        this.deal = deal;
    }

    public String getDealDesc() {
        return dealDesc;
    }

    public void setDealDesc(String dealDesc) {
        this.dealDesc = dealDesc;
    }

    public String getExpiry() {
        return expiry;
    }

    public void setExpiry(String expiry) {
        this.expiry = expiry;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

Gradle Build

dependencies {
    . ..

    implementation 'com.google.firebase:firebase-messaging:11.6.2'
    implementation 'com.firebase:firebase-jobdispatcher:0.8.5'

    implementation 'android.arch.lifecycle:extensions:1.0.0'
    implementation 'android.arch.lifecycle:runtime:1.0.3'
    implementation 'com.google.code.gson:gson:2.8.0'

    implementation 'android.arch.persistence.room:runtime:1.0.0'
    annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
}