Cara mendapatkan Konteks di Android MVVM ViewModel

90

Saya mencoba menerapkan pola MVVM di aplikasi android saya. Saya telah membaca bahwa ViewModels seharusnya tidak berisi kode khusus android (untuk mempermudah pengujian), namun saya perlu menggunakan konteks untuk berbagai hal (mendapatkan sumber daya dari xml, menginisialisasi preferensi, dll). Apa cara terbaik untuk melakukannya? Saya melihat itu AndroidViewModelmemiliki referensi ke konteks aplikasi, namun itu berisi kode khusus android jadi saya tidak yakin apakah itu harus ada di ViewModel. Juga yang terkait dengan peristiwa siklus hidup Aktivitas, tetapi saya menggunakan dagger untuk mengelola cakupan komponen jadi saya tidak yakin bagaimana hal itu akan mempengaruhinya. Saya baru mengenal pola MVVM dan Dagger sehingga bantuan apa pun sangat kami hargai!

Vincent Williams
sumber
Untuk berjaga-jaga jika seseorang mencoba menggunakan AndroidViewModeltetapi mendapatkan Cannot create instance exceptionmaka Anda dapat merujuk ke jawaban saya ini stackoverflow.com/a/62626408/1055241
gprathour
Anda tidak boleh menggunakan Konteks dalam ViewModel, sebagai gantinya buatlah UseCase untuk mendapatkan Konteks dari situ
Ruben Caster

Jawaban:

71

Anda dapat menggunakan Applicationkonteks yang disediakan oleh AndroidViewModel, Anda harus memperpanjang AndroidViewModelyang hanya ViewModelmencakup Applicationreferensi.

Jay
sumber
Bekerja seperti pesona!
SPM
Bisakah seseorang menunjukkan ini dalam kode? Saya di Jawa
Biswas Khayargoli
55

Untuk Model Tampilan Komponen Arsitektur Android,

Ini bukan praktik yang baik untuk meneruskan Konteks Aktivitas Anda ke ViewModel Aktivitas karena ini adalah kebocoran memori.

Oleh karena itu untuk mendapatkan konteks dalam ViewModel Anda, kelas ViewModel harus memperluas Kelas Model Tampilan Android . Dengan begitu Anda bisa mendapatkan konteks seperti yang ditunjukkan pada kode contoh di bawah ini.

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

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

    //... ViewModel methods 

}
devDeejay
sumber
2
Mengapa tidak langsung menggunakan parameter aplikasi dan ViewModel normal? Saya tidak melihat gunanya "getApplication <Application> ()". Itu hanya menambahkan boilerplate.
Tanggal
50

Bukan berarti ViewModels tidak boleh berisi kode khusus Android untuk mempermudah pengujian, karena abstraksi-lah yang membuat pengujian lebih mudah.

Alasan mengapa ViewModels tidak boleh berisi instance Konteks atau apa pun seperti Tampilan atau objek lain yang memegang Konteks adalah karena ia memiliki siklus proses yang terpisah dari Aktivitas dan Fragmen.

Yang saya maksud dengan ini adalah, katakanlah Anda melakukan perubahan rotasi pada aplikasi Anda. Hal ini menyebabkan Aktivitas dan Fragmen Anda menghancurkan dirinya sendiri sehingga membuatnya kembali. ViewModel dimaksudkan untuk bertahan selama keadaan ini, jadi ada kemungkinan error dan pengecualian lain terjadi jika masih memegang View atau Konteks ke Aktivitas yang dihancurkan.

Mengenai bagaimana Anda harus melakukan apa yang ingin Anda lakukan, MVVM dan ViewModel bekerja sangat baik dengan komponen Penyatuan Data JetPack. Untuk sebagian besar hal Anda biasanya akan menyimpan String, int, atau dll, Anda dapat menggunakan Databinding untuk membuat Tampilan menampilkannya secara langsung, sehingga tidak perlu menyimpan nilai di dalam ViewModel.

Tetapi jika Anda tidak menginginkan Penyatuan Data, Anda masih bisa meneruskan Konteks di dalam konstruktor atau metode untuk mengakses Sumber Daya. Hanya saja, jangan memegang contoh Konteks itu di dalam ViewModel Anda.

