Незадолго до того, как вы погрузитесь в чтение, обновленная версия демонстрационного проекта Firebase и соответствующая статья находятся в конце статьи, так что обязательно ознакомьтесь с ней :)

Несколько дней назад я занялся написанием приложения чата для студентов здесь, в COBE, чтобы у нас было простое приложение для общения в чате и задания вопросов, если мы когда-нибудь застрянем на проблеме (один раз, когда все будет готово). Вместо того, чтобы иметь полностью встроенный бэкэнд, на который мы будем отправлять и из которого мы будем получать сообщения, я решил использовать Firebase - простую в использовании базу данных в реальном времени, которая хранит свои данные в формате JSON. Несмотря на то, что Firebase предоставляет полную документацию по API и использованию, я обнаружил, что ей не хватает деталей при попытке применить ее к шаблону архитектуры, такому как MVP, поэтому я решил попытаться объяснить, как я понял реализацию в Android. Также я расширил шаблон MVP новым слоем, специально для Firebase - Interactors.

Создание базового шаблона Firebase

Я не буду вдаваться в подробности создания учетной записи на Firebase и «5-минутного быстрого старта», я просто перейду к реализации.

Сначала мы смотрим, какие каталоги нам нужны в нашем шаблоне Firebase, например - Firebase создает пользователей в отдельной базе данных, и при создании сохраняемая информация представляет собой электронную почту, пароль (который вы не можете напрямую наблюдать) и уникальный UID. (случайно сгенерированный ключ, который остается с пользователем на протяжении всей его жизни), поэтому, если бы мы хотели сохранить имя пользователя, мы не могли бы… Вот почему нам нужен каталог «Пользователи» в нашем шаблоне, который будет содержать имя пользователя и, возможно, аватар, поэтому мы можем хранить некоторую конкретную информацию.

У нас также может быть каталог curentUsers, который будет содержать всех пользователей, которые в настоящее время вошли в наше приложение Chat. Нам определенно нужен каталог Messages, в котором мы будем хранить наши сообщения.

Итак, наши три каталога: Users, currentUsers, Messages…
Ссылки на них выглядят так:
«https: // ‹your-firebase› / currentUsers /»
«https: // ‹Your-firebase› / Пользователи / »
« https: // ‹your-firebase› / Сообщения / »

Это ссылки на каталоги, которые мы используем, когда хотим добавить / получить данные, и в основном все, что нам понадобится для работы пользователя и системы обмена сообщениями.

Перейдем к разговору об Android. Если вы импортировали зависимость Firebase в Gradle, у вас должны быть доступны все клиентские функции Firebase ... В нашем приложении чата будет 4 экрана:
- Главный экран для выбора варианта входа (Вход или Регистрация) и отображения количества пользователи, вошедшие в систему в настоящее время
- Вход для фактической аутентификации пользователя
- Экран регистрации, на котором мы создаем новых пользователей
- Экран чата (который может отображать фрагмент чата или фрагмент ListOfUsers)

Главный экран

Здесь мы ищем, какую точку входа хочет пользователь (Регистрация или Вход), и отображаем количество текущих пользователей в TextView.

MainActivityPresenter:

public class MainActivityPresenterImpl implements MainPresenter {
    private final MainView mainView;
    private final MainInteractor interactor;

    public MainActivityPresenterImpl(MainView view) {
        this.mainView = view;
        interactor = new MainInteractor(this);
    }


    @Override
    public void receiveRequest() {
        interactor.receiveRequest();
    }

    @Override
    public String getNumberOfUsers(long numberOfUsers) {
        return "Online users: " + String.valueOf(numberOfUsers);
    }

    @Override
    public void sendNumberOfChildren(long number) {
        mainView.setNumberOfUsersTextView(getNumberOfUsers(number));
    }
}

ГлавныйИнтератор:

public class MainInteractor implements MInteractor {
    private final Firebase mainRef = new Firebase("https://<your-firebase>/currentUsers");
    private final MainPresenter presenter;

