Новый сервис Firebase от Google способен покрыть большинство потребностей мобильного разработчика и решить проблемы, которых у тебя еще нет. Сервис бесплатный при соблюдении лимитов, а чтобы за них выйти, нужно очень постараться. В этой статье я начну перечислять возможности сервиса и поделюсь своим опытом работы с ним.

Сказать, что по нашей улице прошел инкассатор, — это ничего не сказать. Займемся изучением инструмента вплотную!

 

Farewell VK, hello Firebase

В своих предыдущих статьях я рассматривал VK API как бесплатный бэкенд мобильных приложений. У него есть ряд преимуществ. Хостинг безлимитный, типов контента много, управлять им может даже школьник, достаточно объяснить ему структуру наполнения приложения...

Но один недостаток фатален — негибкость сервера. Запись на стене нельзя редактировать через какое-то время, и не получится отправить в приложение сообщение об ее обновлении. Нужно каждый раз выкачивать все объекты со стены и отслеживать все изменения, строго перебирая записи. Серьезные проекты так не работают, и «хватит это терпеть!» ©.

На наше счастье, Google выкупила компанию Firebase и открыла ее для использования всем желающим.

 

Подробнее, что там есть интересного

Твори, расти, зарабатывай!
Твори, расти, зарабатывай!

В нашем распоряжении имеются:

  • Analytics — аналитика по приложению: размер аудитории, информация о пользователях, события в приложении и прочее.
  • Authentication — пользователи могут привязать свои учетные записи к приложению, а к ним мы можем привязать любые данные. Из коробки поддерживаются следующие провайдеры авторизации: Google, Facebook, Twitter, GitHub, анонимный вход и имейл-пароль для своей регистрации. Не хватает только VK-авторизации.
Просто включи свой провайдер
Просто включи свой провайдер
  • Realtime Database — самая настоящая база данных, работает с живыми изменениями в реальном времени.
  • Storage — хранилище для файлов пользователей, можно легко сделать персональное хранилище, а можно и делиться файлами.
  • Hosting — тут просто моментальное развертывание веб-приложений и мобильных приложений с помощью безопасной глобальной сети доставки контента.
  • Test Lab for Android — тестируй приложения Android на самых разных устройствах.
  • App Indexing — свяжи информацию с веб-сайта с внутренними страницами приложения, также есть возможность индексировать данные приложения и отображать их в результатах поиска на устройстве.
  • Crash Reporting — сбор информации о сбоях в приложении (на ранних версиях и сам был источником крашей, но вроде починили).
  • Notifications — уведомления, замена старым Google Cloud Messaging.
  • Remote Config — способ менять поведение приложения прямо со своего сервера, изменяя нужные параметры.
  • Dynamic Links — полезный способ прокинуть контекст в приложение (например, пользователь читал про аспирин на твоем сайте, перешел в маркет, установил приложение, и ему открылась страница с аспирином).
  • AdMob — рекламный сервис с множеством форматов, по праву занимает лидирующие позиции в мобильной рекламе. У этой сети рекламы всегда много, и она модерируется.
 

А за что попросят деньги?

Деньги с нас справедливо попросят, если наш бизнес действительно разрастется. Когда он упрется в бесплатные лимиты, платить нам уже будет с чего.

Бесплатно нам доступно:

  • Realtime Database:
    • 100 единовременных подключений
    • 1 Гбайт хранилища
    • 10 Гбайт в месяц трафика
  • Storage:
    • 5 Гбайт хранилища
    • 1 Гбайт в день трафика
    • 20 000 операций загрузок в день
    • 50 000 операций скачивания в день
  • Hosting:
    • 1 Гбайт хранилища
    • 10 Гбайт в месяц трафика
    • Custom domain hosting & SSL
  • Test Lab:
    • запуск не более пятнадцати тестов в день (десять на виртуальных и пять на физических устройствах)

Более подробно читай здесь.

Наш бесплатный Spark
Наш бесплатный Spark
 

Аутентификация в приложении

Чтобы пользователь мог сохранять настройки приложения на сервере, нужно создать учетную запись. Firebase позволяет это делать при помощи создания собственной учетной записи, как на любом сайте с имейлом-паролем. Также можно привязаться к учетным записям Google, Facebook, Twitter, GitHub. В своих приложениях я использую аккаунты Firebase и Google.

Разрешенные способы входа
Разрешенные способы входа

Хороший пример кода для вдохновения ты найдешь здесь. А как сделать свою регистрацию, внятно описано тут.

