Статьи

Создайте приложение для отслеживания веса с помощью Cloud Firestore

Хранение данных вашего приложения в облаке очень важно в наши дни, потому что пользователи, как правило, владеют несколькими устройствами и хотят, чтобы их приложения были синхронизированы между собой. С Cloud Firestore , базой данных NoSQL в реальном времени, доступной на платформе Firebase, сделать это проще и безопаснее, чем когда-либо прежде.

В предыдущем уроке я познакомил вас со всеми мощными функциями Cloud Firestore. Сегодня я покажу вам, как использовать его вместе с другими продуктами Firebase, такими как FirebaseUI Auth и Firebase Analytics, для создания простого, но хорошо масштабируемого приложения для отслеживания веса.

Чтобы следовать этому пошаговому руководству, вам потребуется:

  • последняя версия Android Studio
  • учетная запись Firebase
  • и устройство или эмулятор под управлением Android 5.0 или выше

Чтобы иметь возможность использовать продукты Firebase в своем проекте Android Studio, вам потребуется плагин Google Services Gradle, файл конфигурации Firebase и несколько зависимостей implementation . С помощью Firebase Assistant вы можете получить их все очень легко.

Откройте помощника, перейдя в Инструменты> Firebase . Затем выберите параметр « Аналитика» и нажмите на ссылку « Записать событие аналитики» .

Fierbase Assistant panel

Теперь вы можете нажать кнопку « Подключиться к Firebase» , чтобы подключить проект Android Studio к новому проекту Firebase.

Connect to Firebase dialog

Однако для фактического добавления плагина и зависимостей implementation вам также необходимо нажать кнопку « Добавить аналитику в приложение» .

Приложение для отслеживания веса, которое мы создаем сегодня, будет иметь только две функции: сохранение весов и отображение их в виде списка, отсортированного в обратном хронологическом порядке. Мы, конечно, будем использовать Firestore для хранения весов. Однако для их отображения в виде списка мы будем использовать компоненты, связанные с Firestore, доступные в библиотеке FirebaseUI . Поэтому добавьте следующую зависимость implementation в файл build.gradle модуля app :

1
implementation ‘com.firebaseui:firebase-ui-firestore:3.2.2’

Пользователи должны иметь возможность просматривать только свои веса, а не веса всех, кто использует приложение. Следовательно, наше приложение должно иметь возможность однозначно идентифицировать своих пользователей. FirebaseUI Auth предлагает эту возможность, поэтому добавьте следующую зависимость:

1
implementation ‘com.firebaseui:firebase-ui-auth:3.2.2’

Нам также понадобятся несколько виджетов Material Design, чтобы придать нашему приложению приятный вид. Поэтому обязательно добавьте в качестве зависимостей библиотеку поддержки проектирования и библиотеку диалогов материалов .

1
2
implementation ‘com.android.support:design:26.1.0’
implementation ‘com.afollestad.material-dialogs:core:0.9.6.0’

Наконец, нажмите кнопку « Синхронизировать сейчас» , чтобы обновить проект.

Аутентификация Firebase поддерживает множество провайдеров идентификации. Тем не менее, все они отключены по умолчанию. Чтобы включить один или несколько из них, вы должны посетить консоль Firebase.

В консоли выберите проект Firebase, созданный на предыдущем шаге, перейдите в раздел « Аутентификация » и нажмите кнопку « Настроить метод входа» .

Firebase Authentication home screen

Чтобы разрешить пользователям входить в наше приложение с использованием учетной записи Google, включите Google в качестве поставщика, дайте значимое общедоступное имя проекту и нажмите кнопку « Сохранить» .

Google identity provider configuration

Google — самый простой поставщик удостоверений, который вы можете использовать. Он не требует настройки, и вашему проекту Android Studio не потребуется никаких дополнительных зависимостей для него.

Вы должны включить Firestore в консоли Firebase, прежде чем начать его использовать. Для этого перейдите в раздел « База данных » и нажмите кнопку « Начать» на бета- карте Cloud Firestore .

Cloud Firestore card

Теперь вам будет предложено выбрать режим безопасности для базы данных. Убедитесь, что вы выбрали опцию Пуск в заблокированном режиме и нажмите кнопку Включить .

Security mode selection screen

В заблокированном режиме по умолчанию никто не сможет получить доступ или изменить содержимое базы данных. Следовательно, теперь вы должны создать правило безопасности, которое позволяет пользователям читать и писать только те документы, которые им принадлежат. Начните с открытия вкладки « Правила ».

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

Поскольку пользователи будут добавлять несколько весовых записей в свои документы, использование подколлекции для хранения этих записей является идеальным. Давайте назовем weights подгруппы.

