- PVSM.RU - https://www.pvsm.ru -
В этой статье я бы хотел поговорить об одном из классических шаблонов проектирования в Android-разработке: фабричном методе (Fabric method). Изучать его мы будем на примере работы с Firebase Cloud Messaging (далее FCM). Цель — донести до начинающих разработчиков, пока не овладевших в полной мере всеми достоинствами ООП, важность применения приёмов объектно-ориентированного проектирования. [1]

FCM — один из готовых сервисов передачи сообщений (так называемых пушей), работающий по модели «издатель-подписчик». Если вы в очередной раз получаете нотификацию на свой девайс о новом сообщении (новости/скидке/новом товаре и многом другом), то велика вероятность, что эта функциональность реализована посредством FCM или его аналога. На данный момент FCM позиционируется Google как эталонное решение. Поэтому и статья написана из расчёта, что читатель либо уже знаком с этим сервисом, либо ему, скорее всего, выпадет возможность с ним познакомиться.
Написать обработку push-сообщений в приложении, применяя фабричный метод [2] — отличный повод раз и навсегда разобраться с этим шаблоном. Проектируя UI-объекты или объекты бизнес-логики, новичку простительно сделать ошибку: не предусмотреть расширение количества объектов и/или не заложить возможность легко изменять логику работы каждого из них. Но, как показывает опыт, обработка пушей зачастую усложняется и расширяется в течение всего периода разработки проекта. Представьте, если бы вам пришлось писать приложение ВКонтакте. Пользователь получает кучу различных нотификаций по пушу, которые выглядят по-разному и по нажатию открывают разные экраны. И если модуль, отвечающий за обработку пуш-уведомлений, изначально был спроектирован неправильно, то каждый новый пуш — это привет, новые if else, привет, регрессионное тестирование и вам, новые баги, тоже привет.
Представим стартап, идея которого — наладить коммуникацию между воспитателями детского сада и родителями детей, которые этот детский сад посещают. Единое приложение и для воспитателей, и для родителей не подходит, потому что через учительское приложение контент создаётся, а через родительское — потребляется. Учтём и третий вид пользователей сервиса — администрацию детского сада. Им мобильное приложение не нужно в силу того, что основная часть их рабочего дня проходит за рабочим столом, но им нужен удобный способ оповещать родителей и воспитателей о важных новостях.
Итого:
Проект в Android Studio будет иметь следующую структуру

