
В крупных проектах, при реализации логики трекинга событий, часто встают перед проблемой загрязнения кода вызовами методов трекинга, неудобством явного связывания объектов с событиями и поддержкой этих событий при изменении моделей или 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}
На мой взгляд мы даже этим простым проектиком решили несколько проблем и возможно съэкономили какое то количество времени для рутиных действий.
Если потратить еще какое-то количество времени, то можно было неплохо оптимизировать логику аспекта, например, немного переделать хранение обьектов, чтобы не получать их каждый раз через рефлексию.
Если кто-то вдруг надумает взяться и довести до ума эту штуку то милости прошу сюда.
