In the previous article, we built up a spring boot application that provides API's that work with Firebase and specifically the Firebase Cloud Console to enable notification sending. If you haven't read that article, please go through it here.

In this article, we create an android application that consumes these API's and also adds some functionality to better work with notifications. We will be starting with notifications on Android 7 and below, and at the end of the next section (part 3), we will see how to handle notifications on Android 8+.

By the end of this tutorial, you will have understood the following:

  • Creating an android project in android studio
  • Creating a simple splash screen (bonus)
  • Adding a project to firebase console
  • Adding the firebase libraries
  • Creating the firebase service that will be used to display notifications on the app
  • Create a login/create user screen to save/update the token in the backend
  • Work with retrofit to send and receive data with apis
  • Create and display a simple notification from within the app
  • Create a send message screen to send messages to users only with their username
  • Create a topic subscription screen - in this case weather that handles subscribing and unsubscribing to a topic
  • Create a settings screen for a user to turn off push notifications (strictly from the backend)
  • Create channels to show how notifications work on android 8 and above

Create the android project

Open android studio and choose start a new android studio project. I am using Android studio 4.0.1 for this.

Select an empty activity as the screenshot shows and click next.

Project creation screen on android studio
Project creation screen on android studio

Set the app name, package name and save location for the project. I am using Java for this walkthrough. Click finish and wait for the app to finish building, and you are ready to go. The java file that has been created is called MainActivity.java out of the convention, but we will edit this to SplashScreenActivity.java. Also, rename the XML file to activity_splash_screen to match the java file.

Creating the splash screen (bonus)

I believe all android apps should have a splash screen to handle data loading in the cold boot of an app. We are going to create the splash screen functionality for this app. To get started, make sure the AndroidManifest.xml file has this entry for the splash screen:

<activity
            android:name=".SplashScreenActivity"
            android:theme="@style/Theme.AppCompat.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

We put the 'NoActionBar' theme to make sure the action bar is not visible in this activity. This makes the splash screen fill up the entire view area minus the status bar. For the layout file activity_splash_screen.xml, here are the contents:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    android:background="@android:color/white"
    tools:context=".SplashScreenActivity">


    <TextView
        android:id="@+id/txtSplashscreenDescription"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        android:textColor="@android:color/black"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

This is a simple ConstraintLayout with a Textview. The end result is a text in the middle of the screen (for any device size) that displays the app name. You can edit this screen to have whatever you want users to see when the app first boots up from a cold start. For the activity file, we will add a handler that will automatically start a different activity after a given time based on the login status. Here is the SplashScreenActivity.java file:

public class SplashScreenActivity extends BaseActivity {

    public static final long SPLASH_SCREEN_DELAY_TIME = 1500;

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

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Intent intent = null;
                intent = new Intent(SplashScreenActivity.this, LoginActivity.class);
                startActivity(intent);

                finish();

            }
        }, SPLASH_SCREEN_DELAY_TIME);
    }

}

Create a login activity based on an empty activity for the code above to work. We will be adding more functionality to this splash screen to make it more dynamic and logical.

Firebase Integration

Creating an android project on firebase console

Now that we have a running app, we need to connect it to firebase. On a browser, navigate to https://console.firebase.google.com/ and select the project that you used in the backend/spring walkthrough. Select add an app and select the android icon as shown below:

In the next screen, put in your app specific data, from the package name to the nickname and tap Register app.

Once the app is registered, follow the instructions to add the json file to your android project and run the app on an emulator or physical device until you see the following screen:

At this point, we are done with firebase console and can go back to fully working on android studio.

Adding the firebase libraries

For the android app to communicate with the project in the firebase console, we also need to add firebase libraries to the android project. Follow these instructions to do so.

In the project gradle file, make sure to add google-services in the dependencies section:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.1"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files

        //add this line
        classpath 'com.google.gms:google-services:4.3.4'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

In the app build.gradle file, add the google-services plugin at the top of the file:

apply plugin: 'com.google.gms.google-services'

And add the firebase dependencies in the dependencies section:

implementation platform('com.google.firebase:firebase-bom:25.12.0')
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-messaging'

Firebase provide a BOM (bill of materials) which handles setting dependencies on all libraries under it. This makes us only focus on the code and not libraries.

With this, we are done with linking firebase in our app. At this point after gradle syncs, run the app to make sure everything is still working as it should.

Firebase Messaging Service

If we were to send/generate a notification at this point, firebase and android would have complete control over it and we would not be able to do anything app related with the notification. To solve this, we create our own firebase messaging service where we can have complete control over notifications. Here are the contents for the custom service named MyMessagingService.

public class MyMessagingService extends FirebaseMessagingService {

    @Override
    public void onNewToken(@NonNull String s) {
        super.onNewToken(s);
        Log.d("new token", s);

    }

