6 апреля 2011 в 17:41

RoboGuice или «Андроид подсел на инъекции»

imageRoboGuice — это библиотека, которая позволяет пользоваться всеми преимуществами Contexts and Dependency Injection при разработке приложений на Андроиде.
Как несложно догадаться, RoboGuice основан на Google Guice.
Сразу оговорюсь, что в качестве перевода слова «injection» я буду использовать слово «инъекция».

Зачем колоться?


Думаю, что у многих читателей сразу возникнет вопрос: «Зачем эти сложности с CDI на мобильной платформе? Наверняка это всё занимает много места и медленно работает.»
Попробую убедить таких читателей в обратном, а именно в том, что CDI на мобильной платформе очень даже жизнеспособен и существенно облегчает жизнь разработчикам.

RoboGuice
Лирическое отступление

После эпохи увлечения и повсеместного использования XML, наступило отрезвление, когда все устали от «портянок» XML файлов. Начались поиски новых подходов, которые позволили бы сократить объём конфигурационного кода и ускорить разработку. Одним из таких выходов стал CDI, основанный на аннотациях. Вторым могу назвать подход "програмиирование по соглашениям" (Convention over configuration или coding by convention). Разработчикам понравилось, идеи развивались, появилось несколько имплементаций CDI, многие крупные библиотеки стали использовать CDI через аннотации.
В итоге CDI был стандартизирован для Java в виде JSR-330, a также как (JSR-299), официально включённой в Java EE 6.
Лично я впервые увидел CDI в библиотекe Seam. И был приятно удивлён простотой и изящностью подхода. Экономия времени разработки, упрощение конфигурирования, уменьшение общего объёма и соответственно возможных ошибок.
С течением времени аннотации проникли почти всюду. После XML-мании, наступила эпоха аннотаций. И было бы очень странно, если бы они не добрались и до Андроида.

Так зачем это на мобильной платформе?

Ответ простой: чтобы сделать разработку быстрее и проще, а код чище и надёжнее.
Чтобы не быть голословным, возьмём пример из Roboguice Get Started.
Вот так выглядит обычный Activity класс:
class AndroidWay extends Activity { 
    TextView name; 
    ImageView thumbnail; 
    LocationManager loc; 
    Drawable icon; 
    String myName; 

    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.main);
        name      = (TextView) findViewById(R.id.name); 
        thumbnail = (ImageView) findViewById(R.id.thumbnail); 
        loc       = (LocationManager) getSystemService(Activity.LOCATION_SERVICE); 
        icon      = getResources().getDrawable(R.drawable.icon); 
        myName    = getString(R.string.app_name); 
        name.setText( "Hello, " + myName ); 
    } 
} 

Здесь 19 строчек кода. 5 строчек кода в методе onCreate() — это банальная инициализация полей класса, и только одна, последняя строка, несёт смысловую нагрузку: name.setText(). Более сложные имплементации Activity классов могут содержать ещё больше кода для иницилизации объектов.
Теперь же сравните с тем, что позволяет RoboGuice:
class RoboWay extends RoboActivity { 
    @InjectView(R.id.name)             TextView name; 
    @InjectView(R.id.thumbnail)        ImageView thumbnail; 
    @InjectResource(R.drawable.icon)   Drawable icon; 
    @InjectResource(R.string.app_name) String myName; 
    @Inject                            LocationManager loc; 

    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.main);
        name.setText( "Hello, " + myName ); 
    } 
}

Теперь onCreate() намного лаконичнее и проще для восприятия; минимум шаблонного кода, максимум кода, касающегося логики приложения.

Первый укол


Чтобы «первый укол» получился более жизненным, я подсажу на инъекции хороший знакомый пример с developer.android.com: Notepad Tutorial. Он прекрасно разобран по полочкам, исходные коды его доступны и его вполне будет достаточно для модификации и внедрения идей CDI.
Чтобы начать использовать RoboGuice в проекте, необходимо добавить две библиотеки в classpath: RoboGuice и guice-2.0-no_aop.jar (но не guice-3.0).
Также можно добавить как зависимость в maven (да-да, Andoird-проект и Maven вполне себе совместимы, но это тема для отдельной статьи):
<dependency>
      <groupId>org.roboguice</groupId>
      <artifactId>roboguice</artifactId>
      <version>1.1</version>
</dependency>

