Akademia Lekcje
- Lekcja 1 – Activity (link)
- Lekcja 2 – MVVM (jesteś tutaj)
- Lekcja 3 – Obsługa Bazy Danych – Room (link)
- Lekcja 4 – Wdrożenie DI (link)
MVVM czyli Model-View-ViewModel to jeden z najbardziej popularnych wzorców do tworzenia struktury kodu Androidowych aplikacji. W tym artykule przedstawię Ci idee MVVM byś mógł korzystać z niego bez problemów. Otrzymasz też dostęp do przykładowej (bardzo prostej) aplikacji, dzięki której utrwalisz sobie zasady stosowania tego wzorca. W kolejnych artykułach stopniowo będę Ci podawał dodatkowe informacje i ciekawostki na temat MVVM, ale kiedy już załapiesz podstawy z tego artykułu będzie z górki! Także.. zaparz sobie kawę i do dzieła!

Celem MVVM jest rozdzielenie kodu na różne warstwy, z których każda będzie mieć swoje własne obowiązki. Dzięki takiej strukturze sprawisz, że kod będzie bardziej czytelny, testowalny i łatwy w utrzymaniu. To są właśnie, kluczowe aspekty budowania kodu, który wytrzymuje próbę czasu i późniejsze aktualizacje.
MVVM … Model, View oraz ViewModel
Teraz przyszedł czas na bardziej szczegółowy opis warstw omawianego wzorca.
Warstwa – Model
Jak już wiemy z wcześniejszego artykułu, na warstwę Modelu składają się komponenty odpowiedzialne za obsługę danych w aplikacji. Dane mogą pochodzić z różnych źródeł np. z zewnętrznego serwera, lokalnej bazy danych, itp. …
Pamiętaj, że Model jest niezależny od obiektów View oraz systemowych komponentów Twojej aplikacji więc przykładowo, jest niezależny od cyklu życia aplikacji.
Warstwa – View
Jest to po prostu interfejs z użytkownikiem. Na cele tego artykułu przyjmijmy, że ta warstwa aplikacji to standardowy ekran aplikacji (w smartfonie).
Warstwa – ViewModel
Można powiedzieć, że to swego rodzaju odpowiednik Controllera z MVC, ale… byłoby to stwierdzenie bardzo na wyrost. Jest on swoistym spoiwem między Modelem, a View… ale konkretnie! Jaką rolę pełni ViewModel w naszej architekturze?
Otóż w uproszczeniu… Zawiera on logikę biznesową, odpowiedzialną za komunikację View z Modelem. Przykładowo (tak jak na załączonym wyżej obrazku), ViewModel może przekazać żądanie do Modelu dotyczące dodania lub modyfikacji danych. Może też wysłać żądanie do Modelu o pobranie aktualnych danych, które potem udostępni dla interfejsu użytkownika (np. Fragmentu lub Aktywności).
LiveData – biblioteka, która ułatwia prace ze zmianą danych.
W skrócie… LiveData to „posiadacz” danych (eng. data holder), który powiadamia przypisanego mu obserwatora, gdy dane się zmienią. Jeśli teraz wydaje Ci się to trudne, to spokojnie. Wszystko zobaczysz na przykładzie.
Projekt – Lista Filmów
Na ten moment tyle teorii Ci wystarczy. Przejdźmy do praktyki. Zastosowanie wzorca MVVM zademonstruję Ci na BARDZO prostej aplikacji byś od razu mógł załapać o co w tym chodzi.
Aplikacja – krótki opis
Aplikacja (repo pod tym linkiem: [LINK]) wyświetla na ekranie listę filmów. Oczywiście pozwala także zapisywać nowe i dodawać je do listy. Możemy też usuwać wybrane pozycje.