    public MainInteractor(MainPresenter pre) {
        this.presenter = pre;
    }

    @Override
    public void receiveRequest() {
        mainRef.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                presenter.sendNumberOfChildren(dataSnapshot.getChildrenCount());
            });
    }
}

Что здесь происходит? В интеракторе у нас есть ссылка Firebase, параметром конструктора которой является ссылка (каталог currentUsers), и мы добавляем прослушиватель к ссылке, который отправляет один запрос в каталог CurrentUsers Firebase и получает DataSnapshot - специальную функцию Firebase ... Снимок по своей сути представляет собой список всех объектов данных в указанном каталоге, поэтому, если мы выполним dataSnapshot.getChildrenCount (), мы просто получим количество объектов, находящихся в данный момент в каталоге, что равно количеству пользователей в сети! Мы отображаем его в TextView, и пользователь видит, сколько его сверстников в сети. Довольно простой, но мощный, поскольку мы используем этот принцип запросов данных во всех аспектах взаимодействия с нашей Firebase.

Экран регистрации

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

У нас есть несколько простых EditTexts, один для имени пользователя, один для электронной почты и один для пароля .. Сетка смайликов на выбор (в настоящее время одна строка, добавит больше) и индикатор выполнения для отображения анимация вращения во время аутентификации. Кнопка «Зарегистрировать» принимает значения, объединенные из фрагментов, и отправляет их в Presenter:

public class FirebaseUserRegisterPresenterImpl implements FirebaseUserRegisterPresenter {
    private final RegisterView registerView;
    private final RegisterInteractor interactor;

    public FirebaseUserRegisterPresenterImpl(RegisterView view) {
        this.registerView = view;
        this.interactor = new RegisterInteractor(this);
    }


    @Override
    public void receiveRegisterRequest(String username, String email, String password, String emoji) {
        interactor.receiveRegisterRequest(username, email, password, emoji);
        registerView.spinProgressBar();
    }

    @Override
    public void onFailure() {
        registerView.onFailure();
        registerView.stopProgressBar();
    }

    @Override
    public void onSuccess() {
        registerView.onSuccess();
        registerView.stopProgressBar();
    }
}

Интерактор:

public class RegisterInteractor implements RInteractor {
    private Firebase userRef = new Firebase("https://<your-firebase>/Users/");
    private final FirebaseUserRegisterPresenter presenter;

    public RegisterInteractor(FirebaseUserRegisterPresenter pre) {
        this.presenter = pre;
    }

    @Override
    public void receiveRegisterRequest(final String username, String email, String password, final String emoji) {
        userRef.createUser(email, password, new Firebase.ValueResultHandler<Map<String, Object>>() {
            @Override
            public void onSuccess(Map<String, Object> stringObjectMap) {
                String uid = stringObjectMap.get("uid").toString();
                userRef = new Firebase("https://<your-firebase>/Users/" + uid);
                userRef.setValue(createUser(username, emoji));
                presenter.onSuccess();
            }

            @Override
            public void onError(FirebaseError firebaseError) {
                presenter.onFailure();
            }
        });
    }

    @Override
    public Map<String, Object> createUser(String username, String emoji) {
        Map<String, Object> user = new HashMap<>();
        user.put("username", username);
        user.put("emoji", emoji);
        return user;
    }
}

Здесь у нас есть несколько новых функций:
- Методы .createUser (), .push () и .setValue ()
- UID пользователя

.CreateUser () - создает пользователей! В отдельной базе данных, поэтому, когда мы создаем пользователя, нам также необходимо создать его объект в каталоге / Users (чтобы найти его). Это делается путем «проталкивания».
Указанный .push () «проталкивает» глубже в каталог, создавая подкаталог со случайным сгенерированным ключом для его имени, но перед этим мы прикрепляем UID к ссылке. , чтобы мы могли сравнить каталоги с UID пользователей. UID - это случайно сгенерированный ключ, и, используя его в качестве имени подкаталога (и параметра в объекте User), мы можем позже найти, какое имя пользователя соответствует определенному UID, и получить имя пользователя после входа в систему или даже удалить дочерний элемент currentUsers (вывести пользователя из системы).
Метод .setValue () добавляет объект (или объекты) в каталог, поэтому мы можем просто хранить любые данные, которые захотим.

