Android MVVM ViewModel'de Bağlam nasıl edinilir


96

Android uygulamamda MVVM modelini uygulamaya çalışıyorum. ViewModels'in android'e özel kod içermemesi gerektiğini okudum (testi kolaylaştırmak için), ancak çeşitli şeyler için bağlam kullanmam gerekiyor (xml'den kaynak alma, tercihleri ​​başlatma vb.). Bunu yapmanın en iyi yolu nedir? Bunun AndroidViewModeluygulama bağlamına bir referansı olduğunu gördüm , ancak android'e özgü kod içeriyor, bu yüzden bunun ViewModel'de olması gerekip gerekmediğinden emin değilim. Ayrıca bunlar Etkinlik yaşam döngüsü olaylarına da bağlıdır, ancak bileşenlerin kapsamını yönetmek için hançer kullanıyorum, bu yüzden bunun onu nasıl etkileyeceğinden emin değilim. MVVM modelinde ve Dagger'da yeniyim, bu yüzden her türlü yardım için minnettarım!


Birinin kullanmaya AndroidViewModelCannot create instance exception
çalışıp

Bir ViewModel'de Bağlam kullanmamalısınız, bunun yerine Bağlamı bu şekilde elde etmek için bir UseCase oluşturun
Ruben Caster

Yanıtlar:


79

ApplicationTarafından sağlanan bir bağlamı kullanabilirsiniz AndroidViewModel, genişletmeniz gerekir AndroidViewModelki bu sadece ViewModelbir Applicationreferans içerir .


Büyü gibi çalıştı!
SPM

64

Android Mimari Bileşenleri için Modeli Görüntüle,

Aktivite Bağlamınızı Aktivitenin ViewModel'ine bir bellek sızıntısı olarak geçirmek iyi bir uygulama değildir.

Dolayısıyla, ViewModel'inizdeki bağlamı elde etmek için, ViewModel sınıfı Android Görünüm Modeli Sınıfını genişletmelidir . Bu şekilde, aşağıdaki örnek kodda gösterildiği gibi bağlamı elde edebilirsiniz.

class ActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val context = getApplication<Application>().applicationContext

    //... ViewModel methods 

}

3
Neden doğrudan uygulama parametresi ve normal bir ViewModel kullanmıyorsunuz? "GetApplication <Application> ()" da bir anlam göremiyorum. Sadece standart metin ekler.
inanılmaz

Neden hafıza sızıntısı olsun?
Ben Butterworth

Anlıyorum, çünkü bir etkinlik, görünüm modelinden daha sık yok edilecek (örneğin, ekran dönerken). Ne yazık ki, bellek çöp toplama tarafından serbest bırakılmayacak çünkü görünüm modelinde hala bir referans var.
Ben Butterworth

52

ViewModels, testi kolaylaştırmak için Android'e özel kod içermemelidir, çünkü testi kolaylaştıran soyutlamadır.

ViewModels'in bir Bağlam örneğini veya Görünümler gibi bir şeyi veya bir Bağlamı tutan diğer nesneleri içermemesinin nedeni, bunun Etkinlikler ve Parçalardan ayrı bir yaşam döngüsüne sahip olmasıdır.

Bununla demek istediğim, uygulamanızda bir rotasyon değişikliği yaptığınızı varsayalım. Bu, Aktivitenizin ve Parçanızın kendisini yok etmesine ve böylece kendini yeniden yaratmasına neden olur. ViewModel, bu durum sırasında varlığını sürdürmek için tasarlanmıştır, bu nedenle, yok edilen Aktivite için hala bir Görünüm veya Bağlam tutuyorsa, çökme ve diğer istisnaların olma ihtimali vardır.

Yapmak istediğiniz şeyi nasıl yapmanız gerektiğine gelince, MVVM ve ViewModel, JetPack'in Databinding bileşeniyle gerçekten iyi çalışıyor. Genellikle bir String, int, vb. Depolayacağınız çoğu şey için, View'lerin doğrudan görüntülemesini sağlamak için Databinding'i kullanabilirsiniz, böylece değeri ViewModel içinde saklamanız gerekmez.

Ancak Veri Bağlama istemiyorsanız, yine de Oluşturucu içindeki Bağlamı veya Kaynaklara erişmek için yöntemleri iletebilirsiniz. ViewModel'inizde bu Context'in bir örneğini tutmayın.


