可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a ViewModel class just like the one defined in the Connecting ViewModel and repository section of Architecture guide. When I run my app I get a runtime exception. Does anyone know how to get around this? Should I not be injecting the ViewModel? Is there a way to tell the ViewModelProvider
to use Dagger to create the model?
public class DispatchActivityModel extends ViewModel {
private final API api;
@Inject
public DispatchActivityModel(API api) {
this.api = api;
}
}
Caused by: java.lang.InstantiationException: java.lang.Class has no zero argument constructor
at java.lang.Class.newInstance(Native Method)
at android.arch.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:143)
at android.arch.lifecycle.ViewModelProviders$DefaultFactory.create(ViewModelProviders.java:143)
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:128)
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:96)
at com.example.base.BaseActivity.onCreate(BaseActivity.java:65)
at com.example.dispatch.DispatchActivity.onCreate(DispatchActivity.java:53)
at android.app.Activity.performCreate(Activity.java:6682)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2619) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2727)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1478)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6121)
回答1:
You need to implement your own ViewModelProvider.Factory
. There is an example app created by Google demonstrating how to connect Dagger 2 with ViewModels. LINK. You need those 5 things:
In ViewModel:
@Inject
public UserViewModel(UserRepository userRepository, RepoRepository repoRepository) {
Define annotation:
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
Class<? extends ViewModel> value();
}
In ViewModelModule:
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(UserViewModel.class)
abstract ViewModel bindUserViewModel(UserViewModel userViewModel);
In Fragment:
@Inject
ViewModelProvider.Factory viewModelFactory;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
userViewModel = ViewModelProviders.of(this, viewModelFactory).get(UserViewModel.class);
Factory:
@Singleton
public class GithubViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
@Inject
public GithubViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
@SuppressWarnings("unchecked")
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
Provider<? extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
回答2:
Today I learnt a way to avoid having to write factories for my ViewModel
classes:
class ViewModelFactory<T : ViewModel> @Inject constructor(
private val viewModel: Lazy<T>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = viewModel.get() as T
}
EDIT: As pointed out by @Calin in the comments, we are using Dagger's Lazy
in the code snippet above, not Kotlin's.
Rather than injecting the ViewModel
, you can inject a generic ViewModelFactory
into your activities and fragments and obtain an instance of any ViewModel
:
class MyActivity : Activity() {
@Inject
internal lateinit var viewModelFactory: ViewModelFactory<MyViewModel>
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
this.viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(MyViewModel::class.java)
...
}
...
}
I used AndroidInjection.inject(this)
as with the dagger-android
library, but you can inject your activity or fragment the way you prefer. All that is left is to make sure you provide your ViewModel
from a module:
@Module
object MyModule {
@JvmStatic
@Provides
fun myViewModel(someDependency: SomeDependency) = MyViewModel(someDependency)
}
Or applying the @Inject
annotation to its constructor:
class MyViewModel @Inject constructor(
someDependency: SomeDependency
) : ViewModel() {
...
}
回答3:
I believe there is a second option if you don't want to use the factory mentioned in Robert's answer. It is not necessarily better solution but it is always good to know the options.
You can leave your viewModel with default constructor and inject your dependencies just as you do in case of activities or other elements created by system.
Example:
ViewModel:
public class ExampleViewModel extends ViewModel {
@Inject
ExampleDependency exampleDependency;
public ExampleViewModel() {
DaggerExampleComponent.builder().build().inject(this);
}
}
Component:
@Component(modules = ExampleModule.class)
public interface ExampleComponent {
void inject(ExampleViewModel exampleViewModel);
}
Module:
@Module
public abstract class ExampleModule {
@Binds
public abstract ExampleDependency bindExampleDependency(ExampleDependencyDefaultImplementation exampleDependencyDefaultImplementation);
}
Cheers,
Piotr
回答4:
What may not be obvious in the question is that the ViewModel cannot be injected that way because the ViewModelProvider default Factory that you get from the
ViewModelProvider.of(LifecycleOwner lo)
method with only the LifecycleOwner parameter can only instantiate a ViewModel that has a no-arg default constructor.
You have a param: 'api' in your constructor:
public DispatchActivityModel(API api) {
In order to do that you need to create a Factory so that you can tell it how to create itself. The sample code from google gives you Dagger config and Factory code as mentioned in the accepted answer.
DI was created to avoid use of the new() operator on dependencies because if the implementations change, every reference would have to change as well. The ViewModel implementation wisely uses a static factory pattern already with ViewProvider.of().get() which makes its injection unnecessary in the case of no-arg constructor. So in the case you don't need to write the factory you don't need to inject a factory of course.
回答5:
I'd like to provide a third option for anyone stumbling upon this question. The Dagger ViewModel library will allow you to inject in a Dagger2-like way with ViewModels optionally specifying the ViewModel's scope.
It removes a lot of the boilerplate and also allows the ViewModels to be injected in a declarative way using an annotation:
@InjectViewModel(useActivityScope = true)
public MyFragmentViewModel viewModel;
It also requires a small amount of code to setup a module from which the fully dependency injected ViewModels can be generated and after that it's as simple as calling:
void injectFragment(Fragment fragment, ViewModelFactory factory) {
ViewModelInejectors.inject(frag, viewModelFactory);
}
On the ViewModelInjectors class which is generated.
DISCLAIMER: it is my library but I believe it's also of use to the author of this question and anyone else wanting to achieve the same thing.
回答6:
The default ViewModel factory used to get an instance of your DispatchActivityModel
in your view constructs the ViewModels using assumed empty constructors.
You can write your custom ViewModel.Factory
to get around it, but you'd stil need to take care of completing the dependency graph yourself if you want to provide your API
class.
I wrote a small library that should make overcoming this common more straightforward and way cleaner, no multibindings or factory boilerplate needed, while also giving the ability to further parametrise the ViewModel
at runtime:
https://github.com/radutopor/ViewModelFactory
@ViewModelFactory
public class DispatchActivityModel extends ViewModel {
private final API api;
public DispatchActivityModel(@Provided API api) {
this.api = api;
}
}
In the view:
public class DispatchActivity extends AppCompatActivity {
@Inject
private DispatchActivityModelFactory2 viewModelFactory;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appComponent.inject(this);
DispatchActivityModel viewModel = ViewModelProviders.of(this, viewModelFactory.create())
.get(UserViewModel.class)
}
}
Like I mentioned, you can also easily add runtime parameters to your ViewModel
instances as well:
@ViewModelFactory
public class DispatchActivityModel extends ViewModel {
private final API api;
private final int dispatchId;
public DispatchActivityModel(@Provided API api, int dispatchId) {
this.api = api;
this.dispatchId = dispatchId;
}
}
public class DispatchActivity extends AppCompatActivity {
@Inject
private DispatchActivityModelFactory2 viewModelFactory;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appComponent.inject(this);
final int dispatchId = getIntent().getIntExtra("DISPATCH_ID", -1);
DispatchActivityModel viewModel = ViewModelProviders.of(this, viewModelFactory.create(dispatchId))
.get(UserViewModel.class)
}
}