Для связки Google-аккаунта с приложением я делаю следующее. В методе OnCreate нужной Activity создаю объекты GoogleApiClient, FirebaseAuth и слушателя аутентификации FirebaseAuth.AuthStateListener.

// [START config_signin]
// Configure Google Sign In
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
    .requestIdToken(getString(R.string.default_web_client_id))
    .requestEmail()
    .build();
// [END config_signin]

mGoogleApiClient = new GoogleApiClient.Builder(this)
    .enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
    .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
    .build();

// [START initialize_auth]
mAuth = FirebaseAuth.getInstance();
// [END initialize_auth]

// [START auth_state_listener]
mAuthListener = new FirebaseAuth.AuthStateListener() {
    @Override
    public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
        FirebaseUser user = firebaseAuth.getCurrentUser();

        if (user != null) {
            // User is signed in
            Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid());
        } else {
            // User is signed out
            Log.d(TAG, "onAuthStateChanged:signed_out");
        }
        // [START_EXCLUDE]
        updateUI(user);
        // [END_EXCLUDE]
    }
};
// [END auth_state_listener]

Чтобы запустить аутентификацию, используем простой метод:

private void signIn() {
    Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
    startActivityForResult(signInIntent, RC_SIGN_IN);
}

Запущенная активити предложит нам выбрать учетную запись Google из хранящихся на устройстве. После выбора нужно обработать результат в методе onActivityResult:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
    if (requestCode == RC_SIGN_IN) {
        GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
        if (result.isSuccess()) {
            // Google Sign In was successful, authenticate with Firebase
            GoogleSignInAccount account = result.getSignInAccount();
            firebaseAuthWithGoogle(account);
        } else {
            // Google Sign In failed, update UI appropriately
            // [START_EXCLUDE]
            Toast.makeText(SettingsActivity.this, R.string.auth_failed,
            Toast.LENGTH_SHORT).show();
            flipCard();
            updateUI(null);
            // [END_EXCLUDE]
        }
    }
}

После выполнения функции firebaseAuthWithGoogle сработает наш слушатель аутентификации mAuthListener.

private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
    Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());
    // [START_EXCLUDE silent]
    showProgressDialog();
    // [END_EXCLUDE]

    AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
    mAuth.signInWithCredential(credential)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
        @Override
        public void onComplete(@NonNull Task<AuthResult> task) {
            Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());

            // If sign in fails, display a message to the user.
            // If sign in succeeds the auth state listener 
            // will be notified and logic to handle the signed in
            // user can be handled in the listener.
            if (!task.isSuccessful()) {
                Log.w(TAG, "signInWithCredential", task.getException());
                Toast.makeText(SettingsActivity.this, R.string.auth_failed,
                Toast.LENGTH_SHORT).show();
            }
            // [START_EXCLUDE]
            hideProgressDialog();
            flipCard();
            // [END_EXCLUDE]
        }
    });
}

Отключить приложение от учетной записи поможет метод revokeAccess(): в его колбэке обновляем интерфейс приложения.

private void revokeAccess() {
    // Firebase sign out
    mAuth.signOut();
    if (mGoogleApiClient.isConnected())
        // Google revoke access
        Auth.GoogleSignInApi.revokeAccess(mGoogleApiClient).setResultCallback(new ResultCallback<Status>(){
            @Override
            public void onResult(@NonNull Status status) {
                updateUI(null);
            }
        });
}
 

Данные пользователя

Информацию о текущем привязанном пользователе мы получаем из объекта FirebaseUser:

  • getPhotoUrl() — вернет null или ссылку на аватар пользователя;
  • getEmail() — имейл-адрес;
  • getUid() — уникальный ID пользователя в системе;
  • getDisplayName() — имя пользователя;
  • getProviderData().get(1).getProviderId() — подскажет, как пользователь аутентифицировался (проверь на equals("password"), если через email/пароль).

Имея ссылку на аватар пользователя, можно в одну строчку кода загрузить его и отобразить в приложении. Для этого есть множество сторонних библиотек: Glide, Fresco, Picasso. Но если на счету каждый килобайт, то можно использовать свой AsyncTask. Вызываем загрузку так:

ImageView iv_header = (ImageView) mDrawerHeader.findViewById(R.id.iv_header);
if (user.getPhotoUrl() != null) {
    String img = user.getPhotoUrl().toString();
    DownloadImageTask imgTask = new DownloadImageTask(iv_header);
    imgTask.execute(img);
}

