Хранение данных вашего приложения в облаке очень важно в наши дни, потому что пользователи, как правило, владеют несколькими устройствами и хотят, чтобы их приложения были синхронизированы между собой. С Cloud Firestore , базой данных NoSQL в реальном времени, доступной на платформе Firebase, сделать это проще и безопаснее, чем когда-либо прежде.
В предыдущем уроке я познакомил вас со всеми мощными функциями Cloud Firestore. Сегодня я покажу вам, как использовать его вместе с другими продуктами Firebase, такими как FirebaseUI Auth и Firebase Analytics, для создания простого, но хорошо масштабируемого приложения для отслеживания веса.
Предпосылки
Чтобы следовать этому пошаговому руководству, вам потребуется:
- последняя версия Android Studio
- учетная запись Firebase
- и устройство или эмулятор под управлением Android 5.0 или выше
1. Настройка проекта
Чтобы иметь возможность использовать продукты Firebase в своем проекте Android Studio, вам потребуется плагин Google Services Gradle, файл конфигурации Firebase и несколько зависимостей implementation
. С помощью Firebase Assistant вы можете получить их все очень легко.
Откройте помощника, перейдя в Инструменты> Firebase . Затем выберите параметр « Аналитика» и нажмите на ссылку « Записать событие аналитики» .
Теперь вы можете нажать кнопку « Подключиться к Firebase» , чтобы подключить проект Android Studio к новому проекту Firebase.
Однако для фактического добавления плагина и зависимостей 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’
|
Наконец, нажмите кнопку « Синхронизировать сейчас» , чтобы обновить проект.
2. Настройка проверки подлинности Firebase
Аутентификация Firebase поддерживает множество провайдеров идентификации. Тем не менее, все они отключены по умолчанию. Чтобы включить один или несколько из них, вы должны посетить консоль Firebase.
В консоли выберите проект Firebase, созданный на предыдущем шаге, перейдите в раздел « Аутентификация » и нажмите кнопку « Настроить метод входа» .
Чтобы разрешить пользователям входить в наше приложение с использованием учетной записи Google, включите Google в качестве поставщика, дайте значимое общедоступное имя проекту и нажмите кнопку « Сохранить» .
Google — самый простой поставщик удостоверений, который вы можете использовать. Он не требует настройки, и вашему проекту Android Studio не потребуется никаких дополнительных зависимостей для него.
3. Настройка облачного Firestore
Вы должны включить Firestore в консоли Firebase, прежде чем начать его использовать. Для этого перейдите в раздел « База данных » и нажмите кнопку « Начать» на бета- карте Cloud Firestore .
Теперь вам будет предложено выбрать режим безопасности для базы данных. Убедитесь, что вы выбрали опцию Пуск в заблокированном режиме и нажмите кнопку Включить .
В заблокированном режиме по умолчанию никто не сможет получить доступ или изменить содержимое базы данных. Следовательно, теперь вы должны создать правило безопасности, которое позволяет пользователям читать и писать только те документы, которые им принадлежат. Начните с открытия вкладки « Правила ».
Прежде чем мы создадим правило безопасности для нашей базы данных, мы должны завершить, как мы будем хранить данные в ней. Допустим, у нас будет коллекция верхнего уровня с именами users
содержащая документы, представляющие наших пользователей. Документы могут иметь уникальные идентификаторы, идентичные идентификаторам, которые служба аутентификации Firebase генерирует для пользователей.
Поскольку пользователи будут добавлять несколько весовых записей в свои документы, использование подколлекции для хранения этих записей является идеальным. Давайте назовем weights
подгруппы.
На основе приведенной выше схемы теперь мы можем создать правило для пути 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;
}
}
}
|
Наконец, нажмите кнопку « Опубликовать» , чтобы активировать правило.
4. Аутентификация пользователей
Наше приложение должно использоваться только в том случае, если пользователь вошел в него с использованием учетной записи 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()
}
}
}
|
На этом этапе, если вы запустите приложение в первый раз, вы должны увидеть экран входа, который выглядит следующим образом:
При последующих запусках — благодаря Google Smart Lock, который включен по умолчанию — вы автоматически войдете в систему.
5. Определение макетов
Наше приложение нуждается в двух макетах: один для основного действия и один для записей веса, которые будут показаны как элементы в обратном хронологически упорядоченном списке.
Макет основного действия должен иметь виджет 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>
|
6. Создание модели
На предыдущем шаге вы видели, что с каждой записью веса связаны вес и время. Чтобы сообщить об этом 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)
}
|
7. Создание уникальных пользовательских документов
Когда пользователь пытается создать запись веса в первый раз, наше приложение должно создать отдельный документ для пользователя в коллекции 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.
8. Добавление весовых записей
Когда пользователи нажимают кнопку плавающего действия нашего приложения, они должны иметь возможность создавать новые записи веса. Чтобы позволить им набирать их веса, давайте теперь создадим диалог, содержащий виджет 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.
9. Отображение записей веса
Теперь пришло время заполнить виджет 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, вы увидите, что на нем также появятся те же записи веса.
Вывод
В этом руководстве вы увидели, как быстро и легко создать полнофункциональное приложение для отслеживания веса для Android, используя Cloud Firestore в качестве базы данных. Не стесняйтесь добавлять больше функциональности к нему! Я также предлагаю вам попробовать опубликовать его в Google Play. С планом Firebase Spark , который в настоящее время предлагает 1 ГБ хранилища данных бесплатно, у вас не будет проблем с обслуживанием как минимум нескольких тысяч пользователей.
И пока вы здесь, посмотрите другие наши посты о разработке приложений для Android!