Pull to refresh

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

Reading time 8 min
Views 12K
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 стоит использовать в своих проектах. Плюсы определённо перевешивают минусы.
Tags:
Hubs:
+31
Comments 23
Comments Comments 23

Articles