    @Override
    public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {

    }
}

To make sure firebase can work with this service, we have to register it in the manifest file within the application property as shown:

    <service
        android:name=".fcm.MyMessagingService"
        android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>

Now if you run the app again and look at logcat, you will see the log with a tag "new token" and the value will contain a long random characters/numbers string that looks like this;

e1TPD8u5Tlu3x9x7uIcwGS:APA91bGSc4ShbzXZP0sG4IT0avqrJHQjv2BSV2JJlEjT8suo23YIqLutQH0OaOiBNM9fY-JPCDlkcY84ZrViV1nZbJdEaLrwczsaV5LqnH4VizXrVG6xozYna97ccw4WDD2chEwWT6XU

This is the device specific token that is used to send notifications to this device! Please note that this token is 'refreshed' when the app is uninstalled or data is cleared. Keep reading to find out how to handle this.

Give yourself a pat on the back as now all that is left to do is consume the apis and display the notification. You can send notifications using this token directly from firebase console but that is not in the scope for this tutorial .You can read more on that here. You can also use that link to troubleshoot any issues you get with the libraries so far.

Login

Now that we have firebase setup with the app and we have received a token successfully, we can now proceed with the login section of the app.

Helper classes

Before we start working on the login activity, there are a couple of files that are needed.

Constants.java:

public class Constants {
    public static final String SHARED_PREFS_NAME = "SHARED_PREFS";
    public static final String LOGGED_IN = "LOGGED_IN";
    public static final String API = "[replace with your ip address for the spring project]";
    public static final String USER_NAME = "USER_NAME";
    public static final String USER_TOKEN = "USER_TOKEN";
    public static final String SUBSCRIBED = "SUBSCRIBED";
}

BaseActivity.java:

public class BaseActivity extends AppCompatActivity {
    /**
     * Get an instance of the shared preferences editor
     */
    public SharedPreferences.Editor getSharedPrefsEditor() {
        SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(Constants.SHARED_PREFS_NAME, 0);
        return sharedPreferences.edit();
    }

    /**
     * Get shared prefs
     */
    public SharedPreferences getSharedPrefs() {
        return getApplicationContext().getSharedPreferences(Constants.SHARED_PREFS_NAME, 0);
    }


}

A few notes on the base activity file.

All activities moving forward will extend this base file to have the following methods:

  • getSharedPrefsEditor - this method returns a shared preference editor
  • getSharedPrefs - this method returns an instance of shared preferences

Simple notification logic

Here are the updated contents for the login xml file:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:id="@+id/coordinator"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    tools:context=".LoginActivity">

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

        <Button
            android:id="@+id/btnCreateUser"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginLeft="50dp"
            android:layout_marginEnd="50dp"
            android:layout_marginRight="50dp"
            android:text="@string/create_user"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnLogin"
            app:layout_constraintVertical_bias="0.297" />

        <Button
            android:id="@+id/btnSendPush"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginLeft="50dp"
            android:layout_marginTop="40dp"
            android:layout_marginEnd="50dp"
            android:layout_marginRight="50dp"
            android:layout_marginBottom="76dp"
            android:text="@string/send_push_notification"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnCreateUser" />

        <EditText
            android:id="@+id/editTextUserName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="100dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            android:ems="10"
            android:hint="@string/enter_your_username"
            android:inputType="textPersonName"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btnLogin"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginLeft="50dp"
            android:layout_marginTop="40dp"
            android:layout_marginEnd="50dp"
            android:layout_marginRight="50dp"
            android:text="@string/login"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/editTextUserName" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

For this screen we have the following:

  • Edittext - this is used to capture the username
  • Login button - we use this to login with the data in the username edittext
  • Create user button - we use this to make an api call to the backend to save the token we received and also to get a username
  • Send push button - we use this to generate and send a notification from the app without the backend
public class LoginActivity extends BaseActivity {

    public static final String CHANNEL_ID = "notifications_channel";
    EditText editTextUserName;
    Button btnLogin;
    Button btnCreateUser;
    Button btnSendPush;
    RetrofitClient client;
    CoordinatorLayout coordinatorLayout;

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

        // hooks
        btnCreateUser = findViewById(R.id.btnCreateUser);
        btnSendPush = findViewById(R.id.btnSendPush);
        editTextUserName = findViewById(R.id.editTextUserName);
        btnLogin = findViewById(R.id.btnLogin);
        client = getClient();
        coordinatorLayout = findViewById(R.id.coordinator);

        btnSendPush.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                final NotificationCompat.Builder builder = buildFirstNotification();
                final NotificationCompat.Builder builder2 = buildSecondNotification();