Затем необходимо создать класс, наследуемый от RoboApplication:
public class NotepadApplication extends RoboApplication {
}

и прописать его в AndroidManifest.xml:
<application android:name="com.android.demo.notepad3.NotepadApplication">


Теперь перепишем немного NoteEdit класс. Добавим инъекции ресурсов и NotesDbAdapter, наследуемся от RoboActivity.
Было:
public class NoteEdit extends RoboActivity {
    private NotesDbAdapter mDbHelper;
    private EditText mTitleText;
    private EditText mBodyText;
    private Long mRowId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.note_edit);
        setTitle(R.string.edit_note);

        mDbHelper = new NotesDbAdapter(this);
        mDbHelper.open();

        mTitleText = (EditText) findViewById(R.id.title);
        mBodyText = (EditText) findViewById(R.id.body);

        Button confirmButton = (Button) findViewById(R.id.confirm);

        ...
    }
...
}

Стало:
public class NoteEdit extends RoboActivity {
    @Inject NotesDbAdapter mDbHelper;
    @InjectView(R.id.title) EditText mTitleText;
    @InjectView(R.id.title) EditText mBodyText;
    @InjectView(R.id.confirm) Button confirmButton;
    @InjectResource(R.string.edit_note) String title;

    private Long mRowId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.note_edit);
        setTitle(title);

        mDbHelper.open();

        ...
    }
...
}

Примерно также модифируем Notepadv3. Наследуем от RoboListActivity, добавляем инъекцию для NotesDbAdapter и убираем инициализацию этого NotesDbAdapter из onCreate().
public class Notepadv3 extends RoboListActivity { 
    ...
    @Inject private NotesDbAdapter mDbHelper;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.notes_list);
        mDbHelper.open();
        fillData();
        registerForContextMenu(getListView());
    }
...
}

И обновляем конструктор NotesDbAdapter, чтобы Context подставлялся автоматически с помощью RoboGuice.
    @Inject
    public NotesDbAdapter(Context ctx) {
        this.mCtx = ctx;
    }

Запускаем, проверяем — работает!
Кода стало меньше, функциональность осталась, читаемость повысилась.
Стоит отметить, что RoboGuice может делать инъекции не только класса Context, но и многих других служебных объектов Андроид, в т.ч. Application, Activity и различных сервисов. Полный список доступен здесь.

Увеличиваем дозу


Для большей демонстрации возможностей RoboGuice сделаем рефакторинг класса NotesDbAdapter. На мой взгляд, он сильно перегружен и требует декомпозиции DatabaseHelper и самого NotesDbAdapter. При росте проекта DatabaseHelper определённо будет использоваться несколькими классами. Например, появится TasksDbAdapter. Тем более лично мне не по душе насильственный вызов mDbHelper.open();. Этот метод надо вызвать в двух местах. Велик соблазн забыть сделать этот вызов и получить ошибку. Итак, приступим.
Начнём с NotesDbAdapter. Здесь куча лишних полей:
  • Context mCtx, который используется только для инициализации DatabaseHelper
  • Сам DatabaseHelper mDbHelper, потому что фактически используется только SQLiteDatabase mDb
  • Статические поля: DATABASE_CREATE, DATABASE_VERSION, DATABASE_NAME, которые имеют немного отношения к логике сохранения и получения заметок в/из базы данных.

Фактически хотелось бы оставить только SQLiteDatabase mDb. Остальное убрать. К тому же сделаем NotesDbAdapter «синглетоном».
Итого имеем следующую цель:
@Singleton
public class NotesDbAdapter {

    public static final String KEY_TITLE = "title";
    public static final String KEY_BODY = "body";
    public static final String KEY_ROWID = "_id";

    private static final String TAG = "NotesDbAdapter";
    private static final String DATABASE_TABLE = "notes";

    @Inject private SQLiteDatabase mDb;

    public NotesDbAdapter() {
    }
...
}

KEY_TITLE, KEY_BODY, KEY_ROWID, DATABASE_TABLE, TAG оставлены, т.к. они имеют самое непосредственное отношение к NotesDbAdapter.
Метода open() в новом классе нет. Перенесём логику этого метода в DatabaseHelper.
Теперь нам необходимо откуда-то получать SQLiteDatabase mDb. Очевидно, что это должен делать DatabaseHelper. Чтобы научить его это делать, имплементируем интерфейс com.google.inject.Provider, который методом get() возвращает тот или иной созданный объект.
К тому же делаем инъекцию контекста приложения в конструкторе.
В итоге класс DatabaseHelper будет иметь следующий вид:
@Singleton
public class DatabaseHelper extends SQLiteOpenHelper implements Provider<SQLiteDatabase>{
    private static final String TAG = "DatabaseHelper";