1
Anladığım kadarıyla, android'e özgü kodun dahil edilmesi, düz JUnit testlerinden çok daha yavaş olan enstrümantasyon testlerinin çalıştırılmasını gerektiriyordu. Şu anda tıklama yöntemleri için Veri Bağlama kullanıyorum, ancak bunun xml'den veya tercihler için kaynakları almaya nasıl yardımcı olacağını anlamıyorum. Tercihler için modelimin içinde de bir bağlama ihtiyacım olacağını yeni fark ettim. Şu anda yaptığım şey, Dagger'ın uygulama bağlamını enjekte etmesini sağlamaktır (bağlam modülü bunu uygulama sınıfı içindeki statik bir yöntemden alır)
Vincent Williams

@VincentWilliams Evet, bir ViewModel kullanmak kodunuzu UI bileşenlerinizden ayırmanıza yardımcı olur ve bu da test yapmanızı kolaylaştırır. Ancak, söylediğim şey, herhangi bir Bağlam, Görünüm veya benzerini dahil etmemenin birincil nedeninin test nedenlerinden değil, çökmelerden ve diğer hatalardan kaçınmanıza yardımcı olabilecek ViewModel'in yaşam döngüsünden kaynaklandığıdır. Veritabanına gelince, bu size kaynaklar konusunda yardımcı olabilir, çünkü koddaki kaynaklara erişmeniz gereken çoğu zaman bu String, color, dimen'i mizanpajınıza uygulama ihtiyacından kaynaklanır, bu da veritabanının doğrudan yapabileceği.
Jackey

3
Bir değer formu görünüm modeline dayalı bir metin görünümündeki metni değiştirmek istersem, dizenin yerelleştirilmesi gerekir, bu nedenle görünüm modelimde kaynaklara bağlam olmadan, kaynaklara nasıl erişeceğim?
Srishti Roy

3
@SrishtiRoy Veri bağlama kullanıyorsanız, bir TextView metnini görünüm modelinizdeki değere göre değiştirmek kolayca mümkündür. ViewModel'inizin içindeki bir Bağlama erişmeye gerek yoktur, çünkü bunların tümü düzen dosyalarında gerçekleşir. Ancak, ViewModel'inizde bir Bağlam kullanmanız gerekiyorsa, ViewModel yerine AndroidViewModel'i kullanmayı düşünmelisiniz. AndroidViewModel, getApplication () ile çağırabileceğiniz Uygulama Bağlamını içerir, böylece ViewModel'iniz bir bağlam gerektiriyorsa bu İçerik ihtiyaçlarınızı karşılamalıdır.
Jackey

1
@Pacerier ViewModel'in temel amacını yanlış anladınız. Endişelerin ayrılığı meselesi. ViewModel, sorumluluğu View katmanı tarafından görüntülenen verileri korumak olduğu için herhangi bir görünüme referans tutmamalıdır. UI bileşenleri, yani görünümler, Görünüm katmanı tarafından korunur ve Android Sistemi, gerekirse Görünümleri yeniden oluşturur. Eski Görünümlere referans tutmak, bu davranışla çelişir ve bellek sızıntılarına neden olur.
Jackey

16

Kısa cevap - Bunu yapma

Neden ?

Görünüm modellerinin tüm amacını ortadan kaldırır

Görünüm modelinde yapabileceğiniz hemen hemen her şey, LiveData örnekleri ve diğer çeşitli önerilen yaklaşımlar kullanılarak etkinlik / parça içinde yapılabilir.


26
O halde neden AndroidViewModel sınıfı var?
Alex Berdnikov

1
@AlexBerdnikov MVVM'nin amacı, görünümü (Etkinlik / Parça) ViewModel'den MVP'den daha fazla izole etmektir. Böylece test etmesi daha kolay olacak.
hushed_voice

3
@free_style Açıklama için teşekkürler, ancak soru hala geçerli: ViewModel'de bağlamı korumamalıysak, neden AndroidViewModel sınıfı var? Tüm amacı uygulama bağlamı sağlamak, değil mi?
Alex Berdnikov

7
@AlexBerdnikov Viewmodel içinde Activity bağlamını kullanmak bellek sızıntılarına neden olabilir. Dolayısıyla, AndroidViewModel Sınıfını kullanarak, herhangi bir bellek sızıntısına neden olmayacak (umarız) Uygulama Bağlamı tarafından sağlanacaksınız. Dolayısıyla, AndroidViewModel kullanmak, etkinlik bağlamını ona iletmekten daha iyi olabilir. Ancak yine de bunu yapmak testi zorlaştıracaktır. Bu benim onunla ilgili düşüncem.
hushed_voice

