Argumen tambahan Android ViewModel

108

Apakah ada cara untuk meneruskan argumen tambahan ke AndroidViewModelkonstruktor kustom saya kecuali konteks Aplikasi. Contoh:

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;

    public MyViewModel(Application application, String param) {
        super(application);
        appDatabase = AppDatabase.getDatabase(this.getApplication());

        myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
}

Dan ketika saya ingin ViewModelmenggunakan kelas khusus saya, saya menggunakan kode ini di fragmen saya:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)

Jadi saya tidak tahu bagaimana String parammemasukkan argumen tambahan ke dalam kebiasaan saya ViewModel. Saya hanya bisa melewatkan konteks Aplikasi, tetapi tidak argumen tambahan. Saya sangat menghargai setiap bantuan. Terima kasih.

Edit: Saya telah menambahkan beberapa kode. Saya harap sekarang lebih baik.

Mario Rudman
sumber
tambahkan lebih banyak detail dan kode
hugo
Apa pesan kesalahannya?
Moses Aprico
Tidak ada pesan kesalahan. Saya tidak tahu di mana harus menyetel argumen untuk konstruktor karena ViewModelProvider digunakan untuk membuat objek AndroidViewModel.
Mario Rudman

Jawaban:

214

Anda harus memiliki kelas pabrik untuk ViewModel Anda.

public class MyViewModelFactory implements ViewModelProvider.Factory {
    private Application mApplication;
    private String mParam;


    public MyViewModelFactory(Application application, String param) {
        mApplication = application;
        mParam = param;
    }


    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        return (T) new MyViewModel(mApplication, mParam);
    }
}

Dan saat membuat instance model tampilan, Anda melakukan seperti ini:

MyViewModel myViewModel = ViewModelProvider(this, new MyViewModelFactory(this.getApplication(), "my awesome param")).get(MyViewModel.class);

Untuk kotlin, Anda dapat menggunakan properti yang didelegasikan:

val viewModel: MyViewModel by viewModels { MyViewModelFactory(getApplication(), "my awesome param") }

Ada juga opsi baru lainnya - untuk mengimplementasikan HasDefaultViewModelProviderFactorydan mengganti getDefaultViewModelProviderFactory()dengan instantiation pabrik Anda dan kemudian Anda akan memanggil ViewModelProvider(this)atau by viewModels()tanpa pabrik.

mlyko
sumber
4
Apakah setiap ViewModelkelas membutuhkan ViewModelFactory?
dmlebron
6
tetapi setiap ViewModelbisa / akan memiliki DI yang berbeda. Bagaimana Anda tahu instance mana yang mengembalikan create()metode?
dmlebron
1
ViewModel Anda akan dibuat ulang setelah orientasi berubah. Anda tidak dapat membuat pabrik setiap saat.
Tim
3
Itu tidak benar. ViewModelMetode pencegahan kreasi baru get(). Berdasarkan dokumentasi: "Mengembalikan ViewModel yang ada atau membuat yang baru dalam cakupan (biasanya, fragmen atau aktivitas), yang terkait dengan ViewModelProvider ini." lihat: developer.android.com/reference/android/arch/lifecycle/…
mlyko
2
bagaimana dengan menggunakan return modelClass.cast(new MyViewModel(mApplication, mParam))untuk menghilangkan peringatan
jackycflau
23

Terapkan dengan Injeksi Ketergantungan

Ini lebih maju dan lebih baik untuk kode produksi.

Dagger2 , Square's AssistedInject menawarkan implementasi siap produksi untuk ViewModels yang dapat memasukkan komponen yang diperlukan seperti repositori yang menangani permintaan jaringan dan database. Ini juga memungkinkan injeksi manual argumen / parameter dalam aktivitas / fragmen. Berikut adalah garis besar singkat dari langkah - langkah untuk diimplementasikan dengan kode Gists berdasarkan posting rinci Gabor Varadi, Dagger Tips .

Dagger Hilt , adalah solusi generasi berikutnya, dalam alfa mulai 12/7/20, menawarkan kasus penggunaan yang sama dengan penyiapan yang lebih sederhana setelah pustaka dalam status rilis.