Экран входа в систему

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

Что происходит, когда пользователь нажимает «Войти»?

Эта часть сложна, мы знаем, что наши пользователи находятся в отдельной базе данных, поэтому, когда мы регистрируем пользователя, как мы узнаем, под каким именем пользователя он / она идет?

В этом вся цель каталога / Users, как упоминалось ранее. Также, назвав его после UID пользователя, мы можем просто искать каталог с совпадающим UID (если, например, мы хотим экстраполировать определенные фрагменты информации от конкретного пользователя). Кроме того, если мы назовем объекты по UID, мы можем ввести объект с указанным UID и удалить его в onDestroy () активности чата - чрезвычайно простой способ выхода пользователя из системы.

Презентатор входа в систему:

public class FirebaseLoginPresenterImpl implements FirebaseLoginPresenter {
    private final LoginView loginView;
    private final LoginInteractor interactor;


    public FirebaseLoginPresenterImpl(LoginView view) {
        this.loginView = view;
        interactor = new LoginInteractor(this);
    }

    @Override
    public void receiveUserLogin(String email, String password) {
        loginView.spinProgressBar();
        interactor.attemptToLogIn(email, password);

    }

    @Override
    public void onFailure() {
        loginView.stopProgressBar();
        loginView.onFailure();
    }

    @Override
    public void onSuccess(String user, String uid) {
        loginView.stopProgressBar();
        loginView.logTheUserIn(user, uid);
    }
}

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

Интерактор:

public class LoginInteractor implements LInteractor {
    private Firebase userRef = new Firebase("https://<your-firebase>/Users/");
    private final FirebaseLoginPresenter presenter;

    public LoginInteractor(FirebaseLoginPresenter pre) {
        this.presenter = pre;
    }

    @Override
    public void attemptToLogIn(String email, String password) {
        userRef.authWithPassword(email, password, new Firebase.AuthResultHandler() {
            @Override
            public void onAuthenticated(final AuthData authData) {
                userRef = new Firebase("https://<your-firebase>/Users/" + authData.getUid()); //retrieve the user data
                userRef.addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot dataSnapshot) {
                        User user = dataSnapshot.getValue(User.class);
                        Firebase loggedUser = new Firebase("https://<your-firebase>/currentUsers/" + authData.getUid()); //helps us log the user out later on
                        loggedUser.setValue(createUser(user.getUsername(), user.getEmoji()));
                        presenter.onSuccess(user.getUsername(), authData.getUid(), user.getEmoji());
                    }

                    @Override
                    public void onCancelled(FirebaseError firebaseError) {
                    }
                });
            }

            @Override
            public void onAuthenticationError(FirebaseError firebaseError) {
                presenter.onFailure();
            }
        });
    }

    @Override
    public Map<String, Object> createUser(String user, String emoji) {
        Map<String, Object> userToCreate = new HashMap<>();
        userToCreate.put("username", user);
        userToCreate.put("emoji", emoji);
        return userToCreate;
    }
}

Если аутентификация пользователя прошла успешно, мы получаем имя пользователя для указанного пользователя и отправляем его на экран чата, но перед этим мы добавляем пользователя в каталог / currentUsers, чтобы мы могли просто видеть, кто вошел в систему. Получены данные AuthData. по умолчанию и служит для отображения некоторых данных о пользователе, специфичных для Firebase (например, UID, специального ключа, сгенерированного при аутентификации ..)

Экран чата

ChatActivity использует 2 фрагмента, один для службы обмена сообщениями, а другой для отображения списка активных пользователей. Щелкнув значок меню списка один раз, мы заменим фрагмент обмена сообщениями фрагментом списка, а щелкнув его еще раз, мы вытолкнем BackStack (и вернемся назад!).

