YOU DA REAL MVP

Sesekali nulis rada serius isinya, tentang Android MVP Pattern yang saya sendiri baru benar-benar paham konsepnya minggu ini. Biasanya, kita membuat aplikasi android dengan pattern MVC, yang bukan Model View Controller itu, tapi Massively View Controller yang artinya semua logic code dan business code disimpan di View.

Sekilas tentang MVP

Apa itu? apa MVP?

MVP atau Model View Presenter adalah sebuah konsep dimana kita memisahkan logic code atau business code dari View ke Presenter, jadi tugas utama dari Presenter adalah sebagai penyedia data dari Model, selain sebagai penyedia data, Presenter juga bisa menyimpan logic yang biasa kita simpan di View dan menerima action yang dikirim dari View. Model itu sendiri adalah sekumpulan dari data structure yang dikumpulkan menjadi satu class, tugasnya untuk menyimpan data dari API response atau menyimpan data untuk API request.

Jadi, gambarannya seperti ini

Model <-> Presenter <-> View

Secara garis besar, Presenter adalah penghubung antara Model dan View. Yang artinya, View tidak lagi berkomunikasi dengan Model karena Presenter yang akan menyajikan Model pada View

Mengapa MVP?

Ini pertanyaan saya dulu, kenapa harus MVP? Selain untuk gaya-gayaan atau keren-kerenan Fungsi utama dari MVP adalah sebagai berikut:

  • Beban kerja dari View lebih ringan karena tidak mengurusi http request atau http response.
  • Membagi tugas ke setiap layer sehingga code menjadi TESTABLE.
  • Membagi kode-kode yang bejibun menjadi bagian-bagian kecil dan sesederhana mungkin.
  • Mempermudah code maintenance.

Kapan harus menggunakan MVP?

Kapan-kapan bisa, tapi ketika fitur dan kode di aplikasi kita semakin bertambah, inilah saatnya untuk beralih. Biar kualitas dari aplikasi juga lebih meningkat, begitu.

Contoh Kasus

Contohnya, saya punya aplikasi sederhana, yaitu aplikasi penghitung luas dari nilai panjang * lebar yang hanya akan memiliki 1 buah layout dan 1 buah kelas Activity, oke?

Kira-kira seperti ini kode yang biasa ditemui.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="io.github.sendz.hitungluas.MainActivity">
<EditText
android:id="@+id/edit_text_panjang"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:hint="Panjang" />
<EditText
android:id="@+id/edit_text_lebar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/edit_text_panjang"
android:inputType="number"
android:hint="Lebar" />
<Button
android:id="@+id/button_hitung"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/edit_text_lebar"
android:text="Hitung"/>
</RelativeLayout>
package io.github.sendz.hitungluas;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private int panjang;
private int lebar;
private EditText inputPanjang;
private EditText inputLebar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
inputPanjang = (EditText) findViewById(R.id.edit_text_panjang);
inputLebar = (EditText) findViewById(R.id.edit_text_lebar);
Button reset = (Button) findViewById(R.id.button_hitung);
reset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
panjang = Integer.valueOf(inputPanjang.getText().toString());
lebar = Integer.valueOf(inputLebar.getText().toString());
int luas = panjang * lebar;
new AlertDialog.Builder(MainActivity.this).setTitle("Luas").setMessage(String.valueOf(luas)).show();
}
});
}
}

Refactor

Yang akan dilakukan selanjutnya adalah, memecah beberapa baris kode di MainActivity.java dan mengekstraknya menjadi method atau function. Istilahnya, kita akan melakukan refactor atau merubah kode menjadi lebih baik.

Sampai saat ini, aplikasi berjalan dengan baik.

Aplikasi Hitung Luas

Selanjutnya, kita akan membuat tiga buah file baru bernama MainPresenter, MainView dan MainModel, dimana MainActivity akan mengimplementasikan MainView dan juga mendeklarasikan MainPresenter di dalamnya, dan MainModel akan berisi variabel dari luas. Fungsi dari MainView sendiri adalah agar MainPresenter bisa berinteraksi dengan code yang ada di MainActivity.

Hal yang akan dilakukan adalah:

  1. Membuat class baru bernama MainPresenter, MainModel dan interface baru bernama MainView;
  2. Implementasikan MainView di MainActivity;
  3. Deklarasikan MainPresenter di MainActivity dengan parameter MainView;
  4. Pindahkan fungsi hitungLuas(int panjang, int lebar) ke MainPresenter;
  5. Buat fungsi baru void tampilkanLuas(int luas) di MainView;
  6. Tambahkan anotasi @Override di atas tampilkanLuas(int luas) di MainActivity;
  7. Panggil hitungLuas(panjang, lebar) dari MainPresenter di MainActivity;
  8. Panggil tampilkanLuas(luas) dari MainActivity melalui MainView di MainPresenter;

