18ヵ月前「Creating a Cloud Backend for Your Android App Using Parse(Parseを使ってAndroidアプリ用のクラウドバックエンドを構築する)』という記事を執筆しました。2016年前半、Facebookは2017年1月28日もParseのサービスを終了すると発表し、現在このサービスへの新規サインアップはできません。驚きのこの発表に伴い、Parseに依存して開発してきた人はデータ移行が可能な代替手段を探さざるを得なくなりました。
このニュースを受け、再びこのトピックを取り上げて別のBaaS(Backend as a Service)を使ったAndroidアプリケーションのデータ管理方法を紹介しなければ、と考えました。
この記事では、2014年10月にGoogle傘下に入ったポピュラーなバックエンドプラットホーム「Firebase」について詳しく紹介します。
自分で(バックエンドを)構築することとBaaSへの依存について、メリットとデメリットを比較検討するのは良いことです。BaaSプラットホームのサービス終了はParseがはじめてではなく(たとえばStackMobも終了しました)最後でもないでしょう。こうしたプラットホームの1つに依存して開発する場合、いつも移行に備え、代替案を用意しておくことが必要です。
Firebaseを使えばNoSQLクラウドデータベースにデータを格納・同期できます。データはJSONとして格納され、接続されているすべてのクライアントにリアルタイムで同期され、アプリがオフライン状態でも利用可能です。EメールアドレスとパスワードによるFacebook、Twitter、GitHub、Googleへのユーザー認証や匿名認証ができ、既存の認証システムとも統合できるAPIが提供されています。リアルタイムデータベースと認証以外にCloud Messaging(クラウドメッセージング)、Storage(ストレージ)、Hosting(ホスティング)、Remote Config(リモート設定)、Test Lab(テストラボ)、Crash Reporting(クラッシュレポート)、Notification(通知)、App Indexing(アップインデクシング)、Dynamic Links(ダイナミックリンク)、Invites(招待)、AdWords(アドワーズ)、AdMob(アドモブ)など多数のサービスもあります。
この記事ではシンプルなTo Doアプリを作成し、Firebaseへのデータの保存と検索、ユーザー認証、データの読み出し/書き込みの許可設定、サーバー上でのデータ検証に関する方法を紹介します。
このプロジェクト用のコードはGitHubにあります。
プロジェクトのセットアップ
始めるにあたって(Android Studioで)「To Do」という名前で「CREATE NEW PROJECT」(新規プロジェクトを作成)します。次のウィンドウで「Minimum SDK」で「API 15」を選択し、次のウィンドウで「Basic Activity(基本のアクティビティ)」を選択します。最後のウィンドウで「終了(Finish)」をクリックし、設定をデフォルトのままにしておきます。
Androidプロジェクトを始める前にfirebase.google.comでアカウントを作成してください。アカウントにログイン後Firebase consoleに進んでアプリのデータを置くプロジェクトを作成します。
「プロジェクト名(name)」と「国/地域(country/region)」を入力します。
「国/地域」は自分の組織や会社が属する国または地域を表します。この選択により収益レポート(revenue reporting)用に適切な通貨も設定されます。プロジェクト名(ここではSPToDoApp)と地域の設定が終わったら、「Create Project(プロジェクトを作成)」ボタンをクリックします。これでプロジェクトが作成され、プロジェクトのコンソールが開きます。コンソールで「Add Firebase to your Android App(AndroidアプリにFirebaseを追加)」をクリックします。
ポップアップウィンドウにAndroidプロジェクトのパッケージ名を入力します。アプリでGoogle Sign-In(Googleサインイン)、App Invites(アプリへの招待)、Dynamic Links(ダイナミックリンク)などのGoogle Playサービスを利用する場合、SHA-1の署名証明書欄の入力が必要です。サービスをなにも利用しない場合は空欄にしておきます。アプリにこれらのサービスを追加する場合、こちらのページにkeytool(キーツール)を使った署名証明書のSHA-1ハッシュ取得に関する情報があります。このページにはリリースとデバッグ双方の証明書フィンガープリントを取得するための説明が載せられています。ここまで完了したら「Add App(アプリを追加)」ボタンをクリックします。
「Add App(アプリを追加)」をクリックすると、コンピューターにgoogle-services.jsonファイルがダウンロードされます。ダイアログボックスの次のページには、ダウンロードされたJSONファイルがどこに入っているかを示す情報があります。ダウンロード済みのファイルを見つけ、Androidプロジェクトの「app」モジュールのルートディレクトリに移動させます。
JSONファイルにはAndroidアプリがFirebaseサーバーと通信するのに必要な構成設定が格納されています。FirebaseプロジェクトへのURL、APIキーのなどの詳細も含まれています。Firebaseの以前のバージョンでは、アプリのコードに手動で追加することが必要でしたが、プロセスが簡素化されて必要なデータを格納した1個のファイルを使用するだけでよくなりました。
バージョン管理を使用し、公開リポジトリにコードを保存している場合、情報が共用されないようにgoogle-services.jsonファイルを.gitignoreファイルとして置きます。
完了したら、ダイアログウィンドウで「Continue(続行)」をクリックすると、セットアップに関する指示がさらに表示されます。
Projectレベルのbuild.gradleファイルで、buildscriptのdependenciesノードに次のコードを追加します。ダウンロード済みのgoogle-services.jsonファイルをロードする、Gradle用のGoogle servicesプラグインをインクルードできます。
classpath 'com.google.gms:google-services:3.0.0'
gradleファイルを正しく編集できたことを確認してください。
次にAppレベルのbuild.gradleファイルの末尾に以下のコードを追加して、Gradleプラグインを有効にします。
apply plugin: 'com.google.gms.google-services'
build.gradleファイルが正しく設定されているか、再度確認してください。
次に、同じファイルに以下の依存オブジェクトを追加します。Firebaseには機能によって別個のSDKがあります。ここではリアルタイムデータベースと認証に必要なライブラリーを追加します。
compile 'com.google.firebase:firebase-database:9.4.0'
compile 'com.google.firebase:firebase-auth:9.4.0'
メニューアイテムで、File→New→→Activity→Empty Activityと展開して空のアクティビティ(empty activity)を作成し、名前をLogInActivityとします。もう1つ作成してSignUpActivityと名付けます。
レイアウトファイル「activity_log_in.xml 」を次のように変更します。
< ?xml version="1.0" encoding="utf-8"?>
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".LoginActivity" >
<edittext android:id="@+id/emailField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:ems="10"
android:inputType="textEmailAddress"
android:hint="@string/email_hint" >
<requestfocus></requestfocus>
</edittext>
<edittext android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/emailField"
android:layout_below="@+id/emailField"
android:ems="10"
android:hint="@string/password_hint"
android:inputType="textPassword"></edittext>
<button android:id="@+id/loginButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/passwordField"
android:layout_below="@+id/passwordField"
android:text="@string/login_button_label"></button>
<textview android:id="@+id/signUpText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/loginButton"
android:layout_centerHorizontal="true"
android:layout_marginTop="69dp"
android:text="@string/sign_up_text"></textview>
</relativelayout>
レイアウトファイル「activity_sign_up.xml」を次のように変更します。
< ?xml version="1.0" encoding="utf-8"?>
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".SignUpActivity" >
<edittext android:id="@+id/emailField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:ems="10"
android:inputType="textEmailAddress"
android:hint="@string/email_hint" >
<requestfocus></requestfocus>
</edittext>
<edittext android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/emailField"
android:layout_below="@+id/emailField"
android:ems="10"
android:inputType="textPassword"
android:hint="@string/password_hint"></edittext>
<button android:id="@+id/signupButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/passwordField"
android:layout_below="@+id/passwordField"
android:text="@string/sign_up_button_label"></button>
</relativelayout>
上のレイアウトはログイン画面とサインアップ画面用のものです。
「values/strings.xml」に以下を追加します。
<string name="password_hint">Password</string>
<string name="email_hint">Email</string>
<string name="sign_up_button_label">Sign Up</string>
<string name="signup_error_message">Please make sure you enter an email address and password!</string>
<string name="signup_error_title">Error!</string>
<string name="signup_success">Account successfully created! You can now Login.</string>
<string name="login_error_message">Please make sure you enter an email address and password!</string>
<string name="login_error_title">Error!</string>
<string name="login_button_label">Login</string>
<string name="sign_up_text">Sign Up!</string>
<string name="title_activity_login">Sign in</string>
<string name="add_item">Add New Item</string>
<string name="action_logout">Logout</string>
「activity_main.xml」でFloatingActionButtonに関するマークアップを削除します。
<android .support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"></android>
さらに「MainActivity.java」で次の「FAB」に関するコードも削除します。
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
「res/menu/menu_main.xml」で「settings item」を以下のように「Logout item」に置きかえます。
<item android:id="@+id/action_logout"
android:orderInCategory="100"
android:title="@string/action_logout"
app:showAsAction="never"></item>
MainActivityを開き、onOptionsItemSelected(MenuItem)でaction_settingsの「id」をaction_logoutの「id」に置きかえます。
if (id == R.id.action_logout) {
return true;
}
ユーザーがログアウトするためのメニューアイテムです。
「content_main.xml」を以下のように変更します。
< ?xml version="1.0" encoding="utf-8"?>
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".MainActivity"
tools:showIn="@layout/activity_main"
android:orientation="vertical">
<listview android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</listview>
</linearlayout><linearlayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom">
<edittext android:id="@+id/todoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"></edittext>
<button android:id="@+id/addButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_item"></button>
</linearlayout>
「activity_main.xml」にはcontent_main.xmlをロードするincludeタグがあります。このアプリではTo Doアイテムがリストビューの形で表示され、画面の大部分を占めます。画面の下部にテキスト編集フィールドとリストにアイテムを追加するためのボタンがあります。
アプリのUI(ユーザーインターフェイス)をセットアップしたところで、Firebaseにデータを保存し、そこからデータを検索する方法を説明していきます。
セキュリティとルール
Firebaseサーバーでデータを検索し保存する前に認証の設定と、データへのアクセスを制限し保存前にユーザー入力を検証するルールの追加が必要です。
認証
Firebase APIにはメール/パスワード(email/password)、Facebook、Twitter、GitHub、Google、anonymous認証用のビルトインメソッドがあります。記事ではEメールアドレスとパスワードでの認証を使います。
Firebase consoleで作成中のプロジェクトをクリックし、プロジェクトのダッシュボードを開きます。
左のウィンドウから「Database(データベース)」を選択すると、JSONフォーマットでプロジェクトのデータを確認できます。
FirebaseではデータベースのすべてのデータをJSONオブジェクトとして格納します。テーブルやレコードはありません。JSONのツリーにデータを追加すると、データは既存のJSON構造内のキーとなります。
この時点ではルートノードのみ確認できます。
ノードの上にマウスポインターを持っていくと「+」と「x」のコントロールが表示され、「+」でツリーにデータを追加でき、「x」でノードを削除できます。
左のウィンドウで「Auth(認証)」をクリックし右側の「Sign In Method(ログイン方法)」タブを選択します。所定のプロバイダーの「Email/Password(メール/パスワード)」認証を有効にしてください。
Android StudioでMainActivityに以下のメソッドを追加します。
private void loadLogInView() {
Intent intent = new Intent(this, LogInActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
Login画面に移動しactivityスタックをクリアするメソッドです。ユーザーはLogin画面で「Back(戻る)」ボタンを押してもメインアクティビティに戻れなくなります。
以下の変数をクラスに追加します。
private FirebaseAuth mFirebaseAuth;
private FirebaseUser mFirebaseUser;
次にonCreate()メソッドの末尾に以下を追加します。
// Initialize Firebase Auth
mFirebaseAuth = FirebaseAuth.getInstance();
mFirebaseUser = mFirebaseAuth.getCurrentUser();
if (mFirebaseUser == null) {
// Not logged in, launch the Log In activity
loadLogInView();
}
これでログイン済みのユーザーをチェックできます。ユーザーがログインしていない場合、getCurrentUser()メソッドはnullを返し、ログイン済みの場合、そのユーザーについての詳細を含むFirebaseUserオブジェクトを返します。ユーザーがログインしていない場合、loadLogInView()メソッドが呼び出されてユーザーにログイン画面を表示します。
アプリを起動すると、ログインページにリダイレクトされます。
以下をLogInActivityに追加します。
protected EditText emailEditText;
protected EditText passwordEditText;
protected Button logInButton;
protected TextView signUpTextView;
private FirebaseAuth mFirebaseAuth;
LogInActivityクラスのonCreate()メソッドを次のように変更します。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_log_in);
// Initialize FirebaseAuth
mFirebaseAuth = FirebaseAuth.getInstance();
signUpTextView = (TextView) findViewById(R.id.signUpText);
emailEditText = (EditText) findViewById(R.id.emailField);
passwordEditText = (EditText) findViewById(R.id.passwordField);
logInButton = (Button) findViewById(R.id.loginButton);
signUpTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(LogInActivity.this, SignUpActivity.class);
startActivity(intent);
}
});
logInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String email = emailEditText.getText().toString();
String password = passwordEditText.getText().toString();
email = email.trim();
password = password.trim();
if (email.isEmpty() || password.isEmpty()) {
AlertDialog.Builder builder = new AlertDialog.Builder(LogInActivity.this);
builder.setMessage(R.string.login_error_message)
.setTitle(R.string.login_error_title)
.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.create();
dialog.show();
} else {
mFirebaseAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(LogInActivity.this, new OnCompleteListener<authresult>() {
@Override
public void onComplete(@NonNull Task</authresult><authresult> task) {
if (task.isSuccessful()) {
Intent intent = new Intent(LogInActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(LogInActivity.this);
builder.setMessage(task.getException().getMessage())
.setTitle(R.string.login_error_title)
.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.create();
dialog.show();
}
}
});
}
}
});
}
このコードで画面の要素とFirebaseの認証SDKのエントリポイント「FirebaseAuthオブジェクト」を開始します。タップするとログインactivityが開くログインテキストビューにイベントリスナーを追加します。ログインボタン上の別のイベントリスナーは、ユーザー入力の検証を実行し、ユーザーが両方のフィールドに入力したことを確認します。次にsignInWithEmailAndPassword()でFirebaseサーバーを呼び出します。この関数はユーザーのEメールアドレスとパスワードを取得してAuthResultのTaskを返します。Taskが成功であればユーザーがメインアクティビティにリダイレクトされ、そうでなければエラーメッセージが表示されることを確認してください。以下は登録済みでないユーザーがログインしようとした場合に表示されるエラーメッセージです。
アプリでtask.getException().getMessage()メソッドで返されるものより良いメッセージをユーザーに表示したい場合もあります。返される「Exception(例外)」を確認してユーザーに表示するエラーメッセージを決定できます。認証が失敗した場合、次の例外処理から1つを選べます。
- FirebaseAuthInvalidUserException:Eメールアドレスに対応するユーザーアカウントが存在しないか無効である場合に返される
- FirebaseAuthInvalidCredentialsException:パスワードが正しくない場合に返される
signInWithEmailAndPassword()メソッド以外に、次のメソッドもユーザーログインに使用できます。
- signInWithCredential(AuthCredential)メソッド:ユーザーは所定のAuthCredentialでログインする。このメソッドを使ってユーザーはFirebase認証システムにログインできる。最初の証明書は直接ユーザーから、あるいはEmailAuthCredentialの場合Google Sign-InやFacebookなど、サポートされている認証SDKから取得できる
- signInAnonymously()メソッド:ユーザーは匿名でログインでき、証明書は必要ない。アプリにすでに匿名のユーザーがログイン済みの場合を除き、このメソッドはFirebase認証システムに新規のアカウントを作成する
- signInWithCustomToken(String)メソッド:ユーザーは所定のカスタムトークンでログインする。サーバーからFirebase認証のカスタムトークンを取得後、ユーザーはこのメソッドを使ってFirebase認証システムにログインできる
Login(ログイン)機能が実装されたら、Sign Up(サインアップ)を設定します。
SignUpActivityを次のように変更します。
package com.echessa.todo;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
public class SignUpActivity extends AppCompatActivity {
protected EditText passwordEditText;
protected EditText emailEditText;
protected Button signUpButton;
private FirebaseAuth mFirebaseAuth;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sign_up);
// Initialize FirebaseAuth
mFirebaseAuth = FirebaseAuth.getInstance();
passwordEditText = (EditText)findViewById(R.id.passwordField);
emailEditText = (EditText)findViewById(R.id.emailField);
signUpButton = (Button)findViewById(R.id.signupButton);
signUpButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String password = passwordEditText.getText().toString();
String email = emailEditText.getText().toString();
password = password.trim();
email = email.trim();
if (password.isEmpty() || email.isEmpty()) {
AlertDialog.Builder builder = new AlertDialog.Builder(SignUpActivity.this);
builder.setMessage(R.string.signup_error_message)
.setTitle(R.string.signup_error_title)
.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.create();
dialog.show();
} else {
mFirebaseAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(SignUpActivity.this, new OnCompleteListener</authresult><authresult>() {
@Override
public void onComplete(@NonNull Task</authresult><authresult> task) {
if (task.isSuccessful()) {
Intent intent = new Intent(SignUpActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(SignUpActivity.this);
builder.setMessage(task.getException().getMessage())
.setTitle(R.string.login_error_title)
.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.create();
dialog.show();
}
}
});
}
}
});
}
}
createUserWithEmailAndPassword()メソッドは与えられたEメールアドレスとパスワードで新規のユーザーアカウントを作成します。成功するとユーザーはアプリにログインできます。AuthResultのTaskはこのオペレーションの結果を返します。登録が成功するとユーザーはメインアクティビティにリダイレクトされ、そうでない場合エラーメッセージが表示されることを確認してください。アプリでスローするexception(例外)を確認し、ユーザーに表示するエラーメッセージを決められます。アカウント作成でのエラー時にスローされる利用可能な例外は以下のとおりです。
- FirebaseAuthWeakPasswordException – パスワードの強度が十分でない場合
- FirebaseAuthInvalidCredentialsException – Eメールアドレスが誤っている場合
- FirebaseAuthUserCollisionException – 与えられたEメールアドレスのアカウントがすでに存在する場合
アプリを起動してログイン画面で「Sign Up(サインアップ)」のテキスト表示をタップすると、Sign Up(サインアップ)画面が開きます。
アカウントを登録します。Firebaseには、パスワードは6文字以上など、デフォルト設定があります。
無効な形式の「Eメールアドレス」でログインしようとすると、次のエラーが出ます。
登録が成功すると、メインアクティビティが表示されます。Firebase Consoleで「Auth」内の「Users(ユーザー)」を開くと登録済みのユーザーが確認できます。
Firebaseチームは、認証を追加し共通UI要素をFirebaseデータベースに接続するプロセスを簡素化するオープンソースライブラリー「FirebaseUI」を構築しました。このライブラリーの詳細についてはドキュメントを参照してください。
認証とデータバリデーション
ユーザーの識別はセキュリティの一部に過ぎません。ユーザーが特定できたら、Firebaseデータベースにおけるデータへのアクセス制御が必要になります。
FirebaseにはFirebaseサーバー上で適用され、アプリでのセキュリティ(レベル)を決定するルールを指定する宣言型言語があります。「Database」の「Rules(ルール)」タブでルールを編集できます。
セキュリティルールによってデータベース各部へのアクセス制御ができます。Firebaseにはデフォルトでユーザーに認証を要求するセキュリティルールが搭載されています。
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
FirebaseデータベースのルールはJavaScriptライクな構文になっており、4つのタイプがあります。
- .read – ユーザーにデータの読み出しが許可されているかどうかを示す
- .write – ユーザーにデータの書き込みが許可されているかどうかを示す
- .validate – 適正な形の値の様態、子要素の有無、データタイプを定義する
- .indexOn – 子(キー)を指定してインデックスし、オーダー(順序付け)とクエリをサポートする
.readと.writeで設定されたルールは下位層に伝播するため、次のルールセットによってパス/foo/(ノードfooと呼ばれる場合もある)でデータのリードアクセス権限が与えられると、たとえば/foo/bar/bazなど下位層のパスでも同様に権限が与えられます。ちなみに上位層での.readと.writeルールはデータベースで下位層のルールをオーバーライドするため、この例ではパス/foo/bar/bazをfalseに設定しても、依然として/foo/bar/bazへのリードアクセスが許可されてしまいます。
{
"rules": {
"foo": {
".read": true,
".write": false
}
}
}
.validateルールは下位層には伝播しません。
ルールを下のように変更して「Publish(公開)」をクリックします。
{
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid",
"items": {
"$item_id": {
"title": {
".validate": "newData.isString() && newData.val().length > 0"
}
}
}
}
}
}
}
上のルールではauth != null && auth.uid == $uidとすることにより、usersノード(およびその子ノード)でのデータの読み書きに関し、uidがログイン済みのユーザーID(auth.uid)と一致する場合のみ許可されるように制限しています。$uidはこのノードで値を保持する変数であり、ノード名ではありません。このルールでユーザーはこのノードとその子ノードでのデータの読み書きに認証を求められるだけでなく、ユーザーが所有するデータにのみアクセスできるという制限を加えられます。
Firebaseデータベースのルールにはサーバーサイドのタイムスタンプ、認証情報などのパスを参照できるビルトイン変数と関数が搭載されています。こうした変数と関数を使って多様なルールを構築できます。これらの変数と関数によってルールは強力で柔軟性のあるものとなり、サーバーサイドのタイムスタンプなどさまざまなパスの参照も可能になります。
アプリのルールで、ビルトイン変数authを使えます。この変数はユーザー認証後に利用できるようになり、異なるプロバイダー間で有効な一意の英数字識別子auth.uidを含むユーザー関連データを格納しています。
以下の変数が利用可能です。
- now – UNIX時間ベースの現在時刻(ミリ秒)。特にSDKのfirebase.database.ServerValue.TIMESTAMPで作成されたタイムスタンプの検証に適している
- root – オペレーション試行前に存在しているFirebaseデータベース内のルートパスを示すRuleDataSnapshot
- newData – オペレーション試行後に存在するであろうデータを示すRuleDataSnapshot。これには書き込まれた新しいデータと既存のデータが含まれる
- data – オペレーション試行前に存在したデータを示すRuleDataSnapshot
- $variables – IDと動的な子キーを示すワイルドカードパス
- auth – 認証済みユーザーのトークンペイロード
FirebaseではJSONフォーマットでデータが格納されます。データベースでは各ユーザーがitemsという名前の「to-do」アイテムの配列を持つことになります。各itemsにはtitleがあります。上のコードでは「/users//items」に書き込まれるデータが1文字以上の文字列であることを確認するデータバリデーションを追加しています。こうしておくとtitleが空のままitemsが保存されることはありません。
必ず「公開」ボタンをクリックしてください。そうしないとルールが保存されません。「Rules published(ルールを公開しました)」というメッセージを確認してください。
バリデーションでの「ルール」は素晴らしいものですが、アプリでのデータバリデーションコードの代わりになるわけではありません。アプリで入力を検証しパフォーマンスを改善する必要は依然としてあります。
データの保存と検索
MainActivityに次の変数を追加します。
private DatabaseReference mDatabase;
private String mUserId;
次に以下のようにonCreate()メソッドを変更します。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Initialize Firebase Auth and Database Reference
mFirebaseAuth = FirebaseAuth.getInstance();
mFirebaseUser = mFirebaseAuth.getCurrentUser();
mDatabase = FirebaseDatabase.getInstance().getReference();
if (mFirebaseUser == null) {
// Not logged in, launch the Log In activity
loadLogInView();
} else {
mUserId = mFirebaseUser.getUid();
// Set up ListView
final ListView listView = (ListView) findViewById(R.id.listView);
final ArrayAdapter<string> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
listView.setAdapter(adapter);
// Add items via the Button and EditText at the bottom of the view.
final EditText text = (EditText) findViewById(R.id.todoText);
final Button button = (Button) findViewById(R.id.addButton);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mDatabase.child("users").child(mUserId).child("items").push().child("title").setValue(text.getText().toString());
text.setText("");
}
});
// Use Firebase to populate the list.
mDatabase.child("users").child(mUserId).child("items").addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
adapter.add((String) dataSnapshot.child("title").getValue());
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
adapter.remove((String) dataSnapshot.child("title").getValue());
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
}
FirebaseDatabase.getInstance().getReference()メソッドでデータベースのルートノードの参照を作成します。次に、クリックするとFirebaseにデータが保存される「Add New Item(新しいアイテムを追加)」ボタンにリスナーを設定します。
Firebaseリアルタイムデータベースへのデータの書き込みには4つのメソッドがあります。
- setValue() – users/<user -id>/<username>など所定のパスへのデータの書き込みまたは置換
- push() – データリストへの追加。push()メソッドが呼び出されるたびにFirebaseはuser-posts/<user -id>/<unique -post-id>のような一意識別子として使用できる一意のキーを生成する
- updateChildren() – すべてのデータを置換することなく、所定のパスの一部のキーを更新する。
- runTransaction() – 同時更新で破損するおそれのある複合データを更新する
下のコードでデータが保存されます。
mDatabase.child("users").child(mUserId).child("items").push().child("title").setValue(text.getText().toString());
.childは指定されたノードが存在する場合そのノードを参照し、存在しない場合ノードを作成します。上のコードは/users/<user id>/items/<item id>/titleパスに入力されたテキストを保存します。.push()メソッドで一意のキーを使用する新しい子locationを生成します。それを使ってitemが追加されるたびにその一意のキーを生成します。.setValue()メソッドでは定義されたパスへのデータの書き込みや置換を実行します。
Firebaseからデータを検索するには、addChildEventListener()メソッドでdatabase referenceにリスナーを追加します。次のタイプのデータ検索イベントを使ってリッスンできます。
- ValueEventListenerクラスのonDataChange()メソッド – パスの内容全体の変化に対して読み出しとリッスンを実行する
- ChildEventListenerクラスのonChildAdded()メソッド – アイテムのリストを検索するか、またはアイテムのリストへの追加についてリッスンする。リストへの変更の監視にはonChildAdded()メソッド、onChildRemoved()メソッドとの併用が提案されている
- ChildEventListenerクラスのonChildChanged()メソッド – リスト内のアイテムへの変更についてリッスンする。onChildAdded()メソッド、onChildRemoved()メソッドと併用してリストへの変更を監視する
- ChildEventListenerクラスのonChildMoved()メソッド – リストから削除されるアイテムについてリッスンする。onChildAdded()メソッド、onChildChanged()メソッドと併用してリストへの変更を監視する
- ChildEventListenerクラスのonChildMoved()メソッド – 配列されたリスト内のアイテムの順序変更についてリッスンする。イベントはアイテムの順序を変更(現在のorder-byメソッドに基づいて)するイベントに後続する
このシンプルなアプリではonChildRemoved()メソッドでアイテムを削除でき、onChildAdded()メソッドでリストにアイテムを追加できます。
リスナーはデータのsnapshot(スナップショット)「DataSnapshot」を受け取ります。snapshotとはFirebaseデータベースにおける特定の位置の単一ポイントにおけるその時点のデータのスナップショットです。snapshotでgetValue()メソッドを呼び出すと、データがJavaオブジェクト形式で返されます。getValue()メソッドで返されるタイプとしてBoolean、String、Long、Double、Map<string , Object>、List<object>が可能です。その場所にデータが存在しない場合、snapshotはnullを返します。それでデータの使用前にnullが返ってくるか確認することをお勧めします。最後に検索したデータをリストビューに追加します。
アプリを起動していくつかのアイテムを追加してみてください。アイテムがリストとデータベースに追加されます。
ノード上の緑色の「+」コントロールをクリックしてデータを入力すると、Firebaseコンソールからデータを追加できます。必ず正しいフォーマットでデータを入力します。そうしないとデータの読み出し時にAndroidアプリがクラッシュしてしまいます。製品用のアプリでは、不正なデータが入力された場合でもアプリが決してクラッシュしないようバリデーションを追加する必要があります。
コンソールからアイテムを追加するには、アイテムのノード上で「+」をクリックし、「name(名前)」フィールドに次のように入力します。末尾に「/title」をつけてフルパスを入力してください。
/123/title
「value(値)」のフィールドに「This was added using the console」と入力して「Save(追加)」をクリックします。
データにアイテムが追加され、Androidアプリを開くとリストが自動的に更新されてこのアイテムが入っているはずです。
このアプリではサーバーにStringを送信しています。これでうまくいきますが、もっと複雑なアプリでは、モデルオブジェクトはもっと複雑になります。
FirebaseではカスタムJavaオブジェクトをDatabaseReference.setValue()に渡し、DataSnapshot.getValue()でオブジェクトにデータを読み出せますが、オブジェクトを定義するクラスに、引数を取らないコンストラクタと代入するプロパティのパブリックゲッター(public getter)が存在していることが条件となります。この点についての詳細はドキュメントで確認してください。
実際に確認するため、Itemという名前のクラスを作成し、そのクラスを次のように変更します。
package com.echessa.todo;
/**
* Created by echessa on 8/27/16.
*/
public class Item {
private String title;
public Item() {}
public Item(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
「Add New Item」ボタンのクリック時のリスナーを次のように変更します。
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Item item = new Item(text.getText().toString());
mDatabase.child("users").child(mUserId).child("items").push().setValue(item);
text.setText("");
}
});
ここではItemオブジェクトを使ってデータベースにデータを保存しています。Itemのコンテンツは子ロケーションにネスト形式でマッピングされます。アプリを起動すると、リストへのデータの追加と、サーバーコンソールでの保存済みのデータの確認ができるようになっているはずです。
データの削除
アプリでデータを保存し、データを検索してリストビューに表示できるようになりました。次にユーザーがリストとFirebaseからアイテムを削除できるようにする必要があります。
MainActivityクラスのonCreate()メソッドでelseブロックの末尾に次のコードを追加します。
// Delete items when clicked
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView< ?> parent, View view, int position, long id) {
mDatabase.child("users").child(mUserId).child("items")
.orderByChild("title")
.equalTo((String) listView.getItemAtPosition(position))
.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.hasChildren()) {
DataSnapshot firstChild = dataSnapshot.getChildren().iterator().next();
firstChild.getRef().removeValue();
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
});
このコードでリストビューにonclickリスナーを設定し、アイテムのタップ時にデータベースをクエリします。タップ位置にある文字列と同じタイトルのアイテムをFirebaseデータベースで検索します。もっと複雑なアプリでは、IDなどオブジェクトに固有のもので検索したい場合もあります。そして最初に現れたアイテムがデータベースから削除されます。リストビューは自動的に更新されます。
Androidアプリを起動してアイテムをタップすると、そのアイテムをFirebaseから削除できます。
ユーザーのログアウト
signout()メソッドが呼び出されるとユーザーのトークンが無効になり、ユーザーはアプリからログアウトします。onOptionsItemSelected()メソッドに次の行を追加すると、ユーザーをログアウトさせログイン画面を表示します。
if (id == R.id.action_logout) {
mFirebaseAuth.signOut();
loadLogInView();
}
Data Droid
記事ではFirebaseを使ってAndroidアプリのユーザーデータを管理する方法を紹介しました。これは入門編でFirebaseにはここで取り上げていない機能がまだまだあり、ドキュメントでさらに調べられます。
※この記事はFirebaseプラットホームの変更を反映して2016年9月に更新されました。
(原文:Creating a Cloud Backend for Your Android App Using Firebase)
[翻訳:新岡祐佳子/編集:Livit]