Я создаю базовое приложение для Android с Dagger 2. У меня было много трудностей с пониманием, как правильно его использовать, пока я не наткнулся на этот замечательный разговор Джейка Уортона. В нем он демонстрирует использование Dagger 2 с приложением "Твиттер". @Inject
22: 44 он показывает, что поля приложения @Inject
могут быть удовлетворены с помощью метода @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. Однако если предположить, что существует больше зависимостей, о которых стоит беспокоиться, будет ли это общий/хороший/удобный для тестирования подход к внедрению зависимостей для Activity и ViewModels в Android?
После вдохновения от ответа EpicPandaForce и некоторых исследований (см. Эту статью) я нашел решение, которым я доволен.
Я решил исключить Dagger 2 из моего проекта, потому что я переусердствовал над ним. Мое приложение опирается на класс репозитория и теперь реализацию ViewModelProvider.Factory
, которые необходимы сразу после запуска приложения. Я узнал достаточно о Dagger для собственного удовольствия, поэтому я чувствую себя комфортно, оставив его вне этого конкретного проекта и создав две зависимости в классе Application
. Эти классы выглядят так:
Мой класс Application, который создает мою фабрику ViewModel
, предоставляет ей свой репозиторий и предоставляет метод getViewModelFactory()
для моих 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
в свой файл build.gradle
:
android {
defaultConfig {
// Espresso
testInstrumentationRunner "com.<my_package>.journeystore.CustomTestRunner"
}
}
Я собираюсь оставить этот вопрос открытым еще на 24 часа, если у кого-то есть что-то полезное, и я выберу это в качестве ответа.