Pada contoh kasus ini, View akan memiliki behavior yang disebut Tell, don't Ask ke Presenter yang artinya View tidak akan menerima returned value dari Presenter, tetapi Presenter lah yang akan memberikan perintah untuk mengeksekusi fungsi yang ada di View beserta data yang dibutuhkan. Oleh karena itu, fungsi dalam MainActivity haruslah mengimplementasikan fungsi dari MainView.

Dan di akhir, code saat ini adalah:

package io.github.sendz.hitungluas;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity implements MainView {
private int panjang;
private int lebar;
private EditText inputPanjang;
private EditText inputLebar;
private Button reset;
private MainPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
renderTampilan();
presenter = new MainPresenter(this);
reset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
panjang = Integer.valueOf(inputPanjang.getText().toString());
lebar = Integer.valueOf(inputLebar.getText().toString());
presenter.hitungLuas(MainActivity.this.panjang, MainActivity.this.lebar);
}
});
}
private void renderTampilan() {
inputPanjang = (EditText) findViewById(R.id.edit_text_panjang);
inputLebar = (EditText) findViewById(R.id.edit_text_lebar);
reset = (Button) findViewById(R.id.button_hitung);
}
@Override
public void tampilkanLuas(MainModel model) {
new AlertDialog.Builder(MainActivity.this).setTitle("Luas").setMessage(String.valueOf(model.getLuas())).show();
}
}
package io.github.sendz.hitungluas;
public class MainModel {
int luas;
public MainModel(int luas) {
this.luas = luas;
}
public int getLuas() {
return luas;
}
public void setLuas(int luas) {
this.luas = luas;
}
}
view raw MainModel.java hosted with ❤ by GitHub
package io.github.sendz.hitungluas;
public class MainPresenter {
private MainView view;
private MainModel model;
public MainPresenter(MainView view) {
this.view = view;
}
public void hitungLuas(int panjang, int lebar) {
int luas = luasPersegi(panjang, lebar);
model = new MainModel(luas);
view.tampilkanLuas(model);
}
private int luasPersegi(int panjang, int lebar) {
return panjang * lebar;
}
}
package io.github.sendz.hitungluas;
public interface MainView {
void tampilkanLuas(MainModel model);
}
view raw MainView.java hosted with ❤ by GitHub

Unit Test

Salah satu kelebihan dari MVP adalah setiap logic code atau business code bisa diuji dengan Unit Test, pada kasus ini saya akan menggunakan Junit4 sebagai library untuk melakukan pengetesan Unit Test.

Tambahkan terlebih dahulu testCompile group: 'org.mockito', name: 'mockito-core', version: '2.2.3' ke file build.gradle di dalam tag dependencies untuk menggunakan Mockito sebagai library pembantu. Mockito memiliki fungsi yang bisa digunakan untuk mereplikasi sebuah kelas dan bisa kita gunakan untuk pengujian pada level Unit Test. Setelah menambahkan baris tadi, refresh gradle dengan klik teks Sync Now pada bar yang muncul lalu klik menu Build -> Rebuild Project.

Buatlah sebuah file dengan nama MainPresenterTest di folder test/java/, atau Klik Kanan -> Go To -> Test -> Create New Test…, gunakan Junit4 sebagai TestLibrary lalu pilih direktori test, bukan androidTest.

Berikut adalah file Unit Test yang saya buat dengan nama MainPresenterTest.

apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "24.0.2"
defaultConfig {
applicationId "io.github.sendz.distanceconverter"
minSdkVersion 22
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.0'
testCompile 'junit:junit:4.12'
testCompile group: 'org.mockito', name: 'mockito-core', version: '2.2.3'
}
view raw build.gradle hosted with ❤ by GitHub
package io.github.sendz.hitungluas;
import org.junit.Before;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class MainPresenterTest {
private MainPresenter presenter;
private MainView view;
@Before
public void setUp() throws Exception {
view = mock(MainView.class);
presenter = new MainPresenter(view);
}
@Test
public void testHitungLuasPersegi() throws Exception {
int luas = presenter.luasPersegi(4, 3);
assertEquals(12, luas);
}
@Test
public void testHitungLuasDanCekApakahHasilDitampilkanDiActivity() throws Exception {
presenter.hitungLuas(4, 3);
verify(view).tampilkanLuas(any(MainModel.class));
}
}

Sekarang, inilah struktur project kita saat ini

Struktur Project

Dan ini hasil dari pengujian Unit Test pada logic code di MainPresenter.

Hasil Unit Test

Dan ini aplikasi dengan tampilan dan hasil yang sama dengan sebelumnya

Aplikasi

Tautan Project di github

Github Project

Sekian, selamat mencoba.