/ Ervin Martirosyan

MVP Project Template Conductor + Nucleus + Rx2

Android introduced fragments in Android 3.0 (API level 11) primarily to support more dynamic and flexible UI designs on large screens, such as tablets. Because a tablet screen is much larger than that of a handset, there's more room to combine and interchange UI components. Fragments allow such designs without the need for you to manage complex changes to the view hierarchy. By dividing the layout of an activity into fragments, you become able to modify the activity's appearance at runtime and preserve those changes in a back stack that's managed by the activity.

Undoubtedly, use of fragments is a good approach in Android development, which gives an opportunity to divide a huge layout and its logic into small parts. This code is easy to maintain in the future. But along with advantages, they have a lot of weak points, such as:

  • extremely complex lifecycle;
  • the lifecycle of nested fragments is unintuitive;
  • no control over when a fragment is actually available: fragment transactions are put into a message queue that would be executed in the future, sometimes even after your Context is gone.

Fragment Lifecycle

Created by Steve Pomeroy, modified to remove the activity lifecycle, shared under the CC BY-SA 4.0 license.

 

In the Alternative

Open source community provides some interesting libraries for developing applications without fragments. The most popular - Flow and Conductor.

The first one (Flow) is a library that allows creating the UI via simple POJO classes, called “screens”. Each screen associated with a custom view with its own functionality and purpose. Flow also provides an opportunity to handle app navigation and backstack independently of the Activity and Fragment.

Unfortunately, Flow is still in the pre-release stage of development, with no official release in view. It finally hit 1.0.0-alpha in February 2016 followed by the latest release, 1.0.0-alpha3, in April 2017.

Therefore, at the time of this post, Conductor is preferable for use.

 

Conductor

From the official doc, Conductor is a small yet full-featured framework that allows building View-based Android applications. It provides a lightweight wrapper around standard Android Views that does just about everything you want.

Conductor is architecture-agnostic and does not try to force any design decisions on the developer. You tend to use either MVP or MVVM, but it would work equally well with standard MVC or whatever else you want to throw at it.

There are a lot of positives that Conductor give us:

  • easy integration
  • no Fragments
  • simple lifecycle and powerful lifecycle management
  • navigation and backstack handling
  • a beautiful and easy-implemented transition between views
  • state persistence
  • callbacks for onActivityResult, onRequestPermissionResult, etc.

The lifecycle of a Controller

 

Conductor has 4 main components:

  • Controller - the core component. It represents a View wrapper, which handles a lifecycle of a View.
  • Router - using for navigation and backstack handling for Controllers. It defers a responsibility to render and push Controller to ViewGroup via ControllerChangeHandler, which is specified in ControllerTransaction.
  • ControllerChangeHandler - a component, which is liable for swapping Controllers. There are some default ControllerChangeHandlers inside the library.
  • ControllerTransaction - using for defining data about adding Controllers. In contrast to FragmentTransaction, ControllerTransaction is executed immediately.

 

Show me Your Code

Let’s see how it works in practice. The example will show the basics of Conductor library in conjunction with Nucleus for MVP architecture. Nucleus is a simple Android library, which utilizes the Model-View-Presenter pattern to properly connect background tasks with visual parts of an application. For more information about Nucleus library, please visit Github page.

The example was written on Android Studio 3.0 using Kotlin. A full code example could be found on Github.

 

Activity

Let’s create a component and a module for Activities. The module and component will have an @ActivityScope, i.e. all the provided dependencies will be tied to Activity lifecycle. For more information about custom scopes, please read this article.

First of all, let’s create BaseActivity, which encapsulates the logic of component creation and injection of each Activity to the graph of dependencies. Also, BaseActivity will set up Conductor’s Router for each of its child. The Router is provided by Dagger2.

For creating Router, we have to use the factory method attachRouter, which receives several parameters:

  • activity - the Activity that will host the Router being attached.
  • container - the ViewGroup, in which the Router's Controller views will be hosted.
  • savedInstanceState - the savedInstanceState passed into the hosting Activity's onCreate method. Used for restoring the Router's state if possible.

ActivityModule.kt

@Module class ActivityModule(private val activity: Activity, private val container: ViewGroup, private val bundle: Bundle?) {

