';

Model-View-ViewModel to kolejny – po Model-View-Presenter – wzorzec architektoniczny, który jest chętnie wykorzystywany podczas tworzenia aplikacji dla Androida. Zakłada on, że architektura aplikacji składa się z trzech komponentów:
1. View – komponent ten odpowiedzialny jest za interakcję z użytkownikiem. Prezentuje dane, jak i obsługuje dane wejściowych poprzez tzw. Data Binding (to zagadnienie omówię poniżej).
2. ViewModel – podobnie jak Controller czy Presenter znane z architektur MVC i MVP, jest to abstrakcja właściwości i funkcji widoku. ViewModel automatyzuje przepływ danych pomiędzy nim samym a widokiem, a także nie tworzy zależności do obiektu widoku.
3. Model – Funkcje i dane aplikacji przedstawiające domenę.

Zaimplementujmy prostą aplikację dla systemu Android, by lepiej zrozumieć ten wzorzec i jego zalety.

Zacząć musimy od prostego widoku.
Stwórzmy okienko zawierające pole edytowalne (dane wejściowe) oraz pole tekstowe wyświetlające wynik procesu (prezentację danych).

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/editText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:ems="10"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toTopOf="@id/editText"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

… a następnie jego Activity.

class MainActivity : AppCompatActivity()
{
  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)

    editText.addTextChangedListener(object : TextWatcher {
      override fun afterTextChanged(p0: Editable?) { }
      override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
      override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int)    
      { }
    })
  }
}

Naszym celem jest obsłużenie tekstu wejściowego z pola editText, przekierowanie go poprzez ViewModel do Modelu, a następnie jego przeprocesowanie i zwrócenie wyniku z powrotem do widoku.
Jak możemy to osiągnąć?

Zacznijmy od utworzenia klasy obserwowalnego ViewModelu, który przydatny nam będzie w Data Bindingu.

import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.ViewModel

open class ObservableViewModel : ViewModel(), Observable
{
    private val callbacks = PropertyChangeRegistry()

    override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
        callbacks.add(callback)
    }

    override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
        callbacks.remove(callback)
    }

    fun notifyChange() = callbacks.notifyCallbacks(this, 0, null)
}

Teraz utworzyć możemy klasę ViewModel dla procesora tekstu:

class ProcessorViewModel : ObservableViewModel(), KoinComponent
{
    private val model: SomeModel by inject()

    val result = ObservableField<String>()

    fun process(text: String) {
        result.set(model.process(text))
        notifyChange()
    }
}

Zapewne powyższy kawałek kodu nie jest w pełni jasny, śpieszę zatem z wyjaśnieniami.
Klasa ProcessorViewModel implementuje interfejs KoinComponent. Koin jest niezwykle praktycznym frameworkiem Dependency Injection dla Kotlina, i choć nie jest istotny w kontekście wzorca MVVM, warto się z nim zaprzyjaźnić.
Opisywany ViewModel oferuje zaledwie jedną funkcję – process(), której zadaniem jest zlecenie modelowi przeprocesowanie tekstu, oraz przypisanie wyniku do obserwowalnego polu result typu ObservableField<String>. W tym właśnie momencie dochodzimy wreszcie do Data Bindingu…

Data Binding pozwala powiązać komponenty UI ze źródłowymi danymi, w sposób deklaratywny, a nie imperatywny. W naszym przypadku oznacza to powiązanie wartość result bezpośrednio z TextView. Każda zmiana result automatycznie powodowałaby widoczne dla użytkownika zmiany w UI. Jak tego dokonać?

Zacząć musimy od konfiguracji naszego projektu w pliku build.gradle.

android {
...
  dataBinding {
    enabled true
  }
}

Gdy dataBinding jest już włączony, przejdźmy do Activity, gdzie utworzymy instancję klasy ProcessorViewModel.

private val viewModel by lazy {
    ViewModelProviders.of(this)[ProcessorViewModel::class.java]
}

Następnie, powiążemy pole viewModel.result z TextView w UI. W pliku activity_main.xml czekają nas większe zmiany. Po pierwsze, by data binding działał poprawnie, cały dotychczasowy layout musi zostać opakowany klasą <layout>. Po drugie, utworzyć musimy instancję klasy ProcessorViewModel, która widoczna będzie w pliku XML i połączona będzie z analogiczną instancją utworzoną w Kotlinie. Następnie powiążemy ją z TextView za pomocą dedykowanej ku temu składni.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
<data>
  <variable name="viewmodel" type="pl.ptprogramming.example_project.viewModels.ProcessorViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/editText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:ems="10"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toTopOf="@id/editText"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="@{viewmodel.result}"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Data binding zadziała, gdy poprawnie powiążemy interesujące nas dane.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    binding.viewmodel = viewModel
    binding.lifecycleOwner = this
    
    editText.addTextChangedListener(object : TextWatcher {
      override fun afterTextChanged(p0: Editable?) { }
      override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
      override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int)    
      {
          p0?.let { viewModel.process(p0.toString()) }
      }
    })

To wszystko! Wspólnymi siłami stworzyliśmy bardzo prostą implementację architektury Model-View-ViewModel dla Androida, w Kotlinie.

MVVM podobny jest do innych wzorców architektonicznych, np. do MVP, lecz szczyci się zaletami, które wyróżniają go na tle konkurencji.

  • Rozdziela logikę biznesową od interfejsu użytkownika i umożliwia w łatwy, deklaratywny sposób łączyć źródła danych z komponentami UI.
  • Nie tworzy zależności pomiędzy ViewModel a View.
  • Umożliwia powiązanie jednego widoku z wieloma ViewModelami, co sprzyja realizacji zasadzy pojedynczej odpowiedzialności (ang. Single Responsibility). Wystarczy w analogiczny sposób dodać kolejny ViewModel do Activity!
Recommend
Share
Tagged in
Leave a reply