1
Depodan res / raw klasöründen dosyaya erişemiyorum?
Fugogugo

15

Doğrudan ViewModel'de bir Context kullanmak yerine yaptığım şeyi, bana ihtiyacım olan kaynakları verecek olan ResourceProvider gibi sağlayıcı sınıfları yaptım ve bu sağlayıcı sınıflarını ViewModel'ime enjekte ettim.


1
ResourcesProvider'ı AppModule'da Dagger ile kullanıyorum. Kaynaklar için bağlam elde etmek için ResourcesProvider veya AndroidViewModel'den bağlam almak için bu iyi bir yaklaşım daha mı iyidir?
Usman Rana

@Vincent: ViewModel içinde Drawable almak için resourceProvider nasıl kullanılır?
Bulma

@Vegeta getDrawableRes(@DrawableRes int id)ResourceProvider sınıfının içine benzer bir yöntem eklersiniz
Vincent Williams

1
Bu, çerçeve bağımlılıklarının etki alanı mantığına (ViewModels) geçmemesi gerektiğini belirten Temiz Mimari yaklaşımına aykırıdır.
IgorGanapolsky

1
@IgorGanapolsky sanal makineleri tam olarak etki alanı mantığı değildir. Etki alanı mantığı, birkaçını adlandırmak için uygulayıcılar ve depolar gibi diğer sınıflardır. Sanal makineler, etki alanınızla etkileşimde bulundukları için "yapıştırıcı" kategorisine girer, ancak doğrudan değildir. Sanal makineleriniz etki alanınızın bir parçasıysa, onlara çok fazla sorumluluk verdiğiniz için kalıbı nasıl kullandığınızı yeniden düşünmelisiniz.
mradzinski

9

TL; DR: Uygulamanın bağlamını ViewModel'lerinizdeki Dagger aracılığıyla enjekte edin ve kaynakları yüklemek için kullanın. Görüntüleri yüklemeniz gerekiyorsa, View örneğini Databinding yöntemlerinden bağımsız değişkenler aracılığıyla geçirin ve bu View bağlamını kullanın.

MVVM iyi bir mimari ve kesinlikle Android geliştirmenin geleceği, ancak hala yeşil olan birkaç şey var. Örneğin bir MVVM mimarisindeki katman iletişimini ele alalım, farklı geliştiricilerin (çok iyi bilinen geliştiriciler) farklı katmanları farklı yollarla iletmek için LiveData'yı kullandığını gördüm. Bazıları ViewModel'i UI ile iletişim kurmak için LiveData'yı kullanır, ancak daha sonra Depolar ile iletişim kurmak için geri arama arayüzlerini kullanırlar veya Interactor / UseCases'e sahiptirler ve onlarla iletişim kurmak için LiveData kullanırlar. Buradaki nokta, her şeyin henüz % 100 tanımlanmadığıdır .

Bununla birlikte, özel sorununuzla ilgili yaklaşımım, String gibi şeyleri strings.xml'imden almak için ViewModels'imde kullanmak üzere DI aracılığıyla bir Uygulamanın bağlamına sahip olmaktır.

Görüntü yüklemeyle uğraşıyorsam, View nesnelerini Databinding adaptör yöntemlerinden geçirmeye ve görüntüleri yüklemek için View bağlamını kullanmaya çalışırım. Neden? çünkü görüntüleri yüklemek için Uygulamanın bağlamını kullanırsanız bazı teknolojiler (örneğin Glide) sorunlarla karşılaşabilir.

Umarım yardımcı olur!


5
TL; DR en üstte olmalı
Jacques Koorts

1
Cevabınız için teşekkür ederim. Bununla birlikte, viewmodel'inizi androidviewmodel'den genişletebilir ve sınıfın kendisinin sağladığı yerleşik bağlamı kullanabilirseniz, bağlamı enjekte etmek için neden hançeri kullanırsınız? Özellikle hançer ve MVVM'nin birlikte çalışmasını sağlayacak saçma standart kod miktarı düşünüldüğünde, diğer çözüm çok daha net görünüyor. Bu konudaki düşüncelerin neler?
Josip Domazet

8

