I have the following class signature:
abstract class BaseActivity<E : ViewModel> : AppCompatActivity() {
protected lateinit var viewModel: E
}
Now I want to initialize my viewModel in a generic way using ViewModelProvider, so:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(MyViewModel::class)
Given that MyViewModel class will be provided in the generic type, I'd say this could potentially be abstracted into the BaseActivity so I dont have to do it for every Activity that extends it.
I tried with:
inline fun <reified E : ViewModel> getViewModelClass() = E::class.java
But then when doing:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModelClass())
I get Cannot use E as reified type paramter. Use class instead
Is there a solution to this?
E in BaseActivity can't be reified, so you can't pass it to any methods which take a reified E.
Your best option may just be to accept the class as a constructor parameter.
abstract class BaseActivity<E : ViewModel>(private val modelClass: Class<E>) : AppCompatActivity() {
protected lateinit var viewModel: E
... viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(modelClass)
}
If BaseActivity wasn't abstract, you could add a factory method:
// outside BaseActivity class itself
fun <reified E : BaseModel> BaseActivity() = BaseActivity(E::class.java)
but it wouldn't help when extending it.
You ca do it in this way:
abstract class BaseActivity<E : ViewModel> : AppCompatActivity() {
protected lateinit var viewModel: E
abstract fun getViewModel():E
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModel())
}
}
Now, you can extend any class from BaseActivity and override the getViewModel() function returning the respective ViewModel class.
Hope this helps.
EDIT
Try this once:
inline fun <reified E> getViewModelClass(): Class<E> {
return E::class.java
}
and use it like this:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModelClass())
https://stackoverflow.com/a/52107111/8832537
You should check this guy's solution. The only downside of his approach is he uses Reflection API to get the generic parameter. I researched a lot but didn't find a solution that doesn't use reflection. If you did find it, let me know. That would be more convenient.
Related
I am new in adroid , so I have a simple project, I want to create simple register project, so I have viewmodel in my project and I amusing Hilt library also in there, and when I build project it is throw an error for
myViewModel = ViewModelProvider(this)[MyViewModel::class.java]
as a "Cannot create an instance of class com.app.myapp.viewModel", I do not know what I missed?
class Register : ComponentActivity() {
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = [ViewModelProvider(this)::class.java]
setContent {
RegisterScreen(myViewModel)
}
}
}
#Composable
fun RegisterScreen(
myViewModel: MyViewModel
) {
}
Reasons may cause system can not create viewModel:
Your viewModel class is not Public
Your package name which contains viewModel contains special keywords (such a "package.new.feature")
If you are using dagger hilt you should putt annotation #HiltViewModel above the class declaration and create constructor like
#HiltViewModel
class viewModel #Inject constructor() : ViewModel()
With the dagger hilt You should use hiltViewModel() function to create instance for compose instead of viewModel()
dependency: androidx.hilt:hilt-navigation-compose
#Composable
fun MyExample (viewModel: MyViewModel = hiltViewModel())
Your ViewModel class does not extend from androidx.lifecycle.ViewModel
You should create your ViewModel class extending from the ViewModel, something like RegisterViewModel.
Take a look at the documentation for more info:
https://developer.android.com/topic/libraries/architecture/viewmodel
You are trying to create a view model from the base class ViewModel. it doesn't work like this
You need to create your own viewmodel class and extend it from the base class ViewModel like this
class MyViewModel : ViewModel() {
}
So your code will be like
class MyViewModel : ViewModel() {
// your implementation
}
class Register : ComponentActivity() {
private lateinit var viewModel: MyViewModel // changes to this line
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[MyViewModel::class.java] // changes to this line
setContent {
RegisterScreen(viewModel)
}
}
}
BUT if you are using compose you should look at the integration between viewmodel and compose
to make your composable use the viewModel without you creating it then passing it to the composable
#Composable
fun MyExample(
viewModel: MyViewModel = viewModel()
) {
// use viewModel here
}
So previously this code was working fine (2.0.1).
abstract class ActivityBaseViewModel<out VM : ViewModel>(clazz: KClass<VM>) : AppCompatActivity() {
protected val viewModel: VM by viewModel(clazz)
Then I updated Koin to 2.2.2 and now it can't compile this code:
Cannot use 'VM' as reified type parameter. Use a class instead.
Seems there is no alternative function like this anymore.
So another solution would be like this:
base view model activity (or fragment)
abstract class ActivityBaseViewModel<out VM : ViewModel> : AppCompatActivity() {
protected abstract val viewModel: VM
...
}
and then in a specific activity (or fragment):
class MainActivity : ActivityBaseViewModel<MainActivityViewModel>() {
override val viewModel: MainActivityViewModel by viewModel()
...
}
I'm following a tutorial where a ViewModel extends an abstract class in order to use coroutines, this is the class that extends:
abstract class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCleared() {
super.onCleared()
job.cancel()
}}
And this is the ViewModel:
class ViewModel(application: Application) : BaseViewModel(application) {}
So in MainActivity I'm trying to instantiate the class like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[ViewModel::class.java]}
In the tutorial the guy perfectly does this but when I try to run the application it throws me an exception:
Caused by: java.lang.RuntimeException: Cannot create an instance of class com.name.nameapp.main.viewmodel.ViewModel
I feel like I'm missing something can you guys point what it is, if you need more information I'll gladly post it
Your ViewModel is child of AndroidViewModel which require an Application object. So you will have to provide the Factory class in order to instantiate the ViewModel. Like so:
val viewModelProvider = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory(application)
)
viewModel = viewModelProvider[MainViewModel::class.java]
If you are using the fragment library from Jetpack
implementation "androidx.fragment:fragment-ktx:1.2.5"
You can use property delegation like so:
val viewModel: ViewModel by viewModels()
When using Room database with ViewModel and LiveDate based on this codelab in order to use queries with parameter I didn't find any solution so I tried to create another constructor for both view model class and repository class but I noticed that in CodeLab they don't use view model constructors at all and instead they use view model provider so I changed it and used
new StoryViewModel(getApplication(),"Fiction");
instead of
new ViewModelProvider(this).get(StoryViewModel.class);
I don't know if there's anything wrong with this or not. It works fine now, but if it's the way why don't we use this always? And why do we use view model provider?
If what I did is wrong then how can I retrieve data from db based on parameters?
Thank you.
you should not use directly viewModel constructor because the system creates the viewModel or gets it from cache. If you want to pass parameter to the viewModel constructor you should use a viewModel factory like this:
class ViewModelFactory(params) : ViewModelProviders.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(params::class.java).newInstance(params);
}
}
...
override fun onCreate(...) {
viewModel = ViewModelProviders.of(this, ViewModelFactory(params)).get(MyViewModel::class.java)s
}
Here is a complete example, built with knowledge I gained from Pierluigi's answer:
class MyFragment: Fragment() {
private lateinit var viewModel: ContactViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activity?.let { activity ->
viewModel = ViewModelProvider(
activity,
MyViewModelFactory(
"mode value",
"nickname value"
)
).get(MyViewModel::class.java)
} ?: throw AssertionError("Unable to get parent activity from fragment")
}
}
class MyViewModelFactory(val mode: String, val nickname: String) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
modelClass.getConstructor(String::class.java, String::class.java)
.newInstance(mode, nickname)
}
class MyViewModel(val mode: String, val nickname: String) : ViewModel()
I'm building the architecture of a new Android application using Kotlin and Android Architecture Components (ViewModel, LiveData) and I'm also using Koin as my dependency injection provider.
The problem is that I'm not been able to initialize the ViewModel in a generic way inside my BaseActivity via koin injection. The current code looks like this:
abstract class BaseActivity<ViewModelType : ViewModel> : AppCompatActivity() {
// This does not compile because of the generic type
private val viewModel by lazy {
// Koin implementation to inject ViewModel
getViewModel<ViewModelType>()
}
#CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Fabric.with(this, Crashlytics())
}
/**
* Method needed for Calligraphy library configuration
*/
#CallSuper
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase))
}
}
I'd like to know if is there a way to do this in Kotlin because I'm pretty sure I would be able to do in Java easily.
Thanks.
The solution was provided by the koin team in version 0.9.0-alpha-11 and the final code looks like this:
open class BaseActivity<out ViewModelType : BaseViewModel>(clazz: KClass<ViewModelType>) :
AppCompatActivity() {
val viewModel: ViewModelType by viewModel(clazz)
fun snackbar(message: String?) {
message?.let { longSnackbar(find(android.R.id.content), it) }
}
fun toast(message: String?) {
message?.let { longToast(message) }
}
}
Here is example of not passing Class and Generic to base implementation
In your base fragment/activity:
abstract class BaseFragment<T : BaseViewModel> : Fragment() {
...
#Suppress("UNCHECKED_CAST")
private val clazz: KClass<T> = ((this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<T>).kotlin
protected val viewModel: T by viewModel(clazz = clazz)
...
}
It looks ugly, but it works.
you can use a delegate version declaration for your ViewModel and avoid using directly a lazy expression. Try with this:
abstract class BaseActivity<T : ViewModel> : AppCompatActivity() {
val model by viewModel<T>()
}
This will give you a lazy of
getViewModel<T>()
Throw an eye on the quick ref: https://insert-koin.io/docs/1.0/getting-started/android-viewmodel/
Hope it will help.