Jackey
sumber
1
Pemahaman saya bahwa menyertakan kode khusus android memerlukan pengujian instrumentasi untuk dijalankan yang jauh lebih lambat daripada pengujian JUnit biasa. Saat ini saya menggunakan Penyatuan Data untuk metode klik tetapi saya tidak melihat bagaimana hal itu akan membantu mendapatkan sumber daya dari xml atau untuk preferensi. Saya baru menyadari bahwa untuk preferensi, saya juga memerlukan konteks di dalam model saya. Apa yang saya lakukan saat ini adalah meminta Dagger menyuntikkan konteks aplikasi (modul konteks mendapatkannya dari metode statis di dalam kelas aplikasi)
Vincent Williams
@VincentWilliams Ya, menggunakan ViewModel membantu mengabstraksi kode Anda dari komponen UI, yang memudahkan Anda untuk melakukan pengujian. Tapi, apa yang saya katakan adalah bahwa alasan utama untuk tidak menyertakan Konteks, Tampilan, atau sejenisnya bukan karena alasan pengujian, tetapi karena siklus hidup ViewModel yang dapat membantu Anda menghindari kerusakan dan kesalahan lainnya. Sedangkan untuk penyatuan data, ini dapat membantu Anda dengan sumber daya karena sebagian besar waktu yang Anda perlukan untuk mengakses sumber daya dalam kode adalah karena harus menerapkan String, warna, dimen ke dalam tata letak Anda, yang dapat dilakukan penyatuan data secara langsung.
Jackey
Oh ok saya mengerti maksud Anda tetapi penyatuan data tidak akan membantu saya dalam kasus ini karena saya perlu mengakses string untuk digunakan dalam model (Ini bisa diletakkan di kelas konstanta daripada xml saya kira) dan juga untuk menginisialisasi SharedPreferences
Vincent Williams
3
jika saya ingin mengalihkan teks dalam tampilan teks berdasarkan pada model tampilan bentuk nilai, string harus dilokalkan, jadi saya perlu mendapatkan sumber daya di viewmodel saya, tanpa konteks bagaimana cara mengakses sumber daya?
Srishti Roy
3
@SrishtiRoy Jika Anda menggunakan penyatuan data, sangat mungkin untuk mengalihkan teks TextView berdasarkan nilai dari viewmodel Anda. Anda tidak perlu mengakses Konteks di dalam ViewModel karena semua ini terjadi di dalam file tata letak. Namun, jika Anda harus menggunakan Konteks dalam ViewModel, Anda harus mempertimbangkan untuk menggunakan AndroidViewModel, bukan ViewModel. AndroidViewModel berisi Konteks Aplikasi yang bisa Anda panggil dengan getApplication (), sehingga harus memenuhi kebutuhan Konteks Anda jika ViewModel Anda memerlukan konteks.
Jackey
15

Jawaban singkat - Jangan lakukan ini

Kenapa?

Ini mengalahkan seluruh tujuan model tampilan

Hampir semua yang dapat Anda lakukan dalam model tampilan dapat dilakukan dalam aktivitas / fragmen dengan menggunakan instance LiveData dan berbagai pendekatan lain yang direkomendasikan.

humble_wolf
sumber
21
Mengapa kelas AndroidViewModel bahkan ada?
Alex Berdnikov
1
@AlexBerdnikov Tujuan MVVM adalah untuk mengisolasi tampilan (Aktivitas / Fragmen) dari ViewModel bahkan lebih dari MVP. Sehingga akan lebih mudah untuk menguji.
hushed_voice
3
@free_style Terima kasih atas klarifikasinya, tetapi pertanyaannya tetap: jika kita tidak boleh menjaga konteks dalam ViewModel, mengapa kelas AndroidViewModel ada? Seluruh tujuannya adalah untuk memberikan konteks aplikasi, bukan?
Alex Berdnikov
6
@AlexBerdnikov Menggunakan konteks Aktivitas di dalam model tampilan dapat menyebabkan kebocoran memori. Jadi dengan menggunakan AndroidViewModel Class Anda akan diberikan oleh Application Context yang tidak (mudah-mudahan) menyebabkan kebocoran memori. Jadi, menggunakan AndroidViewModel mungkin lebih baik daripada meneruskan konteks aktivitas padanya. Namun tetap melakukannya akan membuat pengujian menjadi sulit. Ini pendapat saya.
hushed_voice
1
Saya tidak dapat mengakses file dari res / folder mentah dari repositori?
Fugogugo
14

Apa yang akhirnya saya lakukan alih-alih memiliki Konteks langsung di ViewModel, saya membuat kelas penyedia seperti ResourceProvider yang akan memberi saya sumber daya yang saya butuhkan, dan saya meminta kelas penyedia tersebut dimasukkan ke dalam ViewModel saya

