Dagger 2 Android - зависимости inject () в ViewModels vs Application со ссылками на зависимости - PullRequest
1 голос
/ 10 марта 2019

Я создаю базовое приложение для Android с помощью Dagger 2. Мне было трудно понять, как правильно его использовать, пока я не наткнулся на этот замечательный разговор Джейка Уортона .В нем он демонстрирует использование Dagger 2 с приложением «Твиттер».На ~ 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 и использовать fake компонентов для тестирования (что является одной из моих основных целей здесь).Ранее единственным способом, которым я смог внедрить зависимости, было создание моего компонента внутри ViewModels, что делает невозможным его замену подделками.

Для такого простого приложения, как это, я знаюЯ мог бы просто отказаться от метода inject и сохранить ссылку на хранилище в классе MyApplication.Тем не менее, при условии, что существует больше зависимостей, о которых нужно беспокоиться, будет ли это общий / хороший / дружественный к тестированию подход к внедрению зависимостей для Activity и ViewModels в Android?

1 Ответ

0 голосов
/ 11 марта 2019

После вдохновения от ответа EpicPandaForce и некоторых исследований (см. эту статью ), я нашел решение, которым я доволен.

Я решилудалите Dagger 2 из моего проекта, потому что я слишком сильно его проектировал.Мое приложение опирается на класс репозитория и теперь реализацию ViewModelProvider.Factory, которые необходимы сразу после запуска приложения.Я узнал достаточно о Dagger для собственного удовольствия, поэтому я чувствую себя комфортно, оставив его вне этого конкретного проекта и создав две зависимости в классе Application.Эти классы выглядят так:

Класс My 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;
    }
}

MyViewModel Фабрика, которая создает новые ViewModel с ссылкой на репозиторий.Я буду расширять его по мере добавления дополнительных классов Activity и ViewModel s:

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));
    }
}

My 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 s:

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

...