Dagger 2 Android - зависимости inject () в ViewModels vs Application со ссылками на зависимости

1

Я создаю базовое приложение для 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?

Теги:
dependency-injection
dagger-2

1 ответ

0
Лучший ответ

После вдохновения от ответа 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 часа, если у кого-то есть что-то полезное, и я выберу это в качестве ответа.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню