Android и Data Binding: обработка действий

Не так давно закончили разработку приложения, в котором пришлось обрабатывать одинаковые действия (actions) в различных местах приложения. Казалось бы, стандартная ситуация, и, как всегда, разработчики — ленивые, клиент — сделайте все вчера, у нашего клиента есть свой клиент, и ему — всё нужно позавчера. А значит, сделать все нужно просто, красиво и, главное — меньше лишних строк кода.

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

Исходные данные


Если вы знаете, что такое Instagram, то, добавив слова «закрытый, корпоративный» сможете четко представить суть нашего проекта. Если вкратце, то это закрытая социальная сеть с возможностью публиковать короткие заметки с фото или видео контентом, просматривать их, комментировать, «лайкать», добавлять авторов в список избранных и отдельно отслеживать их публикации, искать друзей, искать публикации и т.п. Каждая публикация может принадлежать разным регионам и быть в одной или нескольких категориях.

Итого имеем несколько экранов («скринов»): список публикаций для конкретной категории, детализация публикации, список комментариев, список отслеживаемых пользователей (following), их публикации, список тех, кто следит уже за тобой (followers), форму профиля пользователей с кучей рейтингов, счетчиков, аватаркой и т.п.

Почти на каждом скрине есть аватарки пользователей (либо собственно список пользователей, либо публикация с аватаркой и именем автора, либо комментарий конкретного пользователя). Также, на разных скринах есть кнопки Follow/Unfollow, Like/Dislike, теги категорий и прочие.

Кликнув по аватарке нужно открыть профиль пользователя. Кликнув по статье — открыть ее детализацию. Кликнув по иконке «Like» или «Follow» — ну вы поняли…

Применяемый подход


Подходя к делу привычным способом, дело решается не так и сложно. К примеру, по клику открываем профиль пользователя:

    findViewById(R.id.some_id).setOnClickListener((v) -> openUserProfile());

    void openUserProfile(){
        Intent = new Intent(this, ProfileActivity.class);
        intent.putExtra(USER_ID, userId);
        startActivity();
    }

В случае «Like» или «Follow» уже посложнее, нужно делать запрос на сервер, ждать его ответа и, в зависимости от результата, менять представление кнопки в конкретном элементе списка. В принципе, тоже ничего сложного. Но, поскольку и «Like» и «Follow» могут быть во многих местах приложения, для облегчения повторного использования логично делегировать их обработку отдельным классам, что в итоге и было сделано. Такие обработчики действий назвали «Action» (FollowUserAction, LikeAction, OpenProfileAction, ...). Все action, обрабатываемые на конкретном скрине собираются и запускаются через некий менеджер ActionHandler. В итоге, открытие того-же экрана профиля пользователя будет выглядеть таким образом:

    mActionHandler = new ActionHandler.Builder()
            .addAction(ActionType.PROFILE, new OpenProfileAction())
            .build();

    findViewById(R.id.some_id).setOnClickListener((v) -> openUserProfile());
    ...

    void openUserProfile(){
        mActionHandler.fireAction(ActionType.PROFILE, user);
    }

