I've been trying to instantiate and use a ViewModel inside two Composables, and I've created them a ViewModelFactory, but the project doesn't build for some reason, says Failed to instantiate a ViewModel. I tried to pass LocalContext.current as Application and also tried without passing that, with no success. I want to pass my MainViewModel to both composables and use ViewModelProvider at both of them.
#Composable
fun LoginScreen(navController: NavController){
val viewmodel: MainViewModel = viewModel(LocalContext.current as ComponentActivity)
#Composable
fun ListingScreen(){
val viewModel: MainViewModel = viewModel()
class MainViewModel(private val dataSource: Dao,
application: Application): ViewModel() {
val loginUser: MutableState<User?> = mutableStateOf(null)
private val lastLoginInfo: MutableState<LoginInfo?> = mutableStateOf(null)
lateinit var videoUrlList: List<String>
init {
viewModelScope.launch {
getLastLogin()
}
initializeVideoList()
}
private fun initializeVideoList(){
videoUrlList = listOf<String>("Some links here"
)
}
fun checkValidity(username: String, password: Int): Boolean{
viewModelScope.launch {
val userEntity = dataSource.getUserByLogin(username = username, password = password).collect {
loginUser.value = it
}
}
return loginUser.value != null
}
fun returnFilename(fileUrl: String): String {
return fileUrl.substringAfterLast("/")
}
fun addLogin(loginInfo: LoginInfo= LoginInfo()){
dataSource.addLogin(loginInfo)
}
private suspend fun getLastLogin(){
dataSource.getLastLogin().collect {
lastLoginInfo.value = it
}
}
#OptIn(ExperimentalTime::class)
private fun checkIfLoginRecent(): Boolean{
return convert((System.currentTimeMillis()-lastLoginInfo.value!!.loginEndTimeMilli)
.toDouble(),
DurationUnit.MILLISECONDS,
DurationUnit.MINUTES)< 5
}
}
class MainViewModelFactory(
private val dataSource: Dao,
private val application: Application
): ViewModelProvider.Factory{
#Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(dataSource, application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
After edit: Now the project builds but still gives the same error saying it couldn't instantiate viewmodel in composable.
class MainActivity : ComponentActivity() {
private lateinit var mainViewModel: MainViewModel
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mainViewModelFactory: MainViewModelFactory = MainViewModelFactory(Database.getInstance(this).Dao, application)
mainViewModel = ViewModelProvider(this, mainViewModelFactory).get(MainViewModel::class.java)
setContent {
MyAndroidApplicationTheme {
// A surface container using the 'background' color from the theme
navController= rememberNavController()
Navigation(mainViewModel)
}
}
}
}
Example Preview code:
#Preview
#Composable
fun LoginScreenPreview(){
val navController = rememberNavController()
val main: MainActivity = MainActivity()
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val dao = Database.getInstance(main.applicationContext).Dao
#Suppress("UNCHECKED_CAST")
return MainViewModel( dataSource = dao, main.application) as T
}
}
val viewModel: MainViewModel = viewModel(
factory =factory)
TextField(value ="can", onValueChange ={} )
LoginScreen(navController, viewmodel = viewModel)
}
I would like to access LiveData in my ViewModel. The problem is that the LiveData<String?> requires access to the activity in order to be computed. This is how I am computing the String.
suspend fun Fragment.getAuthToken(): String? {
val am: AccountManager = AccountManager.get(activity)
val accounts: Array<out Account> = am.getAccountsByType(getAccountType())
return accounts.firstOrNull()?.let {
withContext(Dispatchers.IO) {
am.blockingGetAuthToken(it, getAccountType(), true)
}
}
}
And then computing the LiveData from my fragment like this:
val authTokenLiveData: LiveData<String?> = liveData {
emit(getAuthToken())
}
Kindly, help me get access to the LiveData in my ViewModel or alternatively tell me how I can compute it from the ViewModel.
I have been able to find out how to solve the problem thanks to Nicolas.
Step 1. Create a ViewModelFactory:
#Suppress("UNCHECKED_CAST")
class AccountViewModelFactory(private val accountManager: AccountManager, private val repository: UserRepository) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
AccountViewModel(accountManager, repository) as T
}
Step 2. Initialize the ViewModel from the Fragment:
private val accountViewModel: AccountViewModel by viewModels {
AccountViewModelFactory(getAccountManager(), getUserRepository())
}
Step 3. Create the ViewModel and create the LiveData from the ViewModel:
class AccountViewModel(private val accountManager: AccountManager, private val repository: UserRepository) : ViewModel() {
val authTokenLiveData: LiveData<String?> = liveData {
emit(accountManager.myAuthToken())
}
}
Step 4. Create myAuthToken() and getAccountManager() extension functions.
fun Fragment.getAccountManager() : AccountManager = AccountManager.get(activity)
suspend fun AccountManager.myAuthToken(): String? {
val accounts: Array<out Account> = getAccountsByType(accountType)
return accounts.firstOrNull()?.let {
withContext(Dispatchers.IO) {
blockingGetAuthToken(it, accountType, true)
}
}
}
I develop app with MVVM pattern. I want save UI when user rotate screen.
MyViewModel.kt
class MyViewModel(val repository: SomeRepository,
state : SavedStateHandle) : ViewModel() {
private val savedStateHandle = state
companion object {
const val KEY = "KEY"
}
fun saveCityId(cityId: String) {
savedStateHandle.set(CITY_KEY, cityId)
}
fun getCityId(): String? {
return savedStateHandle.get(CITY_KEY)
}
}
ViewModelFactory.kt
#Suppress("UNCHECKED_CAST")
class ViewModelFactory(
private val repository: SomeRepository,
private val state: SavedStateHandle
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyViewModel(repository,state) as T
}
}
I call it in MainActivity
MainActivity.kt
class MainActivity: AppCompatActivity(), KodeinAware {
private val factory: ViewModelFactoryby instance()
override val kodein by kodein()
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cityId = intent.getStringExtra("cityId") ?: viewModel.getCityId()
if (cityId != null) {
viewModel.saveCityId(cityId!!)
viewModel.getCurrentWeather(cityId!!)
}
}
Here i inject dependencies
Application.kt
class ForecastApplication: Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this#ForecastApplication))
bind<SomeApi>() with singleton {
Retrofit.create()
}
bind<WeatherRepository>() with singleton {
WeatherRepository(instance())
}
bind() from provider {
WeatherViewModelFactory(
instance(), instance()
)
}
}
}
And i have this error
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.simpleforecast/com.example.simpleapp.UI.Cities.Activity}:org.kodein.di.Kodein$NotFoundException: No binding found for bind<SavedStateHandle>()
with ?<Activity>().? { ? }
How shoud i build ViewModelFactory and inject Saved State module for ViewModel?
SavedStateHandle is parameter which cannot be bound to the DI graph, because it's retrieved from Fragment (or Activity), therefore you need to do several steps in order to make it work:
1) DI viewmodel definition - since you have custom parameter, you need to use from factory:
bind() from factory { handle: SavedStateHandle ->
WeatherViewModel(
state = handle,
repository = instance()
)
}
2) ViewModel Factory - you need to inherit from AbstractSavedStateViewModelFactory
val vmFactory = object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
val vmFactory: ((SavedStateHandle) -> WeatherViewModel) = kodein.direct.factory()
return vmFactory(handle) as T
}
}
Inside of the create method you'd retrieve the factory from your DI graph (from step 1).
3) You retrieve ViewModel with the specified factory:
lateinit var vm : WeatherViewModel
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = ViewModelProvider(this, vmFactory)[WeatherViewModel::class.java]
}
or android KTX way:
val vm : WeatherViewModel by viewModels { vmFactory }
MainActivity
class MainActivity : AppCompatActivity() {
#Inject
lateinit var mainViewModelFactory: mainViewModelFactory
private lateinit var mainActivityBinding: ActivityMainBinding
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityBinding = DataBindingUtil.setContentView(
this,
R.layout.activity_main
)
mainActivityBinding.rvmainRepos.adapter = mainAdapter
AndroidInjection.inject(this)
mainViewModel =
ViewModelProviders.of(
this#MainActivity,
mainViewModelFactory
)[mainViewModel::class.java]
mainActivityBinding.viewmodel = mainViewModel
mainActivityBinding.lifecycleOwner = this
mainViewModel.mainRepoReponse.observe(this, Observer<Response> {
repoList.clear()
it.success?.let { response ->
if (!response.isEmpty()) {
// mainViewModel.saveDataToDb(response)
// mainViewModel.createWorkerForClearingDb()
}
}
})
}
}
MainViewModelFactory
class MainViewModelFactory #Inject constructor(
val mainRepository: mainRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>) =
with(modelClass) {
when {
isAssignableFrom(mainViewModel::class.java) -> mainViewModel(
mainRepository = mainRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
MainViewModel
class MainViewModel(
val mainRepository: mainRepository
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
val mainRepoReponse = MutableLiveData<Response>()
val loadingProgress: MutableLiveData<Boolean> = MutableLiveData()
val _loadingProgress: LiveData<Boolean> = loadingProgress
val loadingFailed: MutableLiveData<Boolean> = MutableLiveData()
val _loadingFailed: LiveData<Boolean> = loadingFailed
var isConnected: Boolean = false
fun fetchmainRepos() {
if (isConnected) {
loadingProgress.value = true
compositeDisposable.add(
mainRepository.getmainRepos().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
run {
saveDataToDb(response)
)
}
},
{ error ->
processResponse(Response(AppConstants.Status.SUCCESS, null, error))
}
)
)
} else {
fetchFromLocal()
}
}
private fun saveDataToDb(response: List<mainRepo>) {
mainRepository.insertmainUsers(response)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(object : DisposableCompletableObserver() {
override fun onComplete() {
Log.d("Status", "Save Success")
}
override fun onError(e: Throwable) {
Log.d("Status", "error ${e.localizedMessage}")
}
})
}
}
MainRepository
interface MainRepository {
fun getmainRepos(): Single<List<mainRepo>>
fun getAllLocalRecords(): Single<List<mainRepo>>
fun insertmainUsers(repoList: List<mainRepo>): Completable
}
MainRepositoryImpl
class mainRepositoryImpl #Inject constructor(
val apiService: GitHubApi,
val mainDao: AppDao
) : MainRepository {
override fun getAllLocalRecords(): Single<List<mainRepo>> = mainDao.getAllRepos()
override fun insertmainUsers(repoList: List<mainRepo>) :Completable{
return mainDao.insertAllRepos(repoList)
}
override fun getmainRepos(): Single<List<mainRepo>> {
return apiService.getmainGits()
}
}
I'm quite confused with the implementation of MVVM with LiveData and Rxjava, in my MainViewModel I am calling the interface method and implementing it inside ViewModel, also on the response I'm saving the response to db. However, that is a private method, which won't be testable in unit testing in a proper way (because it's private). What is the best practice to call other methods on the completion of one method or i have to implement all the methods inside the implementation class which uses the interface.
Your ViewModel should not care how you are getting the data if you are trying to follow the clean architecture pattern. The logic for fetching the data from local or remote sources should be in the repository in the worst case where you can also save the response. In that case, since you have a contact for the methods, you can easily test them. Ideally, you could break it down even more - adding Usecases/Interactors.
Is there a way to pass additional argument to my custom AndroidViewModel constructor except Application context.
Example:
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);
}
}
And when I want to user my custom ViewModel class I use this code in my fragment:
MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
So I don't know how to pass additional argument String param into my custom ViewModel. I can only pass Application context, but not additional arguments. I would really appreciate any help. Thank you.
Edit: I've added some code. I hope it's better now.
You need to have a factory class for your ViewModel.
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);
}
}
And when instantiating the view model, you do like this:
MyViewModel myViewModel = ViewModelProvider(this, new MyViewModelFactory(this.getApplication(), "my awesome param")).get(MyViewModel.class);
For kotlin, you may use delegated property:
val viewModel: MyViewModel by viewModels { MyViewModelFactory(getApplication(), "my awesome param") }
There's also another new option - to implement HasDefaultViewModelProviderFactory and override getDefaultViewModelProviderFactory() with the instantiation of your factory and then you would call ViewModelProvider(this) or by viewModels() without the factory.
Implement with Dependency Injection
This is more advanced and better for production code.
Dagger2, Square's AssistedInject offers a production-ready implementation for ViewModels that can inject necessary components such as a repository that handles network and database requests. It also allows for the manual injection of arguments/parameters in the activity/fragment. Here's a concise outline of the steps to implement with code Gists based on Gabor Varadi's detailed post, Dagger Tips.
Dagger Hilt, is the next generation solution, in alpha as of 7/12/20, offering the same use case with a simpler setup once the library is in release status.
Implement with Lifecycle 2.2.0 in Kotlin
Passing Arguments/Parameters
// 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") }
}
Enabling SavedState with 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
}
}
For one factory shared between multiple different view models I'd extend mlyko's answer like this:
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);
}
}
}
And instantiating view models:
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);
With different view models having different constructors.
Based on #vilpe89 the above Kotlin solution for AndroidViewModel cases
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
}
Then a fragment can initiate the viewModel as
class SomeFragment : Fragment() {
// ...
private val myViewModel: SomeViewModel by viewModels {
ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")
}
// ...
}
And then the actual ViewModel class
class SomeViewModel(application: Application, val myExtraParam:String) : AndroidViewModel(application) {
// ...
}
Or in some suitable method ...
override fun onActivityCreated(...){
// ...
val myViewModel = ViewModelProvider(this, ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")).get(SomeViewModel::class.java)
// ...
}
I made it a class in which the already created object is passed.
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;
}
And then
ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
The proper way is to use a dependency injection framework such as Dagger hilt. If not using a DI framework, then do it with ViewModelFactory.
With Dagger Hilt:
A ViewModel with parameters
#HiltViewModel
class MyViewModel #Inject constructor(
private val myRepository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() { ... }
A Repository
class MyRepository #Inject constructor(
private val myRemoteDataSource: MyDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) { ... }
A Module for providing the dependencies/parameters so they can be injected into repositories and ViewModels.
#InstallIn(ViewModelComponent::class)
#Module
object MyProvideModule {
#Provides
fun provideMyDataSource(#ApplicationContext context: Context): MyDataSource {
//code to create MyDataSource...
return MyDataSource(context)
}
#Provides
fun provideCoroutineDispatcher(): CoroutineDispatcher {
return Dispatchers.IO
}
}
A module for binding the repository
#Module
#InstallIn(ViewModelComponent::class)
interface RepositoryModules {
#Binds
fun provideMyRepository(repository: MyRepository): MyRepository
}
Initiating Dagger hilt with the application with the #HiltAndroidApp annotation.
#HiltAndroidApp
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}
Getting the ViewModel in activities
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val myViewModel: MyViewModel by viewModels()
// Other code...
}
Getting the ViewModel in fragments
#AndroidEntryPoint
class MyFragment : Fragment() {
private val myViewModel: MyViewModel by activityViewModels()
// Other code...
}
With ViewModelFactory:
A ViewModel with parameter messageDataStore, where MessageDataStore is a DataStore class or it can be anything else that you want to pass into the ViewModel.
class MyViewModel(
private val messageDataStore: MessageDataStore,
): ViewModel() { ... }
The ViewModel factory class for creating ViewModels
/**
* Factory for all ViewModels.
*/
#Suppress("UNCHECKED_CAST")
class ViewModelFactory constructor(
private val messageDataStore: MessageDataStore,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = with(modelClass) {
when {
isAssignableFrom(MyViewModel::class.java) ->
MyViewModel(messageDataStore)
else ->
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
The application class for creating the dependencies/parameters
class MyApp : Application() {
val messageDataStore: MessageDataStore
get() = MessageDataStore.getInstance(this)
}
Extension functions for getting the factory class in activities and fragments, MyExt.kt
fun AppCompatActivity.getViewModelFactory(savedInstanceState: Bundle?): ViewModelFactory {
val messageDataStore = (applicationContext as MyApp).messageDataStore
return ViewModelFactory(messageDataStore, this, savedInstanceState)
}
fun Fragment.getViewModelFactory(savedInstanceState: Bundle?): ViewModelFactory {
val messageDataStore = (requireContext().applicationContext as MyApp).messageDataStore
return ViewModelFactory(messageDataStore, this.requireActivity(), savedInstanceState)
}
Getting the ViewMode in activities
class MainActivity : AppCompatActivity() {
private lateinit var myViewModel: MyViewModel
// Other code...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm by viewModels<MyViewModel> { getViewModelFactory(savedInstanceState) }
myViewModel = vm
// Other code...
}
}
Getting the ViewModel in Fragments.
class MyFragment : Fragment() {
private lateinit var myViewModel: MyViewModel
//Other code...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val vm by activityViewModels<MyViewModel> { getViewModelFactory(savedInstanceState) }
myViewModel = vm
//Other code...
}
}
(KOTLIN) My solution uses little bit of Reflection.
Lets say you don't want to create the same looking Factory class every time you create new ViewModel class which needs some arguments. You can accomplish this via Reflection.
For example you would have two different Activities:
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)
}
}
And ViewModels for those Activities:
class ViewModel1(private val args: Bundle) : ViewModel()
class ViewModel2(private val args: Bundle) : ViewModel()
Then the magic part, Factory class's implementation:
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
}
}
}
In Kotlin, since the caller of the ViewModel and the ViewModel itself run in different coroutines, it is more natural and convenient to pass data between them using kotlinx.coroutines.channels.Channel:
class NewViewModel : ViewModel() {
private val newData: MutableLiveData<Service.DataEntry?> by lazy {
MutableLiveData<Service.DataEntry?>().also {
viewModelScope.launch {
val channel = Service.ParamChannel // type Channel<Params>
val params = channel.receive()
it.value = Service.postSomething(params)
}
}
}
fun getData(): LiveData<Service.DataEntry?> {
return newData
}
}
// Calling code:
val model: NewViewModel by viewModels()
model.getData().observe(this) { newData ->
if (newData != null) {
...
}
else
{
...
}
}
runBlocking {
Service.ParamChannel.send(theParams)
}
This is part of working code which I anonymized for demo purposes.
I wrote a library that should make doing this more straightforward and way cleaner, no multibindings or factory boilerplate needed, while working seamlessly with ViewModel arguments that can be provided as dependencies by 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"
}
}
In the view:
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
})
}
}
Why not do it like this:
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);
}
}
}
}
and then use it like this in two steps:
MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)