Apps Category

Cover photo by James Sutton, unsplash In this series, I have described why I make the apps named Call Mom and Call Dad, and some of the issues with creating a phone call reminder app, like using persistant storage, showing notifications and setting alarms. This fourth and final part describes how you can build multiple apps with similar functionality without creating unnecessary code duplication. If you just want the summary and some links, go to the Summary section at the bottom.

Don’t forget to call your dad too 👴

As I explained in the first article of this series, my reason for creating the Call Mom app was that my mother passed away. At first, I used the app for calling my dad, so I soon started looking into how to create two very similar apps with different names, and some small differences in text and color resources.

My first attempt involved a huge refactoring. All the parts common to both apps were broken out into its own module, without any resources, and every Activity class was made into an abstract class. Then, in each app module, a non-abstract implementation of each activity class was created, connecting it to the resources found in that app module. This method quickly became completely unsustainable, and I started looking for a better way.

Flavor dimensions to the rescue

After a while, I learned about product flavors. This mechanism is mainly used for creating different app variants. If you want to publish a free app, and also provide a paid version with some more functionality, product flavors is probably what you are looking for. I had already started thinking about what I could add to my apps to make "premium" versions to sell. This might not happen for Call Mom and Call Dad, but I did understand that the two apps could be realized as two different flavors of the same app.

This made the development much easier. Instead of three messy modules, I only needed one app module. The first step was to configure a flavor dimension named callwho, and then one product flavor for each app, where the last part of each app's package name is set:

// build.gradle file for the app module - not the project

apply plugin: 'com.android.application'

android {
    defaultConfig {
        applicationId "se.atornblad"
        ...
    }

    flavorDimensions "callwho"

    productFlavors {
        calldad {
            dimension "callwho"
            applicationIdSuffix ".calldad"
        }
        callmom {
            dimension "callwho"
            applicationIdSuffix ".callmom"
        }
    }

    ...
}

The configuration shown above will create two different "callwho" flavors of my application:

  • Product flavor "calldad", application id se.atornblad.calldad
  • Product flavor "callmom", application id se.atornblad.callmom

With just this one configuration change, you can now compile and package two apps that are identical. Instant code sharing with zero code duplication. What makes this interesting is when you introduce the differences between the two apps flavors.

Start by creating flavor-specific resources for each app. If you have just recently created your app, you probably have one single strings.xml file that looks something like this:

<!-- This is ./src/main/res/values/strings.xml -->
<resources>
    <string name="app_name">Call Reminder App</string>
    <string name="action_settings">Settings</string>
    <string name="call_button_text">Call Now</string>
    ...
</resources>

A mistake that would be easy to make is to copy this file twice and change the values that are different between app flavors. That would introduce a lot of duplication. Instead, create one override xml file for each app. Start by creating two new strings.xml files:

  • ./src/calldad/res/values/strings.xml
  • ./src/callmom/res/values/strings.xml
<!-- This is ./src/calldad/res/values/strings.xml -->
<resources>
    <string name="app_name">Call Dad</string>
    <string name="call_button_text">Call Dad Now</string>
    ...
</resources>
<!-- This is ./src/callmom/res/values/strings.xml -->
<resources>
    <string name="app_name">Call Mom</string>
    <string name="call_button_text">Call Mom Now</string>
    ...
</resources>

After saving these files, when you look at the Project tool window, you will only see one of them. It will look something like this:

> values
      colors.xml
      dimens.xml
    > strings (2)
          strings.xml
          strings.xml (calldad)
      styles.xml

This is when I started thinking I had done something wrong. I double-checked and double-double-checked my build.gradle file, and meticulously checked the spelling of every directory and strings.xml file that I had. Don't worry! This is just Android Studio telling you which product flavor is the active one.

To switch between product flavors and build types, open the Build Variants tool window. That's where you can switch between your app's different variants. Switch from calldadDebug to callmomDebug, and your resources will look like this:

> values
      colors.xml
      dimens.xml
    > strings (2)
          strings.xml
          strings.xml (callmom)
      styles.xml

Go through the same process to create flavor-specific layouts, drawables, menus and other resource types.

If you want flavor-specific variants of your Java or Kotlin classes, create a folder called ./src/calldad/java and mimic the directory structure of ./src/main/java. If the flavor-specific java directory contains a file named exactly like in the non-specific main directory, the specific one will be used instead.

Multidimensional app configurations 🧊

Android Studio is not limited to just one flavor dimension. You could, for example, create one callwho flavor to separate calling mom from calling dad, and another market flavor to separate a free ad-supported app from a paid premium one with extra features.