Хорошо, идем дальше. Чтобы еще уменьшить количество лишнего кода — подключаем поддержку Android Data Binding и в коде бизнес логики оставляем только ActionHandler. То, какое действие выполнять и по клику на какую кнопку пропишем в самом layout файле. Например, для экрана со списком публикаций, имеем:

    mBinding = DataBindingUtil.inflate(..., R.layout.item_post);
    mBinding.setActionHandler(getActionHandler());
    mBinding.setPost(getPost());

    void initActionHandler() {
        mActionHandler = new ActionHandler.Builder()
                .addAction(ActionType.PROFILE, new OpenProfileAction())
                .addAction(ActionType.COMMENTS, new OpenCommentsAction())
                .addAction(ActionType.POST_DETAILS, new OpenPostDetailsAction())
                .addAction(ActionType.FOLLOW, new FollowUserAction())
                .addAction(ActionType.LIKE, new LikeAction())
                .addAction(ActionType.MENU, new CompositeAction((TitleProvider)(post) -> post.getTitle(),
                        new ActionItem(ActionType.SOME_ACTION_1, new SomeMenuAction(), "Title 1"),
                        new ActionItem(ActionType.SOME_ACTION_2, new SomeMenuAction(), "Title 2"),
                        new ActionItem(ActionType.SOME_ACTION_3, new SomeMenuAction(), "Title 3"),
                .build();
    }


item_post.xml
<layout>
    <data>
        <variable name="actionHandler" type="com.example.handler.ActionHandler" />
        <variable name="post" type="com.example.model.Post" />
    </data>

    <FrameLayout>
        
        <ImageView
            android:id="@+id/avatar"
            ...
            app:actionHandler="@{actionHandler}"
            app:actionType="@{ActionType.PROFILE}"
            app:model="@{post}" />

        <FrameLayout
            android:id="@+id/post_container"
            ...
            app:actionHandler="@{actionHandler}"
            app:actionType="@{ActionType.POST_DETAILS}"
            app:actionTypeLongClick="@{ActionType.MENU}"
            app:model="@{post}">
            ...
        </FrameLayout>

        <TextView
            android:id="@+id/comments"
            ...
            app:actionHandler="@{actionHandler}"
            app:actionType="@{ActionType.COMMENTS}"
            app:model="@{post}" />

        <ImageView
            android:id="@+id/like"
            ...
            app:actionHandler="@{actionHandler}"
            app:actionType="@{ActionType.LIKE}"
            app:model="@{post}" />
            ...
    </FrameLayout>
</layout>

Теперь, если, например, на каком-то экране нужно будет заблокировать открытие профиля по клику, или добавить/убрать пункт меню, отображаемый по длинному нажатию (actionTypeLongClick="@{ActionType.MENU}), все что нужно сделать — добавить или удалить в одном месте соответствующую Action.

Использование Data Binding также позволяет из самой Action поменять модель (например, добавить «лайк») и сразу-же увидеть изменения на экране без каких-либо дополнительных коллбеков, вызывающих notifyDataSetChanged() для RecyclerView.

Вот примеры некоторых action:

    public class OpenProfileAction extends IntentAction<IUserHolder> {
        
        @Override
        public boolean isModelAccepted(Object model) {
            return model instanceof IUserHolder;
        }

        @Nullable @Override
        public Intent getIntent(@Nullable View view, Context context, String actionType, IUserHolder model) {
            return ProfileActivity.getIntent(context, model);
        }

        @Override
        protected ActivityOptionsCompat prepareTransition(Context context, View view, Intent intent) {
            // Prepare shared element transition
            Activity activity = getActivityFromContext(context);
            if (activity == null) return null;
            return ActivityOptionsCompat
                    .makeSceneTransitionAnimation(activity, view, ProfileActivity.TRANSITION_NAME);
        }
    }

    public class LikeAction extends RequestAction<ModelResponse<Like>, Post> {

        @Override
        public boolean isModelAccepted(Object model) {
            return model instanceof Post;
        }

        @Override
        protected Observable<ModelResponse<Like>> getRequest(Context context, RestApiClient apiClient, Post model) {
            return apiClient.setLike(model.postId, !model.isLiked);
        }

        protected void onSuccess(Context context, View view, String actionType, Post oldModel, ModelResponse<Like> response) {
            oldModel.setLiked(response.getModel().isLiked); // automatically rebind icon for "like" button
        }
    }


Итоги


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

В итоге идея развивалась, прошла еще два других проекта, и, в конце-концов, воплотилась в небольшую библиотеку — action-handler.

Сейчас в ней есть заготовки для таких часто встречаемых action:
  • .IntentAction — для запуска Activity, старта Service, или отправки Broadcast;
  • .DialogAction — для любых действий, требующих сперва показать диалог для подтверждения;
  • .RequestAction — для выполнения запросов в интернет;
  • .CompositeAction — Составная, может содержать другие action и отображать их в виде меню/списка.


Ссылки


Библиотека с простенькими примерами — https://github.com/drstranges/ActionHandler
  • +10
  • 8.2k
  • 4
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 4

    0
    А что юзер видит в промежуточном состоянии пока идёт запрос, скажем для действия Like?
      0
      Может быть все что угодно, к примеру — иконка лайка меняется на иконку прогресса и потом в зависимости от ответа сервера а лайк или дизлайк…
      0
      Предлагаю по вкусу ещё упрощение: сделать ActionHandler синглтоном. Тогда его вообще не нужно будет прописывать в разметке: BindingAdapter'у не потребуется передавать его как параметр.
      И подписаться на него можно будет, например с фильтром по айдишникам кнопок (по тэгам или по моделям, если речь идёт о списках).
        0
        Это так, но, как по мне, в этом случае становиться сложнее переиспользовать разметку (layout) элементов списка. Есть, к примеру, разметка для элемента типа «User», в разных местах она может выглядеть полностью идентичной, но вот набор действия будет разный.

        Еще возможный вариант это передавать ActionHandler как DataBindingComponent.

      Only users with full accounts can post comments. Log in, please.