Пожалуйста, опишите ошибку

Нашли баг? Помогите нам его исправить, заполнив эту форму

Оптимизация кода с помощью Butter Knife

Олег Шелякин
Android-разработчик

<

p>При разработке мобильных приложений каждый разработчик сталкивается с «boilerplate code», сильно надоедающим из-за присутствия в любом приложении под Android.

butter-knife2

А что же такое этот boilerplate code? Здесь все достаточно просто: в программировании boilerplate code (шаблонный код) — это часть кода, которая должна быть включена во многие места с небольшими отличиями или без них. Boilerplate — это повторение, то, чего необходимо избегать для хорошего стиля программирования. Рассмотрим пример:

EditText nameView = (EditText) findViewById(R.id.nameView);

А если View вам понадобится объявить в поле класса? Тогда количество строк кода увеличивается минимум в два раза.

private EditText nameView;

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main_old);
   nameView = (EditText) findViewById(R.id.nameView);
}

Тем не менее, этот код необходим. В итоге получаем раздувание Activity, код становиться запутанным, громоздким, нечитабельным и его написание требует времени, которое можно потратить на более важные вещи, например, бизнес логика приложения и валидация введенных данных.

Если верить википедии, существует три способа решения данной проблемы.

Один из них метапрограммирование — вид программирования, связанный с созданием программ, которые порождают другие программы как результат своей работы (в частности, на стадии компиляции их исходного кода), либо программ, которые меняют себя во время выполнения (самомодифицирующийся код). Если говорить по-простому, то это вид программирования, в котором вы пишете программу, генерирующую другую программу. Это достаточно обширная тема, которая не может быть описана одной статьей. Поэтому мы затронем лишь одну из библиотек, реализованную с помощью этого вида программирования.

Решение проблемы с помощью библиотеки Butter Knife

Итак, в мире Android разработки уже существуют замечательные инструменты, которые помогут нам, разработчикам, справиться с нудной писаниной этого boilerplate code. Один из этих инструментов, который я хотел бы представить, это Butter Knife — популярная библиотека для «инжектирования» (далее я буду использовать термин «инициализация») Views, разработанная Джейком Уортоном (Square Inc.). Данная библиотека использует аннотации, на основе которых генерируется тот самый boilerplate code. В спецификации написано, что это никак не влияет на производительность приложения, так как кодогенерация происходит во время компиляции.

Работать с библиотекой будем в среде разработки Android Studio.

Первое что нужно сделать, это добавить зависимость в ваш проект, используя gradle. Добавьте эту часть кода в build.gradle на уровень проекта (project-level).

buildscript {

   repositories {
       mavenCentral()
   }

   dependencies {
       classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
   }

}

Добавьте эту часть кода в build.gradle на уровень модуля приложения (app/gradle.build).

apply plugin: 'android-apt'

dependencies {

   compile 'com.jakewharton:butterknife:8.2.1'
   apt 'com.jakewharton:butterknife-compiler:8.2.1'

}

Также не забудьте нажать кнопку sync project.

Итак, что же все-таки умеет Butter Knife?

  1. Инициализирует наши views с помощью аннотации @BindView:
    @BindView(R.id.nameView) EditText nameView;
    @BindView(R.id.surnameView) EditText surnameView;
    
    Так же мы можем использовать метод:
    EditText nameView = ButterKnife.findById(this, R.id.nameView);
    
    Он упрощает код для инициализации views в Fragment, Activity, Dialog и т.д. Нам не нужно явно приводить к типу, так как данный метод делает это автоматически.
  2. Инициализирует ресурсы, которые находятся в папках drawable, values и т.д.
    @BindString(R.string.app_name) String appName;
    @BindColor(R.color.colorAccent) int colorAccent;
    @BindDimen(R.dimen.button_height) int buttonHeight;
    
  3. Мы можем аннотировать поля любого класса, который использует views (Activity, Fragment или ViewHolder), но для этого нам необходимо вызвать одну из реализаций статического перегруженного метода ButterKnife.bind(...), чтобы делегировать ему инициализацию наших views. Приведу список реализаций данного метода. Вы можете выбрать именно тот, который подойдет для вашей задачи:
    bind(@NonNull Activity target)
    bind(@NonNull View target)
    bind(@NonNull Dialog target)
    
    bind(@NonNull Object target, @NonNull Activity source)
    bind(@NonNull Object target, @NonNull View source) 
    bind(@NonNull Object target, @NonNull Dialog source)
    
    Например, в адаптере для RecyclerView. Так выглядит класс ViewHolder:
    class ViewHolder extends RecyclerView.ViewHolder {
    
       @BindView(R.id.textView) TextView textView;
       @BindView(R.id.deleteItemView) ImageView deleteItemView;
    
       public ViewHolder(View itemView) {
           super(itemView);
           ButterKnife.bind(this, itemView);
       }
    }
    
  4. C помощью аннотации @OnClick нам больше не нужно использовать анонимные классы для обработчиков событий:
    @OnClick (R.id.button)
    void onClick() {
       Log.d(TAG, "You have tapped me!!!");
    }
    
    Передавать аргументы в метод обработчика теперь необязательно, как видно из предыдущего примера. Но даже если вам необходимо передать туда ваше view, то вас никто не ограничивает. Для примера покажем, как сделать кнопку недоступной по клику по ней:
    @OnClick(R.id.button)
    void onClick(Button button) {
       button.setEnabled(false);
    }
    
  5. Также можем добавить несколько views для одного обработчика. Допустим, у нас есть какое-то количество полей (EditText), которые мы валидируем. Если они не прошли валидацию, то выводим ошибку в каждом поле. Чтобы ошибка удалялась по нажатию на каждом view, можно сделать что-нибудь такое:
    @OnTouch({R.id.nameView, R.id.phoneView, R.id.ageView})
    boolean onTouchField(EditText view){
       view.setError(null);
       return true;
    }
    