Firestore database structure

На основе приведенной выше схемы теперь мы можем создать правило для пути users/{user_id}/weights/{weight} . Правило будет состоять в том, что пользователю разрешается чтение и запись в путь, только если переменная {user_id} равна идентификатору аутентификации Firebase пользователя.

Соответственно обновите содержимое редактора правил.

1
2
3
4
5
6
7
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{user_id}/weights/{weight} {
      allow read, write: if user_id == request.auth.uid;
    }
  }
}

Наконец, нажмите кнопку « Опубликовать» , чтобы активировать правило.

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

Чтобы проверить, есть ли у пользователя идентификатор, мы можем просто проверить, что свойство currentUser класса FirebaseAuth не является нулевым. Если оно пустое, мы можем создать намерение входа, вызвав метод createSignInIntentBuilder() класса AuthUI .

В следующем коде показано, как это сделать для Google в качестве поставщика удостоверений:

01
02
03
04
05
06
07
08
09
10
11
12
13
if(FirebaseAuth.getInstance().currentUser == null) {
    // Sign in
    startActivityForResult(
        AuthUI.getInstance().createSignInIntentBuilder()
                .setAvailableProviders(arrayListOf(
                        AuthUI.IdpConfig.GoogleBuilder().build()
                )).build(),
        1
    )
} else {
    // Already signed in
    showUI()
}

Обратите внимание, что мы вызываем метод с именем showUI() если действительный идентификатор уже присутствует. Этот метод еще не существует, поэтому создайте его и пока оставьте его тело пустым.

1
2
3
private fun showUI() {
    // To do
}

Чтобы поймать результат намерения входа, мы должны переопределить метод действия onActivityResult() . Внутри метода, если значение аргумента resultCode равно RESULT_OK а свойство currentUser больше не равно нулю, это означает, что пользователю удалось успешно войти в систему. В этом случае мы должны снова вызвать метод showUI() для визуализации пользовательского интерфейса.

Если пользователь не может войти в систему, мы можем отобразить тост и закрыть приложение, вызвав метод finish() .

Соответственно добавьте следующий код в действие:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
override fun onActivityResult(requestCode: Int, resultCode: Int,
                                                  data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(requestCode == 1) {
        if(resultCode == Activity.RESULT_OK
              && FirebaseAuth.getInstance().currentUser != null) {
            // Successfully signed in
            showUI()
        } else {
            // Sign in failed
            Toast.makeText(this, «You must sign in to continue»,
                           Toast.LENGTH_LONG).show()
            finish()
        }
    }
}

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

Account selection dialog

При последующих запусках — благодаря Google Smart Lock, который включен по умолчанию — вы автоматически войдете в систему.

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

Макет основного действия должен иметь виджет RecyclerView , который будет действовать как список, и виджет FloatingActionButton , который пользователь может нажать, чтобы создать новую запись веса. Поместив их в виджет RelativeLayout , XML-файл макета вашей активности должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version=»1.0″ encoding=»utf-8″?>
<RelativeLayout
    xmlns:android=»http://schemas.android.com/apk/res/android»
    xmlns:app=»http://schemas.android.com/apk/res-auto»
    xmlns:tools=»http://schemas.android.com/tools»
    android:layout_width=»match_parent»
    android:layout_height=»match_parent»
    tools:context=»com.tutsplus.weighttracker.MainActivity»>
 
    <android.support.v7.widget.RecyclerView
        android:layout_width=»match_parent»
        android:layout_height=»match_parent»
        android:id=»@+id/weights»>
 
    </android.support.v7.widget.RecyclerView>
 
    <android.support.design.widget.FloatingActionButton
        android:layout_width=»wrap_content»
        android:layout_height=»wrap_content»
        android:layout_alignParentRight=»true»
        android:layout_alignParentBottom=»true»
        android:layout_margin=»16dp»
        android:src=»@drawable/ic_add_black_24dp»
        android:tint=»@android:color/white»
        android:onClick=»addWeight»/>
 
</RelativeLayout>

Мы связали обработчик события щелчка с именем addWeight() с виджетом FloatingActionButton . Обработчик еще не существует, поэтому создайте для него заглушку внутри действия.

1
2
3
fun addWeight(v: View) {
    // To do
}

Чтобы упростить компоновку записи весов, у нас будет всего два виджета TextView : один для отображения веса, а другой для отображения времени, когда была создана запись. Использование виджета LinearLayout в качестве контейнера для них будет достаточно.

