Не так давно закончили разработку приложения, в котором пришлось обрабатывать одинаковые действия (actions) в различных местах приложения. Казалось бы, стандартная ситуация, и, как всегда, разработчики — ленивые, клиент — сделайте все вчера, у нашего клиента есть свой клиент, и ему — всё нужно позавчера. А значит, сделать все нужно просто, красиво и, главное — меньше лишних строк кода.
Особенностью проекта было то, что большая часть экранов содержала одинаковые или очень похожие сущности и позволяла пользователям выполнять над ними ряд одинаковых действий. О том, как мы решили данную задачу, и будет рассказано в этой статье.
Если вы знаете, что такое Instagram, то, добавив слова «закрытый, корпоративный» сможете четко представить суть нашего проекта. Если вкратце, то это закрытая социальная сеть с возможностью публиковать короткие заметки с фото или видео контентом, просматривать их, комментировать, «лайкать», добавлять авторов в список избранных и отдельно отслеживать их публикации, искать друзей, искать публикации и т.п. Каждая публикация может принадлежать разным регионам и быть в одной или нескольких категориях.
Итого имеем несколько экранов («скринов»): список публикаций для конкретной категории, детализация публикации, список комментариев, список отслеживаемых пользователей (following), их публикации, список тех, кто следит уже за тобой (followers), форму профиля пользователей с кучей рейтингов, счетчиков, аватаркой и т.п.
Почти на каждом скрине есть аватарки пользователей (либо собственно список пользователей, либо публикация с аватаркой и именем автора, либо комментарий конкретного пользователя). Также, на разных скринах есть кнопки Follow/Unfollow, Like/Dislike, теги категорий и прочие.
Кликнув по аватарке нужно открыть профиль пользователя. Кликнув по статье — открыть ее детализацию. Кликнув по иконке «Like» или «Follow» — ну вы поняли…
Подходя к делу привычным способом, дело решается не так и сложно. К примеру, по клику открываем профиль пользователя:
В случае «Like» или «Follow» уже посложнее, нужно делать запрос на сервер, ждать его ответа и, в зависимости от результата, менять представление кнопки в конкретном элементе списка. В принципе, тоже ничего сложного. Но, поскольку и «Like» и «Follow» могут быть во многих местах приложения, для облегчения повторного использования логично делегировать их обработку отдельным классам, что в итоге и было сделано. Такие обработчики действий назвали «Action» (FollowUserAction, LikeAction, OpenProfileAction, ...). Все action, обрабатываемые на конкретном скрине собираются и запускаются через некий менеджер ActionHandler. В итоге, открытие того-же экрана профиля пользователя будет выглядеть таким образом:
Хорошо, идем дальше. Чтобы еще уменьшить количество лишнего кода — подключаем поддержку Android Data Binding и в коде бизнес логики оставляем только ActionHandler. То, какое действие выполнять и по клику на какую кнопку пропишем в самом layout файле. Например, для экрана со списком публикаций, имеем:
item_post.xml
Теперь, если, например, на каком-то экране нужно будет заблокировать открытие профиля по клику, или добавить/убрать пункт меню, отображаемый по длинному нажатию (actionTypeLongClick="@{ActionType.MENU}), все что нужно сделать — добавить или удалить в одном месте соответствующую Action.
Использование Data Binding также позволяет из самой Action поменять модель (например, добавить «лайк») и сразу-же увидеть изменения на экране без каких-либо дополнительных коллбеков, вызывающих notifyDataSetChanged() для RecyclerView.
Вот примеры некоторых action:
Таким образом, получилось организовать очень гибкую и, надеюсь, довольно понятную логику обработки действий в приложении.
В итоге идея развивалась, прошла еще два других проекта, и, в конце-концов, воплотилась в небольшую библиотеку — action-handler.
Сейчас в ней есть заготовки для таких часто встречаемых action:
Библиотека с простенькими примерами — https://github.com/drstranges/ActionHandler
Особенностью проекта было то, что большая часть экранов содержала одинаковые или очень похожие сущности и позволяла пользователям выполнять над ними ряд одинаковых действий. О том, как мы решили данную задачу, и будет рассказано в этой статье.
Исходные данные
Если вы знаете, что такое 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