DownloadImageTask в фоне загружает картинку, а в UI-потоке устанавливает ее в нужный ImageView:

public class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
    private ImageView ivImageView;
    private Context context;

    public DownloadImageTask(ImageView ivImageView) {
        this.ivImageView = ivImageView;
        this.context = ivImageView.getContext();
    }

    @Override
    protected Bitmap doInBackground(String... params) {
        String imageUrl = params[0];
        Bitmap resultImage = null;
        File f = new File(context.getCacheDir(), String.valueOf(imageUrl.hashCode()));
        if (f.exists()) {
            resultImage = BitmapFactory.decodeFile(f.getAbsolutePath());
            // Log.d("DownloadImageTask", "f.exists()");
        } else {
            try {
                URL url = new URL(imageUrl);
                HttpURLConnection ucon = (HttpURLConnection) url.openConnection();
                Bitmap image = BitmapFactory.decodeStream(ucon.getInputStream());
                FileOutputStream fos = new FileOutputStream(f);
                image.compress(Bitmap.CompressFormat.PNG, 100, fos);
                fos.close();
                fos.flush();
                resultImage = BitmapFactory.decodeFile(f.getAbsolutePath());
                // Log.d("DownloadImageTask", "file downloaded");
            } catch (Exception e) {
                Log.e("getImage", e.toString());
            }
        }
        return resultImage;
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        if (ivImageView != null) {
            ivImageView.setImageBitmap(result);
        }
    }
}
Раздел «Аутентификация» в консоли разработчика
Раздел «Аутентификация» в консоли разработчика
 

Работа с Realtime Database

Читать данные из Realtime Database можно и не представившись, для описания уровней доступа к информации используются правила. Описываются они в древовидной форме, можно задать отдельные правила на каждую ветвь.

Древовидная структура данных
Древовидная структура данных

Пример правил из моего проекта
Пример правил из моего проекта

В моем проекте настройки пользователей хранятся в ветке history. Пользователь может читать только свою ветвь и только если он представился. Для проверки работы правил доступа есть симулятор.

Проверяем правила доступа прямо в консоли
Проверяем правила доступа прямо в консоли

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

В бесплатный тариф укладываемся с запасом
В бесплатный тариф укладываемся с запасом

Резервные копии на текущем тарифе недоступны, но если бизнес разрастется, то обязательно купим и настроим.

 

Пишем и читаем данные с Android

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

Начнем с записи. Нам надо получить ссылку на объект DatabaseReference и у него добраться до нужной ветки, в которую будем записывать переменную. Доступные нам типы переменных:

  • String;
  • Long;
  • Double;
  • Boolean;
  • Map<String, Object>;
  • List<Object>.

Вот как просто объект User записывается в ветку users:

private void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);
    mDatabase.child("users").child(userId).setValue(user);
}

Получить уникальный userId пользователя после аутентификации очень просто: user.getUid(). Чтобы обновить данные, нужно просто вызвать setValue() с новыми данными. Уникальный ключ для каждого объекта позволяют получить методы push() и getKey(). В следующем примере я запрашиваю ключ для еще не добавленной записи, а потом создаю ветку с этим ключом и в нее сохраняю объект:

String key = mDatabase.child(mUserId).push().getKey();
wi.ID = key;
Map<String, Object> postValues = wi.toMap();
mDatabase.child(mUserId).child(key).setValue(postValues);

Для чтения данных используются слушатели ValueEventListener, их нужно устанавливать на интересующие нас ветки методом addValueEventListener. Событие будет происходить каждый раз при обновлении данных на сервере, при первом подключении к БД, и просто так про запас еще раза два-три может произойти. Так что будь готов к этому морально и практически. Если тебе нужно прочитать данные один раз и больше не мучиться, то используй метод addListenerForSingleValueEvent.

Вот пример одноразового получения списка объектов для текущего пользователя из ветки history.

private void syncMainList() {
    // Забираю данные
    FirebaseDatabase database = FirebaseDatabase.getInstance();
    DatabaseReference myRef = database.getReference("history");
    myRef.child(mUserId).addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) { 
            for (DataSnapshot ds : dataSnapshot.getChildren()) {
                WifiInfo wi = ds.getValue(WifiInfo.class);
                addToList(wi);
            }
            // Устанавливаю слушатель на список
            setUpListener();
        }

        @Override
        public void onCancelled(DatabaseError error) {
            // Failed to read value
            Log.w(TAG, "Failed to read value.", error.toException());
        }
    });
}