Struktura – przegląd
- Warstwą View jest klasa FilmsActivity oraz jej przynależny plik layoutu films_activity.xml
- Warstwą ViewModel jest klasa FilmsViewModel.
- Źródłem danych będzie lokalna baza danych. Do komunikacji z nią użyjemy biblioteki Room.
- Warstwa Modelu jest tutaj nieco bardziej skomplikowana, ale o tym już za chwilę.
Struktura – Warstwa View
Tak wygląda to bazowo:
typealias OnRemoveFilmListener = (filmModel: FilmModel) -> Unit
class FilmsActivity : AppCompatActivity() {
private val adapter: FilmsAdapter by lazy {
FilmsAdapter(onRemoveFilmListener = {
/* TODO Remove film action */
})
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_films)
initRecyclerView()
initListeners()
}
private fun initRecyclerView() {
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
}
private fun initListeners() {
saveButton.setOnClickListener
// TODO Save new film - action
}
}
}
Struktura – Źródło danych
Tak jak wspomniałem wcześniej, naszym źródłem danych będzie lokalna baza danych. Komunikacja z tą bazą (działania CRUD) odbywać się będzie za pomocą biblioteki Room. Temat tej biblioteki poruszę w kolejnym artykule. By opanować MVVM znajomość Rooma nie jest konieczna więc możesz śmiało przekopiować kod ze zdalnego repozytorium dostępnego pod tym linkiem: [LINK].
Struktura – Warstwa Modelu
Do obsługi danych w naszej aplikacji zastosujemy wzorzec Repository. Służy on ważnemu celowi: oddziela źródła danych od reszty aplikacji.
Dzięki zastosowaniu tego wzorca, nasz FilmsViewModel nie będzie musiał wiedzieć, skąd i w jaki sposób dane będą pobierane (i dlatego, możemy darować sobie opanowanie Rooma bo takie dane równie dobrze moglibyśmy pobierać z SharedPreferences lub Cache aplikacji). Po prostu nasz ViewModel będzie wysyłał żądanie do Repository, a Repository zadba o dostarczenie odpowiednich danych lub ich modyfikację.