Implementasikan dengan Lifecycle 2.2.0 di Kotlin

Meneruskan Argumen / Parameter

// Override ViewModelProvider.NewInstanceFactory to create the ViewModel (VM).
class SomeViewModelFactory(private val someString: String): ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(someString) as T
} 

class SomeViewModel(private val someString: String) : ViewModel() {
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory("someString") } 
}

Mengaktifkan SavedState dengan Arguments / Parameters

class SomeViewModelFactory(
        private val owner: SavedStateRegistryOwner,
        private val someString: String) : AbstractSavedStateViewModelFactory(owner, null) {
    override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, state: SavedStateHandle) =
            SomeViewModel(state, someString) as T
}

class SomeViewModel(private val state: SavedStateHandle, private val someString: String) : ViewModel() {
    val feedPosition = state.get<Int>(FEED_POSITION_KEY).let { position ->
        if (position == null) 0 else position
    }
        
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
        
     fun saveFeedPosition(position: Int) {
        state.set(FEED_POSITION_KEY, position)
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory(this, "someString") } 
    private var feedPosition: Int = 0
     
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        someViewModel.saveFeedPosition((contentRecyclerView.layoutManager as LinearLayoutManager)
                .findFirstVisibleItemPosition())
    }    
        
    override fun onViewStateRestored(savedInstanceState: Bundle?) {
        super.onViewStateRestored(savedInstanceState)
        feedPosition = someViewModel.feedPosition
    }
}
Adam Hurwitz
sumber
Saat menimpa buat di pabrik, saya mendapat peringatan yang mengatakan 'ItemViewModel to T' yang tidak
dicentang
1
Peringatan itu tidak menjadi masalah bagi saya sejauh ini. Namun, saya akan memeriksanya lebih jauh ketika saya merefaktor pabrik ViewModel untuk menyuntikkannya menggunakan Dagger daripada membuat instance melalui fragmen.
Adam Hurwitz
15

Untuk satu pabrik yang dibagi di antara beberapa model tampilan berbeda, saya akan memperluas jawaban mlyko seperti ini:

public class MyViewModelFactory extends ViewModelProvider.NewInstanceFactory {
    private Application mApplication;
    private Object[] mParams;

    public MyViewModelFactory(Application application, Object... params) {
        mApplication = application;
        mParams = params;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (modelClass == ViewModel1.class) {
            return (T) new ViewModel1(mApplication, (String) mParams[0]);
        } else if (modelClass == ViewModel2.class) {
            return (T) new ViewModel2(mApplication, (Integer) mParams[0]);
        } else if (modelClass == ViewModel3.class) {
            return (T) new ViewModel3(mApplication, (Integer) mParams[0], (String) mParams[1]);
        } else {
            return super.create(modelClass);
        }
    }
}

Dan membuat contoh model tampilan:

ViewModel1 vm1 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), "something")).get(ViewModel1.class);
ViewModel2 vm2 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123)).get(ViewModel2.class);
ViewModel3 vm3 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123, "something")).get(ViewModel3.class);

Dengan model tampilan berbeda memiliki konstruktor berbeda.

rzehan
sumber
8
Saya tidak merekomendasikan cara ini karena beberapa alasan: 1) parameter di pabrik jenisnya tidak aman - dengan cara ini Anda dapat memecahkan kode Anda saat runtime. Selalu mencoba untuk menghindari pendekatan ini bila mungkin 2) memeriksa jenis model tampilan sebenarnya bukan cara OOP dalam melakukan sesuatu. Karena ViewModels dicor ke tipe dasar, sekali lagi Anda dapat memecahkan kode selama runtime tanpa peringatan apa pun selama kompilasi .. Dalam kasus ini, saya sarankan menggunakan default pabrik android dan meneruskan parameter ke model tampilan yang sudah dipakai.
mlyko
@mlyko Tentu, ini semua adalah keberatan yang valid dan metode sendiri untuk menyiapkan data viewmodel selalu merupakan opsi. Namun terkadang Anda ingin memastikan bahwa viewmodel telah diinisialisasi, karenanya menggunakan konstruktor. Jika tidak, Anda sendiri harus menangani situasi "viewmodel belum diinisialisasi". Misalnya jika viewmodel memiliki metode yang mengembalikan LivedData dan pengamat dilampirkan ke berbagai metode siklus hidup View.
rzehan
3

