Don't forget to build that app 📲
Tags: android, java, app, tutorial
Published: 2019-09-23 17:23:08

In the first part of this series, I describe how and why I started building the apps named **Call Mom** and **Call Dad**. This part describes the initial work needed to build the first useful version, including some Java pointers. If you just want the summary and some links, go to the [Summary section](#summary) at the bottom.

# Don’t forget to build that app 📲

For this task, I went for writing the app in [Java](https://dev.to/t/java). Learning techniques that were completely new to me, like [Kotlin](https://dev.to/t/kotlin) or [React Native](https://dev.to/t/reactnative), were not the main focus for me at the time, though I have gotten into both of those later. So I installed [Android Studio](https://developer.android.com/studio), launched it, and started taking baby-steps forward.

Coming from the world of modern Java and C#, and moving to Java 7 _(the default Java version in Android Studio when I started)_ felt like a huge step back in time.

- no [lambda expressions](https://en.wikipedia.org/wiki/Anonymous_function)
- difficult date/time support
- no [Stream API](https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/), and nothing even close to [LINQ](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/) and its extension methods for collections
- no switch statements on strings
- no [type inference](https://en.wikipedia.org/wiki/Type_inference) for local variables
- [type erasure in generics](https://en.wikipedia.org/wiki/Generics_in_Java#Problems_with_type_erasure), which makes reflection a nightmare
- no [null-coalesce](https://en.wikipedia.org/wiki/Null_coalescing_operator) or [null-conditional](https://en.wikipedia.org/wiki/Safe_navigation_operator) operators
- no value types _(immutable data structures)_
- no explicitly nullable or non-nullable variable declarations

Fortunately, I was able to remedy **some** of these by [activating Java 8 compatibility](https://developer.android.com/studio/write/java8-support) in Android Studio, and setting `minSdkVersion` to API level 24 [in the manifest file](https://developer.android.com/guide/topics/manifest/uses-sdk-element).

## Navigating the unknowns

Android has been around for more than a decade now, and is a really mature platform for both users and developers. Unfortunately, this also means that there is a lot of outdated information out there on [blogs](https://www.sitepoint.com/writing-your-first-android-app-2/), [video tutorials](https://www.youtube.com/watch?v=JkwWWyJEIzw) and [StackOverflow questions](https://stackoverflow.com/questions/7830369/can-i-use-eclipse-to-create-android-apps). Even in [the official documentation](https://developer.android.com/guide) there are contradictions, and ambiguities when it comes to how to implement specific things. The best practices for one version quickly become frowned upon, or turn deprecated in a newer version of the Android SDK. At the same time, developers are encouraged to always target the latest version.

# Compatibility with earlier Android versions 🤜🤛

Fortunately, there are compatibility libraries that let developers target the bleeding-edge devices and use newer features, while automatically falling back to older equivalent APIs or simulating the new behaviors on older devices. So this problem has been solved. The problem is just that it has been solved twice.

[//]: # "Comments in Markdown are strange beasts - don't include when publishing..."
[//]: # "https://guides.codepath.com/android/Migrating-to-the-AppCompat-Library"

## Support libraries

When learning how to use the [RecyclerView](https://developer.android.com/guide/topics/ui/layout/recyclerview) and the [CardView](https://developer.android.com/guide/topics/ui/layout/cardview) to allow the user to pick the correct contact to call from a list, I did it according to what I could find in the official documentation, by adding references to the [Support Libraries](https://developer.android.com/tools/support-library/features.html#v7). All was good for a while, and I used the Support Libraries for a lot of different things, like showing notifications correctly on all supported Android versions.

Later, when I wanted to add persistent data storage, I had to add references to [AndroidX](https://developer.android.com/jetpack/androidx). After a while, the compiler started complaining about conflicts between different `RecyclerView` implementations. The conflicts came from me referencing those classes in code, Android Studio asking to automatically add `import` statements, and me picking the wrong ones.

[//]: # "https://developer.android.com/topic/libraries/support-library/packages"

## Android Jetpack

[//]: # "https://developer.android.com/jetpack/androidx"
[//]: # "https://developer.android.com/jetpack/androidx/migrate"

Lately, Android development has seen a number of improvements to architecture and standardized components for all kinds of things, like data storage, user interface elements, notifications, media and security. Separate from the platform APIs, the [Android Jetpack](https://developer.android.com/jetpack) suite also includes an updated take on how to do version compatibility. From the [AndroidX Overview](https://developer.android.com/jetpack/androidx) page:

> AndroidX fully replaces the Support Library by providing feature parity and new libraries.

This is all very nice, but the top search result for `RecyclerView`, for example, at the time of me writing this, still leads to [the older version](https://developer.android.com/reference/android/support/v7/widget/RecyclerView). It's something to be aware of.

If you are working on an app that depends on the older Support Libraries, there are ways to [easily and automatically migrate](https://developer.android.com/jetpack/androidx/migrate) to AndroidX. In my experience, automatic migration works fine. Also, newer versions of Android Studio tries to coerce _(and even force)_ you to use the newer compatibility libraries.

To ensure a consistent user experience across multiple Android versions, here are a few tips to consider:

- Let your activities extend `AppCompatActivity` instead of `Activity`:

```java
import androidx.appcompat.app.AppCompatActivity;

public class MyActivity extends AppCompatActivity {
}
```

- Use `ContextCompat` instead of calling `Context` methods directly, when suitable methods exist:

```java
import androidx.core.content.ContextCompat;

// Start other activities from inside an activity like this:
ContextCompat.startActivity(this, intent, options);
// And not like this:
this.startActivity(intent, options)

// Get some resources from inside an activity like this:
Drawable picture = ContextCompat.getDrawable(this, R.drawable.pic);
// And not like this:
Drawable picture = getDrawable(R.drawable.pic);

// Check permissions like this:
int permissionState = ContextCompat.checkSelfPermission(this, CALL_PHONE);
// And not like this:
int permissionState = checkSelfPermission(CALL_PHONE);
```

- Use `NotificationManagerCompat` instead of `NotificationManager`:

```java
import androidx.core.app.NotificationManagerCompat;

// Get the Notification Manager like this:
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
// And not like this:
NotificationManager = getSystemService(NOTIFICATION_SERVICE);
```

# Persistent data 💾

To handle the user's selection of which contact to call, and the notification frequency, I needed to store data persistently, so nothing would get lost between app restarts. I also needed to store the time of the user's most recent call to be able to calculate the date and time for the next notification.

At first, I went with a Room database to store everything, and ended up creating a lot of `AsyncTask` solutions to actively read data when needed or write data after user input. This approach was what I could find when I searched for answers. However, using the [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) approach is much more efficient and straight-forward for subscribing to changed data across an entire app. Also, a Room database might not be the best storage for every bit of data your app needs to store.

## SharedPreferences

When storing very simple data, like single strings or numeric values, keeping that in a Room database is probably overkill. Reading and writing Room data can not be done in the UI thread, so you have to use `LiveData`, `AsyncTask` or other asynchronous mechanisms to read or store values.

The [SharedPreferences](https://developer.android.com/training/data-storage/shared-preferences) APIs provide a key-value store that you can use directly from your `Activity` code, without spawning worker threads or worrying about synchronization issues. To read data, start by calling the `getSharedPreferences` method.

```java
// First open the preferences file called "prefs"
// If it doesn't exist, it gets created automatically
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);

// Read an integer named "launches" from the preferences
// If that values doesn't exist, let it return zero
int numberOfAppLaunches = prefs.getInt("launches", 0);

// Read a string named "username"
// If that value doesn't exist, let it return null
String username = prefs.getString("username", null);
```

Your app can maintain multiple different preferences files _(the first argument to `getSharedPreferences`)_ to separate groups of data. In my apps, I haven't used that feature, but it can be useful for avoiding name collisions.

To update your app's `SharedPreferences`, you first need to create an `Editor` object, put the new value into the editor, and call `apply()`, which saves the changes to the preferences file asynchronously, without disturbing the UI thread.

```java
// Open the preferences file called "prefs"
SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE);

// Create an Editor
SharedPreferences.Editor prefsEditor = prefs.edit();

// Update the value
numberOfAppLaunches += 1;
prefsEditor.putInt("launches", numberOfAppLaunches);

// Save changes to the preferences file
prefsEditor.apply();
```

## Room databases

For storing more complex data, you should consider the [Room persistance library](https://developer.android.com/jetpack/androidx/releases/room). This give you access to the lightweight database engine [SQLite](https://www.sqlite.org/index.html), hidden behind an abstraction layer that helps you focus on designing your data model instead of getting sidelined by more complex things like connections and SQL query syntax beyond simple `SELECT` queries. Combined with the [LiveData architecture](https://developer.android.com/topic/libraries/architecture/livedata), you get a fully reactive data flow, based on the [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern).

Start by defining your data classes. Each data class is annotated as an `@Entity` and translates to a single table in your SQLite database. This is how a simple `MomData` entity class could look:

```java
import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity
public class MomData {
    @PrimaryKey(autoGenerate = true)
    public long id;

    public String name;
    public String number;
}
```

Then define your data access methods. These are Java interfaces, annotated as `@Dao`, and should reflect every data use case in your app, like retreiving all instances from the database table, getting one specific instance by id, searching for instances matching some input, updating an existing instance or adding instances of your entity to the database:

```java
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;

@Dao
public interface MomDao {
    @Query("SELECT * FROM MomData")
    MomData[] getAllMoms();

    @Query("SELECT * FROM MomData WHERE id = :id")
    MomData getMomById(long id);

    @Query("SELECT * FROM MomData WHERE name = :whatName")
    MomData[] getAllMomsWithName(String whatName);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void addOrUpdate(MomData mom);
}
```

This data access interface lets your app:

- List all moms in the database with the `getAllMoms` method
- Get one specific mom using its id with the `getMomById` method
- List all moms with a specific name with the `getAllMomsWithName` method
- Add a new mom, or update an existing one, with the same `addOrUpdate` method; the `onConflict` parameter of the `@Insert` annotation tells Room to replace the row in the database if the id matches an existing row, or to create a new row if the `MomData` object is a new one

As you can see, some SQL knowledge is required for creating queries, and if you find yourself having a need for more complex `JOIN` or `WHERE` clauses, you might want to investigate other ORM solutions, like [GreenDao](http://greenrobot.org/greendao/) which has a sofisticated [QueryBuilder](http://greenrobot.org/greendao/documentation/queries/) concept.

Finally, you create an abstract class that extends the `RoomDatabase` class, which handles connections correctly for you:

```java
import androidx.room.Database;
import androidx.room.RoomDatabase;

// Add all your app's entity classes to the entities array
@Database(entities = { MomData.class }, version = 1)
public abstract class CallMomDatabase extends RoomDatabase {
    // Create an abstract DAO getter for each DAO class
    public abstract MomDao getMomDao();
}
```

Now, to use the database, you need to create a `RoomDatabase.Builder` object, that will create the database if it doesn't already exist, and establish a connection to it:

```java
// From inside a method in an Activity:
RoomDatabase.Builder<CallMomDatabase> builder =
    Room.databaseBuilder(this, CallMomDatabase.class, "callmomdb");
CallMomDatabase db = builder.build();

// Get a list of all moms
MomData[] allMoms = db.getMomDao().getAllMoms();

// Close the connection to clean up
db.close();
```

However, you are not allowed to perform any database queries from your app's UI thread, which means the code above can not be called from any `onClick`-like methods.

My first solution to this was to create a lot of `AsyncTask` implementations, to create new worker threads any time I needed to read from, or write to, the database. This mostly worked fine, but I had to think about thread synchronization issues myself, which is always a pain. I do not recommend building your app this way. When I found out about [LiveData](https://developer.android.com/topic/libraries/architecture/livedata), database connectivity could be made much cleaner and more robust, by adding just a little bit more code.

## LiveData – a Room with a View

[//]: # "Check out this Medium post for some LiveData input"
[//]: # "https://proandroiddev.com/when-and-why-to-use-android-livedata-93d7dd949138"

Making sure that your app's views show the correct data from your model at all times can be tricky, especially when you have to take the [Activity Lifecycle](https://developer.android.com/guide/components/activities/activity-lifecycle) into consideration. Your `Activity` object can get created and destroyed, paused and resumed, at any time, outside of your control, even when the user does a simple thing like turning their phone from portrait to landscape orientation. To know when, and how, to save the view state and when to read it back is not completely trivial.

Luckily, Android Jetpack provides a concept of [Lifecycle-Aware Components](https://developer.android.com/topic/libraries/architecture/lifecycle.html), that solves a large part of that problem. One such component is [LiveData](https://developer.android.com/topic/libraries/architecture/livedata), that is used to wrap a mutable value _(a simple value or an object)_ in a lifecycle-aware observable. Any observer, such as an `Activity` or a `Fragment` will receive updated values exactly when they need to, at the correct times in their lifecycle. Even though `LiveData` objects can be used with any type of data from any source, they are especially useful for dealing with entities living in a `Room` database.

First, you need to refactor the `Dao` interface to leverage the `LiveData` mechanism. You'll need to wrap the return type of any data that you need to observe in a `LiveData<>` generic class.

```java
import androidx.lifecycle.LiveData;

@Dao
public interface MomDao {
    @Query("SELECT * FROM MomData")
    LiveData<MomData[]> getAllMoms();

    // ...
}
```

Next, you should create a `ViewModel` implementation to contain all the data that your view needs to render. You could move the code to build your `Database` object in here, but if your app has multiple `ViewModel` classes, you might want to move that code to some helper method and implement the Singleton pattern.

```java
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;

public class MainActivityViewModel extends ViewModel {
    private LiveData<MomData[]> allMoms;
    private final CallMomDatabase database;

    public MainActivityViewModel() {
        RoomDatabase.Builder<CallMomDatabase> builder =
            Room.databaseBuilder(this, CallMomDatabase.class, "callmomdb");
            database = builder.build();
    }

    public LiveData<MomData[]> getAllMoms() {
        if (allMoms == null) {
            allMoms = database.getMomDao().getAllMoms();
        }
        return allMoms;
    }
}
```

Notice that `database.close()` is no longer called. This is because `LiveData` needs the database connection to stay open. Finally, in your `Activity` you need to create an `Observer` to listen to changes in your data, and update your view correspondingly. Targeting Java 8, the most readable way to do this is by using a [Method Reference](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html), in this case the `this::allMomsChanged` reference:

```java
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProviders;

public class MainActivity extends AppCompatActivity {
    private MainActivityViewModel model;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Get an instance of the view model
        model = ViewModelProviders.of(this).get(MainActivityViewModel.class);

        // Start observing the changes by telling what method to call
        // when data is first received and when data changes
        model.getAllMoms().observe(this, this::allMomsChanged);
    }

    private void allMomsChanged(@Nullable final MomData[] allMoms) {
        // This is where you update the views using the new data
        // passed into this method.
    }
}
```

The collaboration between `Room` and `LiveData` ensures that whenever data is changed in your database, the `allMomsChanged` method above is called automatically, to allow the UI to reflect the changes in data.

# Setting alarms ⏰

A reminder app, such as **Call Mom** and **Call Dad**, need to be able to alert the user at specific times, even if their device is sleeping, and the alerts need to work correctly even if the device is rebooted. There is a mechanism in android called the [Alarm Manager](https://developer.android.com/training/scheduling/alarms), which you can use to wake the app up and run code on a schedule. The `AlarmManager` class has lots of different methods to set these alarms, and `AlarmManagerCompat` can help you set alarms in a way that is consistent across Android versions. You need to be careful when selecting which method to use, because if you design your alarm badly, your app can drain the battery of a device.

## Setting the alarm

I decided to use the `AlarmManagerCompat.setAlarmClock` method for these apps, because the main purpose of the alarms is to notify the user about a scheduled call. The `setAlarmClock` method limits the number of alarms to at most one per 15 minutes, so if your app needs to schedule code to run that don't notify the user, or that needs to run more than every 15 minutes, you should use some other method of the `AlarmManager` or `AlarmManagerCompat` classes, or use some different approach.

```java
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.core.app.AlarmManagerCompat;

public class MyAlarms extends BroadcastReceiver
    private AlarmManager alarmManager;
    private Context appContext;
    private final static int REQUEST_CODE = 1;

    // The current application context must be passed into this constructor
    public MyAlarms(Context appContext) {
        this.appContext = appContext;

        // Get the AlarmManager
        alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    }

    public void setAlarm(long timeInMilliseconds) {
        // Create an intent that references this class
        Intent intent = new Intent(context, getClass());

        // Create a pending intent (an intent to be used later)
        // If an identical pending intent already exists, the FLAG_UPDATE_CURRENT
        // flag ensures to not create duplicates
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            appContext, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        );

        // Set the alarm to call the onReceive method at the selected time
        AlarmManagerCompat.setAlarmClock(alarmManager, timeInMilliseconds, pendingIntent, pendingIntent);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // This method will get called when the alarm clock goes off
        // Put the code to execute here
    }
}
```

To set an alarm, create an instance of `MyAlarms` and call the `setAlarm` method, passing in the millisecond timestamp for the desired alarm time:

```java
// From inside an Activity or a Service:
MyAlarms myAlarms = new MyAlarms(this);

// Set the alarm to go off after an hour
// An hour = 60 minutes * 60 seconds * 1000 milliseconds
long afterAnHour = System.currentTimeMillis() + 60 * 60 * 1000;
myAlarms.setAlarm(afterAnHour);
```

## Detecting device reboots

One problem with using `AlarmManager` is that all scheduled alarms are lost when the user reboots their device. To allow alarms to work properly even after a reboot, your app needs to detect device reboots, and when a reboot is done, schedule the alarm again. This requires you to save the alarm time in some persistant storage, for example `SharedPreferences`, when the alarm is set, to read from storage when a reboot is detected, and schedule the same alarm again.

The operating system sends broadcast messages to all apps that listen to `BOOT_COMPLETED` actions. To have your app get notified, start by declaring the `RECEIVE_BOOT_COMPLETED` permission, and adding an `intent-filter` to your reciever in the `AndroidManifest.xml` file:

```xml
<manifest ...>
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"> />
  ...

  <application ...>
    ...

    <receiver android:name=".MyAlarms" android:enabled="true">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
      </intent-filter>
    </receiver>

  </application>
</manifest>
```

Then in your `BroadcastReceiver` implementation, expand the `onReceive` method to check what type of message is received, and reschedule the alarm as needed. Also, when scheduling an alarm, save the alarm time in SharedPreferences.

```java
public class MyAlarms extends BroadcastReceiver
    private AlarmManager alarmManager;
    private Context appContext;
    private final static int REQUEST_CODE = 1;
    private final static long TIME_NOT_SET = 0;

    public MyAlarms(Context appContext) {
        this.appContext = appContext;
        alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    }

    public void setAlarm(long timeInMilliseconds) {
        Intent intent = new Intent(context, getClass());
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
            appContext, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        );
        AlarmManagerCompat.setAlarmClock(alarmManager, timeInMilliseconds, pendingIntent, pendingIntent);

        // Open shared preferences and save the alarm time
        SharedPreferences prefs = appContext.getSharedPreferences("alarms", Context.MODE_PRIVATE);
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putLong("alarmtime", timeInMilliseconds);
        prefsEditor.apply();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // Check if this broadcast message is about a device reboot
        if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
            // Yes it is! Get the last saved alarm time from shared preferences
            SharedPreferences prefs = appContext.getSharedPreferences("alarms", Context.MODE_PRIVATE);
            long savedAlarmTime = prefs.getLong("alarmtime", TIME_NOT_SET);

            // Is there a saved alarm time?
            if (savedAlarmTime != TIME_NOT_SET) {
                // Reschedule the alarm!
                setAlarm(savedAlarmTime);
            }
        }
        else {
            // This is not a device reboot, so it must be the alarm
            // clock going off. Do what your app needs to do.
        }
    }
}
```

# Showing notifications 🚩

The main purpose of these apps is to notify the user when it's time to call. First of all, you'll need to create at least one [Notification Channel](https://developer.android.com/training/notify-user/channels), so that your app works in Android Oreo _(version 26)_ and later. By creating channels, users can allow or deny notifications depending on their content. Be sure to provide good names and descriptions for your channels.

## NotificationCompat

Notifications is one of those concepts that have changed a lot over the course of Android's history, so there are quite a lot of quirks to handle differently, depending on what version of Android your user's device runs. Luckily, AndroidX contains the `NotificationCompat` and `NotificationManagerCompat` classes that take some of those pains away.

```java
public class MyNotifications {
    private final static String CHANNEL_ID = "MAIN";
    private final static int ID = 12345;
    private final static int IMPORTANCE = NotificationManager.IMPORTANCE_DEFAULT;

    // You should definitely get the NAME and DESCRIPTION from resources!
    private final static String NAME = "Call reminders";
    private final static String DESCRIPTION = "These notifications remind you to call your mom";

    public void createChannel(Context context) {
        // Only do this if running Android Oreo or later
        if (Build.VERSION.SDK_INT <>= Build.VERSION_CODES.O) return;

        // Get the NotificationManager
        NotificationManager notificationManager = context.getSystemService(NotificationManager.class);

        // Create and configure the channel
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, NAME, IMPORTANCE);
        channel.setDescription(DESCRIPTION);
        channel.setShowBadge(true);
        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);

        // Create the channel
        notificationManager.createNotificationChannel(channel);
    }

    // When a channel has been created, call this method to show the
    // notification, and pass a PendingIntent that will get started
    // when the user clicks the notification; preferably you will
    // pass an Activity intent to start.
    public void showNotification(Context context, String title, String text, PendingIntent intentToStart) {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.my_notification_icon)
            .setCategory(NotificationCompat.CATEGORY_REMINDER)
            .setContentTitle(title)
            .setContentText(text)
            .setContentIntent(intentToStart)
            .setOnlyAlertOnce(true)
            .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL);

        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
        notificationManager.notify(ID, builder.build());
    }
}
```

# Summary 🔖

- For compatibility and modern UI elements, ignore the older Support Library, and use [AndroidX](https://developer.android.com/jetpack/androidx)
- Implement simple key-value persistant storage with [SharedPreferences](https://developer.android.com/training/data-storage/shared-preferences)
- Do more complex persistent data storage with [Room](https://developer.android.com/training/data-storage/room)
- Use Room entities and other app state with [LiveData](https://developer.android.com/topic/libraries/architecture/livedata)
- To allow alarms to survive device restarts, listen for the [BOOT_COMPLETED message](https://developer.android.com/reference/android/content/Intent.html#ACTION_BOOT_COMPLETED)
- Show notifications correctly using [NotificationCompat](https://developer.android.com/guide/topics/ui/notifiers/notifications#compatibility)

_Cover photo by [Daria Nepriakhina](https://unsplash.com/photos/pz-TH3jzic8) on [Unsplash](https://unsplash.com/)_