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 Вами<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
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.