Dependency Injections или разработка Android приложений с гибкой архитектурой

В идеальном мире, после старта разработки требования четко закреплены. Но в реальности они могут изменяться в силу разных причин. Столкнувшись с этим, большинство разработчиков понимает: необходимо, чтобы архитектура разрабатываемого приложения была максимально гибкой. Такой подход позволит сократить время, которое тратится на внесение изменений, до минимума. Решить эту задачу как раз и помогают Dependency Injections.

«Инъекции» зависимостей или Dependency Injections (DI)

Позволяют строить гибкую и независимую архитектуру приложений, быстро разрабатывать и внедрять новую функциональность, и предоставляют другие интересные возможности. В этой статье мы кратко расскажем об основных библиотеках DI, которые используем в своих проектах.

Что?

Dependency Injection (DI) — это набор паттернов и принципов разработки, которые позволяют писать достаточно связный код.

Зачем?

Для устранения прямых зависимостей объектов друг от друга. Например, объект A вызывает метод Bar, чтобы отослать email или сохранить данные в базе. Если вынести реализацию этого метода из A, то можно будет использовать разные способы отправки email (или вообще ничего никуда не посылать) или сохранять данные в базе под разными провайдерами.

Преимущества:
  1. Можно заменить какой-нибудь сервис, не являющийся технологически нейтральным.
  2. Можно автоматически тестировать модули программы независимо.
  3. Можно повторно использовать код.

Популярные Dependency Injection библиотеки для Android:

Инъекции классов: Dagger2.

Инъекции View: ButterKnife.

Универсальные: RoboGuice, AndroidAnnotations.

Пример кода без использования Dependency Injection библиотек:

public class TestFragment extends Fragment {
    private Button btn1;
    private Button btn2;
    private TextView text;
    private String stringFromRes;
    private Foo foo;
    
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
					Bundle savedInstanceState) {
        return inflater.inflate(R.layout.test_fragment, container, false);
    }
    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
      btn1 = (Button) view.findViewById(R.id.button_1);
	btn2 = (Button) view.findViewById(R.id.button_2);
	text = (TextView) view.findViewById(R.id.text_1);
	stringFromRes = getString(R.string.test_string);
	foo = new Foo();
	btn1.setOnClickListener(new View.OnClickListener() {
	    @Override
	    public void onClick(View v) {
	    }
	});
	btn2.setOnClickListener(new View.OnClickListener() {
	    @Override
	    public void onClick(View v) {
	    }
	});
   }
}
RoboGuice

RoboGuice

Позволяет внедрять View, ресурсы, системные сервисы и любые другие объекты.

Внедряемый объект Используемая аннотация Пример
View @InjectView
@InjectView(R.id.textView1) TextView textView1;
Ресурсы @InjectResource
@InjectResource(R.string.app_name) String name;
Системные сервисы @SystemService
@Inject LayoutInflater inflater;
POJO @Bean
@Inject Foo foo;

Применяется в: Skype, Cars.com, SwiftKey Keyboard, Starbucks.

Достоинства:

  • Можно внедрять зависимости в private поля.
  • Не нужно инициализировать Views, системные сервисы или ресурсы. Достаточно внедрить их.
  • Меньше кода.
  • Меньше багов/проблем.
  • Можно сосредоточиться на разработке бизнес-логики приложения.

Недостатки:

  • Внедрение зависимостей выполняется при запуске приложения. Для этого используется Reflection, что может снизить производительность.
  • Необходимо использовать RoboActivity/RoboFragment или реализовывать RoboContext вместо Activity/Fragment.
  • Не информативный StackTrace при ошибках.
  • Нельзя внедрять Click Listeners.

Пример кода:

public class TestFragment extends Fragment {
    @InjectView(R.id.button_1) private Button btn1;
    @InjectView(R.id.button_2) private Button btn2;
    @InjectView(R.id.text_1) private TextView text;
    @InjectResource(R.string.test_string) private String stringFromRes;
    @Inject private Foo foo;
    @Nullable @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
				Bundle savedInstanceState) {
        return inflater.inflate(R.layout.test_fragment, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            }
        });
        btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            }
        });
Dagger2

Dagger 2

Применяется для внедрения таких объектов, как: Context, System services, REST service (например, Retrofit), Database manager, Message passing (например, EventBus) и др. Для этого используется аннотация @Inject.

