Zastosowanie
MVC
(Model-View-Controller
) ułatwia organizowanie struktury aplikacji poprzez podział odpowiedzialności na trzy rozdzielne warstwy: widoku (View
), kontrolera (Controller
) oraz modelu (Model
). Warstwa widoku
odpowiedzialna jest za sposób prezentacji danych z modelu w interfejsie użytkownika. Warstwa kontrolera
obsługuje zdarzenia systemu oraz interakcję użytkownika poprzez delegowanie zadań do modelu oraz powiadamianie widoku o bieżącym stanie. Natomiast warstwa modelu
odpowiada za logikę biznesową tzn. za przetwarzanie, przechowywanie i sposób dostarczania danych. Dzięki separacji odpowiedzialności kod staje się bardziej uporządkowany i elastyczny na modyfikacje (zmiana jednej warstwy nie musi pociągać za sobą zmiany kodu w pozostałych warstwach), a warstwa modelu jest zdolna do przeprowadzenia testów. MVC
jest bardziej pojęciem konceptualnym niż zbiorem formalnych reguł implementacji.
Ograniczenia
Ze względu na specyfikę architektury systemu Android
(cykl życia Aktywności
i jej odpowiedzialności) warstwy View
oraz Controller
są często implementowane w Aktywności
przez co nie ma między nimi wyraźnej rozdzielności. Z tego powodu zdolna do przeprowadzenia niezależnych testów jest tylko warstwa Modelu
. Implementacja może zostać usprawniona (zgodnie z ideą wzorca) poprzez przeniesienie warstwy View
do oddzielnej klasy i pozostawieniu Aktywności
odpowiedzialności warstwy Controller
. Jednakże takie rozwiązanie jest tylko połowiczne. Wprowadza sztuczny klasowy podział separacji warstw View
oraz Controller
, ponieważ Aktywność
ciągle jest odpowiedzialna za inicjalizacje widoków w warstwie View
, które są powiązane z jej cyklem życia. Co więcej warstwa View
oraz Controller
są zależne od klas Androidowych i mają dostęp do klasy Model
, co nadal wpływa na podział odpowiedzialności i możliwości niezależnego testowania. Rozwiązaniem tego problemu może być zastosowanie wzorca MVP
.
Użycie
Wzorzec MVC
można wykorzystać w małych projektach o niewielkiej ilości ekranów, gdzie ewentualnemu testowaniu podlega przede wszystkim logika biznesowa.
Implementacja
Klasa View
inicjalizuje widok oraz realizuje zachowania obiektów widoku, zmiany ich właściwości oraz stanów. View
przechowuje referencje do modelu z którego czerpie niezbędne dane do obsługi obiektów widoku. Klasa Controller
nadzoruje poprawną obsługę procesów między interakcją użytkownika i systemem. Przechwytuje zdarzenia oraz akcje użytkownika i systemu, deleguje wykonanie zadania do modelu, a następnie powiadamia widok o stanie żądanej akcji. Klasa Model
implementuje logikę systemu, a także zarządza danymi.
Poniższy listing przedstawia implementacja wzorca MVC
przy zachowaniu klasowej rozdzielności warstw.
public class View {
//some view fields like buttons, textview etc
private TextView textView;
private EditText editText;
private Button button;
private Model model;
//parent view object also can be passed here
public View(Model model) {
this.model = model;
initView();
}
private void initView() {
//init views (in Android like findViewById from passed parent view in constructor)
//set default text/image etc in the views
}
public void showError(String error) {
//show error on the screen (in Android like Toast)
}
public String getInput() {
return editText.getText().toString();
}
public void updateText() {
//update tasks list in TextView
textView.setText(model.getData());
//show response text in the textView
}
}
public class Controller {
private Model model;
private View view;
public Controller(Model model, View view) {
this.model = model;
this.view = view;
}
private void actionRequest() {
//controller catches some event like click on the button
//do proper action
String input = view.getInput();
try {
boolean success = model.addData(input);
if(success)
view.updateText();
else
view.showError("Task has not been added.").
}
catch(Exception exception) {
view.showError("Unexpected error occured.").
}
}
}
public class Model {
private Database database;
//other variables, logic managers or data providers like database, cache, network
public Model() {
//do init staff
//initialize some instances
database = new Database();
}
public boolean addData(String input) {
boolean isSuccess = database.add("TASK", input);
return success;
}
public String getData() {
String tasks = "";
for(int i=0; i<database.size("TASK"); i++)
tasks += database.get("TASK", i) + "\n";
return tasks;
}
}
Klient inicjalizuje obiekt warstw dzięki czemu stają się one zdolne do współpracy.
//initialize MVC objects
Model model = new Model();
View view = new View(model);
Controller controller = new Controller(model, view);
//do some stuff, cach actions and events
controller.actionRequest();
//1. controller catched the event
//2. gets input from view
//3. delegates the job to model
//4. info about job status passed to controller
//5. view is notifed about request finished
//6. new data loaded from model
Przykład
Ekran wyszukiwania trasy ViewControllerActivity
w procesie znajdywania pasażerskiego połączenia kolejowego wykorzystuje komunikacje sieciową. Ponadto zapamiętuje ostatnio poprawanie wprowadzoną stacje początkową i automatycznie uzupełnia polę stacji. Do implementacji tego procesu wykorzystano wzorzec MVC
w wariancie połącznej warstwy View-Controller
realizowaną przez Aktywność
.
public class ViewControllerActivity extends AppCompatActivity {
private EditText startEdit;
private EditText destinationEdit;
private Button searchButton;
private TrackModel model;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = new TrackModel(this);
//in clear MVC inflating layout, listeners and views update
//should be done in separeted View layer
initView();
setListeners();
setViews();
}
//view layer
private void initView() {
setContentView(R.layout.viewcontroller_activity);
startEdit = findViewById(R.id.startEdit);
destinationEdit = findViewById(R.id.destinationEdit);
searchButton = findViewById(R.id.searchButton);
}
//view layer set listener passed from controller
private void setListeners() {
searchButton.setOnClickListener(v -> { searchAction(); });
}
//view layer
private void setViews() {
String home = model.getHomePlace();
startEdit.setText(home);
}
//controller layer runs this method on the view layer
private void showError(String text) {
Toast.makeText(this, text, Toast.LENGTH_LONG).show();
}
//controller layer
private void searchAction() {
//controller get inputs from view
String start = startEdit.getText().toString();
String destination = destinationEdit.getText().toString();
String result = model.search(start, destination);
if(result.equals("EMPTY")) {
showError("Incorrect start or destination place");
}
else {
//show search result in another activity/popup or in list
}
}
//other lifecycle methods
}
public class TrackModel {
//some network provider
private SharedPreferences sharedPref;
private SharedPreferences.Editor editor;
private Context context;
public TrackModel(Context context) {
this.context = context;
sharedPref = context.getPreferences(Context.MODE_PRIVATE);
}
//could be some response message instead of String result
public String search(String start, String destination) {
//use some network library to login
//if response is ok
saveHome(start);
return "14:00 " + start " - " + destination + " 15:30";
return true; //mock
//if response fail just return "EMPTY"
}
public String getHomePlace() {
return sharedPref.getString("home", "");
}
public void saveHome(String start) {
editor = sharedPref.edit();
editor.putString("home", start);
editor.commit();
}
}
Biblioteki
Na pierwszy rzut oka można pomyśleć, że środowisko Android
domyślnie implementuje wzorzec MVC
, a nawet wymusza na programiścię jego realizacje. Takie przekonanie jest jednak pewnym uproszczeniem. Pomimo dość częstej praktyki traktowania Aktywności
jako warstwę View-Controller
, warto te warstwy odseparować. Programista implementujący wzorzec powinien pamiętać, że realizacja wzorca MVC
jest zależna od architektury systemu, dlatego nie ma jedynej słusznej implementacji.