Вот весь список поддерживаемых обработчиков событий: OnCLick, OnLongClick, OnFocusChange, OnItemClick, OnItemLongClick, OnItemSelected, OnPageChanged, OnTextChanged, OnTouch, OnCheckedChanged, OnEditorAction. Нам всего лишь нужно аннотировать метод. Но как узнать, какие аргументы может принимать аннотируемый метод? Для этого достаточно посмотреть под капот. Пример аннотации @OnTouch:

@Target(METHOD)
@Retention(CLASS)
@ListenerClass(
   targetType = "android.view.View",
   setter = "setOnTouchListener",
   type = "android.view.View.OnTouchListener",
   method = @ListenerMethod(
       name = "onTouch",
       parameters = {
           "android.view.View",
           "android.view.MotionEvent"
       },
       returnType = "boolean",
       defaultReturn = "false"
   )
)

public @interface OnTouch {
 /** View IDs to which the method will be bound. */
 @IdRes int[] value() default { View.NO_ID };
}

В значении parameters можно увидеть, какие типы аргументов способен принимать аннотируемый метод.

Также, насколько мы знаем, у интерфейса TextWatcher есть три метода: beforeTextChanged, onTextChanged и afterTextChanged. Но аннотация то всего одна. Как быть? Все очень просто, и было придумано еще до нас. Опять смотрим под капот и видим:

@Target(METHOD)
@Retention(CLASS)
@ListenerClass(
   targetType = "android.widget.TextView",
   setter = "addTextChangedListener",
   remover = "removeTextChangedListener",
   type = "android.text.TextWatcher",
   callbacks = OnTextChanged.Callback.class
)
public @interface OnTextChanged {
 /** View IDs to which the method will be bound. */
 @IdRes int[] value() default { View.NO_ID };

 /** Listener callback to which the method will be bound. */
 Callback callback() default Callback.TEXT_CHANGED;

 /** {@link TextWatcher} callback methods. */
 enum Callback {
   /** {@link TextWatcher#onTextChanged(CharSequence, int, int, int)} */
   @ListenerMethod(
       name = "onTextChanged",
       parameters = {
           "java.lang.CharSequence",
           "int",
           "int",
           "int"
       }
   )
   TEXT_CHANGED,

   /** {@link TextWatcher#beforeTextChanged(CharSequence, int, int, int)} */
   @ListenerMethod(
       name = "beforeTextChanged",
       parameters = {
           "java.lang.CharSequence",
           "int",
           "int",
           "int"
       }
   )
   BEFORE_TEXT_CHANGED,

   /** {@link TextWatcher#afterTextChanged(android.text.Editable)} */
   @ListenerMethod(
       name = "afterTextChanged",
       parameters = "android.text.Editable"
   )
   AFTER_TEXT_CHANGED,
 }
}

Есть три вида callbacks, то есть те самые три метода стандартного интерфейса TextWatcher.

Ну и собственно пример:

@OnTextChanged(value = {R.id.nameView, R.id.phoneView, R.id.ageView},
       callback = OnTextChanged.Callback.BEFORE_TEXT_CHANGED)
       void onBeforeTextChanged (CharSequence sequence){
       textView.setText(sequence.toString());
       }