Diğerlerinin de belirttiği gibi, AndroidViewModeluygulamayı almak için elde edebileceğiniz şeyler var, Contextancak yorumlarda topladığım şeyden, MVVM amacını yenen @drawablekendi içinden s manipüle etmeye çalışıyorsunuz ViewModel.

Genel olarak, gerek bir olması Contextsizin de ViewModelneredeyse evrensel size arasına mantığı bölmek nasıl yeniden düşünmek düşünmelisiniz anlaşılacağı Views ve ViewModels.

ViewModelÇekilebilir öğeleri çözümlemek ve bunları Faaliyet / Parçaya beslemek yerine, Parçanın / Faaliyetin ViewModel,. Diyelim ki, açma / kapama durumu için bir görünümde görüntülenecek farklı çekmecelere ihtiyacınız var - ViewModel(muhtemelen boole) durumunu tutması gereken şey budur, ancak Viewçekilebilir olanı buna göre seçmek işidir.

DataBinding ile oldukça kolay yapılabilir :

<ImageView
...
app:src="@{viewModel.isOn ? @drawable/switch_on : @drawable/switch_off}"
/>

Daha fazla durumunuz ve çekilebilir öğeleriniz varsa, mizanpaj dosyasında gereksiz mantığı önlemek için , örneğin bir değeri (örneğin, kart takımları) çeviren özel bir BindingAdapter yazabilirsiniz.EnumR.drawable.*

Ya da belki Contextkendi içinde kullandığınız bazı bileşenlere ihtiyacınız var ViewModel- o zaman bileşeni dışında ViewModeloluşturun ve Contextiçeri aktarın . DI veya tekli anahtarları kullanabilir veya -bağımlı bileşeni ViewModelin Fragment/ başlatmadan hemen önce oluşturabilirsiniz Activity.

Neden rahatsız: ContextAndroid'e özgü bir şeydir ve s'dekilere bağlı olarak ViewModelkötü bir uygulamadır: birim testinin önünde duruyorlar. Öte yandan, kendi bileşen / servis arayüzleriniz tamamen sizin kontrolünüz altındadır, böylece test için kolayca alay edebilirsiniz.


5

uygulama bağlamına bir referansı vardır, ancak bu, android'e özel kod içerir

İyi haber, Mockito.mock(Context.class)testlerde istediğiniz içeriği kullanabilir ve geri döndürebilirsiniz!

ViewModelNormalde yaptığınız gibi sadece bir kullanın ve ona normalde yaptığınız gibi ViewModelProviders.Factory aracılığıyla ApplicationContext verin.


3

uygulama bağlamına getApplication().getApplicationContext()ViewModel içinden erişebilirsiniz . Kaynaklara, tercihlere vb. Erişmek için ihtiyacınız olan şey budur.


Sanırım sorumu daraltmak için. Viewmodel içinde bir bağlam referansına sahip olmak kötü müdür (bu testi etkilemez mi?) Ve AndroidViewModel sınıfını kullanmak Dagger'ı herhangi bir şekilde etkiler mi? Aktivite yaşam döngüsüne bağlı değil mi? Bileşenlerin yaşam döngüsünü kontrol etmek için Dagger kullanıyorum
Vincent Williams

14
ViewModelSınıf yoktur getApplicationyöntemi.
beroal

4
Hayır, ama AndroidViewModelyapar
4Oh4

1
Ancak Uygulama örneğini yapıcısında
iletmeniz

2
Uygulama bağlamına sahip olmak büyük bir sorun oluşturmaz. Bir etkinlik / parça bağlamına sahip olmak istemezsiniz çünkü parça / etkinlik yok edilirse ve görünüm modelinin şu anda var olmayan bağlama bir referansı varsa, işiniz biter. Ancak, UYGULAMA bağlamını asla yok etmeyeceksiniz, ancak VM'nin hala buna bir referansı var. Sağ? Uygulamanızın çıktığı ancak Viewmodel'in çıkmadığı bir senaryo hayal edebiliyor musunuz? :)
user1713450

3

ViewModel'inizde Android ile ilgili nesneleri kullanmamalısınız, çünkü ViewModel kullanmanın nedeni, java kodunu ve Android kodunu ayırmaktır, böylece iş mantığınızı ayrı ayrı test edebilir ve ayrı bir Android bileşenleri katmanına ve iş mantığınıza sahip olursunuz. ve veriler, çökmelere neden olabileceğinden ViewModel'inizde bağlama sahip olmamalısınız


