Photo by Danting Zhu on Unsplash

Using ReactiveX to Implement Impression Analytics on Android

Stephen Jan
ClassPass Engineering
6 min readApr 24, 2019

--

If there’s one thing that keeps me up at night, it’s the fear of crashes from the ClassPass app tangling with the Android lifecycle. As much as I try to catch issues in advance, the threat of missing something always exists. Building an app on Android can feel like building a house atop ice sheets, while floating down a river.

These days, many in the mobile community point to ReactiveX as the go-to framework to tackle systems that depend on changes from multiple sources. In this article, I describe using RxKotlin and RxJava to implement a complicated Search Item Impression Analytic event.

Search Item Impression

Recently, the ClassPass Analytics team asked the Mobile Engineering team for an Impression event on the search screen. This new feature really embodied the type of change with lots of crash potential because visibility depends heavily on Android lifecycle behavior — which isn’t always predictable, especially when the screen is complicated. I knew if I implemented the feature the way we approached all of our other analytic events, it would have taken forever, and the app would be more prone to crashes.

Why were even doing this? The Analytics team wanted to know which results a user actually viewed. This way, we could get a better idea of which results the user wants to see.

Class Search Screen

Find a Class Search Screen

Since we didn’t want to count impressions for results that the user quickly scrolls through, the Analytics team added the constraint that we would only count results that were on screen for at least one second and ignore others.

Don’t count impressions for spinning

Do count impressions for slow scrolling

Previous Analytics Approach: if{} else {}

Now, this isn’t the first analytic event for the app. But all of our previous events were local to one part of the screen. All we had to do was identify the conditions that would trigger the analytic event and fire event. This wouldn’t have worked for the search impression feature. First, the search screen consists of nested views which makes the logic around it more complicated. Second, the impression event depends on two other events — search item visible and search item hidden.

Nested Views

For example, if I were to take the old approach, consider the scenario where the user scrolls down the list.

  1. User scrolls to item 1, impression for item 1 starts
  2. User scrolls to item 10 item 1 leaves screen, impression for item 1 ends
  3. If the time between scroll 2 and scroll 1 is longer than 1 second, send analytic event.

Another scenario could be the user panning between pages.

  1. User searches for results, impression for item 1 starts
  2. User pans to the next day, impression for item 1 ends
  3. If the time between swipe 2 and swipe 1 is longer than 1 second, send analytic event.

Yet another scenario could be a mix of interactions.

  1. User scrolls to item 1, impression for item 1 starts
  2. User quits app, impression for item 1 ends
  3. If the time between scroll 2 and quit 1 is longer than 1 second, send analytic event.

As you can see, there are many combinations of show and hide. Furthermore, a future change affecting visibility could easily break the logic.

Analytics with ReactiveX

Rather than list out all the combinations of gestures and lifecycle events that hide and show items on the screen, we used ReactiveX to create streams of visible items between the layers. In the following section I detail the logic breakdown.

Step 1: Stream of Visible Items from List

The source events to the innermost List layer are viewAttached, viewDetached, and refreshData. As the user scrolls through the List, Android signals to the app when rows are attached and removed from the List. Using these signals, we checked the view hierarchy to see which rows are in view and which are not. We then compiled the list of results and published them as an Observable stream.

Step 2: Stream of Visible Items from Pager

The list view is encapsulated by a ViewPager. This pager allows the user to swipe between days. When the user changes the current date, this layer will stop subscribing from the previous date’s visible list and subscribe from the selected date’s visible list.

Step 3: Visible List Stream to Impression Stream

The list search layer combines events from the Activity lifecycle and the visible item list stream from the pager into individual impression events for each search item. To compute impressions, we compare the current visible list and the previous visible list.

The following marble diagram illustrates the transformation from the source visibility list event to item impression events. In this particular example there are three user events.

  1. Initial screen state.
  2. User scrolls down one cell — item 1 is now hidden, 5 is visible
  3. User quits — Screen cleared and all items hidden.

Step 4: Impression Steam to Impression Analytic

The input to the final step are the Show Impression and Hide Impression events. The logic chain in this layer includes four main steps.

  1. Match events that correspond to the same item — groupby operator on item id
  2. Transform events into interval events — the time between each show and hide — TimedEvent
  3. Filter events where time elapsed is less than 1 second — filter
  4. Record Analytic

Continuing from the scenario above, consider the case where the user scrolls down at 0.9 seconds, and exits 0.3 seconds later. This means item 1 was on screen for .9 seconds and item 2 for 1.2 seconds.

Marble diagram: no impression event for item 1

Marble diagram: impression event for item 2

ReactiveX Helps Me Sleep

By approaching the problem with ReactiveX, not only did we capture the requirements for this analytic event, we also introduced a new way to compose complex analytics event without introducing instability.

ReactiveX is a relatively new tool for ClassPass. Previously, the main use was only for making API calls. We are quickly discovering applications beyond just network calls. Composing the proper stream does take a lot of critical thinking, but for the amount of sleep I get back, I’d say it’s worth it.

You’re reading the ClassPass Engineering Blog, a publication written by the engineers at ClassPass where we’re sharing how we work and our discoveries along the way. You can also find us on Twitter at @ClassPassEng.

If you like what you’re reading, you can learn more on our careers website.

--

--