Проблема здесь в том, что мы получаем все наши данные из Firebase, а это означает, что мы не можем реализовать Firebase в наших представлениях, но адаптеры ListView / RecyclerView также являются компонентами Android View, так что же нам делать дальше?

И снова ответ - MVP (+ Interactors)! Хорошая архитектура отражается в компонентах, в которых она реализована, что означает, что мы можем писать наши адаптеры также в MVP, они являются компонентом View, у которого есть Presenter, который отправляет новые значения в элементы ListView (и запрашивает указанные значения у Interactor) . Поскольку значения генерируются Interactor, которое ссылается на Firebase, мы можем отделить Android - от Java - от Backend.

Адаптер:

public class CustomMessageRecyclerAdapter extends RecyclerView.Adapter<CustomMessageRecyclerAdapter.ViewHolder> implements MessageAdapterView {
    private final ArrayList<Message> mMessageList = new ArrayList<>();
    private final String user;
    private final MessagePresenterImpl presenter;

    public CustomMessageRecyclerAdapter(String username) {
        this.user = username;
        presenter = new MessagePresenterImpl(this);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.chat_message, parent, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Message current = mMessageList.get(position);
        if (current.getAuthor().equals(user)) {
            holder.mAuthorTextView.setText("You");
        } else {
            holder.mAuthorTextView.setText(current.getAuthor());
        }
        holder.mMessageTextView.setText(current.getMessage());
        holder.mEmojiTextView.setText(current.getEmoji());
    }

    @Override
    public int getItemCount() {
        return mMessageList.size();
    }

    @Override
    public void addItem(Message message) {
        mMessageList.add(message);
        notifyDataSetChanged();
    }

    @Override
    public void request() {
        presenter.requestMessages();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        private TextView mAuthorTextView;
        private TextView mMessageTextView;
        private TextView mEmojiTextView;

        public ViewHolder(View itemView) {
            super(itemView);
            mAuthorTextView = (TextView) itemView.findViewById(R.id.message_author);
            mMessageTextView = (TextView) itemView.findViewById(R.id.message_value);
            mEmojiTextView = (TextView) itemView.findViewById(R.id.message_emoji);
        }
    }
}

Это очень просто, у нас есть метод, который раздувает наш ViewHolder, тот, который заполняет указанный держатель, метод для запроса сообщений из Firebase и тот, который добавляет сообщение в ArrayList, если есть новое сообщение для отображения.

Ведущий:

public class MessagePresenterImpl implements MessagePresenter {
    private final MessageAdapterView adapterView;
    private final MessageInteractor interactor;

    public MessagePresenterImpl(MessageAdapterView view) {
        this.adapterView = view;
        this.interactor = new MessageInteractor(this);
    }

    @Override
    public void sendMessageToAdapter(Message message) {
        adapterView.addItem(message);
    }

    @Override
    public void requestMessages() {
        interactor.request();
    }
}

Интерактор:

public class MessageInteractor {
    private final MessagePresenter presenter;
    private final Firebase mMessagesRef = new Firebase("https://<your-firebase>/messages");
    private final Query mMessageQuery;


    public MessageInteractor(MessagePresenter pre) {
        this.presenter = pre;
        this.mMessageQuery = mMessagesRef.orderByValue().limitToLast(100);
    }