    private static final String DATABASE_CREATE =
        "create table notes (_id integer primary key autoincrement, "
        + "title text not null, body text not null);";
    private static final String DATABASE_NAME = "data";
    private static final int DATABASE_VERSION = 2;
    private SQLiteDatabase mDb;

    @Inject
    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    public SQLiteDatabase get() {
        if (mDb == null || !mDb.isOpen()) {
            mDb = getWritableDatabase();
        }
        return mDb;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(DATABASE_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
       ...
    }
}

Теперь укажем RoboGuice, что класс SQLiteDatabase предоставляется классом-провайдером DatabaseHelper. Для этого создаём конфигурационный класс, т.н. модуль, и регистрируем его в системе (в NotepadApplication)
public class NotepadModule extends AbstractAndroidModule {
    @Override
    protected void configure() {
        bind(SQLiteDatabase.class).toProvider(DatabaseHelper.class);
    }
}

public class NotepadApplication extends RoboApplication {
    @Override
    protected void addApplicationModules(List<Module> modules) {
        modules.add(new NotepadModule());
    }
}

Запускаем, пробуем — работает!

Метод NotepadModule.configure() можно расширить для связывания других типов, например подстановок конкретных имплементаций интерфейсов:
    protected void configure() {
        bind(SQLiteDatabase.class).toProvider(DatabaseHelper.class);
        bind(FilterDao.class).to(FilterDaoHttp.class);
        bind(ItemDao.class).annotatedWith(Http.class).to(ItemDaoHttp.class);
    }

Другими словами, доступны все виды связывания из Google Guice.

Стоит добавить пару слов и тестах. RoboGuice содержит несколько классов для облегчения и ускорения unit-тестирования. RoboUnitTestCase обеспечивает тестирования для классов, не имеющих зависимостей от экосистемы Андроида. RoboActivityUnitTestCase позволяет запускать unit-тесты для Activities. В будущем мне хотелось бы написать отдельную статью про тестирование под Андроидом, со сравнением различных подходов и библиотек. Тестирование под Андроидом ещё не так хорошо разобрано на Хабре, как сама разработка.

Окончательный диагноз


Итак, поставим окончательный диагноз о пользе инъекций:
+ Уменьшение кода, отвечающего за инициализацию объектов и управление жизненным циклом
+ Больше кода, отвечающего за бизнес-логику
+ Уменьшение связанности объектов
+ Минимум мест для конфигурации зависимостей и инициализации объектов
+ Удобство в использовании и подстановке системных объектов и ресурсов (Context, Apllication, SharedPreferences, Resources и прочее)
+ Как итог, ускорение разработки и уменьшение ошибок

— Дополнительная библиотека увеличивает размер приложения.
Спорный минус. Размер guice-2.0-no_aop.jar и roboguice-1.1.1.jar вместе около 545 Кб, что достаточно много для простых приложений, но вполне приемлемо для больших и сложных.
— Уменьшение производительности
Также спорный момент. Несомненно, что инъекции требуют некоторого времени. Однако временные затраты на это невелики и в основном приходятся на момент запуска приложения. Если не использовать инъекции в циклах и анимации, то пользователь не заметит изменения производительности. Тем более производительность смартфонов и DalvikVM постоянно растёт.

Мой личный диагноз: RoboGuice стоит использовать в своих проектах. Плюсы определённо перевешивают минусы.
Андрей Волков @Kipriz
карма
20,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (23)

  • +1
    Я бы 100% использовал эту библиотеку, но только не в простом приложении. В своем простом приложении мне пока и без DI нормально да и не сильно хочется раздувать размер приложения.
    Guice хорошая CDI-реализация и вот теперь она добралась и до Android'а.
    Статью прочитал на одном дыхании. Инмемориз однозначно.
    Напишите все таки по поводу тестирования android-приложений
    • 0
      Как раз разбираюсь с тестированием. Запускать тесты на телефоне или эмуляторе получается, а вот с использованием простых unit-тестов или сторонних библиотек для тестирования под Андроид пока не очень. Как только разберусь, обязательно напишу статью.
  • +4
    И был приятно удивлён простатой и изящностью подхода.