Add another dimension to your app module's build.gradle file:

// build.gradle file for the app module - not the project

apply plugin: 'com.android.application'

android {
    defaultConfig {
        applicationId "se.atornblad"
        buildConfigField 'boolean', 'ENABLE_ADMOB', 'false'
        ...
    }

    flavorDimensions "callwho", "market"

    productFlavors {
        calldad {
            dimension "callwho"
            applicationIdSuffix ".calldad"
        }
        callmom {
            dimension "callwho"
            applicationIdSuffix ".callmom"
        }
        free {
            dimension "market"
            buildConfigField 'boolean', 'ENABLE_ADMOB', 'true'
        }
        premium {
            dimension "market"
            applicationIdSuffix ".premium"
        }
    }

    ...
}

This gradle configuration would produce four different apps:

  • Call Mom (free), application id se.atornblad.callmom
  • Call Dad (free), application id se.atornblad.calldad
  • Call Mom (premium), application id se.atornblad.callmom.premium
  • Call Dad (premium), application id se.atornblad.calldad.premium

One detail that you might notice is that I have created a build configuration field called ENABLE_ADMOB, which is set to false by default, but is set to true for the free flavor. To check this field in an Activity method, you can do something like this:

@Override
protected void onCreate(Bundle savedInstanceState) {
  if (BuildConfig.ENABLE_ADMOB) {
    MobileAds.initialize(this);
    // ... continue to initialize AdMob ads
  }
}

Summary

  • Use product flavors to generate multiple variants of your app
  • Override resources or code that are specific to each variant
  • Put specific resources in the ./src/FLAVOR/res directory
  • Put specific code in the ./src/FLAVOR/java directory
  • Use as many flavor dimensions as you need

Cover photo by James Sutton on Unsplash

Posted by Anders Tornblad on Category Apps Labels
Tweet this

Cover photo by Pavan Trikutam, unsplash The first part of this series, described how and why I started building two apps named Call Mom and Call Dad. The second part covered some aspects of the first version of those apps. This part goes on to describing how to put the user in control, by providing them with a choice. If you just want the summary and some links, go to the Summary section at the bottom.

Don’t forget to give the user a choice 💭

To allow a user to select a contact from their phonebook, the recommended way is to use the contact picker registered by the operating system. Start by creating an Intent and calling startActivityForResult.

// Select a unique identifier between 1 and 65535
private static final int PICK_CONTACT_REQUEST_ID = 12345;

private void startContactSelection() {
    // This URI is used to identify what contact picker the user has
    // picked (or the OS has defaulted) for this device
    Uri uri = Uri.parse("content://contacts/people");

    // Create an intent to start the picker
    Intent pickContactIntent = new Intent(Intent.ACTION_PICK, uri);

    // Tell the picker to only show contacts with phone numbers
    pickContactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);

    // Start the activity. Use the startActivityForResult method instead of
    // the startActivity one, because we want the result of the action!
    startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST_ID);
}

When this method is called, preferably from a button click or other user interaction, the contact picker opens. This worked perfectly on all devices and emulators I tested on, but later when releasing the app publicly, this method caused crashes on some phones.

It turns out some devices don't have any contact picker registered, so the startActivityForResult throws an exception. To remedy this, you first need to check if the Intent really resolves to an action. If it doesn't, you'll need to provide the user with some other way to select the phone number to call.

private void startContactSelection() {
    Uri uri = Uri.parse("content://contacts/people");
    Intent pickContactIntent = new Intent(Intent.ACTION_PICK, uri);

    // Check which component the URI above is connected to
    PackageManager pm = getPackageManager();
    ComponentName component = pickContactIntent.resolveActivity(pm);
    if (component == null) {
        // This device has no contact picker
        // Show an error message to the user, or solve this
        // in some other way. Maybe let the user enter the
        // phone number to call manually in a text box.
        return;
    }

    pickContactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);
    startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST_ID);
}

Getting the contact details

When the user picks one of the contacts from the list and closes the contact picker, your Activity class's onActivityResult method is called, which you must override to be able to read the results of the user's selection.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // First check if this is a result from the intent we started above
    if (requestCode != PICK_CONTACT_REQUEST_ID) return;

    // Then check if the action was successful (the user clicked OK and not Cancel)
    if (resultCode != RESULT_OK) return;

    // Get the base URI for the selected contact, if any
    Uri contactUri = data.getData();
    if (contactUri == null) return;

    // Read fields from the contact data by querying the contact, using a projection
    // The projection is where you pick what data fields you want to read
    // This projection reads the display name and the phone number
    String[] projection = new String[] {
        ContactsContract.Contacts.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER
    };

    // Query the data using a `ContentResolver`
    Cursor cursor = getContentResolver().query(contarcUti, project, null, null, null);

    // If the cursor if valid and has data, get the data field values out
    if (cursor != null && cursor.moveToFirst()) {
        String displayName = cursor.getString(0);
        String phoneNumber = cursor.getString(1);

        // TODO: Store the contact information in some form of persistant storage, to
        // be able to display the contact name, and to call the phone number later
    }
}