    public void request() {
        mMessageQuery.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                presenter.sendMessageToAdapter(dataSnapshot.getValue(Message.class));
            }
//some more auto-generated methods

Адаптеру нужно новое сообщение, он сообщает Presenter запрашивать сообщения, но это не задача Presenter, поэтому он говорит Interactor запрашивать их у Firebase, благодаря чему мы получаем чистую структуру и поток данных, полностью независимые, поэтому изменяя представление, нам не нужно менять все, мы просто настраиваем объекты POJO данных, докладчикам и взаимодействующим не нужно менять то, что они делают, запросы остаются прежними! Поэтому, если мы изменим объем данных, просто добавим больше полей в POJO, если мы хотим отображать его по-другому, просто изменим View (добавив больше виджетов).
Запрос просто означает запрос,. orderByValue () означает, что мы получаем там объекты (значения), .limitToLast (100) означает, что мы всегда получаем последние 100 сообщений. Хотя, если чат активен какое-то время, все сообщения (даже после 100) будут отображаться, пока фрагмент сообщения не будет уничтожен / перезапущен.

Также в onDestroy нашего ChatActivity мы отправляем UID в интерактор (через докладчика), чтобы удалить пользователя из currentUsers (выйти из системы).

public class ChatLoginInteractor implements CLoginInteractor {
    @Override
    public void logTheUserOut(String uid) {
        Firebase userRef = new Firebase("https://<your-firebase>/currentUsers/" + uid);
        userRef.removeValue(); //removes the Child from Firebase
    }
}

Как это работает, шаг за шагом.

Библиотека Firebase для Android очень хорошо построена, документация немного сложна для понимания, но основные принципы легко понять, если вы копаетесь и пытаетесь объединить вещи.

- Ссылка Firebase - это просто ссылка на каталог, в котором вы хотите внести изменения, запросить или просто добавить новые данные
- Слушатели предоставляют нам «Rx-подобные» функции, они постоянно наблюдают за добавлением новых дочерних элементов ( каждый объект в каталоге является дочерним), и мы можем работать с их данными
- DataSnapshot - это список текущих значений в одном каталоге
- AuthData похож на Bundle всех данных для определенного пользователя / request, UID, уникальный ключ…
- Firebase использует синтаксический анализ Джексона, поэтому вашим POJO нужны пустые конструкторы и сгенерированные геттеры / сеттеры
- Вам действительно не нужны специальные клиенты REST, так как DataSnapshot функция может выполнять весь синтаксический анализ данных за вас с помощью .getValue (POJO.class)
- В реальном времени… Все запросы и отправления в Firebase выполняются очень быстро, поскольку все данные отформатированы как объекты JSON
- Прежде чем использовать какие-либо ссылки на Firebase, вы должны вызвать Firebase.setAndroidContext (this) в каждом методе Activity onCreate ().

Заключение

Firebase - чрезвычайно мощный инструмент для простых баз данных Backend, его очень быстро и просто использовать в небольших проектах, но его можно использовать даже для немного более сложных приложений, таких как это приложение для чата.
Это кроссплатформенный, поэтому вы можете создавать Приложения Firebase для Android, iOS и JS с полной поддержкой (я полагаю, JS поддерживает Angular, React и Node) и используют один шаблон Firebase на всех трех основных платформах.

Мой проект не так детализирован, как мог бы, дизайн мог бы получить некоторую любовь, могло бы быть больше запросов и функций, но бизнес-логика работает, так что вы можете иметь полностью работающее небольшое приложение для частного чата! Благодарим за чтение. Следите за обновлениями, в которых я расскажу об использовании подхода MVP в различных проектах и ​​функциях Android. :)

Обновлять

Ознакомьтесь с моей новой статьей о Firebase:



Он содержит проект, представленный в этой статье, как автономный полностью обновленный код, а также новый проект со всеми реализованными новыми функциями Firebase (с реализованной архитектурой MVP, тестами и Dagger 2)!

Если нет, по крайней мере, проверьте этот проект на github:



Филип Бабич - разработчик Android в COBE и студент факультета информатики в FERIT, Осиек. Он большой поклонник Котлина и иногда проводит мини-мастерские и встречи Котлина в Осиеке. Ему нравится узнавать что-то новое, играть в DnD и писать о вещах, которые он любит больше всего. Когда он не занимается программированием, не пишет о кодировании, не учится программировать или не учит других программировать, он питает свою внутреннюю нервозность, играя и просматривая фантастические шоу.