В крупных проектах, при реализации логики трекинга событий, часто встают перед проблемой загрязнения кода вызовами методов трекинга, неудобством явного связывания объектов с событиями и поддержкой этих событий при изменении моделей или ui поведения.
Из-за вышеописанных причин, мне пришло в голову написать свой решение, которое конечно же, не уйдет дальше моего git репозитория и этой статьи.
Кто не боится рефлексии и медленного кода — прошу под кат.
Может не нужно?
В продуктовых приложениях от того, сколько раз тапнут на эту кнопку или как низко прокрутят список зависит развитие приложения как продукта, его внешний интерфейс и функциональность.
Из за этого, код начинают разрезать вызовы методов для трекинга событий, которые разбросаны по всему представлению и еще учитывают состояние определенных объектов.
Я хочу сделать менее болезненным добавление новых событий и решить несколько проблем,
Я хочу добиться следующего:
1) Минимальное количество кода для нового события;
2) Минимально количество кода в представлении;
3) Удобная система привязки обьектов к событиям.
Решение строится на аннотациях, рефлексии и аспектах.
Для реализации аспектной части нашего приложения я буду использовать AspectJ. Он является аспектно-ориентированным расширением для языка Java. На данный момент это, наверное, самый популярный AOP движок.
Кстати этот движок был разработан теми самыми людьми, которые и предложили парадигму аспектов.
Как это работает
Чтобы перехватывать вызов нужных нам методов создаем класс помеченный как @Aspect.
Делаее создаем точку соединения с нашими методами и создаем метод помеченный @Around который будет выполняться на точке соединения. AspectJ функционально богат и поддерживает большое количество вариантов точек срезов и советов, но сейчас не об этом.
@Aspect
public class ViewEventsInjector {
private static final String POINTCUT_METHOD = "execution(@com.makarov.ui.tracker.library.annotations.ViewEvent * *(..))";
@Pointcut(POINTCUT_METHOD)
public void methodAnnotatedWithViewEvent() {
}
@Around("methodAnnotatedWithViewEvent()")
public Object joinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();
Object object = joinPoint.getThis();
Object[] arg = joinPoint.getArgs();
/* зная метод, входные параметры и объект класса чей метод вызывали
мы можем получить всю нужную нам информацию */
Object result = joinPoint.proceed();
return result;
}
}
Реализация
Аннотация для наблюдаемых view
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface LoggerView {
String value();
}
Параметр аннотации — имя view элемента для более удобного чтения событий/логов.
В итоге, после инициализации, у нас есть Map в котором лежат id view элементов, отслеживаемых нами.
Перехват событий полностью ложится на плечи аспектов.
Мы будем ориентироваться на аннотации, которыми у нас помечены методы view событий.
Логика такая:
1) Перехватываем вызов метода;
2) Находим его обработчик, который мы добавили в map с всеми возможными обработчиками методов;
3) Находим по параметрам аннотации все объекты, которые нужно отследить;
4) Создаем обьекта Event из наших полученных данных;
4) Сохраняем событие.
Аннотация для методов, на которые будут повешаны наши события:
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface ViewEvent {
String[] value();
}
Чтобы унифицировать модели, которые мы хотим привязывать к нашим событиям, вводим интерфейс, который должна реализовывать модель:
public interface LoggingModel {
Map<String, String> getModelLogState();
}
Пример реализации интерфейса:
public class Artist implements LoggingModel {
private final String mId;
private final String mName;
public Artist(String id, String name){
mId = id;
mName = name;
}
/* ... */
@Override
public Map<String, String> getModelLogState() {
Map<String, String> logMap = new HashMap<>();
logMap.put("artistId", mId);
logMap.put("artistName", mName);
return logMap;
}
}
Собираем все это вместе
Ну и наконец собираем все это и в несколько аннотаций у нас начинают трекаться нужные нам события.
public class MainActivity extends AppCompatActivity implements View.OnClickListener, TextWatcher{
public static final String TAG = MainActivity.class.getSimpleName();
@LoggerView("first button")
public Button button;
public Button button2;
@LoggerView("test editText")
public EditText editText;
public Artist artist = new Artist("123", "qwe");
public Track track = new Track("ABS");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* инициализация view элементов */
ViewEventsInjector.init();
ViewEventsInjector.inject(this);
}
@Override
@AttachState({"artist","track"})
@ViewEvent(ViewEventsTracker.CLICK)
public void onClick(View v) {
Log.d(TAG, "method onClick - " + v.getId());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
@AttachState({"artist"})
@ViewEvent(ViewEventsTracker.AFTER_TEXT_CHANGED)
public void afterTextChanged(Editable s) {
Log.d(TAG, "afterTextChanged");
}
}
Запускаем проект, пробуем тапнуть по кнопке, затем ввести что-нибудь в текстовое поле.
И видим наши долгожданные логи, без единой строчки логики в представлении.
07-13 13:52:16.406 D/SimpleRepository? Event{nameView='fist button', nameEvent='onClick', mModelList=[Artist@52a30ec8, Track@52a31040], methodParameters = null, mDate = Mon Jul 13 13:52:16 EDT 2015}
07-13 13:52:24.254 D/SimpleRepository? Event{nameView='textView', nameEvent='afterTextChanged', mModelList=[Artist@52a30ec8], methodParameters= {text = hello}, mDate=Mon Jul 13 13:52:24 EDT 2015}
На мой взгляд мы даже этим простым проектиком решили несколько проблем и возможно съэкономили какое то количество времени для рутиных действий.
Если потратить еще какое-то количество времени, то можно было неплохо оптимизировать логику аспекта, например, немного переделать хранение обьектов, чтобы не получать их каждый раз через рефлексию.
Если кто-то вдруг надумает взяться и довести до ума эту штуку то милости прошу сюда.
Комментарии (3)
metrolog_ma Автор
14.07.2015 22:45+1Картинка к статье намекает, что никаких бенчмарков к решению не прилагается:)
andreich
23.07.2015 09:45+1Идея интересная, возможно ее стоит развить. Ускорить можно кодогенерацией.
gurinderu
А почему overhead этого решения не описали? )