【问题标题】:Dagger 2 Android - inject() dependencies into ViewModels vs Application with dependency reference(s)Dagger 2 Android - 将()依赖项注入 ViewModel 与具有依赖项引用的应用程序
【发布时间】:2019-03-10 00:37:13
【问题描述】:

我正在使用 Dagger 2 创建一个基本的 Android 应用程序。在遇到 this great talk by Jake Wharton 之前,我很难理解如何正确使用它。在其中,他演示了使用 Dagger 2 和“Tweeter”应用程序。在~22:44,他展示了应用程序的@Inject 字段可以满足注入方法。他后来展示了一个简单的 Android 实现。

我的应用的 ViewModel 依赖于存储库类。我正在使用 Dagger 2 通过 Application 类将此存储库注入 ViewModel,如下所示:

//In my Dagger 2 component
@Singleton
@Component(module = {MyRepositoryModule.class})
public interface MyRepositoryComponent{
    void inject(MyViewModel viewModel);
}

//In MyApplication
public class MyApplication extends Application{
    private MyRepositoryComponent repoComponent;

    //Instantiate the component in onCreate...

    public MyRepositoryComponent getMyRepositoryComponent(){
        return repoComponent;
    }
}

//Finally, in my ViewModel
public MyViewModel extends AndroidViewModel{
    @Inject
    public MyRepository repo;

    public MyViewModel(@NonNull MyApplication app){
        repo = app.getMyRepositoryComponent().inject(this);
    }
}

我采用这种方法是因为我可以覆盖 MyApplication 类并使用 fake 组件进行测试(这是我的主要目标之一)。以前,我能够注入依赖项的唯一方法是在视图模型内部构建我的组件,这使得无法用假货替代。

对于这样一个简单的应用程序,我知道我可以取消注入方法并在 MyApplication 类中保存对存储库的引用。但是,假设有更多依赖项需要担心,这是否是一种在 Android 中为 Activity 和 ViewModel 注入依赖项的常见/良好/测试友好的方法?

【问题讨论】:

  • 您是否考虑过我在stackoverflow.com/a/50681021/2413303 中概述的方法?它使Activity需要知道Application,而ViewModel将不再需要知道Application(并且可以使用构造函数注入)。
  • 我喜欢使用工厂的想法。我可以实现它只是为了进行构造函数注入,并避免在我的ViewModel 中将我的存储库引用包私有或公开。有没有办法使用这样的东西为Activity 提供一个假的ViewModel 进行测试?还是将ActivityViewModel 放在一起更常见?编辑:Mumi 的回答提到了我刚刚在另一篇文章中看到的多重绑定。在这种情况下,这可能就是我要寻找的东西

标签: java android dependency-injection dagger-2


【解决方案1】:

EpicPandaForce's answer 的启发和一些研究(见this article)之后,我找到了一个我满意的解决方案。

我决定从我的项目中删除 Dagger 2,因为我对它进行了过度设计。我的应用程序依赖于一个存储库类,现在是一个 ViewModelProvider.Factory 实现,一旦应用程序运行,它们都是必需的。我对 Dagger 的了解已经足够让我自己满意了,所以我很乐意将它排除在这个特定的项目之外,并在 Application 类中创建两个依赖项。这些类如下所示:

我的应用程序类,它创建了我的ViewModel 工厂,为它提供了它的存储库,并向我的活动公开了一个getViewModelFactory() 方法:

public class JourneyStoreApplication extends Application {

    private final JourneyStoreViewModelFactory journeyStoreViewModelFactory;

    {
        // Instantiate my viewmodel factory with my repo here
        final JourneyRepository journeyRepository = new JourneyRepositoryImpl();
        journeyStoreViewModelFactory = new JourneyStoreViewModelFactory(journeyRepository);
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    public JourneyStoreViewModelFactory getViewModelFactory(){
        return journeyStoreViewModelFactory;
    }
}

我的ViewModel 工厂,它使用存储库引用创建新的ViewModels。随着我添加更多Activity 类和ViewModels,我将对此进行扩展:

public class JourneyStoreViewModelFactory implements ViewModelProvider.Factory {

    private final JourneyRepository journeyRepository;

    JourneyStoreViewModelFactory(JourneyRepository journeyRepository){
        this.journeyRepository = journeyRepository;
    }

    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if(modelClass == AddJourneyViewModel.class){
            // Instantiates the ViewModels with their repository reference.
            return (T) new AddJourneyViewModelImpl(journeyRepository);
        }
        throw new IllegalArgumentException(String.format("Requested class %s did not match expected class %s.", modelClass, AddJourneyViewModel.class));
    }
}

我的AddJourneyActivity 类,它使用AddJourneyViewModel

public class AddJourneyActivity extends AppCompatActivity {

    private static final String TAG = AddJourneyActivity.class.getSimpleName();

    private AddJourneyViewModel addJourneyViewModel;
    private EditText departureTextField;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_add_journey);

        JourneyStoreApplication app = (JourneyStoreApplication) getApplication();
        addJourneyViewModel = ViewModelProviders
                // Gets the ViewModelFactory instance and creates the ViewModel.
                .of(this, app.getViewModelFactory())
                .get(AddJourneyViewModel.class);

        departureTextField = findViewById(R.id.addjourney_departure_addr_txt);
    }

    //...
}

但这仍然留下了测试的问题,这是我的主要问题之一。 旁注:我将所有ViewModel 类抽象化(仅使用方法),然后为我的真实应用程序和测试代码实现它们。这是因为我发现这比直接ViewModels 更容易extends,然后尝试覆盖他们的方法并隐藏他们的状态以创建一个假版本。

无论如何,我扩展了我的 JourneyStoreApplication 课程(我知道这与我自己相矛盾,但它是一个小课程,因此很容易管理)并用它来创建一个提供我的假 ViewModels 的地方:

public class FakeJourneyStoreApplication extends JourneyStoreApplication {

    private final JourneyStoreViewModelFactory fakeJourneyStoreViewModelFactory;

    {   // Create my fake instances here for my tests
        final JourneyRepository fakeJourneyRepository = new FakeJourneyRepositoryImpl();
        fakeJourneyStoreViewModelFactory = new FakeJourneyStoreViewModelFactory(fakeJourneyRepository);
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    public JourneyStoreViewModelFactory getViewModelFactory(){
        return fakeJourneyStoreViewModelFactory;
    }
}

我伪造了我的ViewModels 并从FakeJourneyStoreViewModelFactory 返回了它们的实例。稍后我可能会简化这一点,因为“假”样板可能比需要的要多。

离开 this guide(第 4.9 节),我扩展了 AndroidJUnitRunner 以提供我的 fake Application 到我的测试:

public class CustomTestRunner extends AndroidJUnitRunner {
    @Override
    public Application newApplication(ClassLoader cl, String className, Context context)
    throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        return super.newApplication(cl, FakeJourneyStoreApplication.class.getName(), context);
    }
}

最后,我将自定义测试运行器添加到我的 build.gradle 文件中:

android {
    defaultConfig {
        // Espresso
        testInstrumentationRunner "com.<my_package>.journeystore.CustomTestRunner"
    }
}

我将把这个问题再开放 24 小时,以防有人要添加有用的东西,然后我会选择这个作为答案。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多