Соответственно, создайте новый XML-файл макета с именем weight_entry.xml и добавьте в него следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version=»1.0″ encoding=»utf-8″?>
<LinearLayout
    xmlns:android=»http://schemas.android.com/apk/res/android»
    android:orientation=»vertical»
    android:layout_width=»match_parent»
    android:layout_height=»match_parent»
    android:padding=»16dp»>
 
    <TextView
        android:layout_width=»wrap_content»
        android:layout_height=»wrap_content»
        style=»@style/TextAppearance.AppCompat.Large»
        android:id=»@+id/weight_view»/>
 
    <TextView
        android:layout_width=»wrap_content»
        android:layout_height=»wrap_content»
        style=»@style/TextAppearance.AppCompat.Small»
        android:id=»@+id/time_view»/>
 
</LinearLayout>

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

Модели Firestore — это, как правило, простые классы данных с необходимыми переменными-членами.

1
2
data class WeightEntry(var weight: Double=0.0,
                      var timestamp: Long=0)

Сейчас также самое подходящее время для создания держателя вида для каждой записи веса. Держатель представления, как вы уже догадались, будет использоваться виджетом RecyclerView для визуализации элементов списка. Поэтому создайте новый класс с именем WeightEntryVH , который расширяет класс RecyclerView.ViewHolder , и создайте переменные-члены для обоих виджетов TextView . Не забудьте инициализировать их с findViewById() метода findViewById() . Следующий код показывает, как сделать это кратко:

1
2
3
4
5
6
7
class WeightEntryVH(itemView: View?)
                        : RecyclerView.ViewHolder(itemView) {
    var weightView: TextView?
                    itemView?.findViewById(R.id.weight_view)
    var timeView: TextView?
                    itemView?.findViewById(R.id.time_view)
}

Когда пользователь пытается создать запись веса в первый раз, наше приложение должно создать отдельный документ для пользователя в коллекции users в Firestore. Как мы решили ранее, идентификатор документа будет ничем иным, как идентификатором аутентификации Firebase пользователя, который можно получить с помощью свойства uid объекта currentUser .

Чтобы получить ссылку на коллекцию users , мы должны использовать метод collection() класса FirebaseFirestore . Затем мы можем вызвать его метод document() и передать uid в качестве аргумента для создания документа пользователя.

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

1
2
3
4
5
6
private fun getUserDocument():DocumentReference {
    val db = FirebaseFirestore.getInstance()
    val users = db.collection(«users»)
    val uid = FirebaseAuth.getInstance().currentUser!!.uid
    return users.document(uid)
}

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

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

Внутри addWeight() , который служит обработчиком события нажатия кнопки, создайте экземпляр MaterialDialog.Builder и вызовите его методы title() и content() чтобы дать вашему диалогу заголовок и содержательное сообщение. Аналогично, вызовите метод inputType() и передайте TYPE_CLASS_NUMBER аргумент TYPE_CLASS_NUMBER в качестве аргумента, чтобы убедиться, что пользователь может вводить только цифры в диалоговом окне.

Затем вызовите метод input() чтобы указать подсказку и связать обработчик событий с диалоговым окном. Обработчик получит вес, введенный пользователем в качестве аргумента.

Наконец, убедитесь, что вы вызываете метод show() для отображения диалогового окна.

01
02
03
04
05
06
07
08
09
10
11
12
MaterialDialog.Builder(this)
   .title(«Add Weight»)
   .content(«What’s your weight today?»)
   .inputType(InputType.TYPE_CLASS_NUMBER
              or InputType.TYPE_NUMBER_FLAG_DECIMAL)
   .input(«weight in pounds», «», false,
           { _, weight ->
 
               // To do
 
           })
   .show()

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

Когда у вас есть коллекция, вы можете вызвать ее метод add() и передать ему новый экземпляр класса WeightEntry для хранения записи.

1
2
3
4
5
6
7
8
getUserDocument()
   .collection(«weights»)
   .add(
       WeightEntry(
           weight.toString().toDouble(),
           Date().time
       )
   )

В приведенном выше коде вы можете видеть, что мы используем свойство time класса Date чтобы связать отметку времени с записью.

Если вы запустите приложение сейчас, вы сможете добавить новые записи веса в Firestore. Вы пока не увидите их в приложении, но они будут видны в консоли Firebase.

Add weight dialog

Теперь пришло время заполнить виджет RecyclerView нашего макета. Поэтому начните с создания ссылки на него с findViewById() метода findViewById() и присвоения ему нового экземпляра класса LinearLayoutManager . Это должно быть сделано внутри showUI() который мы создали ранее.

1
2
val weightsView = findViewById<RecyclerView>(R.id.weights)
weightsView.layoutManager = LinearLayoutManager(this)

