The Browse page refactoring journey on Android

Jintin
Carousell Insider
Published in
7 min readOct 22, 2021

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:

  1. Packing too many features in a single Activity
    Since it is a single Activity without any Fragment, all of our features, including searching, filtering and category selection, are placed in that Activity. Ideally, they should be standalone components.
  2. Presenter logic is way more complex than usual
    The business logic for different features was written in a screen-level presenter. As a result, our presenter has more than 3,000 lines of code, which makes testing and maintaining harder and harder.
  3. Adapter is too complex and took on too much responsibility
    The Adapter lacks support for building several groups of items. However, this is a business requirement for our screens. Hence, we had to manually maintain the start index of each section, so that we can perform insert and delete operations at the correct position. This problem becomes even harder when our RecyclerView is presented as a grid with different span sizes across view types.

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:

  1. Make the child Adapters implement the SpanSizeLookupOwner interface
  2. Create a SpanSizeLookup instance in each of your child Adapters
  3. Create a ConcatSpanSizeLookup instance with your ConcatAdapter and set a max span count

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.

--

--

Jintin
Carousell Insider

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