The Browse page refactoring journey on Android

Jintin
Jintin
Oct 22 · 7 min read

As a classifieds marketplace, search and browse are essential features for users as they buy and sell items. To make the buying and selling process as seamless as possible, we continually make improvements to our platform, finding more efficient ways and adding new features based on product learnings and user feedback.

For the Android team, we found that due to rapid changes, tech debt and premature architecture decisions, our productivity in working on the Browse page got slower over time. We wanted to find a way to make programming more effectively and efficiently.

In this article, we’ll walk you through the problems we faced, the thought process on how we tackled them, and the tools and libraries that played a big part in our solution.

Problems we faced

The app itself is built with multiple Activity structures to keep different features decoupled. However, the Browse page is created using a single Activity with the MVP (Model-View-Presenter) architecture and a RecyclerView that contains more than 10 view types. Here are the problems we faced with this approach:

Refactor, rewrite, or…

In order to eliminate tech debt, refactoring — either as a one-time effort or doing it constantly — is important. In order to keep everybody in the team on the same page, we should clearly explain what are the benefits or problems we want to fix with a refactor.

After recognizing the problems, we brainstormed various approaches to fix them, and each approach had different pros and cons:

Rewrite

The most straightforward option is to rewrite everything in a good manner. However, it can be more complex than you think.

  • Pros:
    - Easy switching: You can have two implementations and switch over to the new one when you’re ready.
    - Probably the cleanest way because you rewrite everything without bothering with legacy code.
  • Cons:
    - Need to replicate all features and behaviors fully. However, there are always hidden features or behaviors, especially we’re lacking detailed documentation describing all of these features, so we aren’t always aware of the expected behavior in some cases.
    - Business has to continue: When a new feature is added during the rewrite, we have to implement it twice, in both the old and new code.
    - It might take months based on the scale of the rewrite. And there could be some factors that terminate the rewrite, like urgent business needs.

Refactor

The second option is to modify and re-architect the existing code, so there is only one implementation.

  • Pros:
    - We can make incremental improvements to our existing code and reap the benefits of these changes right away, without having to wait for the outcome of a prolonged rewrite.
  • Cons:
    - Risks of unexpected side effects are high, which affect existing functionality
    - Refactored code needs to be compatible with the legacy parts which we choose to retain, like function signature or interface which makes it harder to implement a clean architecture sometimes.

Both approaches have their pros and cons. In fact, we don’t have to stick with a single approach, and this is what we eventually did:

  • Split big components into concise components
  • Evaluate each small component independently, and decide whether they need a rewrite or refactor later.

Decision making process

We described the problems in the Activity, Presenter, and Adapter. Let‘s solve them one at a time…

Activity

We used a custom View wrapped by a ViewStub to combine different Views into the same screen and control the visibility when needed. ViewStub is good for a small portion of your page, because it will only create the View when you need it. But then, Activity needs to handle more responsibility than it should, like in the onBackPressed, it needs to check if another View is present before it actually closes.

fun onBackPressed() {
if (viewCollection != null && viewCollection.isVisible()) {
viewCollection.hide()
} else if (viewSearch != null && viewSearch.isVisible()) {
viewSearch.hide()
} else {
super.onBackPressed()
}
}

If you split features into different Activities, this problem will be solved automatically. If you use Fragment, you can also intercept the back press event like this:

activity?.onBackPressedDispatcher?.addCallback(owner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
activity?.supportFragmentManager?.popBackStack()
}
})

Both approaches will make the communication between each layer cleaner and predictable. So, if you have such a complex screen that has many features that use a single Activity, try to split it into multiple Activities or Fragments and delegate feature-specific logic to them.

Presenter

After we split a big Activity into multiple Activities or Fragments, the next step is refactor the Presenter.

We extract feature-specific business logic to Domain classes. They relieve the responsibility of a single Presenter and are easy to share across different Fragments. And it’s also good to think of a feature from a Domain perspective first and try to reuse it across different screens. Now that the Presenter has less code, it makes the intention much clearer and easy to migrate when we want to change to another architecture like MVVM.

Adapter

There are many topics that can be discussed from an Android developer's point of view since this layer is directly related to the UI and will usually be a monster when you need to deal with lots of viewType or grouping.

The first problem for us is grouping: We want to show different items in different sections, but handling this logic is difficult since the data are coming from different sources, and each of the items might have a different span size.

We can leverage ConcatAdapter from Google, which helps us to group different sections easily. A simple example:

val adapterA: AdapterA = …
val adapterB: AdapterB = …
val concatAdapter = ConcatAdapter(adapterA, adapterB)
recyclerView.adapter = concatAdapter
val adapterC: AdapterC = …
concatAdapter.addAdapter(adapterC)

As you can see, each Adapter will handle their data separately, so we don’t need to worry about grouping anymore, but still, we can improve the use of ConcatAdapter:

Single item in Adapter:

It’s not actually a problem, but sometimes Adapters in such use cases will be a bit of an overkill and will require boilerplate code even if we just want to display a simple header or footer, so we can use the SoloAdapter open source project to deal with such cases.

And also, by dividing different sections into different Adapters, some Adapters might only need to deal with a single ViewType. The MonoAdapter open source project can be useful in this case.

Span size lookup:

To simplify setting a span size lookup, use the ConcatAdapterExtension open-source library to:

Example:

val adapter = ConcatAdapter()
val layoutManager = GridLayoutManager(this, spanCount, GridLayoutManager.VERTICAL, false)
layoutManager.spanSizeLookup = ConcatSpanSizeLookup(spanCount) {
adapter.adapters
}
recyclerView.layoutManager = layoutManager

Murphy’s Law

One thing to highlight is that regardless of how well you plan the refactoring, there are still chances that it may cause issues without you being aware. You can and should leverage on automated regression testing to improve your confidence to make these larger code changes.

Always having a Plan B will make your life much easier. Like having a feature flag to turn off the change on production if a problem occurs. We also encountered a careless issue in our first release, but we turned off the change very quickly and at almost no cost.

Conclusion

This is just the first step we took to make future refactoring of this large and challenging feature easier. With smaller and more concise components, we can have more confidence to do more subsequent refactoring, like migrate to Kotlin, adopt MVVM, and many more.

Interested to read more tech learnings? Please subscribe to our blog to see our upcoming updates. Check out our careers site to join us in our Android journey.

Carousell Insider

What's going on under the hood at Carousell

Carousell Insider

What's going on under the hood at one of the world's largest and fastest growing classifieds marketplace. We're on a mission to inspire everyone in the world to start selling.

Jintin

Written by

Jintin

Android/iOS developer, husband and dad. Love to build interesting things to make life easier.

Carousell Insider

What's going on under the hood at one of the world's largest and fastest growing classifieds marketplace. We're on a mission to inspire everyone in the world to start selling.