Creating a schedule editor 📅

Not everyone calls their parents with the same frequency, or at the same time of day. One thing I had to implement was a schedule editor. I wanted it to look similar to the alarm settings input on some phones. That way, most people would find the user interface familiar, and I wouldn't have to completely reinvent something that much more capable UX people have already solved.

HTC Alarm app screenshot

Screenshots, from left: HTC Alarm Clock, DelightRoom Alarmy

Pick the time of day

I started by creating a new Activity where the user would select their calling schedule. At the top of the layout, I put a TimePicker for selecting the time of day to call:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

    <TimePicker android:id="@+id/time_of_day"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />

</LinearLayout>

This lets the user pick hour and minute of day correctly. The display adapts to the user's locale settings so the experience feels consistent and familiar. To read the user selection from code, call the getHour and getMinute methods:

// Get the TimePicker view
TimePicker timeOfDayPicker = findViewById(R.id.time_of_day);

// Read the data selected by the user
int hourOfDay = timeOfDayPicker.getHour();
int minuteOfDay = timeOfDayPicker.getMinute();

Pick a schedule frequency

Below the TimePicker view, I placed a RadioGroup containing four RadioButton views, for picking the type of repetition. All texts in this example are hardcoded, but you should always use resource references for text content.

    <TextView android:text="Repeat:"
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

    <RadioGroup android:id="@+id/repeat_selector"
      android:orientation="vertical"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content">

        <RadioButton android:id="@+id/repeat_daily"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Daily" />

        <RadioButton android:id="@+id/repeat_weekly"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Weekly" />

        <RadioButton android:id="@+id/repeat_monthly"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Monthly" />

    </RadioGroup>

The best way I've found to read the user selection from code is to use the getCheckedRadioButtonId method on the RadioGroup object. This lets you switch and perform different tasks depending on which radio button is selected:

// Get the RadioGroup view
RadioGroup repeatSelector = findViewById(R.id.repeat_selector);

// Read the radio button id selected by the user
int checkedId = repeatSelector.getCheckedRadioButtonId();

// Act upon the user's choice
switch (checkedId) {
    case R.id.repeat_daily:
        // Put code here to hide views for picking weekday,
        // or day of month
        break;
    case R.id.repeat_weekly:
        // Put code here to show views for picking weekday
        break;
    case R.id.repeat_monthly:
        // Put code here to show views for picking day of month
        break;
    default:
        // No repetition pattern selected, show an error message
}

Pick a weekday

If the user picks the weekly pattern, they must be able to pick a day of the week to call. A simple way of allowing that choice is to use a NumberPicker.

    <NumberPicker android:id="@+id/day_of_week"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />

Prepare the NumberPicker in the onCreate method. A nice trick is to use the Calendar weekday constants for this. The value of SUNDAY is 1, and the value of SATURDAY is 7, so set the minimum and maximum allowed value to these constants. To get the correct names for the weekdays, you could use one of the DateFormatSymbols methods getShortWeekdays or getWeekdays.

NumberPicker weekdayPicker = findViewById(R.id.day_of_week);
weekdayPicker.setMinValue(Calendar.SUNDAY);
weekdayPicker.setMaxValue(Calendar.SATURDAY);

// Get full weekday names, like "Sunday", "Monday", ...
String[] weekdayNames = DateFormatSymbols.getInstance().getWeekdays();

// The NumberPicker needs the first string to be at index 0, but
// because SUNDAY is defined as 1, the values in the weekdayNames
// array actually start at index 1. So we have to copy indices 1..7
// into a new array at indices 0..6
// The final argument value of 8 is the index directly after the
// last index to copy, which might look confusing.
String[] displayNames = Arrays.copyOfRange(weekdayNames, 1, 8);

// Tell the NumberPicker to use these strings instead of the
// numeric values 1 to 7
weekdayPicker.setDisplayedValues(displayNames);
weekdayPicker.setWrapSelectorWheel(true);

Pick a day of the month

