Action Handling On Android: A ‘GoPay Social’ Case Study
An analysis of updating outer screens on Android when an action is performed in an internal flow.
By Karan Trehan
Peer-to-peer money transactions through mobile apps have become increasingly convenient and effortless. It’s an ever-changing landscape, with a lot of potential to be interactive.
In November 2020, we launched GoPay Social. It allows our users to have a social interaction (called ‘moments’) attached to every peer-to-peer money transaction. Users can see their friends’ moments if their friend has opted for it to be visible, interact with these moments on their feed, and whole lot of things to make these transactions interactive and not tedious.
Read more about GoPay Social here, or check out the video:
Now that you have a fair understanding of what GoPay Social does, let’s dive into how it all happens — The BTS of Android implementation for interactions on our social flows.
Since the user can perform interactions with a moment on the feed, as well as the details screen, the interaction on the detail screen needs to be updated on the outer feed screen as well. These internal flows can have multiple depths. For example:
Feed > Clicked Moment A Details from Feed > Liked on Detail > Back to Feed (should update Moment A)
Feed > Clicked Friend A’s Profile > Clicked Moment A Details from Friend A’s Profile > Liked on Detail > Back to Friend A’s Profile (should update Moment A) > Back to Feed (should update Moment A)
Hence, we needed a solution to:
- Expose user actions in internal screens to external screens
- Handle actions from completely different flows. For example: When a P2P transfer is successful, the user’s feed needs to be updated
- Update the outer screen elements only when a change has occurred in the internal flows and screens
- startActivityForResult() + onActivityResult()
- EventBus / LocalBroadcast
- Or… could it be something else?
startActivityForResult() + onActivityResult()
Using startActivityForResult() one can start another activity and receive a result in its onActivityResult() callback. You need to setResult() from each child activity.
This was the first solution we spiked out. It allows us to build a chain of parent and child activities. Child activities can expose actions to their parent activities and the intricacies of the callback mechanism are handled by the Android framework for us.
This method works well if the callback is required at the single level.
Although, in the below example, we would need to update the post on two screens (Friend Profile and Feed) from the details screen. This chaining of callbacks became complex rapidly with added depth. 😥
- Easy to debug: Since there’s 1:1 mapping, debugging these flows is simple
- Simple flow: With 1:1 mapping, reading the code is simpler too
- Lifecycle-aware: onActivityResult() is lifecycle-aware and handles process deaths gracefully for us
- Callback handling abstracted: The callback handling is abstracted from us by the Android framework
- Chaining becomes difficult: As there’s 1:1 mapping, chaining callbacks from multiple depths is complex
- Boilerplate: Every flow needs the startActivityForResult(), setResult() & onActivityResult() boilerplate
- Tight Coupling: Due to the presence of 1:1 mapping, the flows are very tightly coupled
- Mandatory finish: Each child activity needs to finish in order to send results back
EventBus / LocalBroadcast
Android apps can send or receive broadcast messages from the Android system, other Android apps, and themselves, similar to the publish-subscribe design pattern.
This was another solution we spiked out. This would remove the tight coupling between the activities and allow us send events from one flow to another seamlessly.
This solution breaks if the Android system kills and recreates outer activities due to low memory. The outer activities will not receive the events at all. 😥
- Loose coupling: There is no 1:1 mapping between activities. Any activity can publish an event and any activity can subscribe to them
- Chaining is easy: As there is no tight coupling, chains of multiple levels can be created easily
- Multiple listeners: A single published event can have multiple subscribers
- Difficult to debug: As there is no mapping, event buses can become difficult to debug
- Some boilerplate: Although the amount of boilerplate is the least compared to other approaches, some code is required to publish as well as subscribe and unsubscribe to events
- Not life-cycle aware: If the Android system kills your activities, you will not receive the events in recreated activities
The final solution: LiveData
LiveData is an observable data holder class. LiveData is lifecycle-aware. It respects the lifecycle of app components. This awareness ensures LiveData only updates app component observers which are in an active lifecycle state.
This was the final solution we spiked out. LiveData could bring the advantages of both first and second approaches, with very little overhead.
We created a custom LiveData with two main functions:
- interacted(): To push an event
- interactionConsumed(): To consume an event and clear it from the holder
All the flows worked well in this approach. 🥳
- Multiple listeners: A single LiveData can have multiple observers
- Chaining is easy: As multiple observers can be active on a single LiveData, chaining the events becomes very simple
- Loose coupling: Using an observable pattern allows us to have loose coupling between the flows and LiveData
- Easier to debug: An observable LiveData is easier to debug compared to Event Buses
- Lifecycle-aware: A LiveData is lifecycle-aware and handles process death gracefully
- Boilerplate: This approach involves boilerplate around the LiveData itself, as well as the integrating components
- Can cause memory leaks: As we are using a Singleton LiveData instance, we can overlook some details and cause memory leaks.
Due to its various advantages, we implemented all action interactions on GoPay Social using LiveData and have not had any issues till date. 🖖
To read more stories from our vault, click here.
And we’re hiring! Click below to view open job positions.