Модуль core — общий для двух приложений. Он содержит модули parent и teacher — модули родительского и учительского приложений соответственно.
Задачи на этапе подключения push-уведомлений — показать нотификацию пользователю при изменении данных на сервере вне зависимости от того, открыто приложение у пользователя или нет.
Для учительского и родительского приложений приходят различные виды пушей. Поэтому у нотификаций могут быть разные иконки, а по нажатию на нотификацию открываются разные экраны.
Для учительского приложения:
Для родительского приложения:
После всех подготовительных работ по подключению и настройке работы FCM в Android-проекте обработка push-уведомлений сводится к реализации одного класса, наследника FirebaseMessagingService.
public class MyFirebaseMessagingService extends FirebaseMessagingService {
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
}
}
О подключении и настройке FCM хорошо написано здесь:
→ Официальная документация по подключению FCM в Android проект [3]
→ Репозиторий с примером на аккаунте Firebase [4]
Метод onMessageReceived() принимает объект класса RemoteMessage, который и содержит все, что отправил сервер: текст и заголовок сообщения, Map кастомных данных, время отправки push-уведомления и другие данные.
Есть одно важное условие. Если push был отправлен с текстом и заголовком для нотификации, то в момент, когда приложение свернуто или не запущено, метод onMessageReceived() не сработает. В таком случае библиотека firebase-messaging сама сконфигурирует нотификацию из полученных в push`е параметров и покажет её в статусбаре. На этот процесс разработчик не может повлиять. Но если передавать все необходимые данные (в том числе текст и заголовок для нотификации) через объект data, то все сообщения будут обрабатываться классом MyFirebaseMessagingService. Примеры кода ниже подразумевают именно такое использование FCM. Вся информация о событии передается в объекте data.
→ Подробнее о видах сообщений Firebase [5]
Итак, если бы мы ничего не знали про паттерны проектирования, то реализация поставленной задачи для учительского приложения выглядела бы примерно следующим образом:
public class TeacherFirebaseMessagingService extends FirebaseMessagingService {
private static final String KEY_PUSH_TYPE = "type";
private static final String KEY_PUSH_TITLE = "title";
private static final String KEY_PUSH_CONTENT = "content";
private static final String TYPE_NEW_CHILD = "add_child";
private static final String TYPE_BIRTHDAY = "birthday";
private static final String EMPTY_STRING = "";
private static final int DEFAULT_NOTIFICATION_ID = 15;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
Map<String, String> data = remoteMessage.getData();
if (data.containsKey(KEY_PUSH_TYPE)) {
NotificationCompat.Builder notificationBuilder;
String notificationTitle = null;
if (data.containsKey(KEY_PUSH_TITLE)) {
notificationTitle = data.get(KEY_PUSH_TITLE);
}
String notificationContent = null;
if (data.containsKey(KEY_PUSH_CONTENT)) {
notificationContent = data.get(KEY_PUSH_CONTENT);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
String pushType = data.get(KEY_PUSH_TYPE);
if (pushType.equals(TYPE_NEW_CHILD)) {
builder.setSmallIcon(R.drawable.ic_add_child)
.setContentTitle(notificationTitle != null ? notificationTitle : EMPTY_STRING)
.setContentText(notificationContent != null ? notificationContent : EMPTY_STRING);
} else if (pushType.equals(TYPE_BIRTHDAY)) {
// notificationBuilder = ....
// тут будут задаваться параметры нотификации о дне рождения
}
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(DEFAULT_NOTIFICATION_ID, builder.build());
}
}
Ключи для получения title и content из данных remoteMessage:
private static final String KEY_TITLE = "title";
private static final String KEY_CONTENT = "content";
Типы пушей для учительского приложения:
private static final String TYPE_NEW_CHILD = "add_child";
private static final String TYPE_BIRTHDAY = "birthday";
Конечно, для каждого типа нотификации можно сделать свой private метод который бы возвращал объект класса NotificationCompat.Builder. Но если вспомнить о приложении VK с его большим количеством разных нотификаций и разнообразии действий при нажатии на них, то становятся очевидны огрехи в таком дизайне класса:
При подобном подходе объект класса TeacherFirebaseMessagingReceiver за считанные часы разработки становится огромным неповоротным god object [6]`ом. И поддержка его кода рискует превратиться в сгусток боли ещё до первого релиза приложения. И самое интересное, что что-то подобное придется наворотить в родительском приложении.
Теперь о реализации этого функционала более элегантным способом с помощью паттерна «Фабричный метод».
Базовый класс, который находится в модуле core:
public class CoreFirebaseMessagingService extends FirebaseMessagingService {
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
}
}
Субклассы CoreFirebaseMessagingService будут зарегистрированы в манифестах двух модулей приложений.
Теперь спроектируем объект CoreNotification. Он будет содержать реализацию внешнего вида нотификации в статус баре в зависимости от того, какой тип пуша пришел.
public abstract class CoreNotification {
public static final String KEY_FROM_PUSH = "CoreNotification.FromNotification";
private static final String KEY_TITLE = "title";
private static final String KEY_CONTENT = "body";
protected static final String STRING_EMPTY = "";
protected RemoteMessage remoteMessage;
public CoreNotification(RemoteMessage remoteMessage) {
this.remoteMessage = remoteMessage;
}
protected String getTitleFromMessage() {
Map<String, String> data = remoteMessage.getData();
if (data.containsKey(KEY_TITLE)) {
return data.get(KEY_TITLE);
} else {
return STRING_EMPTY;
}
}
protected String getContentFromMessage() {
Map<String, String> data = remoteMessage.getData();
if (data.containsKey(KEY_CONTENT)) {
return data.get(KEY_CONTENT);
} else {
return STRING_EMPTY;
}
}
public String getTitle() {
return getTitleFromMessage();
}
public String getContent() {
return getContentFromMessage();
}
protected abstract PendingIntent configurePendingIntent(Context context);
protected abstract @DrawableRes int largeIcon();
protected abstract String getNotificationTag();
}
Объект принимает в конструктор и хранит в себе полученный RemoteMessage.
Все абстрактные методы будут переопределены для конкретных нотификаций. STRING_EMPTY может понадобиться в реализациях, поэтому делаем его protected.
Если следовать вышеупомянутой книге «Приёмы объектно-ориентированного проектирования. Паттерны проектирования» [1] или очень, на мой взгляд, доступной для понимания книге для Java-разработчиков Паттерны проектирования [7], то CoreNotification должен быть интерфейсом, а не классом. Такой подход более гибок. Но тогда бы нам пришлось писать код для получения title и content для каждой нотификации во всех реализациях этого интерфейса. Поэтому было принято решение избежать дублирования кода через абстрактный класс, который содержит методы getTitleFromMessage() и getContentFromMessage(). Ведь эти значения для каждого пуша извлекаются одинаково (поля title и content в RemoteMessage.getData() будет присутствовать всегда, так реализован бэкенд). На всякий случай эти методы оставили protected, если title и content для какой-нибудь нотификации необходимо будет получать другим способом.
Далее проектируем абстрактный класс CoreNotificationCreator. Объект этого класса будет создавать и отображать нотификации в статусбаре. Он и будет работать с наследниками класса CoreNotification.
public abstract class CoreNotificationCreator {
private static final String KEY_NOTIFICATION_TAG = "CoreNotificationCreator.TagKey";
private static final String DEFAULT_TAG = "CoreNotificationCreator.DefaultTag";
private static final String KEY_TYPE = "type";
private NotificationManager notificationManager;
public CoreNotificationCreator(Context context) {
notificationManager = ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
}
public void showNotification(Context context, RemoteMessage remoteMessage) {
String notificationType = getNotificationType(remoteMessage);
CoreNotification notification = factoryMethod(notificationType, remoteMessage);
if (notification != null) {
NotificationCompat.Builder builder = builderFromPushNotification(context, notification);
notify(builder);
}
}
private String getNotificationType(RemoteMessage remoteMessage) {
Map<String, String> data = remoteMessage.getData();
if (data.containsKey(KEY_TYPE)) {
return data.get(KEY_TYPE);
}
return "";
}
@Nullable
protected abstract CoreNotification factoryMethod(String messageType, RemoteMessage remoteMessage);
private final static int DEFAULT_NOTIFICATION_ID = 15;
private static final
@DrawableRes
int SMALL_ICON_RES_ID = R.drawable.ic_notification_small;
protected NotificationCompat.Builder builderFromPushNotification(Context context, CoreNotification notification) {
Bitmap largeIcon = BitmapFactory.decodeResource(context.getResources(), notification.largeIcon());
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(SMALL_ICON_RES_ID)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentTitle(notification.getTitle())
.setContentText(notification.getContent())
.setLargeIcon(largeIcon);
builder.getExtras().putString(KEY_NOTIFICATION_TAG, notification.getNotificationTag());
builder.setContentIntent(notification.configurePendingIntent(context));
return builder;
}
private void notify(@NonNull NotificationCompat.Builder builder) {
final String notificationTag = getNotificationTag(builder);
notificationManager.cancel(notificationTag, DEFAULT_NOTIFICATION_ID);
notificationManager.notify(notificationTag, DEFAULT_NOTIFICATION_ID, builder.build());
}
private String getNotificationTag(NotificationCompat.Builder builder) {
Bundle extras = builder.getExtras();
if (extras.containsKey(KEY_NOTIFICATION_TAG)) {
return extras.getString(KEY_NOTIFICATION_TAG);
} else {
return DEFAULT_TAG;
}
}
}
Метод showNotification() — единственный public метод. Его и будем вызывать при получении новых пушей для отображения нотификаций. Все остальные методы — внутренняя реализация создания и отображения нотификации.
Создание нотификаций в Android [8]
public void showNotification(Context context, RemoteMessage remoteMessage) {
String notificationType = getNotificationType(remoteMessage);
CoreNotification notification = factoryMethod(notificationType, remoteMessage);
if (notification != null) {
NotificationCompat.Builder builder = builderFromPushNotification(context, notification);
notify(builder);
}
}
В свою очередь в showNotification() определяется тип пуша, который содержится в данных remoteMessage. И далее тип пуша и объект remoteMessage передаётся фабричному методу, который создаст для нас нужный объект класса CoreNotification.
factoryMethod() @Nullable потому, что может прийти тип пуша, о котором приложение ничего не знает. В теории. Страховка.
Итак, реализация одного класса для двух приложений, который работает с пушами, готова. Дело остается за малым: реализовать для обоих приложений свой конкретный NotificationCreator.
Пример из учительского приложения
public class TeacherNotificationCreator extends CoreNotificationCreator {
public TeacherNotificationCreator(Context context) {
super(context);
}
@Nullable
@Override
protected CoreNotification factoryMethod(String messageType, RemoteMessage remoteMessage) {
switch (messageType) {
case NewChildNotification.TYPE:
return new NewChildNotification(remoteMessage);
case BirthdayNotification.TYPE:
return new BirthdayNotification(remoteMessage);
}
return null;
}
}
В фабричном методе по переменной messageType определяем, какой именно субкласс CoreNotification будет возвращён.
Например, для учительского приложения один из видов нотификаций мог бы быть реализован так:
class NewChildNotification extends CoreNotification {
static final String TYPE = "add_child";
private static final String KEY_CHILD_NAME = "child_name";
NewChildNotification(RemoteMessage remoteMessage) {
super(remoteMessage);
}
@Override
protected PendingIntent configurePendingIntent(Context context) {
Intent intent = new Intent(context, MainActivity.class)
.setPackage(context.getApplicationContext().getPackageName())
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(CoreNotification.KEY_FROM_PUSH, getAddChildInfo());
return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
protected int largeIcon() {
return R.drawable.ic_add_child;
}
@Override
protected String getNotificationTag() {
return getClass().getName() + getChildName();
}
private String getChildName() {
Map<String, String> data = remoteMessage.getData();
if (data.containsKey(KEY_CHILD_NAME)) {
return data.get(KEY_CHILD_NAME);
}
return STRING_EMPTY;
}
private String getAddChildInfo() {
return "New child " + getChildName() + " was added to your group";
}
}
Метод configurePendingIntent() выносится в реализацию конкретной нотификации для того, чтобы оставалась возможность открывать разные экраны с параметрами конкретного push-сообщения
Абсолютно аналогичный подход в родительском приложении:
public class ParentNotificationCreator extends CoreNotificationCreator {
public ParentNotificationCreator(Context context) {
super(context);
}
@Nullable
@Override
protected CoreNotification factoryMethod(String messageType, RemoteMessage remoteMessage) {
switch (messageType) {
case PickUpNotification.TYPE:
return new PickUpNotification(remoteMessage);
case GradeNotification.TYPE:
return new GradeNotification(remoteMessage);
default:
return null;
}
}
}
Аналогичным учительскому приложению создаются уникальные нотификации для родительского приложения со своей реализацией и своим уникальным типом.
В этом репозитории [9] вы найдёте исходный код проекта. Если возникнет желание его собрать, то необходимо будет создать свой проект Firebase. Процесс несложный и бесплатный, а Android Studio во многом его упрощает: Tools → Firebase → Cloud Messaging — удобная генерация необходимых зависимостей в Gradle-скриптах и настройка проекта firebase из студии. И ещё раз официальная пошаговая инструкция [3] для добавления FCM в Android-проект
При добавлении новых пуш уведомлений в конкретное приложение меняется реализация наследника абстрактного CoreNotificationCreator (в две строки) + создаётся новый класс, реализующий абстрактный CoreNotification. При этом логика формирования и отображения существующих нотификаций не меняется. Вероятность реализовать новый субкласс CoreNotification так, что он как-то повлияет на работу остальной рабочей функциональности, стремится к нулю. И каждый субкласс CoreNotification самостоятельно решает:
И самое ценное, на мой взгляд, то, что если заказчик захочет разработать новое приложение для, например, администратора детского сада, то реализация пушей для него никак не затронет работу всей системы уведомлений о push-сообщениях и сводится к наследованию и переопределению по примеру нескольких классов.
Более детальное понимание паттерна "Фабричный метод" и других классических паттернов проектирования даст вам литература, ставшая "настольной" для отдела Android-разработки в нашей компании [10]
Писать в конце статьи о полезности классических паттернов проектирования в работе программиста было бы крайне банально. Я бы даже сказал, пошло. Наверняка на хабре эту полезность доказали уже десятки раз. А вот пример применения в разработке под конкретную платформу, надеюсь, будет полезен Android-джуниорам в этом нелегком, но увлекательном деле.
Выбор конкретного шаблона проектирования для реализации конкретной задачи — тема для жаркой дискуссии. Поэтому добро пожаловать в комментарии. Всем пока!
Автор: alexeenkoff
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/259260
Ссылки в тексте:
[1] приёмов объектно-ориентированного проектирования.: https://ru.wikipedia.org/wiki/Design_Patterns
[2] фабричный метод: https://goo.gl/bBLqR4
[3] Официальная документация по подключению FCM в Android проект: https://firebase.google.com/docs/cloud-messaging/android/client
[4] Репозиторий с примером на аккаунте Firebase: https://github.com/firebase/quickstart-android/tree/master/messaging
[5] Подробнее о видах сообщений Firebase: https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages
[6] god object: https://en.wikipedia.org/wiki/God_object
[7] Паттерны проектирования: https://www.ozon.ru/context/detail/id/20216992/
[8] Создание нотификаций в Android: https://developer.android.com/guide/topics/ui/notifiers/notifications.html
[9] этом репозитории: https://github.com/alexeenkoff/android-fabric-method
[10] нашей компании: https://livetyping.com/ru/
[11] Источник: https://habrahabr.ru/post/332006/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.