   @Provides @ForActivity fun provideActivityInstance() = activity

   @Provides @ForActivity fun provideRouter() = Conductor.attachRouter(activity, container, bundle)

}

 

ActivityComponent.kt

@ForActivity @Subcomponent(modules = arrayOf(ActivityModule::class))

interface ActivityComponent {

   fun inject(activity: MainActivity)

   fun inject(activity: SecondActivity)

   fun activity(): Activity

   fun router(): Router

}

 

BaseActivity.kt

abstract class BaseActivity : AppCompatActivity() {

   @Inject lateinit var router: Router

   private lateinit var component: ActivityComponent

   private lateinit var injector: Injector

   override fun onCreate(savedInstanceState: Bundle?) {

       super.onCreate(savedInstanceState)

       setContentView(layout())

       component = app.component.plus(ActivityModule(this, container(), savedInstanceState))

       injector = Injector(component)

       injector.inject(this)

   }

   override fun onBackPressed() {

       if (!router.handleBack()) {

           super.onBackPressed()

       }

   }

   fun component() = component

   abstract fun container(): ViewGroup

   private fun layout(): Int {

       this.javaClass.kotlin.annotations.forEach { if (it is Layout) return it.layoutRes }

       throw IllegalArgumentException("You should specify Layout annotation")

   }

}

 

We need to explain some additional points of the code above:

1. As Dagger2 has a known issue with base classes injection, we use a special class Injector, which helps to solve the problem.

Injector.kt

class Injector(private val component: Any) {

   @Throws(RuntimeException::class)

   fun inject(injectableObject: Any) {

       val objectClass = injectableObject.javaClass

       try {

           val injectableMethod = findInjectableMethod(objectClass)

           injectableMethod!!.invoke(component, injectableObject)

       } catch (e: IllegalAccessException) {

           e.printStackTrace()

       } catch (e: InvocationTargetException) {

           e.printStackTrace()

       } catch (e: NoSuchMethodException) {

           val detailMessage = "No graph method found to inject " + objectClass.simpleName + ". Check your component"

           val exception = RuntimeException(detailMessage, e)

           throw exception

       } catch (e: NullPointerException) {

           val detailMessage = "No graph method found to inject " + objectClass.simpleName + ". Check your component"

           val exception = RuntimeException(detailMessage, e)

           throw exception

       }

   }

   fun isInjectable(injectableObject: Any): Boolean {

       return try {

           findInjectableMethod(injectableObject.javaClass) != null

       } catch (exception: Exception) {

           false

       }

   }

   @Throws(NoSuchMethodException::class)

   private fun findInjectableMethod(objectClass: Class<out Any>): Method? {

       val cachedMethod = cache[objectClass]

       if (cachedMethod != null) {

           return component.javaClass.getDeclaredMethod(cachedMethod.name, objectClass)

       }

       // Find proper injectable method of component to inject presenter instance

       for (m in component.javaClass.declaredMethods) {

           for (pClass in m.parameterTypes) {

               if (pClass == objectClass) {

                   cache.put(objectClass, m)

                   return m

               }

           }

       }

       return null

   }

   companion object {

       private val cache = HashMap<Class<out Any>, Method>()

   }

}

 

2. Abstract function container() is needed for providing ViewGroup for Conductor’s Router

3. For specifying a layout for each child of BaseActivity, use the annotation @Layout

After all preparations for Activities are completed, let’s see how the child Activity looks like. MainActivity has a single-container layout and will be used only as a container for Conductor’s Controllers.

layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout

   xmlns:android="http:// schemas.android.com/apk/res/android"

   android:id="@+id/container"

   android:layout_width="match_parent"

   android:layout_height="match_parent"/>

 

MainActivity.kt

@Layout(R.layout.activity_main)

class MainActivity : BaseActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {

       super.onCreate(savedInstanceState)

       if (!router.hasRootController()) router.pushController(RouterTransaction.with(MainController()))

   }

   override fun container(): ViewGroup = container

}

 

Presenter

Before creating Controllers, let’s review how Presenters will be created and how to add them to a graph of dependencies automatically.

Nucleus library uses PresenterLifecycleDelegate for creating RxPresenters via PresenterFactory, saving and restoring state, and providing a view interface to RxPresenter.