Vincent Williams
sumber
1
Saya menggunakan ResourcesProvider dengan Dagger di AppModule. Apakah pendekatan yang baik untuk mendapatkan konteks dari ResourcesProvider atau AndroidViewModel lebih baik untuk mendapatkan konteks untuk resource?
Usman Rana
@ Vincent: Bagaimana cara menggunakan resourceProvider untuk mendapatkan Drawable di dalam ViewModel?
HoangVu
@Vegeta Anda akan menambahkan metode seperti getDrawableRes(@DrawableRes int id)di dalam kelas ResourceProvider
Vincent Williams
1
Ini bertentangan dengan pendekatan Arsitektur Bersih yang menyatakan bahwa dependensi framework tidak boleh melintasi batas ke dalam logika domain (ViewModels).
IgorGanapolsky
1
VM @IgorGanapolsky sebenarnya bukan logika domain. Logika domain adalah kelas lain seperti interaktor dan repositori untuk beberapa nama. VM termasuk dalam kategori "perekat" karena mereka berinteraksi dengan domain Anda, tetapi tidak secara langsung. Jika VM Anda adalah bagian dari domain Anda, maka Anda harus mempertimbangkan kembali bagaimana Anda menggunakan pola karena Anda memberi mereka terlalu banyak tanggung jawab.
mradzinski
8

TL; DR: Masukkan konteks Aplikasi melalui Dagger di ViewModels Anda dan gunakan untuk memuat sumber daya. Jika Anda perlu memuat gambar, teruskan instance View melalui argumen dari metode Databinding dan gunakan konteks View tersebut.

MVVM adalah arsitektur yang baik dan jelas merupakan masa depan pengembangan Android, tetapi ada beberapa hal yang masih ramah lingkungan. Ambil contoh komunikasi lapisan dalam arsitektur MVVM, saya telah melihat pengembang yang berbeda (pengembang yang sangat terkenal) menggunakan LiveData untuk mengkomunikasikan lapisan yang berbeda dengan cara yang berbeda. Beberapa dari mereka menggunakan LiveData untuk mengkomunikasikan ViewModel dengan UI, tetapi kemudian mereka menggunakan antarmuka callback untuk berkomunikasi dengan Repositori, atau mereka memiliki Interactors / UseCases dan mereka menggunakan LiveData untuk berkomunikasi dengan mereka. Titik di sini, adalah bahwa tidak semuanya 100% mendefinisikan belum .

Karena itu, pendekatan saya dengan masalah spesifik Anda adalah memiliki konteks Aplikasi yang tersedia melalui DI untuk digunakan di ViewModels saya untuk mendapatkan hal-hal seperti String dari strings.xml saya

Jika saya berurusan dengan pemuatan gambar, saya mencoba melewati objek View dari metode adaptor Databinding dan menggunakan konteks View untuk memuat gambar. Mengapa? karena beberapa teknologi (misalnya Glide) dapat mengalami masalah jika Anda menggunakan konteks Aplikasi untuk memuat gambar.

Semoga membantu!

4gus71n
sumber
5
TL; DR harus berada di atas
Jacques Koorts
1
Terima kasih atas jawaban Anda. Namun, mengapa Anda menggunakan dagger untuk memasukkan konteks jika Anda dapat membuat viewmodel diperpanjang dari androidviewmodel dan menggunakan konteks bawaan yang disediakan kelas itu sendiri? Terutama mengingat jumlah kode boilerplate yang konyol untuk membuat belati dan MVVM bekerja sama, solusi lain tampaknya jauh lebih jelas. Apa pendapat Anda tentang ini?
Josip Domazet
7

Seperti yang telah disebutkan orang lain, ada AndroidViewModelyang dapat Anda peroleh dari untuk mendapatkan aplikasi Contexttetapi dari apa yang saya kumpulkan di komentar, Anda mencoba memanipulasi @drawabledari dalam Anda ViewModelyang mengalahkan tujuan MVVM.

Secara umum, kebutuhan untuk memiliki a Contextdalam Anda ViewModelhampir secara universal menyarankan agar Anda mempertimbangkan untuk memikirkan kembali bagaimana Anda membagi logika antara Views dan ViewModels.

Daripada ViewModelmenyelesaikan drawable dan memasukkannya ke Aktivitas / Fragmen, pertimbangkan untuk meminta Fragmen / Aktivitas menyulap drawable berdasarkan data yang dimiliki oleh ViewModel. Misalnya, Anda memerlukan drawable yang berbeda untuk ditampilkan dalam tampilan untuk status aktif / nonaktif - inilah ViewModelyang harus menahan status (mungkin boolean), tetapi Viewtugasnya adalah untuk memilih drawable yang sesuai.

Ini dapat dilakukan dengan cukup mudah dengan DataBinding :

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

Jika Anda memiliki lebih banyak status dan sumber daya dapat digambar, untuk menghindari logika yang tidak berguna dalam file tata letak, Anda dapat menulis BindingAdapter khusus yang menerjemahkan, katakanlah, sebuah Enumnilai ke dalam R.drawable.*(mis. Setelan kartu)