                // use the notification manager to display the notification
                final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext());

                // comment this line out if you need to display the notification after a set time below
                notificationManager.notify(1, builder.build());

                // uncomment the code block below to set a timer to enable the notification to show when the app is in the background
                /*new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        notificationManager.notify(1, builder.build());
                    }
                }, 5000);*/

                // uncomment the code block below to update the notification
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        notificationManager.notify(1, builder2.build());


                    }
                }, 5000);
            }
        });
    }

    private NotificationCompat.Builder buildFirstNotification() {
        // build the notification tap action
        Intent intent = new Intent(getApplicationContext(), SplashScreenActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);

        // build the notification

        return new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
                .setSmallIcon(R.drawable.notification_icon)
                .setContentTitle("Sample Title")
                .setContentText("Sample body")
                .setContentIntent(pendingIntent)
                .setAutoCancel(true) // set this to remove it when tapped
                //.setShowWhen(false) // disable the timestamp with false
                //.setTimeoutAfter(5000) // this automatically dismisses the notification
                .setPriority(NotificationCompat.PRIORITY_DEFAULT);
    }

    private NotificationCompat.Builder buildSecondNotification() {
        // build the notification tap action
        Intent intent = new Intent(getApplicationContext(), SplashScreenActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);

        // build the notificationsendMessage

        return new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
                .setSmallIcon(R.drawable.notification_icon)
                .setContentTitle("Updated Title")
                .setContentText("Updated body")
                .setContentIntent(pendingIntent)
                .setAutoCancel(true) // set this to remove it when tapped
                //.setShowWhen(false) // disable the timestamp with false
                //.setTimeoutAfter(5000) // this automatically dismisses the notification
                .setPriority(NotificationCompat.PRIORITY_DEFAULT);
    }
}

There is a lot happening with this screen, let us walk through it.

  • buildFirstNotification - this method creates/populates and returns a notification builder object. Inside it we set the intent that leads to the splash screen, we also set notification content and the priority of the notification
  • buildSecondNotification - this method also does the same as the first one
  • btnSendPush.setOnClickListener - here we get instances of both builders, create a notification manager, call notify passing in the fist builder. At this point you should see a notification being displayed in the status bar. We also call a handler with a 5ms delay to update the displayed notification (builder1) with the other notification (builder2). The update is possible because both times we call .notify() we are passing the same id. Different IDs will result in 2 different notifications being displayed.

Now we have a working app that sends local notifications!

User creation

We now need to create a user in the backend and register the token on the server (our server and not firebase).

For that, we need to install retrofit and Gson for api calls and Json serialization. Add the following lines to your app/build.gradle file under the dependencies section:

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

Sync gradle and proceed to create the following models:


BaseModel:

public class BaseModel {
    private boolean success;
    private String errorMessage;

    public BaseModel() {
    }

    // getters and setters ommited for brevity
}

User Model:

public class User extends BaseModel {
    private String userName;
    private String userToken;
    private boolean notifications;

    public User() {
    }

   // getters and setters ommited for brevity
}

Notification model:

public class Notification extends BaseModel {
    private String senderUserName;
    private String recipientUserName;
    private String message;

    public Notification() {
    }

    // getters and setters ommited for brevity  
}

Subscription model:

public class Subscription extends BaseModel {
    private boolean subscribe;
    private String userName;

    public Subscription() {
    }

    // getters and setters ommited for brevity
}

Create the api interface:

public interface NotificationsApi {
    @POST("/users")
    Call<User> handleUser(@Body User user);

    @POST("/sendMessage")
    Call<Notification> sendMessage(@Body Notification notification);

    @POST("/subscribe/weather")
    Call<Subscription> subscribeToWeather(@Body Subscription subscription);

    @POST("/handleNotifications")
    Call<User> handleNotifications(@Body User user);

}

Create the retrofit client:

public class RetrofitClient {
    private static final String BASE_URL = Constants.API;
    private static RetrofitClient mInstance;
    private Retrofit retrofit;

    private RetrofitClient() {
        retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }

    public static synchronized RetrofitClient getInstance() {
        if (mInstance == null) {
            mInstance = new RetrofitClient();
        }
        return mInstance;
    }

    public NotificationsApi getApi() {
        return retrofit.create(NotificationsApi.class);
    }
}

Update the Baseactivity to include this method:

/**
     * Get the retrofit client
     */
    public RetrofitClient getClient() {
        return RetrofitClient.getInstance();
    }

Add the following above the onCreate method in the login activity:

RetrofitClient client;

In the same activity, add the following hook:

client = getClient();

Finally inside on create, add the following listeners:

