Designing Model-View-Presenter Architecture for Android App
Overview
Menentukan arsitektur proyek merupakan langkah paling awal dari pengembangan suatu proyek perangkat lunak, untuk membuat aplikasi yang simple, easy to maintain, dan easy to test.
“we build our computer(systems) the way we build our cities: over time, without a plan, on top of ruins” Ellen Ulman
Dalam pengembangan aplikasi android sendiri, terdapat beberapa patern arsitektur yang sering digunakan, yaitu:
- Model-View-Controller (MVC)
- Model-View-Presenter (MVP)
- Model-View-ViewModel (MVVM)
- Clean Architecture
Sebelum itu, kita harus kenali dulu apa itu Model, View, Controller, Presenter. Secara garis besar, mereka adalah:
Model
Model merupakan bagian yang mengatur segala sesuatu berkaitan dengan data, mulai dari bagaimana bentuk dari data tersebut, hingga ke logic dari data tersebut seperti cara membuat baru, membaca, mengubah atau menghapus data.
View
View merupakan representasi data, bagaimana data tersebut diperlihatkan kepada pengguna.
Controller/Presenter
Controller menghubungkan view dengan model. Controller juga mengatur interaksi yang dilakukan oleh pengguna, seperti menekan tombol, mengisi form, dll.
Lalu, apa yang membedakan MVP dan MVC?
Model-View-Controller
Pada arsitektur ini, view disini hanya sebagai template dari representasi data. view tidak tahu apapun, tidak tahu model apa serta state dari model apa yang akan direpresentasikan. view disini meliputi file xml yang ada di res package.
Controller lah yang mengetahui model dan state yang ada, serta mengatur interaksi yang ada di representasinya. Controller disini meliputi file activities, fragments, dan adapters.
Pada arsitektur ini, view dapat komunkasi dengan model, controller dapat komunikasi dengan model, serta model dan controller dapat saling berkomunikasi.
Model-View-Presenter
Perbedaan MVP dari MVC adalah, view di MVP lebih pintar. view tahu tentang model dan state dari model yang akan direpresentasikannya. Namun, tetap interaksi dari view diatur oleh controller (di MVP disebut presenter).
Presenter terdiri dari logic interaksi antara view dengan model dan interface dari apa saja yang dapat dilakukan oleh presenter.
Mana yang lebih bagus?
Saya sedang mengembangkan aplikasi perangkat lunak berbasis android mengikuti TDD (Test Driven Development).
Pada awalnya saya menggunakan MVC sebagai arsitektur. Saya sendiri merasa ada beberapa kesulitan dari MVC. Pertama, controller terlalu kompleks sebab logic, data manager, dll diatur di controller. Hal tersebut membuat projek tersebut menjadi high coupling, sebab jika saya mengubah view saya juga harus mengubah controller nya juga. Selain itu, hal tersebut membuat saya kesusahan untuk maintain proyek dan membuat unit testing.
Karena itu, saya berpindah dari MVC ke MVP. Keuntungan yang saya dapat adalah, saya memecah controller sehingga menjadi tidak kompleks lagi. Hal ini menjadikan proyeknya tidak lagi high coupling, mudah di maintain dan memudahkan membuat unit testing.
Saya mengalami kesulitan saat melakukan refactor dari MVC ke MVP, sebab saya belum pernah membuat aplikasi berbasis MVP.
MVP App Project
Untuk membuat app MVP, saya menggunakan beberapa teknologi berikut:
- Realm sebagai database lokal.
- Butterknife untuk bind view dengan logicnya
- Dagger2 untuk inject dependency
- Espresso untuk instrumented test
- JUnit dan mockito untuk unit testing
- Jacoco untuk menghitung test coverage
Struktur proyeknya:
- Models. Berisi class model yang mendefinisikan setiap model yang ada, dan data manager yang mengatur komunikasi model ke database (CRUD). Data manager akan dipanggil oleh presenter jika presenter membutuhkan akses ke database, sehingga presenter tidak akan berkomunikasi dengan database. Hal ini memudahkan untuk unit testing sebab di presenter kita tidak akan test tentang komunikasi ke database.
Example.java
public class Example extends RealmObject {
private int number;
private String sentence;
public void setNumber(int number) {
this.number = number;
}
public void setSentence(String sentence) {
this.sentence = sentence;
}
public int getNumber() {
return number;
}
public String getSentence() {
return sentence;
}
}
RealmDataManager.java
public class RealmDataManager {
private final Realm mRealm;
public RealmDataManager(final Realm realm) {
mRealm = realm;
}
public void closeRealm() {
mRealm.close();
}
public void addExample(final int number,
final String sentence,
final OnTransactionCallback onTransactionCallback) {
mRealm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(final Realm realm) {
Example example = realm.createObject(Example.class);
example.setNumber(number);
example.setSentence(sentence);
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
if (onTransactionCallback != null) {
onTransactionCallback.onRealmSuccess();
}
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
if (onTransactionCallback != null) {
onTransactionCallback.onRealmError(error);
}
}
});
}
- Dependency Injector. Berisi class untuk menginject dependecy atau module ke dalam application maupun activity.
- Dependency Injector. Berisi class untuk menginject dependecy atau module ke dalam application maupun activity.
BasePresenter.java
public class BasePresenter<V extends BaseView> implements BasePresenterInterface<V> {
private static final String TAG = "BasePresenter";
private final RealmDataManager mRealmDataManager;
private V view;
@Inject
public BasePresenter(RealmDataManager realmDataManager) {
this.mRealmDataManager = realmDataManager;
}
public RealmDataManager getRealmDataManager() {
return this.mRealmDataManager;
}
@Override
public void onAttach(V view) {
this.view = view;
}
@Override
public void onDetach() {}
public V getView() {
return view;
}
@Override
public void closeRealm() {
mRealmDataManager.closeRealm();
}
}
MainPresenter.java
public class MainPresenter<V extends MainView> extends BasePresenter<V> implements MainPresenterInterface<V>, RealmDataManager.OnTransactionCallback {
private static final String TAG = "MainPresenter";
@Inject
public MainPresenter(final RealmDataManager realmDataManager) { super(realmDataManager); }
@Override
public void getExamples() {
List<Example> examples = getRealmDataManager().getExamples();
if (examples.size() == 0) {
getRealmDataManager().addExample(1, "Anab Ganteng", this);
}
else {
getRealmDataManager().deleteExamples(this);
}
}
@Override
public void closeRealm() {
super.closeRealm();
}
@Override
public void onRealmSuccess() {}
@Override
public void onRealmError(Throwable e){}
}
- View. Berisi 2 package (untuk saat ini), activities dan views. Views berisi interface yang digunakan untuk mengekspos apa saja yang dapat dilakukan view ke presenter. Activitites berisi class yang mengimplementasi view tersebut. View dengan presenter menjalin one-to-one relationship.
- View. Berisi 2 package (untuk saat ini), activities dan views. Views berisi interface yang digunakan untuk mengekspos apa saja yang dapat dilakukan view ke presenter. Activitites berisi class yang mengimplementasi view tersebut. View dengan presenter menjalin one-to-one relationship.
Testing
Well, testing bagi saya merupakan tantangan baru sebab ini baru pertama kalinya. Saya menggunakan espresso untuk instrumented test, dan JUnit serta mockito untuk unit test. Sebenarnya saya ingin menggunakan robolectric, namun ternyata bermasalah saat ingin melakukan test realm, dan saya belum menemukan solusinya(hehe).
Unit test sendiri digunakan untuk test presenter dan model.
public class MainPresenterUnitTest {
@Mock
RealmDataManager mRealmDataManager;
@Mock
MainView mainView;
private MainPresenter mainPresenter;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
mainPresenter = new MainPresenter(mRealmDataManager);
}
@Test
public void testGetExamplesNoExampleFound() {
List<Example> emptyExample = new ArrayList<Example>();
when(mRealmDataManager.getExamples()).thenReturn(emptyExample);
mainPresenter.getExamples();
verify(mRealmDataManager, times(1)).addExample(anyInt(), anyString(), (RealmDataManager.OnTransactionCallback) any());
}
}
Untuk instrumented test digunakan untuk melakukan test kepada activity dan realm.
Lalu saya menggunakan Jacoco untuk menghitung code coverage dari unit test yang telah dibuat.