Öncelikle MVVM yapısı Model, View, ViewModel üzerine kurgulanmaktadır.
- Model: Model, uygulamanın verilerini tutar. Doğrudan view ile iletişim kurmaz. Genel olarak, verileri Observables aracılığıyla ViewModel'e aktarır.
- View: View, uygulamanın arayüzünü kapsamaktadır.
- ViewModel: Model ve View arasında bir köprü görevi görür. Modelden gelen datanın View'e aktarılmasından sorumludur. Verilerin anlık aktarımı için hook veya callback metodları kullanılmaktadır.
MVP Patterni İle Arasındaki Fark
- Middle Layer'de Presenter yerine ViewModel kullanılmaktadır.
- Presenter, View'e ait referanslar tutar. Ancak ViewModelde buna gerek kalmamaktadır.
- Presenter, View'i klasik yolları kullanarak güncellemektedir.
- ViewModel, Model üzerinden gelen verileri iletebilmektedir.
- Presenter ve View 1'e 1 (one to one) ilişki içindedir.
- View ve ViewModelde ise 1'e çok yönlü (one to many) ilişki içindedir.
- ViewModel, View'un onu dinlediğini bilmez.
Android'de MVVM'yi uygulamanın iki yöntemi bulunmaktadır:
- Data Binding
- RX Java
Makalemize Data Binding yöntemini kullanarak devam edelim. Data Binding, verileri doğrudan xml dosyalar üzerinden bağlamak için Google tarafından tanıtılan bir kütüphanedir.
Şimdi MVVM'i kullanarak email ve password içeren basit bir login sayfası oluşturalım. Uygulamamızda View'e ait herhangi bir referans tutulmaksızın ViewModel üzerinden iletilen verinin nasıl gösterildiğini göreceğiz.İpucu: Herhangi bir referans tutulmadan bir sınıfa bildirimde bulunmak nasıl mümkün olabilir?
Bu işlem üç şekilde gerçekleştirilebilir;
- Çift Yönlü Data Binding kullanarak
- Live Data kullanarak
- RxJava kullanarak
Çift Yönlü Data Binding Kullanımı
Çift Yönlü Data Binding, uygulama arayüzünde bulunan nesnelerin verilerle çift taraflı haberleşmesine imkan sunan bir tekniktir. Bu tekniğe göre layout üzerinden ViewModel'e ve ViewModel üzerinden layouta data aktarımı sağlanabilmektedir.
Bizim örnek projemiz ise ViewModel üzerinden veri aktarımı ve yaşanan değişikliklerin Observer yapısı üzerinden yakalanmasını kapsamaktadır.
Bunun için BindingAdapter ve XML'de tanımlanan custom attributelere ihtiyacımız var. Binding Adapter, custom attribute üzerinde gerçekleşen değişiklikleri izleyecektir.
Örnek Android MVVM Projeye ait Klasör Yapısı
Projemizde klasör yapısını aşağıdaki şekilde kurgulayacağız. Projemizde sadece login sayfası kurguladığımızdan dolayı User modeli, LoginViewModel sınıfı ve LoginActivity yeterli olmaktadır.
Data Binding Kütüphanesinin Eklenmesi
Aşağıdaki kod parçasını app klasörünüz altında bulunan build.gradle dosyasına ekleyelim.android { dataBinding { enabled = true } }Eklenen kod parçası data bindingin projemizde aktif hale gelmesini sağlamaktadır.
Dependecylerin Eklenmesi
Aşağıdaki kod parçasını app klasörünüz altında bulunan build.gradle dosyasında dependecy alanına ekleyelim.implementation 'android.arch.lifecycle:extensions:1.1.0'
Model
Uygulamamızda kullanıcıya ait email ve şifre alanlarını tutacak User modeli aşağıdaki şekilde oluşturalım.public class User { private String email; private String password; public User(String email, String password) { this.email = email; this.password = password; } public void setEmail(String email) { this.email = email; } public String getEmail() { return email; } public void setPassword(String password) { this.password = password; } public String getPassword() { return password; } }Projemizde kullanılacak olan çift yönlü data binding işlemi yukarıda da dile getirdiğimiz gibi xml layout üzerinde tanımlanan viewlerin güncellenmesinde veya view üzerinde yapılan işlemin Model üzerine aktarımında kullanılmaktadır.
Çift yönlü data binding için kullanılan syntax @={variable} şeklindedir.
Layout
Login sayfasına ait arayüz için aşağıdaki kodu activity_login.xml dosyası içerisine yapıştıralım.?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="yazilimdersi.info.example.samplemvvm.viewmodels.LoginViewModel" /> </data> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margin="8dp" android:orientation="vertical"> <ImageView android:id="@+id/yazilimdersiImageView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:padding="10dp" android:src="@drawable/icon_app_logo" /> <EditText android:id="@+id/inEmail" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Email" android:inputType="textEmailAddress" android:padding="8dp" android:text="@={viewModel.userEmail}" /> <EditText android:id="@+id/inPassword" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Password" android:inputType="textPassword" android:padding="8dp" android:text="@={viewModel.userPassword}" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:background="@color/colorPrimaryDark" android:textColor="@color/colorWhite" android:onClick="@{()-> viewModel.onLoginClicked()}" android:text="LOGIN" bind:toastMessage="@{viewModel.toastMessage}" /> </LinearLayout> </ScrollView> </layout>Data Bindingde arayüzüne yönelik işlemlerde sayfanın en üst kısmında layout taglerinin tanımlanması gerekmektedir. Yukarıda yapılan tanımlamalar sonrasında ViewModel dataları direkt olarak arayüze bind edebilmektedir. Kodlamada gördüğünüz üzere ()-> viewModel.onLoginClicked() fonksiyonu LoginViewModel sınıfı içerisinde tanımlanmıştır. Kullanıcı login butonuna bastığı anda bu bilginin ViewModel e iletilmesi sağlanmaktadır. Ayrıca ViewModel üzerinden gelen verilerin view e aktarımına örnek olarak (bind:toastMessage="@{viewModel.toastMessage}" kullanımını inceleyebiliriz. toastMessage verisinde herhangi bir değişiklik olduğu takdirde view üzerinde bu mesajın gösterimini sağlanmaktadır.
View Model
Aşağıdaki kod parçasını LoginViewModel.java sınıfı içerisine yapıştıralım.import android.databinding.BaseObservable; import android.databinding.Bindable; import android.text.TextUtils; import android.util.Patterns; import com.android.databinding.library.baseAdapters.BR; import yazilimdersi.info.example.samplemvvm.model.User; public class LoginViewModel extends BaseObservable { private User user; private String successMessage = "Başarılı şekilde giriş yapıldı"; private String errorMessage = "Email veya Şifre hatalı"; @Bindable private String toastMessage = null; public String getToastMessage() { return toastMessage; } private void setToastMessage(String toastMessage) { this.toastMessage = toastMessage; notifyPropertyChanged(BR.toastMessage); } public void setUserEmail(String email) { user.setEmail(email); notifyPropertyChanged(BR.userEmail); } @Bindable public String getUserEmail() { return user.getEmail(); } @Bindable public String getUserPassword() { return user.getPassword(); } public void setUserPassword(String password) { user.setPassword(password); notifyPropertyChanged(BR.userPassword); } public LoginViewModel() { user = new User("",""); } public void onLoginClicked() { if (isInputDataValid()) setToastMessage(successMessage); else setToastMessage(errorMessage); } public boolean isInputDataValid() { return !TextUtils.isEmpty(getUserEmail()) && Patterns.EMAIL_ADDRESS.matcher(getUserEmail()).matches() && getUserPassword().length() > 5; } }LoginViewModel sınıfımızı ViewModel üzerinden extend edebiliriz. Ancak amacımız login butonuna tıklanıldığında Toast mesajı gösterecek bir kurgu olduğundan BaseObservable sınıfını extend etmemiz daha iyi olacaktır.
toastMessage ile alakalı olarak ViewModel sınıfımızda getter ve setter methodlarını tanımlamamız gerekmektedir. settter methodu içerisinde notifyPropertyChanged metodunu çağırmamız gerekmekte. Böylelikle datamızın değiştiği bilgisini View ile paylaşmış oluyoruz. View de yaptığımız tanımlamalara göre aksiyon alacaktır. BR sınıfı proje rebuild edilirken otomatik olarak data binding kütüphanesi tarafından oluşturulmaktadır.
LoginActivity.java sınıfı içinde aşağıdaki kod parçasını yapıştıralım.import androidx.appcompat.app.AppCompatActivity; import androidx.databinding.BindingAdapter; import androidx.databinding.DataBindingUtil; import yazilimdersi.info.example.samplemvvm.R; import yazilimdersi.info.example.samplemvvm.databinding.ActivityLoginBinding; import yazilimdersi.info.example.samplemvvm.viewmodels.LoginViewModel; public class LoginActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityLoginBinding activityLoginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login); activityLoginBinding.setViewModel(new LoginViewModel()); activityLoginBinding.executePendingBindings(); } @BindingAdapter({"toastMessage"}) public static void runMe(View view, String message) { if (message != null) Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show(); } }
DataBinding librarysi sayesinde ActivityLoginBinding sınıfı otomatik olarak oluşturulmaktadır. @BindingAdapter methodunda tanımlı olan toastMessage güncellendiği anda içerisinde bulunan kod parçası trigger etmektedir.
Böylelikle ViewModel, yukarıdaki kodlamada görüldüğü üzere View üzerindeki işlemleri izleyerek Model'i güncellemektedir. Ayrıca Model, notifyPropertyChanged metodunu kullanarak View'i trigger edebilmektedir.
Uygulamamıza ait ekran görüntüsü aşağıdaki şekildedir.