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

в 8:01, , рубрики: action handling, android development, Разработка под android

Не так давно закончили разработку приложения, в котором пришлось обрабатывать одинаковые действия (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

Автор: d_romka

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js