Atau mungkin Anda memerlukan Contextuntuk beberapa komponen yang Anda gunakan di dalam Anda ViewModel- kemudian, buat komponen di luar ViewModeldan kirimkan. Anda dapat menggunakan DI, atau singletons, atau membuat Contextkomponen -dependen tepat sebelum inisialisasi ViewModelin Fragment/ Activity.

Mengapa repot: Contextadalah hal khusus Android, dan bergantung pada mereka ViewModeladalah praktik yang buruk: mereka menghalangi pengujian unit. Di sisi lain, komponen / antarmuka layanan Anda sendiri sepenuhnya di bawah kendali Anda sehingga Anda dapat dengan mudah mengejeknya untuk pengujian.

Ivan Bartsov
sumber
5

memiliki referensi ke konteks aplikasi, namun itu berisi kode khusus android

Kabar baiknya, Anda dapat menggunakan Mockito.mock(Context.class)dan membuat konteks mengembalikan apa pun yang Anda inginkan dalam pengujian!

Jadi gunakan saja ViewModelseperti yang biasa Anda lakukan, dan berikan ApplicationContext melalui ViewModelProviders.Factory seperti biasa.

EpicPandaForce
sumber
3

Anda dapat mengakses konteks aplikasi getApplication().getApplicationContext()dari dalam ViewModel. Inilah yang Anda butuhkan untuk mengakses sumber daya, preferensi, dll ..

Alessandro Crugnola
sumber
Saya kira untuk mempersempit pertanyaan saya. Apakah buruk untuk memiliki referensi konteks di dalam viewmodel (bukankah ini memengaruhi pengujian?) Dan apakah menggunakan kelas AndroidViewModel akan memengaruhi Dagger dengan cara apa pun? Bukankah itu terkait dengan siklus hidup aktivitas? Saya menggunakan Dagger untuk mengontrol siklus hidup komponen
Vincent Williams
14
The ViewModelkelas tidak memiliki getApplicationmetode.
beroal
4
Tidak, tapi AndroidViewModeltidak
4Oh4
1
Tetapi Anda harus meneruskan contoh Aplikasi dalam konstruktornya, itu sama seperti mengakses contoh Aplikasi darinya
John Sardinha
2
Tidak masalah besar untuk memiliki konteks aplikasi. Anda tidak ingin memiliki konteks aktivitas / fragmen karena Anda akan terhenti jika fragmen / aktivitas dimusnahkan dan model tampilan masih memiliki referensi ke konteks yang sekarang tidak ada. Tetapi Anda tidak akan pernah memiliki konteks APPLICATION dihancurkan tetapi VM masih memiliki referensi ke sana. Baik? Dapatkah Anda membayangkan skenario di mana aplikasi Anda keluar tetapi Viewmodel tidak? :)
user1713450
3

Anda tidak boleh menggunakan objek yang berhubungan dengan Android dalam ViewModel karena motif penggunaan ViewModel adalah untuk memisahkan kode java dan kode Android sehingga Anda dapat menguji logika bisnis secara terpisah dan Anda akan memiliki lapisan terpisah dari komponen Android dan logika bisnis Anda. dan data, Anda tidak boleh memiliki konteks di ViewModel karena dapat menyebabkan error

Rohit Sharma
sumber
2
Ini adalah pengamatan yang adil, tetapi beberapa pustaka backend masih memerlukan konteks Aplikasi, seperti MediaStore. Jawaban oleh 4gus71n di bawah ini menjelaskan cara berkompromi.
Bryan W. Wagner
1
Ya Anda Dapat Menggunakan Konteks Aplikasi Tetapi Bukan Konteks Aktivitas, Karena Konteks Aplikasi hidup sepanjang siklus hidup aplikasi tetapi tidak Konteks Aktivitas karena Meneruskan Konteks Aktivitas ke proses asinkron apa pun dapat mengakibatkan kebocoran memori. Konteks yang disebutkan dalam posting saya adalah Aktivitas Konteks. Tetapi Anda Tetap Harus Berhati-hati untuk tidak meneruskan konteks ke proses asinkron apa pun meskipun itu adalah konteks aplikasi.
Rohit Sharma
2

Saya mengalami kesulitan SharedPreferencessaat menggunakan ViewModelkelas jadi saya mengambil saran dari jawaban di atas dan melakukan yang berikut menggunakan AndroidViewModel. Semuanya terlihat bagus sekarang

Untuk 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>"));
    }

}

Dan di 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;
    }
}
davejoem
sumber
0

Saya membuatnya seperti ini:

@Module
public class ContextModule {

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

Dan kemudian saya baru saja menambahkan di AppComponent ContextModule.class:

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

Dan kemudian saya memasukkan konteks di ViewModel saya:

@Inject
@Named("AppContext")
Context context;
loopidio
sumber
0

Gunakan pola berikut:

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