For the montly pattern, the user can pick the day of the month from a NumberPicker ranging from 1 to 31, without any special display text settings. In the layout xml file:

    <NumberPicker android:id="@+id/day_of_month"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />

And prepare the picker in the onCreate method:

NumberPicker monthDayPicker = findViewById(R.id.day_of_month);
monthDayPicker.setMinValue(1);
monthDayPicker.setMaxValue(31);
monthDayPicker.setWrapSelectorWheel(true);

Calculating the correct time for the next notification

Time calculations for scheduling can be tricky, and I have tried to keep the rules as simple as possible. First of all, there is a parameter called notBefore that is used to pass in the time before which the notification should not show. I have decided to set this variable to one hour after the latest phone call.

Then the repetition pattern (daily, weekly or monthly) is used to invoke the corresponding algorithm. For now, this solution only supports the GregorianCalendar.

public class ScheduleModel {
    public final static int DAILY = 1;
    public final static int WEEKLY = 2;
    public final static int MONTHLY = 3;

    // Hour and minute of the day
    private int hour;
    private int minute;

    // Any of DAILY, WEEKLY or MONTHLY
    private int type;

    // Any of the Calendar.SUNDAY .. Calendar.SATURDAY constants
    private int dayOfWeek;

    // Anywhere between 1 and 31
    private int dayOfMonth;

    public ScheduleModel(int hour, int minute, int type, int dayOfWeek, int dayOfMonth) {
        this.hour = hour;
        this.minute = minute;
        this.type = type;
        this.dayOfWeek = dayOfWeek;
        this.dayOfMonth = dayOfMonth;
    }

    public Calendar getNextNotification(Calendar notBefore) {
        // Switch on the type and call the corresponding method
        switch (type) {
            case DAILY:
                return getDailyNext(notBefore);
            case WEEKLY:
                return getWeeklyNext(notBefore);
            case MONTHLY:
                return getMonthlyNext(notBefore);
            default:
                // Unsupported schedule type
                return notBefore;
        }
    }

    private Calendar getDailyNext(Calendar notBefore) {
        // Start at the notBefore date, with the selected time of day
        Calendar next = new GregorianCalendar(
            notBefore.get(Calendar.YEAR),
            notBefore.get(Calendar.MONTH),
            notBefore.get(Calendar.DAY_OF_MONTH),
            hour, minute);
        );

        // If that time is before the earliest allowed time to call,
        // step forward one day
        if (next.before(notBefore)) {
            next.add(Calendar.DAY_OF_YEAR, 1);
        }

        return next;
    }

    private Calendar getWeeklyNext(Calendar notBefore) {
        // Start at the notBefore date, with the selected time of day
        Calendar next = new GregorianCalendar(
            notBefore.get(Calendar.YEAR),
            notBefore.get(Calendar.MONTH),
            notBefore.get(Calendar.DAY_OF_MONTH),
            hour, minute);
        );

        // While that time is before the earliest allowed time to call,
        // or the day isn't the selected weekday, step forward one day
        while (next.get(Calendar.DAY_OF_WEEK) != dayOfWeek || next.before(notBefore)) {
            next.add(Calendar.DAY_OF_YEAR, 1);
        }

        return next;
    }

    private Calendar getMonthlyNext(Calendar notBefore) {
        // Start at the notBefore month, with the selected day of the
        // month and the selected time of day
        Calendar next = new GregorianCalendar(
            notBefore.get(Calendar.YEAR),
            notBefore.get(Calendar.MONTH),
            dayOfMonth,
            hour, minute);
        );

        // If that time is before the earliest allowed time to call,
        // step forward one month
        if (next.before(notBefore)) {
            next.add(Calendar.MONTH, 1);
        }

        return next;
    }
}

The result from the getNextNotification method is passed into the alarms system, to set the time for the next notification.

// First get the last call time and calculate the notBefore value
Calendar notBefore = getLastCallTime();
notBefore.add(Calendar.HOUR_OF_DAY, 1);

// Get the time for the next notification
Calendar next = schedule.getNextNotification(notBefore);

// Set the alarm
myAlarmsInstance.setAlarm(next.getTimeInMillis());

Summary 🔖

Cover photo by Pavan Trikutam on Unsplash

Posted by Anders Tornblad on Category Apps Labels
Tweet this

Cover photo by Daria Nepriakhina, unsplash 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 at the bottom.

Don’t forget to build that app 📲

For this task, I went for writing the app in Java. Learning techniques that were completely new to me, like Kotlin or React Native, were not the main focus for me at the time, though I have gotten into both of those later. So I installed Android 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.