Достоинства:

  • Код генерируется при компиляции, поэтому ошибки выявляются на более ранней стадии.
  • Приложение работает быстрее, т.к. не используется Reflection.
  • Код более читаемый и легко поддерживаемый.
  • Поддержка Lazy-injections («ленивых» внедрений).
  • Разрабатывается специально для использования с Android.
  • Можно подменять реализацию через @Qualifier.

Недостатки:

  • Приходится писать больше кода, чем с RoboGuice (определять модули и компоненты).
  • Нельзя внедрять зависимости в private поля.
  • Необходимо вызывать метод inject() для внедрения зависимостей.
  • Метод inject() нужно вызывать в том классе, в котором внедряются зависимости. Нельзя вызвать этот метод в базовом классе для внедрения зависимостей в производном.
  • Не поддерживается внедрение View. Для этого рекомендуется использовать библиотеку ButterKnife.
ButterKnife

ButterKnife

Это не фреймворк, а библиотека, которая используется только для внедрения View и Click Listeners. Хорошо дополняет Dagger 2.

Внедряемый объект Используемая аннотация

View

Click Listeners

@Bind, @OnClick, @OnLongClick, @OnItemSelected и др.

Пример кода (Dagger 2 + ButterKnife):

public class TestFragment extends Fragment {
    @Bind(R.id.button_1) private Button btn1;
    @Bind(R.id.button_2) private Button btn2;
    @Bind(R.id.text_1) private TextView text;
    private String stringFromRes;
    @Inject private Foo foo;
    public TestFragment() {
        super();
        TestApp.getGraph().inject(this);
    }
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
					Bundle savedInstanceState) {
        return inflater.inflate(R.layout.test_fragment, container, false);
    }
    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        stringFromRes = getString(R.string.test_string);
    }
    
    @OnClick(R.id.button_1)
    protected void onButtonOneCLick(){
        
    }
    @OnClick(R.id.button_2)
    protected void onButtonTwoCLick
Android Annotations

Android Annotations

Как и RoboGuice, этот «универсальный солдат» помогает внедрять любые объекты.

Внедряемый объект Используемая аннотация
View @ViewById
Ресурсы @StringRes
Системные сервисы @SystemService
POJO @Bean

Применяется: Wine Secretary, Park Me Right: Car locator, Siine, on{x}.

Достоинства:

  • Код генерируется при компиляции, поэтому ошибки выявляются на более ранней стадии.
  • Не используется Reflection — работает быстрее.
  • Код более читаемый и легко поддерживаемый.
  • Можно подменять реализацию через указания класса реализующего интерфейс в аннотации @Bean.
  • Хорошо взаимодействует с Dagger и RoboGuice.
  • Имеет очень много компонентов (реализация REST, ORMLite и т.д).

Недостатки:

  • Исходные классы заменяются на «имяКласса_», например TestFragment на TestFragment_.
  • Нельзя внедрять зависимости в private поля.
  • Имеет очень много компонентов, что затрудняет отказ от библиотеки при необходимости.

Пример кода:

@EFragment(R.layout.fragment_test)
public class TestFragment extends Fragment {

    @SystemService
    protected LayoutInflater layoutInflater;

    @ViewById(R.id.button_1)
    protected Button btn1;
    @ViewById
    protected Button button_2;
    @ViewById(R.id.text_1)
    protected TextView text;
    @StringRes(R.string.test_string)
    protected String stringFromRes;
    @Bean(FooImpl.class)
    protected Foo foo;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        text.setText(stringFromRes);
        foo.setTestValue(stringFromRes);
        assert layoutInflater != null;
    }

    @Click(R.id.button_1)
    void onButtonOneClick(){
        Log.v("TestFragment", foo.getTestValue());
    }

    @Click(R.id.button_2)
    void onButtonTwoClick(){
        Log.v("TestFragment", "pressed: " + button_2.getId());

Несколько полезных советов

Использовать ли DI в разработке приложений? Конечно, да. Главное — не ошибиться с выбором DI-фреймворка для конкретного проекта. Для разработки проектов, где максимальная производительность не является первоочередной задачей, RoboGuice — весьма неплохой выбор. Тем, кому, напротив, высокая производительность нужна (и кто хочет обойтись без Reflections в рантайме), больше подойдет Dagger 2. Ну а если есть желание сделать проект, используя одну библиотеку, AndroidAnnotations — то, что надо.