Mobile Compose Refresh Architecture
If you’ve ever posted to a social network from Hootsuite’s iOS or Android application, then you’ve used the composer. The composer is where you write the message body of your post, maybe attach a video or GIF, and select the social networks to send it off to.
The code behind message composition, however, has grown unwieldy. The main activity on Android is somewhere in the order of 3,500 lines long. When it comes to implementing new features, it is difficult to add to and easy to break. As Publisher’s mobile team looked to revamp the design, refine some features and add others, developing a new architecture for message composition was the clear first step.
An End-to-End WalkthroughAt a high level, the architecture for Compose is the same for Android and iOS. It centers around the idea that each section of the composer can be treated as its own separate component. The social network picker is independent of the message body; the message body is independent of the attachment previews. Together they form a stack, where each section of the stack is incognisant of any other.
Each of these sections in the stack has its respective view and view model, where each element knows about the element to its right: the view is aware of the view model; the view model is aware of the message model. The message model is the component that contains all properties of the message: the selected social networks, the attachments, the attached links, etc. When it comes time to send the message, these fields are combined to perform the send. Likewise, when a message needs to be edited, it is these fields that are set with the existing message data.
How then is the message model’s information propagated leftwards to the view models and then the views? Each view model subscribes to the relevant message model properties using Rx. So the attachment preview’s view model, for instance, has subscriptions to the message model’s attachments list as shown below, so that it updates when an attachment is added, and to the list of selected social networks, so that it displays the appropriate attachment metadata.
When it comes to communicating these updates from the view model to the view the two platforms differ. On iOS, the ViewController uses RxSwift to subscribe on the view model, just as was described with the view model and message model above. Data binding to the view itself is done using RxCocoa. On Android, the view binds to Observable fields in the view model using Android’s native data binding library as shown below in a small example of how the attachments preview binds to its view model.
This structure allows for the views to be kept dumb: they perform no calculations. On receiving an event from the message model, the view model takes the updated value, performs any needed calculations, and then updates the value to which the view is bound. This way the views are kept lean. They deal only with updating the view. Previously, a class dedicated to displaying the list of attachments, for example, was also performing the actual attachment upload. Now, that’s handled separately. The view simply updates based on the attachment properties.
Interacting Through the Message ModelThis architecture follows the MVVM pattern, with the key difference, of course, that it is not limited to one view, view model, and model. Instead, there is a single message model and many view models, each with their corresponding views.
To better illustrate how the view models and message model interact, consider the example of the send button. A user should be able to tap the button to send if, at minimum, the message body is non-empty and a social network is selected. As the user types and selects social networks, view models are updating these values in the message model: the profile picker’s view model updates the message model when the selected social networks change and the message editor’s does so on each text change. Meanwhile, a third view model has a subscription setup to observe on these changes to the message model and update the send button accordingly. Every time an update is emitted the latest values for the message body and social networks are checked. If the above conditions are the met, the send button is enabled.
All three view models, then, observe and update the message model without any knowledge of each other. And by extension, any view model can be added or removed without breaking any other. All that would change is if and when the message model is updated. From a development standpoint, the implications of this are quite powerful: we go from a virtual guarantee that something will break each time we add or remove a feature in our current code to a modular architecture that easily withstands these changes.
Working with the Android LifecycleImplementing this architecture in the context of Android’s activity lifecycle presents its own challenges. It was a decided goal that the main activity not get bloated, and more importantly, that there be no temptation for future developers to simply add to this activity and not conform to the intended design. To accomplish this, the main activity, called ComposerActivity, is essentially limited to overriding two methods, onCreate and onDestroy. In onCreate, only two things are done: the databinding is set up and a call to each view model’s setup() method is made, which sets up the view model’s subscriptions to the message model; onDestroy only makes calls to each view model’s respective destroy method, which unsubscribes from any pending subscriptions.
Ideally, structuring the activity in this way makes clear that anything unrelated to subscriptions or view model set up does not belong, though we also resorted to some less than subtle methods ..
There are also instances where the view models require functionality that can only be accessed in the activity itself: for example, finding a view or fragment by id, accessing the fragment manager, starting an activity for a result, or handling the result that comes in. Result handling in particular makes up a substantial portion of the current activity. In the new implementation the ComposerActivity implements a small interface that is passed to the view models via their setup method. This allows the view models to access these functions and listen for and handle results from other activities. The main activity, meanwhile, remains relatively unexposed and free of the bloat typically caused by onActivityResult.
In SummaryHopefully this has given you a solid introduction to the new architecture and the specifics of implementing it on Android. And perhaps more importantly, hopefully this has given you a sense of what an improvement the new architecture is on our current version. All in all, that’s the big takeaway here: we’re now working with an architecture that is easy to add to and not so easy to break.
About the Author
Madeleine is a Software Developer Co-op on the Publisher team at Hootsuite. She enjoys reading, beer, the great outdoors, and doesn’t really like writing about herself in the third person.