Berdasarkan @ vilpe89, solusi Kotlin di atas untuk kasus AndroidViewModel

class ExtraParamsViewModelFactory(private val application: Application, private val myExtraParam: String): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(application, myExtraParam) as T

}

Kemudian sebuah fragmen bisa memulai viewModel sebagai

class SomeFragment : Fragment() {
 ....
    private val myViewModel: SomeViewModel by viewModels {
        ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")
    }
 ....
}

Dan kemudian kelas ViewModel yang sebenarnya

class SomeViewModel(application: Application, val myExtraParam:String) : AndroidViewModel(application) {
....
}

Atau dalam beberapa metode yang sesuai ...

override fun onActivityCreated(...){
    ....

    val myViewModel = ViewModelProvider(this, ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")).get(SomeViewModel::class.java)

    ....
}
MFAL
sumber
Pertanyaannya menanyakan bagaimana cara meneruskan argumen / parameter tanpa menggunakan konteks yang tidak diikuti oleh hal di atas: Apakah ada cara untuk meneruskan argumen tambahan ke konstruktor AndroidViewModel kustom saya kecuali konteks Aplikasi?
Adam Hurwitz
3

Saya menjadikannya kelas di mana objek yang sudah dibuat dilewatkan.

private Map<String, ViewModel> viewModelMap;

public ViewModelFactory() {
    this.viewModelMap = new HashMap<>();
}

public void add(ViewModel viewModel) {
    viewModelMap.put(viewModel.getClass().getCanonicalName(), viewModel);
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    for (Map.Entry<String, ViewModel> viewModel : viewModelMap.entrySet()) {
        if (viewModel.getKey().equals(modelClass.getCanonicalName())) {
            return (T) viewModel.getValue();
        }
    }
    return null;
}

Lalu

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
Danil
sumber
Kita harus memiliki ViewModelFactory untuk setiap ViewModel untuk meneruskan parameter ke konstruktor ??
K Pradeep Kumar Reddy
Tidak. Hanya satu ViewModelFactory untuk semua ViewModels
Danil
Apakah ada alasan untuk menggunakan nama kanonik sebagai kunci hashMap? Bisakah saya menggunakan class.simpleName?
K Pradeep Kumar Reddy
Ya, tetapi Anda harus memastikan tidak ada nama duplikat
Danil
Apakah ini gaya penulisan kode yang disarankan? Anda membuat kode ini sendiri atau membacanya di dokumen android?
K Pradeep Kumar Reddy
1

Saya menulis pustaka yang seharusnya membuat melakukan ini lebih mudah dan lebih bersih, tidak perlu multibindings atau boilerplate pabrik, sambil bekerja dengan mulus dengan argumen ViewModel yang dapat disediakan sebagai dependensi oleh Dagger: https://github.com/radutopor/ViewModelFactory

@ViewModelFactory
class UserViewModel(@Provided repository: Repository, userId: Int) : ViewModel() {

    val greeting = MutableLiveData<String>()

    init {
        val user = repository.getUser(userId)
        greeting.value = "Hello, $user.name"
    }    
}

Dalam tampilan:

class UserActivity : AppCompatActivity() {
    @Inject
    lateinit var userViewModelFactory2: UserViewModelFactory2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        appComponent.inject(this)

        val userId = intent.getIntExtra("USER_ID", -1)
        val viewModel = ViewModelProviders.of(this, userViewModelFactory2.create(userId))
            .get(UserViewModel::class.java)

        viewModel.greeting.observe(this, Observer { greetingText ->
            greetingTextView.text = greetingText
        })
    }
}
Radu Topor
sumber
1

(KOTLIN) Solusi saya menggunakan sedikit Refleksi.

