MVVM
The Layers of MVVM
The code is divided into three separate layers:
- Presentation Layer
- Domain Layer
- Data Layer
We’ll get into more detail about each layer below. For now, our resulting package structure looks like this:
- Models

- Modules

- Networks

The Presentation Layer
This includes our Activities
, Fragments
, and ViewModels
. An Activity
should be as dumb as possible. Never put your business logic in Activities
.
An Activity
will talk to a ViewModel
and a ViewModel
will talk to the domain layer to perform actions. A ViewModel
never talks to the data layer directly.
Here’s how code looks:
Activity Class
class LoginActivity : AppCompatActivity() {
//Data Binding
private lateinit var binding: ActivityLoginBinding
//View Model
private lateinit var viewModel: LoginActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
initVar()
initObserver()
}
private fun initVar() {
viewModel = ViewModelProvider(this)[LoginActivityViewModel::class.java]
progressDialog = Helpers.getProgressDialog(this)!!
}
private fun initObserver() {
viewModel.isLoading.observe(this) { isLoading ->
/// TODO Do your action here
}
viewModel.errorMessage.observe(this) { message ->
/// TODO Do your action here
}
viewModel.loginResponseMutableLiveData.observe(this) { response ->
/// TODO Do your action here
}
}
}ViewModel Class
class LoginActivityViewModel(application: Application) : AndroidViewModel(application) {
//Repository
private var loginRepository: LoginRepository = LoginRepository()
//Object
var loginResponseMutableLiveData: MutableLiveData<ProfileResponse> = MutableLiveData()
//ETC
var isLoading: MutableLiveData<Boolean> = MutableLiveData()
var errorMessage: MutableLiveData<String> = MutableLiveData()
fun postLogin(email: String, password: String) {
when {
email.isEmpty() -> {
errorMessage.value = "Email cannot be empty"
}
password.isEmpty() -> {
errorMessage.value = "Passwowrd cannot be empty"
}
else -> {
val body = LoginRequest(
email = email,
password = password,
)
isLoading.value = true
loginRepository.postLogin(
body,
SharedPreferenceServices.getToken(getApplication()),
object : ApiResponseCallback<ProfileResponse?> {
override fun onSuccess(response: ProfileResponse?) {
isLoading.value = false
loginResponseMutableLiveData.value = response
errorMessage.value = null
}
override fun onFailure(error: String?) {
isLoading.value = false
loginResponseMutableLiveData.value = null
errorMessage.value = error
}
})
}
}
}
}
The Domain Layer
The domain layer contains all the use cases of your application.
In this example, we have:
RetrofitService
, an class forHTTP Request
.class RetrofitService {
companion object {
private val NETWORK_TIMEOUT = 600
fun getRetrofitInstance(context: Context?): Retrofit? {
val builder = GsonBuilder()
val gson = builder
.setLenient()
.create()
val retrofit: Retrofit.Builder = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL_APP)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
val httpClient: OkHttpClient.Builder =
OkHttpClient.Builder()
.addInterceptor(Interceptor { chain: Interceptor.Chain ->
val original = chain.request()
val request =
original.newBuilder()
.method(original.method, original.body)
.build()
chain.proceed(request)
})
.connectTimeout(NETWORK_TIMEOUT.toLong(), TimeUnit.SECONDS)
.readTimeout(NETWORK_TIMEOUT.toLong(), TimeUnit.SECONDS)
.writeTimeout(NETWORK_TIMEOUT.toLong(), TimeUnit.SECONDS)
//Add Logging Interceptor
if (BuildConfig.DEBUG) {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
httpClient.addInterceptor(logging)
}
retrofit.client(httpClient.build())
return retrofit.build()
}
}
}ApiResponseCallback
, an interface class for callback.interface ApiResponseCallback<in TData> {
fun onSuccess(response: TData?)
fun onFailure(error: String?)
}The purpose of the
ApiResponseCallback
is to be a mediator for response from API between yourViewModels
andRepositories
.ApiInterface
, an interface class for call the API endpoints.interface ApiInterface {
@POST(value = "/authorization/handheld/login")
fun postLogin(
@Body body: LoginRequest
): Observable<BaseResponse<ProfileResponse>>
}The purpose of the
ApiInterface
is to be a mediator for call API between yourViewModels
andRepositories
.Singleton
, an class for communicate betweenRepository
andHTTP Request
class Singleton {
companion object {
@SuppressLint("StaticFieldLeak")
private lateinit var context: Context
fun setContext(con: Context) {
context = con
}
private fun getApplicationContext(): Context {
return context
}
fun getService(): ApiInterface? {
return RetrofitService.getRetrofitInstance(getApplicationContext())
?.create(ApiInterface::class.java)
}
}
}
The Data Layer
This has all the repositories which the domain layer can use. This layer exposes a data source API to outside classes:
class LoginRepository {
private val compositeDisposable = CompositeDisposable()
fun postLogin(
body: LoginRequest,
token: String,
callback: ApiResponseCallback<ProfileResponse?>
) {
Singleton.getService(token)?.postLogin(body)
?.observeOn(AndroidSchedulers.mainThread())
?.subscribeOn(Schedulers.io())?.let {
compositeDisposable.add(
it
.subscribe({ response -> callback.onSuccess(response.data) }) { error ->
callback.onFailure(
Helpers.onFailed(error)
)
})
}
}
}
Source Toptal - Abhishek Tyagi