Mimo że moduł Repozytorium może wydawać Ci się zbędny to już teraz chcę pokazać Ci jego podstawowe działanie byś do niego przywykł 😉 W pracy programisty będziesz z niego korzystał bardzo, bardzo często 😉
class FilmsRepository {
/* TODO Should be injected by DI :) */
private val filmsDao: FilmsDao = FilmsApplication.database.filmDao()
fun fetchAllFilms(): LiveData<List<FilmModel>> =
filmsDao.fetchAllFilms()
fun storeFilm(film: FilmModel) {
filmsDao.storeFilm(film)
}
fun removeFilm(filmModel: FilmModel) {
filmsDao.removeFilm(filmModel)
}
}
Jak pewnie zauważyłeś, metoda fetchAllFilms() zwraca wartość typu: LiveData<List<FilmModel>> . Czyli możemy potem zapiąć się na tym posiadaczu danych i w momencie gdy zmienią się jego dane (coś zostanie dodane lub usunięte) będziemy mogli wykonać akcję. Na przykład wyświetlenie zaktualizowanych danych. Zaraz zobaczysz o co chodzi 😉
Struktura – Warstwa ViewModel
Teraz czas na ViewModel. W konstruktorze inicjalizujemy nasze Repository, które po swojej stronie zajmie się komunikacją z bazą. Na cele aplikacji pokazowej, nie stosowałem tutaj Dependency Injection (by nie wprowadzać nie potrzebnego zamieszania). Przyjdzie na to czas. Czym jest i jak stosować DI objaśnię w kolejnych artykułach.
Działania na każdym źródle danych muszą zostać wykonane asynchronicznie by nie blokowały wątku głównego. Z tego powodu zastosowaliśmy AsyncTask. Oczywiście, do działań asynchronicznych warto zastosować ReactiveX (np. RxJava) lub Coroutines, ale na wszystko przyjdzie czas. W kolejnych artykułach pokażę Ci jak ich używać 😉
class FilmsViewModel(
private val repository: FilmsRepository = FilmsRepository()
) : ViewModel() {
fun fetchAllFilmsLiveData(): LiveData<List<FilmModel>> =
repository.fetchAllFilms()
fun removeFilm(filmModel: FilmModel) {
DeleteAsyncTask(repository).execute(filmModel)
}
fun storeFilm(title: String) {
val filmModel = FilmModel(title)
InsertAsyncTask(repository).execute(filmModel)
}
}
private class InsertAsyncTask(private val repository: FilmsRepository) : AsyncTask<FilmModel, Void, Void>() {
override fun doInBackground(vararg params: FilmModel): Void? {
repository.storeFilm(params[0])
return null
}
}
private class DeleteAsyncTask(private val repository: FilmsRepository) : AsyncTask<FilmModel, Void, Void>() {
override fun doInBackground(vararg params: FilmModel): Void? {
repository.removeFilm(params[0])
return null
}
}
Ostatnia prosta! Czyli połączenie naszych warstw w spójną całość
Mamy już nasz Model, View oraz ViewModel. Teraz czas je połączyć by nasza aplikacja działała.
Połączenie View z ViewModel
Byśmy mogli wysyłać żądania do naszego ViewModelu musimy „połączyć” klasę View z ViewModelem. Jak to zrobić? Musimy zainicjalizować ViewModel w klasie View (FilmsActivity).
private val viewModel: FilmsViewModel by viewModels()
Kiedy skorzystasz z metody viewModels(), zadzieje się bardzo fajna rzecz. Nastąpi sprawdzenie czy instancja tej klasy już istnieje, a potem w zależności od wyniku, nastąpi albo utworzenie nowej instancji klasy ViewModelu albo jej zwrócenie (w przypadku gdy już istnieje).
I to tyle. Przejdźmy teraz do wywoływania akcji na obiekcie klasy ViewModelu.
- Pierwszą akcją będzie obsłużenie kliknięcia na przycisk „Save”. Czyli: zapisanie filmu:
private fun initListeners() {
saveButton.setOnClickListener {
viewModel.storeFilm(titleInput.text.toString())
}
}
2. Kolejną akcją będzie obsłużenie usunięcia filmu z listy:
private val adapter: FilmsAdapter by lazy {
FilmsAdapter(
onRemoveFilmListener = { viewModel.removeFilm(it) }
)
}
Teraz pozostaje nam wyświetlenie listy filmów na ekranie i ciągłe nasłuchiwanie na zmiany. Jak to zrobić? Równie prosto. Stwórzmy metodę initObservers() i wywołajmy ją w ciele metody onCreate(…).
private fun initObservers() {
viewModel.fetchAllFilmsLiveData().observe(this, Observer { films ->
films?.let {
adapter.setItems(it)
}
})
}
- Metoda fetchAllFilmsLiveData() zwraca nam „posiadacza” danych (LiveData).
- Zapinamy się na na niego i za pomocą obserwatora nasłuchujemy na zmianę danych.
- Gdy zmiana danych nastąpi, wołana jest metoda adapter.setItems(films) i nasza lista zostaje zaktualizowana.
I tak wygląda podstawowe użycie LiveData. W kolejnych artykułach przybliżę Ci więcej szczegółów i ciekawostek.
I to było by na tyle 🙂
I to już koniec! Wiesz już o co chodzi z wzorcem MVVM. Poznałeś też o czym jest i kiedy stosować wzorzec Repository. To na prawdę spoooro! Mogę śmiało powiedzieć, że zasłużyłeś na nagrodę!
W kolejnym artykule spodziewaj się omówienia biblioteki Room 🙂 W razie jakichkolwiek pytań śmiało daj mi znać w komentarzu, na facebooku lub poprzez formularz kontaktowy
PS. wpadnij też po NIESPODZIANKĘ i zapisz się na mailing by nie przegapić maili merytorycznych oraz kolejnych postów
Pozdrawiam, Kamil Krzysztof.