Fortunately, I was able to remedy some of these by activating Java 8 compatibility in Android Studio, and setting minSdkVersion to API level 24 in the manifest file.

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, video tutorials and StackOverflow questions. Even in the official documentation 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.

Support libraries

When learning how to use the RecyclerView and the 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. 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. 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.

Android Jetpack

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 suite also includes an updated take on how to do version compatibility. From the AndroidX Overview 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. 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 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:
import androidx.appcompat.app.AppCompatActivity;

public class MyActivity extends AppCompatActivity {
}
  • Use ContextCompat instead of calling Context methods directly, when suitable methods exist:
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:
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 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 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.

// 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.

// 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. This give you access to the lightweight database engine SQLite, 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, you get a fully reactive data flow, based on the 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:

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:

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 which has a sofisticated QueryBuilder concept.

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

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:

// 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, database connectivity could be made much cleaner and more robust, by adding just a little bit more code.

LiveData – a Room with a View

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 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, that solves a large part of that problem. One such component is 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.

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.

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, in this case the this::allMomsChanged reference:

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, 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.

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:

// 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:

<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.

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, 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.

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
  • Implement simple key-value persistant storage with SharedPreferences
  • Do more complex persistent data storage with Room
  • Use Room entities and other app state with LiveData
  • To allow alarms to survive device restarts, listen for the BOOT_COMPLETED message
  • Show notifications correctly using NotificationCompat

Cover photo by Daria Nepriakhina on Unsplash

Posted by Anders Tornblad on Category Apps Labels
Tweet this

Cover photo by Jez Timms, unsplash I've been a professional software developer for more than 20 years, and I still love the process of learning new things. One good way of learning, in my opinion, is to use unfamiliar technology to create things for yourself to use. Even if the first thing you build, using your new skills, might appear small or simple, the lessons learned are still real and will lead you forward.

If you want to learn HTML and CSS, start by learning enough to create your personal web site. Or learn enough about Unity to make the first version of that game you feel someone should have already built. Or dive into Python or Bash scripts, and automate your most boring tasks at work. Go on Udemy, Codecademy or Skillshare, and explore what is available to you.

I wanted to learn more about native Android development, so I made an app.

(Content warning: death of a loved one!)

Don’t forget to call your mom 👵

It all started when my mother passed away. She had COPD, a horrible disease that gets worse in a series of infrequent exacerbations over several years. She would be stable for months and then, unexpectedly, find herself hooked up to a respirator for days before coming back home. Each relapse would make her normal state slightly worse than before.

Over time, we all started getting used to it and acclimated ourselves to every "new normal". We actually never thought that the next one would be the final one.

Until it was.

You should call more often

When it happened, I had a lot of regrets. I used to call once or twice a week just to give some shallow status update and to get one in return. In retrospect, I should have asked the deeper questions, been a better listener, opened up more. I definitely should have called more often.

Having been a computer geek for the majority of my life, as part of processing my loss, I started throwing code at the pain. I made my first Android app, and I named it Call Mom. The app was supposed to be the reminder I would have needed long before. The first version just gave a me notification every day, and when the notification was clicked, it started a phone call to ... my dad.

You should go the distance

To go through with this, I had to learn a lot about the Android ecosystem, like how the Activity lifecycle works, how to set alarms that would survive a device reboot, how to store persistent data, and how to properly show notifications to the user. All of that, and some more, is the focus for the next article in this series.

After using the app myself for a while, I decided to release it to the public. Before doing this, I had to add some features to the app. First of all, I couldn't ship an app with my dad's phone number hardcoded into it, so I had to learn how to allow users to pick from their device's contact list. Also, I realized that not everyone wants to call their mom every day, so I created a way for users to pick a call schedule, and update the app's alarm system to give notifications at the correct times. These changes are the focus for the third article.

Finally, I realized that an app named Call Mom is only useful for connecting with roughly half of the world's parents out there. I decided to also release an app named Call Dad. Of course, making a completely new app for this would create almost 100 % code duplication. Instead, I found a way of doing this properly, with no code duplication at all. I'll explain it all in the fourth and last article of this series.

You should do this for yourself 👉😃

Every time I sit down to tinker with the apps, to fix a bug or to add some new feature, I'm reminded of my mom, in a good way. And getting reminded to call every day, has definitely improved my relationship with my dad. Of course I could have all that without building apps for it, but making them has been therapeutical. I feel I'm creating something useful, with a purpose.

So start building something for yourself. It's worth it.

Cover photo by Jez Timms on Unsplash

Posted by Anders Tornblad on Category Apps Labels
Tweet this