Clean Android Code: Activities and Fragments

Anton Averin
7 min readJun 8, 2016

--

Clean Android Code articles series:

  1. Clean Android Code: Main
  2. Clean Android Code: Building with Gradle
  3. Clean Android Code: Preparation
  4. Clean Android Code: Activities & Fragments
  5. Clean Android Code: Network & Data
  6. Clean Android Code: Navigation & UI
  7. Clean Android Code: Event Bus
  8. Clean Android Code: Error Handling
  9. Clean Android Code: ViewPager with View Pages
  10. Clean Android Code: Clean Adapter
  11. Clean Android Code: …

Activities and Fragments are the most critical parts of Android application. Both represent an application screen that user will see. The difference would be that Fragment is more like a sub screen, or an independent part of the screen.

They are tied together by a lifecycle, that can become pretty complex

There are quite some posts in the net about how Fragments are broken. Square advocated against using them and in general community isn’t very happy with Fragments. And even though more often Fragments are just miss-used to replace ViewGroup and custom Views, there are still some bad issues you stumble upon when using them.

On the latest Google I/O Adam Powell made a very nice talk about what and why in Fragments, and how Android team is making them better, so hopefully most critical issues are or will be resolved soon.

What the Fragment?

But let’s see how can we clean things up when building UI.

Clean screens

The biggest problem with Activities and Fragments in Android is that they end up doing too much. Activity/Fragment will be showing things on the screen, and preparing what to show, and talking to the network somehow, and when screen becomes more complex code quickly goes out of hand and it’s impossible to understand what is really going on.

This is the code of a Fragment from my project from a couple years ago.

You can try to walk from function to function, but if right now anything breaks in the app it will be very difficult to fix anything. And, to be fair, in some cases code gets even worser then that.

Such code is hard to read and understand, difficult to maintain and extremely difficult to refactor because there are no tests.

In the end, Screen becomes a kind of a god-class that knows everything about anybody — navigation, network, processing data. That strongly contradicts with clean architecture concepts.

How can we clean this up?

Easily. We just make screen as dumb as possible, and move all the responsibilities to other classes that we can make more precise and clear.

DataBinding will help us remove boilerplate code like setText or setVisibility — lots of such lines of code are usually in Activity when screen is complex

We introduce a kind of class, who’s job would be preparing everything needed for screen to operate correctly — a Presenter. It will be reacting to UI events, talking back to the screen(View) to display something, and asking network for data. Somebody would also like to add Interactor’s too, I usually do it only when presenter starts getting out of hand. A good thing with Presenter is that you can completely test it.

Another neat thing that we can do is make sure Activity and Fragment doesn’t really do UI in terms of UI logic. As an entry points to the screens, they will be tying things together, but the real job of doing UI will be delegated to ViewExtension. ViewExtension is something that knows how to properly display some particular part of the screen. You could have NavigationDrawerViewExtension, or RefreshableRecylerViewExtension, or even ClusteringGoogleMapViewExtension. And, even though ViewExtension is kind of UI, you can test it completely.

Screen(View) will know about Presenter, so it can talk to it. Presenter will only know about a ViewContract, so it will be able to talk back to View indirectly. A ViewExtension will be a part of View, because we just move things to another class for better readability. But how would ViewExtension talk to Presenter? Someone would suggest EventBus, but we can also use Delegate pattern — it will be more exact and we don’t really need those ViewExtension events everywhere.

Now that was a lot of reading, let’s do some coding.

First, let’s introduce some contracts that our View would comply to

Hint: In Kotlin file can have many interfaces and classes in it

interface ViewContract

interface ScreenContract: ViewContract
interface ViewExtensioninterface EventsDelegate
interface EventsDelegatingViewExtension<D : EventsDelegate> : ViewExtension {
var eventsDelegate: D?
}

ViewContract is something a View would comply to

ScreenContract for screens

ViewExtension for view extensions

EventsDelegate is a contract for events, that ViewExtension can fire

And EventsDelegatingViewExtension with abstract property, so we would not forget our events

Hint: In Kotlin interfaces can have properties, that would have to be backed by implementation in class, that implements and interface. Also, interfaces can have a default implementation. We will see an example of that in later articles

A BasePresenter is something that knows about something, that complies to the ScreenContract — a view

abstract class BasePresenter<V : ScreenContract> {
var view: V? = null
}

Lifecycle

One important topic that I didn’t cover is lifecycle. You see, if we will be using ViewExtensions to implement things like GoogleMapViewExtension or NavigationDrawerViewExtension, we will have to pass some events to MapView or NavigationView. And our presenters would also want to handle some lifecycle events, like doing a network request when screen shows up, for example.

To resolve this, here is a suggestion. Let’s have some LifecycleDelegate classes, and subscribe our components to receive various lifecycle evens. BaseActivity and BaseFragment would be responsible for passing events properly to all delegates.

interface MainLifecycleDelegate {

fun onCreate(savedInstanceState: Bundle? = null) {
}

fun onStart() {
}

fun onResume() {
}

fun onPause() {
}

fun onSaveInstanceState(outState: Bundle?) {
}

fun onStop() {
}

fun onLowMemory() {

}

fun onDestroy() {
}
}
interface ExtraLifecycleDelegate {
fun onBackPressed(): Boolean {
return false
}

fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

}