А если нам нужно постоянно отслеживать изменяющийся список, то в этом поможет слушатель ChildEventListener, который позволяет слушать не единичный элемент, а всю дочернюю ветку:

private void setUpListener() {
    FirebaseDatabase database = FirebaseDatabase.getInstance();
    DatabaseReference myRef = database.getReference("history");
    myRef.child(mUserId)
        .addChildEventListener(new ChildEventListener() {
        @Override
        public void onChildAdded(DataSnapshot dataSnapshot, String s) {
            WifiInfo wi = dataSnapshot.getValue(WifiInfo.class);
            addToList(wi);
        }

        @Override
        public void onChildChanged(DataSnapshot dataSnapshot, String s) {
            WifiInfo wi = dataSnapshot.getValue(WifiInfo.class);
            updateList(wi); // Пока только из активных в историю
        }

        @Override
        public void onChildRemoved(DataSnapshot dataSnapshot) {
            WifiInfo wi = dataSnapshot.getValue(WifiInfo.class);
            removeFromList(wi);
        }

        @Override
        public void onChildMoved(DataSnapshot dataSnapshot, String s) {
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {
        }
    });
}

Здесь пять событий, сами собой объясняющие свою логику. Будь готов к срабатыванию их, причем многократному: на моем опыте при ручном добавлении элемента из приложения событие onChildAdded для этого элемента отрабатывало три раза.

 

Узнай своего пользователя при помощи Analytics

Старый инструмент Google Analytics создавался изначально как инструмент для работы с вебом. Позже его адаптировали под нужды мобайла, но ограничение его было существенным. В новой аналитике Firebase собирается данных намного больше. Например, данные об удалении приложения, обновлении ОС на устройстве, очистка кеша приложения. Множество событий отслеживается автоматически и не требует нашего вмешательства.

Если мы хотим собрать события внутри приложения, то нам поможет класс FirebaseAnalytics. В своих проектах, чтобы иметь возможность вызывать методы аналитики в любом месте проекта, я размещаю ссылку на объект аналитики в классе Application. Инициализирую его один раз при создании приложения:

public class App extends Application {
    private static FirebaseAnalytics mFirebaseAnalytics;

    public void onCreate() {
        super.onCreate();
        mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
    }

    public static void selectContent(String type, String id) {
        Bundle bundle = new Bundle();
        bundle.putString(FirebaseAnalytics.Param.ITEM_ID, id);
        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, type);
        mFirebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, bundle);
    }
}

Метод select_content можно вызвать одной строчкой. Например, соберем данные по использованию пунктов меню: App.selectContent("меню","окно о-программе") или App.selectContent("меню","окно настройки").

Установив отслеживание событий в приложении, мы будем знать, что пользователя интересует больше всего. Данные, отправленные методом logEvent, нужно искать в консоли проекта на вкладке «События».

События нашего проекта
События нашего проекта

Все события на рисунке собраны системой автоматически, кроме select_content (его реализация описана выше). Вот детали по этому методу (тут ясно видно, куда отправились параметры CONTENT_TYPE и ITEM_ID):

Карточка события select_content
Карточка события select_content

Полный список событий FirebaseAnalytics доступен тут.

 

Про подключение и вместо заключения

После настройки всех необходимых модулей Firebase в консоли необходимо получить файл конфигурации с настройками проекта google-services.json. Там же можешь добавить контрольные суммы сертификатов SHA (это можно сделать через ассистент прямо из Android Studio: Tools → Firebase). Я добавляю два сертификата: один от дебаг-ключа и один релизный. Файл google-services.json размести в папку app проекта. В gradle-файле проекта не забудь подключить нужные библиотеки, например:

// Firebase Authentication
compile 'com.google.firebase:firebase-auth:10.0.1'

// Firebase Data Base
compile 'com.google.firebase:firebase-database:10.0.1'

// Google Sign In SDK (only required for Google Sign In)
compile 'com.google.android.gms:play-services-auth:10.0.1'

// Firebase-аналитика
compile 'com.google.firebase:firebase-core:10.0.1'

И в конце допиши apply plugin: 'com.google.gms.google-services', этот плагин обработает файл google-services.json.

Эта статья — только первый подход к такому тяжелому снаряду, как Firebase. Недавно этот снаряд стал еще тяжелее, а значит, нам есть что изучать и использовать.

Думаю, в будущем хакеры найдут достойное применение такой мощной и бесплатной технологии :).

Оставить мнение