Katakanlah Anda tidak ingin membuat kelas Pabrik yang tampak sama setiap kali Anda membuat kelas ViewModel baru yang memerlukan beberapa argumen. Anda dapat melakukannya melalui Refleksi.

Misalnya, Anda akan memiliki dua Aktivitas berbeda:

class Activity1 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putString("NAME_KEY", "Vilpe89") }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel1::class.java)
    }
}

class Activity2 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putInt("AGE_KEY", 29) }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel2::class.java)
    }
}

Dan ViewModels untuk Aktivitas tersebut:

class ViewModel1(private val args: Bundle) : ViewModel()

class ViewModel2(private val args: Bundle) : ViewModel()

Kemudian bagian ajaibnya, implementasi kelas Pabrik:

class ViewModelWithArgumentsFactory(private val args: Bundle) : NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        try {
            val constructor: Constructor<T> = modelClass.getDeclaredConstructor(Bundle::class.java)
            return constructor.newInstance(args)
        } catch (e: Exception) {
            Timber.e(e, "Could not create new instance of class %s", modelClass.canonicalName)
            throw e
        }
    }
}
vilpe89
sumber
0

Mengapa tidak melakukannya seperti ini:

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;
    private boolean initialized = false;

    public MyViewModel(Application application) {
        super(application);
    }

    public initialize(String param){
      synchronized ("justInCase") {
         if(! initialized){
          initialized = true;
          appDatabase = AppDatabase.getDatabase(this.getApplication());
          myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
   }
  }
}

lalu gunakan seperti ini dalam dua langkah:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)
Amr Berag
sumber
2
Inti dari meletakkan parameter dalam konstruktor adalah menginisialisasi model tampilan hanya sekali . Dengan implementasi Anda, jika Anda memanggil myViewModel.initialize(param)di onCreateaktivitas, misalnya, dapat disebut beberapa kali pada yang sama MyViewModelmisalnya sebagai pengguna berputar perangkat.
Sanlok Lee
@Sanlok Lee Ok. Bagaimana menambahkan kondisi ke fungsi untuk mencegah inisialisasi saat tidak diperlukan. Periksa jawaban saya yang telah diedit.
Amr Berag
0
class UserViewModelFactory(private val context: Context) : ViewModelProvider.NewInstanceFactory() {
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return UserViewModel(context) as T
    }
 
}
class UserViewModel(private val context: Context) : ViewModel() {
 
    private var listData = MutableLiveData<ArrayList<User>>()
 
    init{
        val userRepository : UserRepository by lazy {
            UserRepository
        }
        if(context.isInternetAvailable()) {
            listData = userRepository.getMutableLiveData(context)
        }
    }
 
    fun getData() : MutableLiveData<ArrayList<User>>{
        return listData
    }

Panggil Viewmodel di Activity

val userViewModel = ViewModelProviders.of(this,UserViewModelFactory(this)).get(UserViewModel::class.java)

Untuk referensi lebih lanjut: Contoh Android MVVM Kotlin

Dhrumil Shah
sumber
Pertanyaannya menanyakan bagaimana cara meneruskan argumen / parameter tanpa menggunakan konteks yang tidak diikuti oleh hal di atas: Apakah ada cara untuk meneruskan argumen tambahan ke konstruktor AndroidViewModel kustom saya kecuali konteks Aplikasi?
Adam Hurwitz
Anda bisa meneruskan argumen / parameter apa pun dalam konstruktor viewmodel kustom Anda. Di sini konteks hanyalah sebuah contoh. Anda bisa meneruskan argumen kustom apa pun dalam konstruktor.
Dhrumil Shah
Dimengerti. Praktik terbaiknya adalah tidak meneruskan konteks, tampilan, aktivitas, fragmen, adaptor, siklus proses tampilan, mengamati observable yang sadar siklus proses tampilan, atau menyimpan sumber daya (drawable, dll.) Di ViewModel karena tampilan dapat dihancurkan dan ViewModel akan tetap ada dengan yang usang informasi.
Adam Hurwitz