On my first day at Clubhouse, I set out to build our Android app from scratch. Casual.
Building an Android app from scratch isn’t something you typically have the chance to do as an engineer — and doing it from zero to 100 in ten weeks with a tiny team has been an example of having to figure it all out as we go, as quickly as we can.
A big part of working at Clubhouse is acknowledging that we will face challenges, and that there is no blueprint laid out for us. In moments like these we are scrappy and resourceful, knowing that we have no choice but to figure it out. In the end, we always find a way, which is one of our core values.
This post is a peek into my experience - what our process was like, what challenges we solved, and the things we learned along the way.
Going in, my goals were:
- Launch a high quality Clubhouse Android client: in terms of maintainability, reliability, testability, and extensibility
- Consistency with modern Android development practices: this would get us a long way towards the definitions of high quality above
- Clarity for developers: we’re a small team and allowing non-Android developers to jump into the codebase and quickly see a pattern for how we build features would go a long way in helping us move fast.
- Build a great team: to do the above!
Luckily, while we were starting from scratch with the codebase, we were not starting from ground zero with the app; we already had a great iOS app that we could build off of. The iOS app gave me a launchpad to craft an app that made sense for Android. For example, from building iOS we had learned that large rooms caused performance issues related to updating the UI with the state of the room so we knew to architect rooms on Android with this in mind.
Daniel, our only iOS engineer at the time, set up a weekly sync so we could walk through technical details of how the iOS app worked and chart a path towards what we needed to build on Android. We had a huge added bonus of learning from the team’s experiences from the months of building and maintaining the iOS codebase. He also provided very helpful flowcharts like the below as references:
For the first month or so, it was just me! This made my workflow incredibly simple. My days usually looked like: Weekly sync with Daniel to talk about progress and what was up next, syncs with Rohan (our CTO) to check in on progress and discuss hiring, lots and lots of heads down time coding.
Daniel reviewed Android PRs in the beginning (what a rockstar), and he also helped interview other engineers to join the fun. About five weeks in, David, Harsan, and Yury joined, and we introduced a daily 15-minute Android team check in, and got more clarity on a launch timeline as we moved more and more quickly. Here’s an early version of a lovely gantt chart created by David:
What We Built
So what did we actually build?
The Clubhouse Android app is a single-activity, multiple-fragments MVI app written 100% in Kotlin. There’s a bit to unpack there, so let’s go through it.
Having only worked with multi-activity apps in my professional work, I was already familiar with the pain points caused by having multiple entry points into an Android app and opted to avoid them. With a single-activity app, we have a simplified lifecycle. For example, `onStart` and `onStop` only happens if your app is being foregrounded and being backgrounded instead of being also called as you’re navigating from activity to activity in the app. We can also avoid `startActivityForResult` or calling `finish` and other workarounds to share data and state between screens.
I’m sure every Android engineer has their own personal journey with Fragments — war stories and all — but we’re using AndroidX Fragments as our view layer in the app. However, our views are intentionally simple (more on that later), and it doesn’t matter much whether we’re using fragments or custom views on the view layer — at least not so far. Android views are typically the most difficult part of an app to test; UI tests are slow, brittle and require an emulator or physical device. Given this, our Fragments are as simple as possible and just observe the view state from the view model and render the layout accordingly.
We’re using the Jetpack Navigation components to implement navigation between our various screens, though we’re already hitting against the limits of the component (why and how I’ll save for a future blog post).
We’re also using Airbnb’s Mavericks framework with some additional modifications for an MVI architecture pattern. With this, we can represent every screen’s state as an immutable Kotlin data class owned by the ViewModel that contains all logic required to render the layout. For more ephemeral, action-driven state (e.g. user inputs like a button click, or one time messages like toasts and banners), we extended our Mavericks ViewModels to process incoming user input as a Kotlin Flow of Intents and to emit one-off events as a Kotlin Flow of Effects that the view subscribes to.
We can then define Effects and Intents as follows:
And process them as follows in our ViewModels and in our Fragments:
This MVI architecture also allowed us to address potential performance issues with relative ease. For example, we knew from learnings from the iOS app that we would eventually run into UI lag issues for very large rooms due to constant updates to the view state as listeners joined, left, and went up on stage. To solve this, we could use kotlin flow features to batch view state updates that are usually frequent (join room, leave room, make speaker) and to throttle updates of the channel view state in the viewmodel at a rate based on the size of the room.
Given our architecture pattern and goal to create clarity for developers, adding a new feature to the Clubhouse app boils down to the image below.
We’ll have the UI as a Fragment — It should be as simple as possible, mostly subscribing to state updates from its ViewModel and rendering the UI accordingly. The ViewModel provides the data for a specific UI component and emits updates as state objects. Our Repos handle data operations; they act as the reconciliation layer between server and local data sources.
I learned a ton throughout this process, but what surprised me the most was how long it took to do the foundational work that was needed for us to get to something that looked like the Clubhouse app we all love and use today. I was probably working on onboarding for a month out of the 10 weeks because, in the beginning, every new screen built came with a bunch of setup work! Let’s create our network layer! We need a local data layer, gotta go build that!
And like most engineering projects, the hardest part isn’t necessarily in the technical work! What I found most challenging was context switching into the many different hats I’ve needed to wear: from interviewing and onboarding other engineers to grow the team — talk about building the rocket ship while flying it — to communicating progress and blockers as we went along, and to adjusting to getting to know my new coworkers in a remote world.
Ultimately, the key to our success has been our ability as a team to ruthlessly prioritize — even before we started building and as we went along, there were features that we marked as fast-follows to the initial Android launch. It was not feasible to try to launch the Android app with every feature from iOS at the get-go. Not only because it would have pushed our timeline back, but also because iOS was still releasing and shipping new features as we were building. We couldn’t play an eternal game of catch up so we focused on shipping core product features then filling in gaps as fast follows.
Like many mobile apps, the Clubhouse app probably seems like a small app — but building it revealed a huge surface area and some interesting challenges (ask me about building our “half profile” bottom sheet one day). I got the chance to move quickly and flex multiple skill sets and muscles to ship Android out in 10 weeks. And this is a regular occurrence here at Clubhouse — soon after the Android launch, we built and launched Backchannel on Android and iOS in 5 weeks! (Stay tuned for a future post about that!)
Thanks for reading! If you like working on big challenges like this or want to help us ship our first feature built with Jetpack Compose? Join us!
— Mopewa Ogundipe, Software Engineer
This post is part of our engineering blog series, Technically Speaking. If you liked this post and want to read more, click here.