btnCreateUser.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                String userToken = getSharedPrefs().getString(Constants.USER_TOKEN, null);
                if (userToken == null) {
                    Toast.makeText(LoginActivity.this, "Token is null", Toast.LENGTH_SHORT).show();
                    return;
                }
                User user = new User();
                user.setUserToken(userToken);

                client.getApi().handleUser(user).enqueue(new Callback<User>() {
                    @Override
                    public void onResponse(Call<User> call, Response<User> response) {
                        if (response.isSuccessful()) {
                            User user = response.body();
                            if (user.isSuccess()) {
                                // save the info from the server to the api
                                saveSharedPrefs(user);

                                Snackbar snackbar = Snackbar.make(coordinatorLayout, "Account created successfully", BaseTransientBottomBar.LENGTH_INDEFINITE);
                                snackbar.setAction("Ok", new View.OnClickListener() {
                                    @Override
                                    public void onClick(View v) {
                                        // navigate to the home screen
                                        Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                                        startActivity(intent);
                                    }
                                });

                                snackbar.show();

                            } else {
                                Toast.makeText(LoginActivity.this, user.getErrorMessage(), Toast.LENGTH_SHORT).show();
                            }
                        } else {
                            try {
                                Toast.makeText(LoginActivity.this, response.errorBody().string(), Toast.LENGTH_SHORT).show();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    @Override
                    public void onFailure(Call<User> call, Throwable t) {
                        Toast.makeText(LoginActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String userName = editTextUserName.getText().toString().trim();
                if (TextUtils.isEmpty(userName)) {
                    Toast.makeText(LoginActivity.this, "Please enter a user name", Toast.LENGTH_SHORT).show();
                    return;
                }

                String token = getSharedPrefs().getString(Constants.USER_TOKEN, null);
                if (TextUtils.isEmpty(token)) {
                    Toast.makeText(LoginActivity.this, "No token found, please clear data and try again", Toast.LENGTH_SHORT).show();
                    return;
                }

                User user = new User();
                user.setUserName(userName);
                user.setUserToken(token);

                client.getApi().handleUser(user).enqueue(new Callback<User>() {
                    @Override
                    public void onResponse(Call<User> call, Response<User> response) {
                        if (response.isSuccessful()) {
                            User user = response.body();
                            if (user.isSuccess()) {
                                // save the info from the server to the api
                                saveSharedPrefs(user);

                                Snackbar snackbar = Snackbar.make(coordinatorLayout, "Welcome back", BaseTransientBottomBar.LENGTH_INDEFINITE);
                                snackbar.setAction("Ok", new View.OnClickListener() {
                                    @Override
                                    public void onClick(View v) {
                                        // navigate to the home screen
                                        Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                                        startActivity(intent);
                                    }
                                });

                                snackbar.show();
                            } else {
                                Toast.makeText(LoginActivity.this, user.getErrorMessage(), Toast.LENGTH_SHORT).show();
                            }
                        } else {
                            try {
                                Toast.makeText(LoginActivity.this, response.errorBody().string(), Toast.LENGTH_SHORT).show();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    @Override
                    public void onFailure(Call<User> call, Throwable t) {
                        Toast.makeText(LoginActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });

With the new update, we make two api calls with the button clicks:

  • btnCreateUser.setOnClickListener - here we make an api call to create a user and register a token. We do some validations on the token and handle the response from the api
  • btnLogin.setOnClickListener - here we send the username and token from the edittext and sharedPreferences. We also validate the response from the api

In both listeners, we add entries to shared preferences

To get the token saved on the device, we make the following change on the service we created earlier. This just saves the token under the selected constant in shared preferences.

@Override
    public void onNewToken(@NonNull String s) {
        super.onNewToken(s);

        SharedPreferences preferences = getApplicationContext().getSharedPreferences(Constants.SHARED_PREFS_NAME, 0);
        SharedPreferences.Editor editor = preferences.edit();
        editor.putString(Constants.USER_TOKEN, s);
        editor.apply();

    }

With the above change, we can now edit the splashscreen.java file to be able to navigate the user automatically to the correct screen.

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

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                SharedPreferences sharedPreferences = getSharedPrefs();

                Intent intent = null;
                if (sharedPreferences.getBoolean(Constants.LOGGED_IN, false)) {
                    // meaning user has been created and we have logged in
                    intent = new Intent(getApplicationContext(), MainActivity.class);
                } else {
                    // meaning no user has been created and we have not logged in
                    intent = new Intent(getApplicationContext(), LoginActivity.class);

                }
                startActivity(intent);

                finish();

            }
        }, SPLASH_SCREEN_DELAY_TIME);
    }

Create the Main Activity plus a layout file base on the empty activity template and now you can run the app. When you enter a random username and click login, you should get an error. If you click 'Create User' and a token was saved initially, the app should navigate you to the main activity screen. If there was no token, you will get an appropriate error.

Conclusion:

That is it for this article. We saw how to integrate firebase to an android app and how to send local notifications plus how to handle saving of tokens in a server backend.

In part 3, we will be working on sending direct messages to users, topic subscription and notification muting.

Find the GitHub repo for this article here.

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.