Engineering at Sprout: Building an Android month picker

26 Jun

Note: This article was based on Material Components version 1.2.0-beta01 as of June 1, 2020.

In my three and a half years working on a small Android team at Sprout Social, one of the main things that motivates me to come into work every day is the freedom and trust from our company to tackle a problem in whatever way we deem best.

The freedom to research and explore many different solutions to a problem we deem necessary, while accounting for a timeframe to deliver on product updates, enables us to find the best solution for both our customers and our software.

One such challenge involved building a UI component for our new Mobile Reporting feature. This new component was a month picker, which allowed our users to scope a date range for an analytics report.

The starting place we picked was the existing Material Components Library. Rather than starting from scratch, this library is actively maintained and aligns with the Material specifications. With this library as a foundation, we could likely reduce the amount of logic we’d have to write ourselves.

In this article, I’ll cover how we approached this process, some unique factors in building for the Sprout Android app, a few “gotchas” that came up (and were fixed) along the way, and what to know if you’re working on a similar project.

Introduction

The Android Material Components 1.1.0 Release introduced a new Date Picker UI Component. One of the welcome additions of this new MaterialDatePicker over the AppCompat CalendarView is the ability to select a range of dates using either a Calendar View or a Text Input Field.

The old AppCompat CalendarView was not very flexible. It was a good component for the limited use case it was meant to solve; that is, selecting a single date and optional minimum and maximum dates to specify an allowed date range bound.

The new MaterialDatePicker was built with more flexibility to allow the use of expanded functionality of behavior. It works through a series of interfaces that one could implement to tweak and modify the behavior of the picker.

This behavior modification is done at runtime through a set of builder pattern functions on the MaterialDatePicker.Builder class.

This means we are able to extend the base behavior of this MaterialDatePicker through composable interface components.

Note: While there are a number of different components the MaterialDatePicker utilizes, in this article we will cover the Date Selection Component only.

Date range picker

The Sprout Social Android team was in the process of building our Analytics Reports Section.

This new section would allow our users to select a set of filters and a set of date ranges that the report would cover.

The MaterialDatePicker came with some pre-built components that we could leverage to accomplish our use case.

For our most common case, allowing a user to select a range of dates, the pre-built MaterialDatePicker would suffice:

With this code block, we get a Date Picker that allows users to select a date range.

Monthly date picker

One of the Sprout Social reports that has more unique date selection is the Twitter Trends Report.

This report differs from the others in that instead of allowing any kind of date range, it enforces a single month selection, meaning a user can only select March 2020 vs March 3 to March 16, 2020.

Our web app handles this by using a dropdown form field:

The MaterialDatePicker does not have a way to enforce such a restriction with the pre-built Material Date Range Picker discussed in the previous section. Fortunately, MaterialDatePicker was built with composable parts that allow us to expand the default behavior for our particular use case.

Date selection behavior

The MaterialDatePicker leverages a DateSelector as the interface used for the selection logic of the picker.

From the Javadoc:

“Interface for users of {@link MaterialCalendar<S>} to control how the Calendar displays and returns selections…”

You’ll notice that the MaterialDatePicker.Builder.dateRangePicker() returns a builder instance of RangeDateSelector, which we used in the example above.

This class is a pre-built selector that implements DateSelector.

Brainstorming a monthly date selection behavior

For our use case, we wanted a way to have our users select an entire month as a selected date range; e.g. May 2020, April 2020, etc.

We thought that the pre-built RangeDateSelector referenced above got us most of the way there. The component allowed a user to select a date range and enforce a [lower, upper] bound.

The only thing that was missing was a way to enforce a selection to auto-select the entire month. The default behavior of RangeDateSelector has the user select a start date and an end date.

We wanted a behavior so that when a user selects a day in the month, the picker will then auto-select the entire month as the date range.

The solution we decided on was to extend the RangeDateSelector and then override the day selection behavior to auto-select the entire month instead.

Luckily, there is a function we can override from the interface DateSelector called: select(selection: Long).

This function will be invoked when a user selects a day in the picker, with the selected day passed in UTC milliseconds from the epoch.

Implementing a monthly date selection behavior

The implementation turned out to be the simplest part, since we have a clear function we can override to get the behavior we want.

The basic logic will be this:

  1. User selects a day.
  2. The select() function is invoked with the selected day in a Long UTC milliseconds from the epoch.
  3. Find the first and last day of the month from the given day passed to us.
  4. Make a call to super.select(1st of month) & super.select(last day of month)
  5. The parent behavior from RangeDateSelector should work as expected, and select the month as a date range.

Putting it all together

Now that we have our Custom MonthRangeDateSelector, we can set up our MaterialDatePicker.

To take the example further, we can process the result of the selection like so:

The result will look like this:

Gotchas

There was just one major issue that made it difficult to arrive at this solution.

The primary components used to build our MonthRangeDateSelector were the class RangeDateSelector and the interface DateSelector. The version of the library used in this article (1.2.0-beta01) restricted the visibility of these two files, to discourage extending or implementing them.

As a result, although we could successfully compile our new MonthRangeDateSelector, the compiler did show a very scary warning to discourage us from doing so:

One way to hide this compiler warning is to add a @Suppress("RestrictedApi") like so:

This experience illustrates how, even though the Material Components Library has provided some great new components to the Android Developer Community, it is still a work in progress.

A great part of this library is the openness to feedback from the Android Community! After discovering this component visibility restriction, I opened an issue on the Github Project, and even opened a PR to address it right away.

This open feedback loop between the Material Components Team and the Android Community breeds great collaboration and results for everyone.

Conclusion

The new MaterialDatePicker has some great out of the box functionality that will likely cover most use cases of date selection.

However, the best part of it over something like the AppCompat CalendarView is that it is built in a composable way. Therefore, it can be easily extended and modified for specific use cases, whereas it would be much harder to accomplish such things in the CalendarView.

Special thanks

I’d like to highlight some folks that helped peer-review this article:

This post Engineering at Sprout: Building an Android month picker originally appeared on Sprout Social.

If you liked Engineering at Sprout: Building an Android month picker by Felipe Roriz Then you'll love Miami Internet Marketing Consultant

Leave a Reply

Your email address will not be published. Required fields are marked *