Я создаю базовое приложение для Android с помощью Dagger 2. Мне было очень трудно понять, как его правильно использовать, пока я не наткнулся на это замечательное выступление Джейка Уортона. В нем он демонстрирует использование Dagger 2 с приложением «Tweeter». В ~22:44 он показывает, что поля @Inject приложения могут быть удовлетворены методом вставки. Позже он показывает простую реализацию Android.
ViewModels моего приложения полагаются на класс репозитория. Я использую Dagger 2 для внедрения этого репозитория в ViewModels через класс Application, например:
//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 и использовать компоненты подделка для тестирования (что является одной из моих основных целей здесь). Раньше единственным способом внедрить зависимости было создание моего компонента внутри ViewModels, что делало невозможным замену подделками.
Я знаю, что для такого простого приложения можно просто отказаться от метода inject и сохранить ссылку на репозиторий в классе MyApplication. Однако предполагая, что есть больше зависимостей, о которых нужно беспокоиться, будет ли это общий/хороший/удобный для тестирования подход к внедрению зависимостей для действий и моделей представления в Android?
Мне нравится идея использования фабрики. Я могу реализовать это только для внедрения конструктора и избежать необходимости делать ссылку на мой репозиторий приватным или общедоступным пакетом в моем ViewModel. Есть ли способ использовать что-то подобное, чтобы предоставить Activity подделку ViewModel для тестов? Или чаще держать Activity и ViewModel вместе? Редактировать: в ответе Муми упоминаются мультибиндинги, которые я только что видел в другой статье. Это может быть то, что я ищу в этом случае




После вдохновения от Ответ EpicPandaForce и некоторых исследований (см. эта статья) я нашел решение, которым я доволен.
Я решил исключить 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 фабрика, которая создает новые ViewModel со ссылкой на репозиторий. Я буду расширять это, добавляя больше классов Activity и ViewModel:
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 абстрактными (только с методами), а затем реализовал их для своего реального приложения и тестового кода. Это потому, что мне проще, чем extend напрямую использовать мои ViewModel, а затем пытаться переопределить их методы и скрыть их состояние, чтобы создать поддельную версию.
В любом случае, я расширил свой класс JourneyStoreApplication (я знаю, что противоречу самому себе, но это небольшой класс, поэтому им легко управлять) и использовал его, чтобы создать место для предоставления моих поддельных ViewModel:
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;
}
}
Я сделал поддельные реализации своих ViewModel и вернул их экземпляры из FakeJourneyStoreViewModelFactory. Я мог бы упростить это позже, так как, вероятно, больше «поддельных» шаблонов, чем должно быть.
Отказавшись от это руководство (раздел 4.9), я расширил AndroidJUnitRunner, чтобы предоставить подделка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 часа, если у кого-то есть что добавить, и я выберу это в качестве ответа.
Вы рассмотрели подход, который я изложил в stackoverflow.com/a/50681021/2413303? Это заставляет действие знать о приложении, но ViewModel больше не нужно знать о приложении (и может использовать внедрение конструктора).