Немного отвлекусь от темы. Надеюсь, вы заметили, что аннотация, тип переменной и ее имя находятся в одной строке. Возможно, кого-нибудь это встревожило, потому как если нажать прекрасное сочетание трех клавиш (Ctl+Alt+L — Windows; Alt+Cmd+L — Mac OS) и выполнить форматирование кода, то произойдет что-то очень нехорошее:

@BindView(R.id.nameView)
EditText nameView;
@BindView(R.id.surnameView)
EditText surnameView;
@BindView(R.id.ageView)
EditText ageView;

Согласитесь, жутко смотрится, даже использовать аннотации перехотелось, ведь количество строк кода увеличилось. Но ведь это лишь стандартные настройки вашей IDE, которые можно с легкостью исправить в несколько кликов. Как это сделать, если у вас Android Studio или Intelij Idea:

 

Preferences(Settings for Windows) -> Editor -> Code Style -> Wrapping and Braces -> напротив Fields Annotation ставим Do not wrap -> apply.

 

Теперь, если вы нажмете это сочетание клавиш, то такого больше не повторится. Но ведь все равно возникает вопрос: если настройки для code style были по умолчанию, то зачем их менять, ведь мы можем нарушить code style rules? Но это не совсем так, а вернее это совсем не так. Недавно вышла отличная статья об code style rules для Android разработчиков. Как раз в ней можно найти одно из правил:

  • Когда вы используете аннотации в поле класса, убедитесь в том, что они находятся на одной линии, если позволяет пространство.

Вернемся к основной части статьи.

Имея в своем арсенале приведенные выше инструменты Butter Knife можно уменьшить количество строк кода в вашем приложении и решить многие задачи. В частности, задачи связанные с группой views, из-за которых, как правило, код сильно разрастается.

Например, перед вами стоит задача запрещать/разрешать использование группы views по клику на button или toggle button. Конечно, для решения такой задачи, если вы недостаточно опытный разработчик, можно поместить данную группу views в отдельный layout, что приводит к сильной вложенности в вашей разметке. Это в свою очередь сильно сказывается на скорости отрисовки фреймов на экране.

Ну а если вы поопытнее, то могли бы сделать вот так:

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

   EditText nameView = (EditText) findViewById(R.id.nameView);
   EditText surnameView = (EditText) findViewById(R.id.surnameView);
   EditText ageView = (EditText) findViewById(R.id.ageView);
   EditText phoneView = (EditText) findViewById(R.id.phoneView);
   Button enableFieldsButton = (Button) findViewById(R.id.enableFieldsButton);
   Button disableFieldsButton = (Button) findViewById(R.id.disableFieldsButton);

   final EditText [] fieldsEditText = new EditText[]{nameView, surnameView, ageView, phoneView};

   enableFieldsButton.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
           setEnableFields(fieldsEditText, false);
       }
   });

   disableFieldsButton.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
           setEnableFields(fieldsEditText, false);
       }
   });
}

private void setEnableFields (EditText [] fieldsEditText, boolean enable){
   for (EditText editText : fieldsEditText) {
       editText.setEnabled(enable);
   }
}

Или использовать Butter Knife. Как раз таки для изящного решения подобных задач предусмотрены внутренние интерфейсы Action и Setter.

  • ButterKnife.Action — можно использовать для того, чтобы выполнить какое-нибудь действие с группой Views;
  • ButterKnife.Setter<T, V> — можно использовать для того, чтобы задать состояние атрибутов группы Views;

Нам всего лишь необходимо с помощью аннотации @BindViews создать List ваших views и использовать статический метод apply (...) у класса ButterKnife. Вот как бы выглядел код с использованием ButterKnife.Setter:

@BindViews({R.id.nameView, R.id.surnameView, R.id.ageView, R.id.phoneView})
List fields;

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_butter_knife);
   ButterKnife.bind(this);
}

private ButterKnife.Setter<EditText, Boolean> ENABLED = new ButterKnife.Setter<EditText, Boolean>() {
   @Override
   public void set(@NonNull EditText view, Boolean value, int index) {
       view.setEnabled(value);
   }
};

@OnClick(R.id.enableFieldsButton)
void enableFields (){
   ButterKnife.apply(fields, ENABLED, true);
}

@OnClick(R.id.disableFieldsButton)
void disableFields (){
   ButterKnife.apply(fields, ENABLED, false);
}

Если посчитать по строкам, то пример с Butter Knife занимает 25 строчек, а предыдущий 35 (импорты, название пакета, название класса не учитывались), что не может не радовать, да и насколько стало приятнее читать код. Согласны? Так же можно задать property для группы view:

ButterKnife.apply(fields, View.ALPHA, 0.5F);