2
Bu makul bir gözlemdir, ancak arka uç kitaplıklarından bazıları hala MediaStore gibi Uygulama bağlamlarını gerektirir. Aşağıdaki 4gus71n'nin cevabı nasıl ödün verileceğini açıklıyor.
Bryan W. Wagner

1
Evet Uygulama Bağlamını Kullanabilirsiniz, ancak Etkinliklerin Bağlamını Kullanamazsınız, Uygulama Bağlamı uygulama yaşam döngüsü boyunca yaşadığından ancak Etkinlik Bağlamını herhangi bir eşzamansız işleme Etkinlik Bağlamını Aktarmak bellek sızıntılarına neden olabilir. Gönderimde belirtilen Bağlam Etkinliktir. Bağlam: Ancak yine de, uygulama bağlamı olsa bile hiçbir zaman uyumsuz sürece bağlamı aktarmamaya dikkat etmelisiniz.
Rohit Sharma

2

Sınıfı SharedPreferenceskullanırken sorun yaşıyordum , ViewModelbu yüzden yukarıdaki cevaplardan tavsiyeler aldım ve aşağıdakileri kullanarak yaptım AndroidViewModel. Şimdi her şey harika görünüyor

İçin AndroidViewModel

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;

public class HomeViewModel extends AndroidViewModel {

    private MutableLiveData<String> some_string;

    public HomeViewModel(Application application) {
        super(application);
        some_string = new MutableLiveData<>();
        Context context = getApplication().getApplicationContext();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        some_string.setValue("<your value here>"));
    }

}

Ve içinde Fragment

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;


public class HomeFragment extends Fragment {


    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        final View root = inflater.inflate(R.layout.fragment_home, container, false);
        HomeViewModel homeViewModel = ViewModelProviders.of(this).get(HomeViewModel.class);
        homeViewModel.getAddress().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String address) {


            }
        });
        return root;
    }
}

0

Bunu şu şekilde yarattım:

@Module
public class ContextModule {

    @Singleton
    @Provides
    @Named("AppContext")
    public Context provideContext(Application application) {
        return application.getApplicationContext();
    }
}

Ve sonra AppComponent'e ContextModule.class'ı ekledim:

@Component(
       modules = {
                ...
               ContextModule.class
       }
)
public interface AppComponent extends AndroidInjector<BaseApplication> {
.....
}

Ve sonra ViewModel'ime bağlamı ekledim:

@Inject
@Named("AppContext")
Context context;

0

Aşağıdaki düzeni kullanın:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}

0

ViewModel'e bir Bağlamın enjekte edilmesiyle ilgili sorun, Bağlamın ekran dönüşüne, gece moduna veya sistem diline bağlı olarak herhangi bir zamanda değişebilmesi ve döndürülen tüm kaynakların buna göre değişebilmesidir. Basit bir kaynak kimliği döndürmek, getString ikameleri gibi ekstra parametreler için sorunlara neden olur. Yüksek düzeyli bir sonuç döndürmek ve işleme mantığını Aktiviteye taşımak test etmeyi zorlaştırır.

Benim çözümüm, ViewModel'in daha sonra Aktivitenin Bağlamında çalıştırılacak bir işlevi oluşturup döndürmesini sağlamaktır. Kotlin'in sözdizimsel şekeri bunu inanılmaz derecede kolaylaştırıyor!

ViewModel.kt:

// connectedStatus holds a function that calls Context methods
// `this` can be elided
val connectedStatus = MutableLiveData<Context.() -> String> {
  // initial value
  this.getString(R.string.connectionStatusWaiting)
}
connectedStatus.postValue {
  this.getString(R.string.connectionStatusConnected, brand)
}
Activity.kt  // is a Context

override fun onCreate(_: Bundle?) {
  connectionViewModel.connectedStatus.observe(this) { it ->
   // runs the posted value with the given Context receiver
   txtConnectionStatus.text = this.run(it)
  }
}

Bu, ViewModel'in görüntülenen bilgileri hesaplamak için tüm mantığı tutmasına izin verir, birim testleri ile doğrulanır ve Aktivite, hataları gizlemek için dahili bir mantık olmaksızın çok basit bir sunumdur.


Ve veri bağlama desteğini etkinleştirmek için basit bir @BindingAdapter("android:text") fun setText(view: TextView, value: Context.() -> String) { view.text = view.context.run(value) }
BindingAdapter eklemeniz yeterli
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.