PresenterFactory is an interface with only one method createPresenter(), which returns the instance of RxPresenter. Nucleus library provides its own factory that uses reflection for RxPresenters creation - ReflectionPresenterFactory. For automatic adding of RxPresenters into a dependency graph, we will create a wrapper over an existing class ReflectionPresenterFactory. It will add each created RxPresenter to a graph if it is possible.

DaggerPresenterFactory.kt

class DaggerPresenterFactory<P : RxPresenter<out Any>, out PF : PresenterFactory<P>>(

       private val presenterFactory: PF, component: Any) : PresenterFactory<P> {

   private val injector: Injector = Injector(component)

   override fun createPresenter(): P {

       val presenter = presenterFactory.createPresenter()

       try {

           if (injector.isInjectable(presenter)) injector.inject(presenter)

       } catch(exception: Exception) {

           Timber.e(exception, this.javaClass.simpleName)

       }

       return presenter

   }

}

 

Controller

First of all, we need to specify the component and module that are bound to Controller lifecycle. They will be used for adding presenters to the dependency graph.

ControllerModule.kt

@Module @ForController class ControllerModule(private val controller: Controller

 

ControllerComponent.kt

@ForController

@Subcomponent(modules = arrayOf(ControllerModule::class))

interface ControllerComponent {

   fun inject(presenter: MainPresenter)

   fun inject(presenter: DetailPresenter)

}

 

The next step is to create a base abstract Controller, which encapsulates the logic of creating presenters via PresenterLifecycleDelegate. As well as in Activity, the layout for each Controller is provided via annotation @Layout. The code of NucleusController is presented below.

NucleusController.kt

abstract class NucleusController<P : RxPresenter<out Any>> : Controller, ViewWithPresenter<P> {

   private val presenterDelegate by lazy {

       PresenterLifecycleDelegate<P>(

               DaggerPresenterFactory<P, ReflectionPresenterFactory<P>>(

                       ReflectionPresenterFactory.fromViewClass<P>(javaClass)!!, screenComponent()

               )

       )

   }

 

   constructor() : this(null)

   constructor(bundle: Bundle?) : super(bundle)

   private val lifecycleListener = object : Controller.LifecycleListener() {

       override fun postCreateView(controller: Controller, view: View) {

           super.postCreateView(controller, view)

           onViewCreated(view)

       }

   }

   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {

       val view = inflater.inflate(layout(), container, false)

       addLifecycleListener(lifecycleListener)

       return view

   }

   @CallSuper

   open fun onViewCreated(view: View) {

       // here presenter will be created

       presenterDelegate.onResume(this)

   }

   override fun onDestroy() {

       presenterDelegate.onDestroy(true)

       removeLifecycleListener(lifecycleListener)

       super.onDestroy()

   }

   private fun layout(): Int {

       this.javaClass.kotlin.annotations.forEach { if (it is Layout) return it.layoutRes }

       throw IllegalArgumentException("You should specify Layout annotation")

   }

   override fun onDestroyView(view: View) {

       presenterDelegate.onDropView()

       super.onDestroyView(view)

   }

   override fun getPresenter(): P = presenterDelegate.presenter

   override fun getPresenterFactory(): PresenterFactory<P> = presenterDelegate.presenterFactory!!

   override fun setPresenterFactory(presenterFactory: PresenterFactory<P>?) {

       presenterDelegate.presenterFactory = presenterFactory

   }

 

   private fun screenComponent(): ControllerComponent =

           (activity as BaseActivity).component().plus(ControllerModule(this))

}

 

And now all is ready for using Controllers with Nucleus RxPresenters: each Controllerhas to be extended from NucleusController. To bind RxPresenter to Controller, Nucleus requires a special annotation - @RequiresPresenter(presenterClass: KClass). RxPresenter is created in onViewCreated method, and it is expected that we will start using Controller in this method or later according to Controller lifecycle (set data to views, make calls to RxPresenter, etc.).

 

All Together

Check the example out on Github. There you’ll find the discussed source code with a real life example - fetching bitcoin pricing index for the last 30 days.

This template is easy to reuse and extend for multiple common Android projects.


This article was created due to substantial and contribution from Daniil Senchugov.

Ervin
Martirosyan
Subscribe for regular updates