Lalit Hajare
5 min readJul 3, 2020

--

Making Android-Activity Great Again !!!

There is a very old and battle tested architectural design pattern called ‘MVC’ that most of us developers have heard of. But, when it comes to Android, the pattern is relatively hard to implement, might be because the android is an embedded device and requires some low level coding to be done, there are system callbacks, UI management, runtime permissions management, networking, access to sensors.

After observing all the above mentioned points, you must be agreeing that it’s hard to maintain ‘Activity’ as a pure controller, because, most of the system callbacks like permissions, networking and UI management is related to Activity and this makes Activity a ‘God’ object.

So, this is our problem statement over here, to “reduce the responsibilties of Activity and making it a pure controller”.

The pattern we are going to follow here is actually MVVM, but, I made some tweaks to it, so, we can imagine it to be ‘MVC’. What we will be doing here is, we are going to separate out related classes that do controlling and have business logic into ‘Controller’ module. While the UI management is done by a different class called ‘UIInteractor’. The ‘Model’ module will be consisting of repositories, network models(called schemas), local models.

The communication between UIInteractor and Activity will be done through an interface. So, any Activity will not be tied to any layout file, UI management and it can control any other UI just by implementing it’s interface.

Okay! so lot’s of talk, now lets see the diagram and code;

Here is the basic component diagram of the login process, just observe the diagram you will see that ‘Layout files’ is separated from LoginActivity, it means the UI management work like data binding, view binding, showing dialogs, toasts is all done by LoginUIInteractor. The LoginActivity and LoginViewModel just do the controlling of LoginUIInteractor.

Here is the basic structure of LoginActivity;

class LoginActivity : BaseActivity(), LoginUIInteractor.LoginController {

/**
* Consists the controller logic for this class
*/
lateinit var loginViewModel: LoginViewModel

/**
* Used to delegate the responsiblity of UI operations
*/
@Inject
lateinit var mLoginUIInteractor: LoginUIInteractor


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

loginViewModel = ViewModelProvider(this, providerFactory).get(LoginViewModel::class.java)

mLoginUIInteractor.loginController = this

setContentView(mLoginUIInteractor.getRootView())

observeLoginValidation()

observeLoginResult()
}
/**
* Shows the validation messages if invalid input is given to login form
*/
private fun observeLoginValidation() {
loginViewModel.validationMsg.observe(this, Observer { msg ->
mLoginUIInteractor
.showValidationDialog(msg)
})
}

/**
* Observes the result of login API call.
* Uses LoginUIInteractor to show the response from Server
*/
private fun observeLoginResult() {
loginViewModel.result.observe(this, Observer { result ->
mLoginUIInteractor
.loading = false
if
(result is Result.Success) {
val response = result.data as LoginResponseSchema
mLoginUIInteractor.showLoginSuccessDialog(response.message!!)
} else if (result is Result.Error) {
val err = result.errorResponseSchema
mLoginUIInteractor
.showLoginFailDialog(err.data[0].msg)
}
})
}

/**
* Initiates the Login API call
*/
override fun loginUser(email: String, password: String) {
loginViewModel.login(email, password)
}

/**
* Intent to open the Signup Screen
*/
override fun openSignupScreen() {
mScreenNavigator.OnBoarding().openSignupScreen(this)
}

override fun openForgotPasswordScreen() {
mScreenNavigator.OnBoarding().openForgotPasswordScreen(this)
}
}

As you can see LoginActivity consists of two dependencies: LoginUIInteractor and LoginViewModel. The LoginActivity implements LoginUIInteractor.LoginController which is used by LoginUIInteractor to communicate with LoginActivity and only controlling logic with system callbacks is left inside of LoginActivity.

Here is the code for LoginUIInteractor;

class LoginUIInteractor @Inject constructor(
val mContext: Context,
val mValidationManager: ValidationManager
) : BaseObservable() {

var loginController: LoginController? = null

/**
* Provides the view to which this interactor operates
*/
fun getRootView(): View {
val binding: ActivityLoginBinding = DataBindingUtil.inflate(
LayoutInflater.from(mContext),
R.layout.activity_login,
null,
false
)
binding.loginUIInteractor = this
return
binding.root
}

// ***************** UI ELEMENTS ****************************

/**
* Form fields to submit
*/
var email: String = ""
var
password: String = ""

/**
* Determines when to show progressbar on login screen & hide login button
*/
@Bindable
var loading: Boolean = false
set
(value) {
field = value
notifyPropertyChanged(BR.loading)
}

// ***************** UI ELEMENTS ****************************

//*********************** MESSAGES ****************************

fun showValidationDialog(message: String) {
makeToast(mContext, message)
}

fun showLoginSuccessDialog(message: String) {
makeToast(mContext, message)
}

fun showLoginFailDialog(message: String) {
makeToast(mContext, message)
}

//*********************** MESSAGES ****************************

// ********************* EVENTS ********************************

fun login() {
if (checkValidation())
loginController?.loginUser(email, password)
}

private fun checkValidation(): Boolean {
//Validation Logic
}

fun openSignupScreen() {
loginController?.openSignupScreen()
}

fun openForgotPasswordScreen() {
loginController?.openForgotPasswordScreen()
}

// ********************* EVENTS ********************************

/**
* A standard to communicate with the controller.
*/
interface LoginController {
fun loginUser(email: String, password: String)
fun openSignupScreen()
fun openForgotPasswordScreen()
}

}

As you can see LoginUIInteractor extends BaseObservable which is used for view & data binding purposes. So it works in following sequence;

  1. LoginUIInteractor notifies LoginActivity of some user events, through the interface LoginController.
  2. LoginActivity acts upon it either by taking help from LoginViewModel for async operations like API call or Database operations, gets the result and
  3. LoginActivity commands the LoginUIInteractor by calling it’s functions.

Here is the code for LoginViewModel;

class LoginViewModel @Inject constructor(
/**
* Calls underlying API endpoint & returns API result
*/
val authRepository: AuthRepository
) :
BaseViewModel() {
/**
* Private Mutable Data, client program cannot access this data
*/
private val mLoginResult = MediatorLiveData<Result<Any>>()
private val mValidationMessage = MediatorLiveData<String>()

/**
* Public Immutable Data, client program can only observe changes
*/
val result: LiveData<Result<Any>> = mLoginResult
val validationMsg
: LiveData<String> = mValidationMessage

/**
* Get login result from `AuthRepository` & update the LiveData
*/
fun login(email: String, password: String) {
val loginRequestSchema = LoginRequestSchema(email, password)
launch {
val
response = authRepository.login(loginRequestSchema)
mLoginResult.postValue(response)
}
}

}

The API call result is actually obtained in this class using AuthRepository. This result is observed by LoginActivity, LoginActivity then acts upon the result.

Hence, in this manner, we separated the UI management overhead from LoginActivity. This becomes useful while unit testing the code or making random UI changes as client demands, which often occur in case of application development, the code that accesses native Android features like displaying dialogs, toasts in part of UIInteractor class and activity can be unit tested. Now LoginActivity can act as controller for any View as it is not tied to any particular layout file.

So, here actually we achieved MVC using MVVM by shifting ViewModel & Activity into Controller and UIInteractor into View and Repositories, Local Models/Network Schemas into Model.

--

--

Lalit Hajare

I’m a student in field of Android development. Works @DeutscheTelekom