Wzorce Architektoniczne – MVVM

W tym artykule opowiem Ci o wzorcu MVVM. Przybliżę Ci jego idee oraz na podstawie prostej aplikacji poznasz jego zastosowanie.

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.

MVVM 2

Struktura – przegląd

  1. Warstwą View jest klasa FilmsActivity oraz jej przynależny plik layoutu films_activity.xml
  2. Warstwą ViewModel jest klasa FilmsViewModel.
  3. Źródłem danych będzie lokalna baza danych. Do komunikacji z nią użyjemy biblioteki Room.
  4. 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.

  1. 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)
        }
    })
}
  1. Metoda fetchAllFilmsLiveData() zwraca nam „posiadacza” danych (LiveData).
  2. Zapinamy się na na niego i za pomocą obserwatora nasłuchujemy na zmianę danych.
  3. 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.

  1. Link Do Repozytorium na GitHub
  2. Link do Niespodzianki 🙂