Согласитесь мощная штука, тут все зависит от вас, и если правильно применять эти инструменты, то вы сможете решить большое количество обыденных задач.

Стоит обратить внимание на то, что в спецификации для данной библиотеки указывается о необходимости вызывать метод unbind в методе жизненного цикла фрагмента OnDestroyView, так как жизненный цикл фрагмента отличается от жизненного цикла в Activity. Честно говоря, это не является правилом, вы можете вызывать этот метод, а можете и не вызывать. На гитхабе в issues данной библиотеки я нашел ответ на вопрос:

  • Some guy: «It’s not clear if ButterKnife.unbind is required for fragments»
  • Jake Wharton: «It is not required for ButterKnife. The code does not care whether or not you do this. As to fragments, I’m not sure if it’s strictly required or not as I don’t use them and never cared to look. The documentation certainly seems to indicate that it’s something you should do.»

Так что, решать Вам!

Надеюсь, вы заметили, что во всех примерах область видимости наших views минимум package-private. На самом деле, аннотируемые поля не могут быть private или static. И на то есть несколько причин. Посмотреть те самые генерированные классы можно по данному пути:

 

app -> build -> generated -> source -> apt -> debug -> имя_вашего_пакета

 

Здесь можно увидеть, как проходит инициализация и приведение к типу (кастование) ваших views. В моем случае выглядит это вот так:

public ButterKnifeActivity_ViewBinding(final T target, final Finder finder, Object source) {
 this.target = target;

View view;
view = finder.findRequiredView(source, R.id.nameView, "field 'nameView', method 'onBeforeTextChanged', and method 'onTouch'");
target.nameView = finder.castView(view, R.id.nameView, "field 'nameView'", EditText.class);
view2131492945 = view;

target.surnameView = finder.findRequiredViewAsType(source, R.id.surnameView, "field 'surnameView'", EditText.class);
view = finder.findRequiredView(source, R.id.ageView, "field 'ageView', method 'onBeforeTextChanged', and method 'onTouch'");
target.ageView = finder.castView(view, R.id.ageView, "field 'ageView'", EditText.class);
view2131492947 = view;
…
}

Думаю, теперь понятно, почему аннотируемое поле не может быть private, у генерированного класса не было бы доступа к полям target класса.

Предвидя возможные вопросы о нарушении одного из важнейших принципов ООП, а именно — инкапсуляции, отвечу: конечно нарушает. Но сильно ли это может повлиять на ваше приложение? Ведь никто в здравом уме не будет напрямую обращаться к полю класса, а именно views, и менять его состояние. Конечно, могут быть разные ситуации, но это очень плохая практика — напрямую обращаться к полю класса. Для этого есть геттеры сеттеры, в которых, например, можно сделать проверку на null экземпляра view. Если данное view не проинициализировано, тогда мы получим NullPointerException. А еще лучше использовать интерфейс, если вы хотите сделать что-нибудь с вашей Activity через другой класс для меньшей связности объектов. Но это еще одна огромная тема, о которой можно очень долго и много разговаривать.

Почему поле не может быть static??? Думаю, что ответ скрыт так же в предыдущем абзаце. Ведь тогда мы сможем обращаться к аннотированному полю без инициализации содержащего класса, то есть нашего Activity. И все поля будут null, то есть, по сути, в этом нет смысла.

Хотелось бы упомянуть о том, что существуют и другие подобные библиотеки. Одна из них — AndroidAnnotations library, которая содержит в себе практически все возможности, которые присутствуют в Butter Knife, но дополнительно к этому еще другие достаточно мощные инструменты, которые позволяют справиться с многими задачами, такими как реализация многопоточности, хранение состояния вашего Activity и даже свое собственное REST API. Конечно, может показаться, что лучше использовать AndroidAnnotations library. Но везде есть свои «за» и «против». Нужно понимать одну важную вещь — перегружать свой проект сторонними библиотеками стоит лишь в том случае, если вы полноценно используете все инструменты той или иной библиотеки. Если вам достаточно инициализации views, обработчиков событий, то смело используйте Butter Knife.

На этом все, статья получилась достаточно длинной, но я старался, чтобы все было максимально понятно, а потому писал простым языком. Надеюсь, объяснил кое-какие моменты работы с данной библиотекой, и вы теперь будете использовать ее в своих приложениях максимально эффективно.

Читать и комментировать

Краснодар

Коммунаров, 268,
3 эт, офисы 705, 707

+7 (861) 200 27 34

Хьюстон

3523 Brinton trails Ln Katy

+1 833 933 0204

Москва

+7 (495) 145-01-05