Виджет RecyclerView должен отображать все документы, которые присутствуют в коллекции weights документа пользователя. Кроме того, последние документы должны появиться первыми. Чтобы удовлетворить эти требования, теперь мы должны создать запрос, вызвав методы collection() и orderBy() .

Ради эффективности вы можете ограничить количество значений, возвращаемых запросом, вызвав метод limit() .

Следующий код создает запрос, который возвращает последние 90 весовых записей, созданных пользователем:

1
2
3
val query = getUserDocument().collection(«weights»)
               .orderBy(«timestamp», Query.Direction.DESCENDING)
               .limit(90)

Используя запрос, мы теперь должны создать объект FirestoreRecyclerOptions , который мы будем использовать позже для настройки адаптера нашего виджета RecyclerView . Когда вы передаете экземпляр query setQuery() его построителя, убедитесь, что вы указали, что возвращаемые результаты имеют форму объектов WeightEntry . Следующий код показывает вам, как это сделать:

1
2
3
4
val options = FirestoreRecyclerOptions.Builder<WeightEntry>()
                   .setQuery(query, WeightEntry::class.java)
                   .setLifecycleOwner(this)
                   .build()

Возможно, вы заметили, что мы делаем нашу текущую деятельность владельцем жизненного цикла объекта FirestoreRecyclerOptions . Это важно, потому что мы хотим, чтобы наш адаптер правильно реагировал на общие события жизненного цикла, такие как открытие или закрытие приложения пользователем.

На этом этапе мы можем создать объект FirestoreRecyclerAdapter , который использует объект FirestoreRecyclerOptions для настройки самого себя. Поскольку класс FirestoreRecyclerAdapter является абстрактным, Android Studio должна автоматически переопределить свои методы для создания кода, который выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
val adapter = object:FirestoreRecyclerAdapter<WeightEntry,
                                       WeightEntryVH>(options) {
 
    override fun onBindViewHolder(holder: WeightEntryVH,
                            position: Int, model: WeightEntry) {
        // To do
    }
 
    override fun onCreateViewHolder(parent: ViewGroup?,
                            viewType: Int): WeightEntryVH {
        // To do
    }
}

Как видите, класс FirestoreRecyclerAdapter очень похож на класс RecyclerView.Adapter . На самом деле, это происходит от этого. Это означает, что вы можете использовать его так же, как и класс RecyclerView.Adapter .

В onCreateViewHolder() все, что вам нужно сделать, — это накачать файл макета weight_entry.xml и вернуть на WeightEntryVH объект держателя представления WeightEntryVH .

1
2
val layout = layoutInflater.inflate(R.layout.weight_entry, null)
return WeightEntryVH(layout)

А внутри onBindViewHolder() вы должны использовать аргумент model для обновления содержимого виджетов TextView , которые присутствуют внутри держателя представления.

Хотя обновление виджета weightView , обновление виджета timeView немного сложнее, поскольку мы не хотим показывать метку времени в миллисекундах непосредственно пользователю.

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

01
02
03
04
05
06
07
08
09
10
// Show weight
holder.weightView?.text = «${model.weight} lb»
 
// Show date and time
val formattedDate = DateUtils.formatDateTime(applicationContext,
                    model.timestamp,
                    DateUtils.FORMAT_SHOW_DATE or
                    DateUtils.FORMAT_SHOW_TIME or
                    DateUtils.FORMAT_SHOW_YEAR)
holder.timeView?.text = «On $formattedDate»

Наконец, не забудьте указать виджет RecyclerView на только что созданный адаптер.

1
weightsView.adapter = adapter

Приложение готово. Теперь вы сможете добавлять новые записи и видеть, что они появляются в списке почти сразу. Если вы запустите приложение на другом устройстве, имеющем ту же учетную запись Google, вы увидите, что на нем также появятся те же записи веса.

Weight entries shown as a list

В этом руководстве вы увидели, как быстро и легко создать полнофункциональное приложение для отслеживания веса для Android, используя Cloud Firestore в качестве базы данных. Не стесняйтесь добавлять больше функциональности к нему! Я также предлагаю вам попробовать опубликовать его в Google Play. С планом Firebase Spark , который в настоящее время предлагает 1 ГБ хранилища данных бесплатно, у вас не будет проблем с обслуживанием как минимум нескольких тысяч пользователей.

И пока вы здесь, посмотрите другие наши посты о разработке приложений для Android!

  • Android SDK
    Как кодировать навигационный ящик для приложения Android
    Чике Мгбемена
  • Android SDK
    Начало работы с Cloud Firestore для Android
  • Android SDK
    Как создать приложение для Android-чата с помощью Firebase