fun onNewIntent(intent: Intent?) {

}

fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {

}

fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return false
}
}

This is a full list of events we would possibly want to notify our extension components about. They would be attached somewhat like this:

abstract class BaseActivity : AppCompatActivity() {
private var mainLifecycleDelegates: Array<out MainLifecycleDelegate> = arrayOf()
private var extraLifecycleDelegates: Array<out ExtraLifecycleDelegate> = arrayOf()

protected open fun setMainLifecycleDelegates(vararg mainDelegates: MainLifecycleDelegate) {
mainLifecycleDelegates = mainDelegates
}

protected open fun setExtraLifecycleDelegates(vararg extraDelegates: ExtraLifecycleDelegate) {
extraLifecycleDelegates = extraDelegates
}
override fun onStart() {
super.onStart()
mainLifecycleDelegates.forEach { it.onStart() }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
extraLifecycleDelegates.forEach {
it
.onActivityResult(requestCode, resultCode, data)
}
super.onActivityResult(requestCode, resultCode, data)
}
}

A full implementation code can be found in BaseActivity and BaseFragment

Another important addition to BaseActivity and BaseFragment classes

abstract fun doInjections(activityComponent: ActivityComponent)

This is needed to make sure all dependencies are injected in our child activites.

And, of course, this article would not be full without an example. Let make some small activity for starters

MainActivity

I would suggest we make an main activity with a navigation drawer. Almost every Android application now has one.

First let’s make a MainPresenter. It will be of ActivityScope, will know about MainScreenContract and will know about NavigationDrawerViewExtension with it’s events

@ActivityScope
class MainPresenter @Inject constructor(
) : BasePresenter<MainScreenContract>(), ExtraLifecycleDelegate, NavigationDrawerViewExtensionDelegate {
lateinit var navigationDrawerViewExtension: NavigationDrawerViewExtensionContractoverride fun showInitialScreen() {
}
override fun showNav1Screen() {
}
override fun showNav2Screen() {
}
}

MainActivity will be tying things together.

We inject the presenter as a dependency. We override doInjections method, where we are doing the binding, injecting in all dagger dependencies (this is the moment when presenter is initialized). Then, we also make sure our presenter subscribes to MainLifecycleDelegate and ExtraLifecycleDelegate. We set the view on the presenter, and we ask it to show initial screen of the application — this would be a main fragment later on. We also inject our NavigationDrawerViewExtension, and make sure it would receive lifecycle events too. We also set an instance on a presenter, so presenter could talk back to view extension. And, of course, NavigationDrawerViewExtension should receive views it will be using to display things.

class MainActivity : BaseActivity(), MainScreenContract {

@Inject lateinit var presenter: MainPresenter
@Inject lateinit var navigationDrawerViewExtension: NavigationDrawerViewExtension

override fun doInjections(activityComponent: ActivityComponent) {
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

activityComponent.injectTo(this)
setMainLifecycleDelegates(presenter, navigationDrawerViewExtension)
setExtraLifecycleDelegates(presenter)

presenter.view = this
presenter.navigationDrawerViewExtension = navigationDrawerViewExtension

navigationDrawerViewExtension.setViews(binding.drawerLayout, binding.navigationView)

presenter.showInitialScreen()

setSupportActionBar(binding.toolbar.toolbar)
}
}

And NavigationDrawerViewExtension — class that actually knows how to handle DrawerLayout and NavigationView. DraweToggleFactory is a helper class passed as a dependency. Notice that we don’t talk to presenter directly, and we actually don’t care who will be handling events — we just pass them via EventsDelegate

You will be using a similar pattern for every screen and view extension. Bad thing — that’s a lot of tying up to do. Good thing — that’s all Activity and Fragment will be doing, and now we can test our UI by mocking all of the dependencies.

MainPresenter itself does not do anything for now, except receiving lifecycle events. MainPresenter is Injectable and is of ActivityScope — it will be unique for the currently displayed Activity. Also, it is a delegate of NavigationDrawerViewExtension — it will handle all events coming from it.

You can find a full implementation code on Github.

I keep mentioning tests, so let’s actually write some

Testing

Out of all classes we have written we wouldn’t test Base things, like BaseActivity and BaseFragment. We can’t test MainActivity, and, actually, there is not much to test there — it’s only tying things up together, delegating all responsibilities to other classes.

We will also have some cases when we have to use a library, or a platform component that we will not be able to test. To make sure that our code operates correctly, such platform classes will need to be wrapped by our class. Example of such class is DrawerToggleFactory.

The good news — we can test presenter and view extension. MainPresenter does nothing at this point, so we will add it’s test later when there will be some logic. Let’s make sure our Navigation Drawer works as expected.

I will be focusing more on testing in further chapters, but feel free to look the code for now.

You can see all changes related to this part of the series on Github branch.

Clean Android Code articles series:

  1. Clean Android Code: Main
  2. Clean Android Code: Building with Gradle
  3. Clean Android Code: Preparation
  4. Clean Android Code: Activities & Fragments
  5. Clean Android Code: Network & Data
  6. Clean Android Code: Navigation & UI
  7. Clean Android Code: Event Bus
  8. Clean Android Code: Error Handling
  9. Clean Android Code: ViewPager with View Pages
  10. Clean Android Code: Clean Adapter
  11. Clean Android Code: …

--

--