Inject Saved State in ViewModelFactory with kodein - android

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 }

Related

Unable to Instantiate ViewModel with a Dao argument using ViewModelFactory and "by viewModel()" inside Composables

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)
}

Jetpack Compose pass parameter to viewModel

How can we pass parameter to viewModel in Jetpack Compose?
This is my composable
#Composable
fun UsersList() {
val myViewModel: MyViewModel = viewModel("db2name") // pass param like this
}
This is viewModel
class MyViewModel(private val dbname) : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
you need to create a factory to pass dynamic parameter to ViewModel like this:
class MyViewModelFactory(private val dbname: String) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = MyViewModel(dbname) as T
}
then use your factory like this in composable functions:
#Composable
fun UsersList() {
val myViewModel: MyViewModel =
viewModel(factory = MyViewModelFactory("db2name")) // pass param like this
}
and now you have access to dbname parameter in your ViewModel:
class MyViewModel(private val dbname) : ViewModel() {
// ...rest of the viewModel logics here
}
The other solutions work, but you have to create a factory for each ViewModel which seems overkill.
The more universal solution is like this:
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>):T = f() as T
}
And use it like this:
#Composable
fun MainScreen() {
val viewModel: MyViewModel = viewModel(factory = viewModelFactory {
MyViewModel("Test Name")
})
}
For ViewModel like this:
class MyViewModel(
val name: String
):ViewModel() {}
If you use Hilt, you get this for free in SavedStateHandle for view model.
Pass the argument to the composable that calls the view model and retrieve it with the same name on view model from saved state handle.
Like this:
On NavHost:
NavHost(
(...)
composable(
route = [route string like this $[route]/{$[argument name]}],
arguments = listOf(
navArgument([argument name]) { type = NavType.[type: Int/String/Boolean/etc.] }
)
) {
[Your composable]()
}
)
)
On view model:
class ViewModel #Inject constructor(savedStateHandle: SavedStateHandle) {
private val argument = checkNotNull(savedStateHandle.get<[type]>([argument name]))
}
Your argument will magically appear without having a view model factory.
Usually there is no common case where you need to do this. In android MVVM viewmodels get their data from repositories through dependency injection.
Here is the official documentation to the recommended android architecture: https://developer.android.com/jetpack/guide#recommended-app-arch
As it was mentioned by #Secret Keeper you need to create factory.
If your ViewModel has dependencies, viewModel() takes an optional
ViewModelProvider.Factory as a parameter.
class MyViewModelFactory(
private val dbname: String
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
return MyViewModel(dbname) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
To create your viewModel you will pass optional parameter. Inside your Composable you can do something like this.
val viewModel: MyViewModel = viewModel(
factory = MyViewModelFactory(
dbname = "myDbName"
)
Here's some Jetpack Compose/Kotlin-specific syntax for implementing the same:
ui/settings/SettingsViewModel.kt
class SettingsViewModel(
private val settingsRepository: SettingsRepository
) : ViewModel() {
/* Your implementation */
}
class SettingsViewModelFactory(
private val settingsRepository: SettingsRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create( modelClass: Class<T> ): T {
if( modelClass.isAssignableFrom( SettingsViewModel::class.java ) ) {
#Suppress( "UNCHECKED_CAST" )
return SettingsViewModel( settingsRepository ) as T
}
throw IllegalArgumentException( "Unknown ViewModel Class" )
}
}
Then:
MainActivity.kt
/* dataStore by preferencesDataStore */
class MainActivity : ComponentActivity() {
private lateinit var settingsRepository: SettingsRepository
// Here we instantiate our ViewModel leveraging delegates and
// a trailing lambda
private val settingsViewModel by viewModels<SettingsViewModel> {
SettingsViewModelFactory(
settingsRepository
)
}
/* onCreate -> setContent -> etc */
}

Correct structure of implementing MVVM LiveData RxJava Dagger Databinding?

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.

Can I use one Factory to bind viewmodel / repository calls with kodein

In this Factory I need to fetch my data from an api using Retrofit and store the cache with room, my Repository rules this app!
I have repository suspended functions that take care of getting my data and some that save/update data getting and saveing/updating require different values to function and I do not know (yet) how to configure it in Kodein
I lack the experience to solve this and there is nothing I found in Stackoverflow to assist me.
I have tried to add both the variables ID:String and the edited entity (CampaignEntry) to the Definition, it complies but crash on running with
No binding found for bind<CampaignEditViewModelFactory>() with ? { String -> ? }
My main Application the bind() is crashing the Application
class MarketingApplication : Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this#MarketingApplication))
...
bind() from factory { id: String, campaignEntry: CampaignEntry ->
CampaignEditViewModelFactory(id, campaignEntry, instance()) }
...
My ViewModel - having to pass the variables id and campaignEntry that is consumed by different calls in one ViewModel might be the issue - but I cannot figure out the correct solution.
class CampaignEditViewModel(
private val id: String,
private val campaignEntry: CampaignEntry,
private val marketingRepository: MarketingRepository
) : ViewModel() {
val campaignToSave by lazyDeferred { marketingRepository.updateCampaign(campaignEntry) }
val campaignToEdit by lazyDeferred { marketingRepository.getCampaignById(id) }
}
my lazyDeferred for clarity
fun <T> lazyDeferred(block: suspend CoroutineScope.() -> T): Lazy<Deferred<T>> {
return lazy {
GlobalScope.async(start = CoroutineStart.LAZY) {
block.invoke(this)
}
}
}
The Repository snap
interface MarketingRepository {
...
suspend fun getCampaignById(campaignId: String): LiveData<CampaignEntry>
suspend fun updateCampaign(campaignEntry: CampaignEntry): LiveData<CampaignEntry>
...
I call the Viewmodel from my fragment like so
class CampaignEditFragment : ScopedFragment(), KodeinAware {
override val kodein by closestKodein()
private val viewModelFactoryInstanceFactory: ((String) -> CampaignEditViewModelFactory) by factory()
...
private fun bindUI() = launch {
val campaignVM = campaignEditViewModel.campaignToEdit.await()
...
btn_edit_save.setOnClickListener {it: View
saveCampaign(it)
...
private fun saveCampaign(it: View) = launch {
campaignEditViewModel.campaignToSave.await()
}
And then lastly the ScopedFragment
abstract class ScopedFragment : Fragment(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
If you need any more code - please ask
Since you are binding with 2 arguments, you need to use factory2:
private val viewModelFactoryInstanceFactory: ((String, campaignEntry) -> CampaignEditViewModelFactory) by factory2()

Android ViewModel additional arguments

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)

Categories

Resources