    Месье знает толк в извращениях!
    • +1
      Из разряда «оговорок по Фрейду» =) Поправил, спасибо.
  • 0
    Зачем нужно наследование от Robo-классов?
    • 0
      Robo-классы делают инъекции в методах onCreate(), setContentView() и прочих. Также они делают другие необходимые операции по инициализации инъекций.
      К тому же необходимо вызывать super.onCreate() метод из своего onCreate(). Иначе инъекции не будут проинициализированы.
      • +1
        Я всегда думал, что инъекции делают из класслоадеров — в таком случае ничего в коде кроме аннотаций менять не нужно.
        • 0
          Может быть. Но в случае RoboGuice используется другой подход. Может Dalvik VM не позволяет переопределять class loaders, может это сложнее в имплементации на порядок.
  • 0
    > и существенно облегчает жизнь разработчикам
    надо в первую очередь думать о пользователях, а не о том — «ух ты как круто и удобно я могу тут запрограммить с помощью 100500 костылейштук о которых все пишут в интернетах»
    • +4
      Несомненно, что о пользователях надо думать в первую очередь. Однако чем проще писать программы, тем больше возможностей и времени у разработчиков облегчить жизнь тем самым пользователям.
  • 0
    Возможно, конечно, я дико торможу, но разве нельзя проинициализировать поля класса во время их объявления?

    Это по сути то же самое, что засовывать весь начальный init в onCreate(), насколько я понимаю. Таким образом мусора будет почти столько же, как и при инъекциях.

    Извините, просто первый пример совершенно не произвел впечатление :-)
    • 0
      как бы матчасть…
      setContentView(R.layout.main);
      вот в этом вся фишка.В вашем варианте, вы будете пытаться получить по findbyview объект, которого нет еще в view родителя, хотя id конечно же у него есть.

      соотвественно вылетит NullPointerExeption

      developer.android.com/reference/android/app/Activity.html#setContentView(int)
    • 0
      А как ты их проинициализируешь? Пока не сделаешь setContentView Андроид просто не знает, где искать.
    • 0
      Согласен, всё-таки «дико торможу» :-)
    • 0
      А если не создавать макет из xml? Ну то есть с помощью инфлейтера сконструировать макет в коде.

      Тогда, наверно, можно проинициализировать представления заранее, но всё равно кода добавится…
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      IDE позволяет это сделать, компиляция проходит без эксцессов.
      • 0
        это проблемы IDE, когда в java не было generics, тоже можно было наворотить так, что компилятору пофиг…
      • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    А где анализы того, что происходит в реальности? Чего такого там на 500кб? Какой код он генерирует?

    Как показывает практика, 90% подобных автоматических штук выходит бяка. Причем в EE это вполне нормально — там все достаточно предсказуемо и тпх, а вот в мобилках… тут производительность и поведение критичны и непредсказуемы
    • 0
      Если под «бякой» вы имеете в виду особенности жизненного цикла мобильных аппов, то roboguice вполне о них знает и корректно обрабатывает.
    • 0
      Сам roboguice-1.1.1.jar занимает около 100 Кб и содержит «обертки» для activity-классов, некоторых других служебных классов (например Application) и имплементации инъекций специфичных для Андроида объектов (например SharedPreferences).
      Остальное занимает guice-2.0-no_aop.jar, который непосредственно и отвечает за возможности dependency injection.
      Никакой непредсказуемой бяки не генерируется, влияние на производительности минимально. Как для RoboGuice, так и Google Guice можно скачать исходные коды и посмотреть что и как работает. Всё открыто и доступно, никакой скрытой магии.
  • 0
    Джус вещь своеобразная. Использую, но только не на андроид. Моё приложение с джусом будет в 7 раз больше, что совсем не приемлемо для моих пользователей. По поводу уменьшения производительности, то её нет, кроме времени запуска, когда guice всё связывает и создаёт все объекты по цепочке! Время запуска приложения является одним из важных параметров, которые стремятся оптимизировать, в guice нужно использовать *Factory чтобы прервать цепочку загрузок в начале, что делает код уже не таким красивым. Вообщем, джус и андройд у меня в голове настолько не совместимы, что я даже не смог перейти в андройд-проект где используют эту связку.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.