PsyTouSan’s LAB

アプリ開発に関することから、くだらないことまで。

【Android Studio】メモアプリの作成 Firebaseを用いてログイン機能を実装

メール・パスワードによるアカウント認証機能を持つメモアプリを作ります。また、作成しメモはクラウドに保存されるため、他の端末からログインすれば同じメモを見ることができますし、編集もできます。メモアプリにわざわざ認証機能を追加する必要があるのかと考えればそれまでですが、勉強の一環として実践しようと思います。

デモンストレーション

こんな感じのやつを作ります。

f:id:PsyTouSan:20220228204345g:plainf:id:PsyTouSan:20220228204134g:plain
左:ログインとログアウト 右:メモの追加と削除

こんな感じのメモアプリを作成します。ログイン、ログアウトの他にも、アカウント作成やメモの作成といった、基本的な機能はすべて備えたメモアプリの作成です。

Firebaseについて

Firebaseを利用する際は、プロジェクトのセットアップを行い、ここで様々な手順を踏んでようやく利用できるようになります。ただ、この作業自体は、セットアップ時に親切な指示を色々としてくれるので、意外と簡単です。
とりあえず、「firebase」と検索をかけて、トップに出てきたGoogleのサイトにアクセスすると、以下の画像のような画面になると思います。この画面で、「使ってみる」をクリックすると、プロジェクトのセットアップや、プロジェクトの管理を行うFirebaseコンソールに遷移します。

f:id:PsyTouSan:20220222234746p:plain
ここの「使ってみる」をクリック

これがFirebaseコンソールです。ここのプロジェクト作成をクリック。

f:id:PsyTouSan:20220223000830j:plain
Firebaseコンソール

あとは、指示に従ってプロジェクトの作成を完了してください。

Firebase Authentication

今回は、メールアドレスとパスワードによる認証方法を使用するので、sign-in-methodタブから、メール/パスワードをクリックします。

f:id:PsyTouSan:20220223085748p:plain
メール/パスワードをクリック

クリックして、メール/パスワードを有効にして保存してください。

f:id:PsyTouSan:20220223090024p:plain
有効にしたら、保存をクリック

Firebase Authenticationの設定はこれで完了です。続いて、Realtime Databaseの設定をしていきます。

Firebase Realtime Database

Realtime Databaseの画面に入ると、最初には以下の画面が表示されているのでデータベースを作成をクリックします。

f:id:PsyTouSan:20220225124913p:plain:w450
「データベースを作成」をクリック

すると、データベースのロケーションを設定します。ここでは米国(us-central1)を選択します。

f:id:PsyTouSan:20220223092624p:plain:w500
ロケーションの指定

次に、セキュリティルールの設定を行います。とりあえずロックモードでいいです。

f:id:PsyTouSan:20220223093103p:plain:w500
ロックモードで開始

完了したら、右下の「有効にする」をクリックしてデータベースの構築を完了してください。詳しいことは、この記事では述べませんので、これ以上に詳しい手法について知りたい場合は、ご自身で調べてみてください。この記事よりよっぽど分かりやすい記事がたくさんアップされています。
とりあえず、アプリ開発の方に進んでいきます。

アプリの仕様

最初に今回開発するアプリの大まかな仕様について決めていきたいと思います。

画面遷移はActivityResultLauncherを主軸とする

シンプルな画面遷移を行う場合、多くはstartActivityメソッドを使うケースが多いですが、個人的にはActivityResultLauncherの方が好きです。アクティビティ間(フラグメント間)で何かしらの値を渡したい場合、例えば、アクティビティAでユーザが入力した文字列を、アクティビティBへ渡したい場合に活用することができます。さらに、最近になってstartActivityForResult()メソッドが非推奨となった影響もあり、このアプリではActivityResultLauncherをメインで使うことにします。

アクティビティの役割

作成するアクティビティの役割をまとめます。それが以下の通りです。




Javaファイル名

XMLファイル名

役割

MemoListActivity

activity_memo_list

メモのリスト表示

CreateMemoActivity

activity_create_memo

メモの作成

LoginActivity

activity_login

既存アカウントにログイン

CreateAccountActivity

activity_create_account

新たなアカウントの作成

MemoData

無し

メモ一つが持つデータ型

CustomListAdapter

無し

独自のリストアダプタ

アクティビティとは言っていますが、MemoDataとCustomListAdapterはアクティビティではなく、ただのJavaファイルです。

アクションバーの非表示

今回は、アクションバーを非表示にしたいと思います。なので、themes.xmlファイルを開いて以下のように記述してください。

<resources xmlns:tools=...>
    <!-- Base application theme. -->

    //以下の文
    <style name="Theme.MemoApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">

        //省略

    </style>
</resources>

文の最後に「NoActionBar」と追記しているだけです。これで、アプリ画面上部に表示されるアクションバーが表示されなくなります。

レイアウトの作成

まずはレイアウトを作成します。右下のFloatingActionボタンを配置して、それをクリックすることで新たなメモを作成できるようにします。また、Stringデータは面倒だし分かりにくくなると思うので使いません。

activity_memo_list

メモのリスト表示を行うアクティビティのレイアウトファイル(activity_memo_list.xml)です。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".MemoListActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/tool_bar"
            android:layout_width="match_parent"
            android:layout_height="?android:actionBarSize" />

    </com.google.android.material.appbar.AppBarLayout>

    <ListView
        android:id="@+id/memo_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/app_bar" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/add_new_memo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="15dp"
        android:layout_marginBottom="15dp"
        android:src="@drawable/add_24"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

特に難しい部分はありません。非表示にしたツールバーを、9行目から19行目の部分で記述しています。言い換えれば、xmlファイルでこの記述をしない場合はツールバーが表示されなくなります。

activity_create_memo

メモをリストに追加するアクティビティのレイアウトファイルです。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".CreateMemoActivity">

    <EditText
        android:id="@+id/memo_title"
        android:layout_width="0dp"
        android:layout_height="60dp"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="40dp"
        android:layout_marginRight="20dp"
        android:hint="メモのタイトルを入力"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/memo_contents"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:hint="コンテンツを入力"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/memo_title" />

    <Button
        android:id="@+id/add_new_memo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginRight="30dp"
        android:backgroundTint="#adadad"
        android:text="追加"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/memo_contents" />

</androidx.constraintlayout.widget.ConstraintLayout>

custom_memo_card.xml

メモリストに表示されるリストのカード一枚分のレイアウトです。これを応用すれば、自分の思うようなウィジェットを、リスト状に表示することができます。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingBottom="6dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/memo_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Date"
            android:textSize="15sp"
            android:layout_marginLeft="5dp" />

        <TextView
            android:id="@+id/memo_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:text="Sample Title"
            android:textSize="27sp"
            android:maxLines="1"
            android:ellipsize="end"
            android:textAppearance="?android:attr/textAppearance"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/memo_contents"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="sample contents "
            android:textSize="18sp"
            android:maxLines="2"
            android:ellipsize="end"
            android:layout_marginLeft="5dp"
            android:layout_marginBottom="6dp"/>

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

今回の開発で初めて知った、少し便利な関数を紹介します

30行目(android:maxLines)

TextViewに表示できる最大行数を設定することができます。指定した行数を超える文字を表示しようとしても、指定した行数を超えることがなくなります。

31行目(android:ellipsize)

今回は"end"の属性を与えています。この場合は、行数を超える文章を表示させようとすると、最後に「...」が追加されます。ユーザ目線では、「この後も文章が続いているんだな」と思わせることができます。

activity_login.xml

ログインを行うためのアクティビティのレイアウトです。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".LoginActivity">

    <EditText
        android:id="@+id/email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textEmailAddress"
        android:layout_marginTop="40dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:hint="メールアドレスを入力"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:hint="パスワードを入力"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/email" />

    <Button
        android:id="@+id/loginButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="ログイン"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/password" />

    <TextView
        android:id="@+id/createAccount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="※アカウントを新たに作成する"
        android:layout_marginTop="16dp"
        android:textSize="14sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.15"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/loginButton" />

</androidx.constraintlayout.widget.ConstraintLayout>

activity_create_account.xml

アカウント作成を行うためのアクティビティのレイアウトです。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".CreateAccountActivity">


    <EditText
        android:id="@+id/email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textEmailAddress"
        android:layout_marginTop="40dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:hint="メールアドレスを入力"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:hint="パスワードを入力"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/email" />

    <EditText
        android:id="@+id/rePassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:hint="もう一度パスワードを入力"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/password" />

    <Button
        android:id="@+id/createAccount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="アカウントを作成する"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/rePassword" />


</androidx.constraintlayout.widget.ConstraintLayout>

inputType属性を与えることで、テキストボックスに様々な特徴を持たせることができます。例えば、"textPassword"を与えると、入力した文字が中黒で表示されるようになります。

Javaソースコード

続いて、Javaソースコードを紹介します。上記に示したアクティビティの順番通りに紹介します。また、Javaのコードは、できるだけコードが短くなるように基本的にはラムダ式で記述しています。

MemoListActivity.java

//パッケージの宣言部分なので省略
package...

//ただのインポート文の羅列なので省略
import...

public class MemoListActivity extends AppCompatActivity {

    private DatabaseReference reference;
    private FirebaseUser user;
    private String uid;

    //CustomListAdapterについては後述します
    private CustomListAdapter customListAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memo_list);

        setSupportActionBar(findViewById(R.id.tool_bar));
    }

    @Override
    protected void onResume() {
        super.onResume();

        //ログイン状態の確認
        user = FirebaseAuth.getInstance().getCurrentUser();
        if (user == null){

            //ログインしていない場合、ログイン画面へ遷移
            Intent intent = new Intent(MemoListActivity.this, LoginActivity.class);
            resultLauncher.launch(intent);
        } else {
            appSetup();
        }
    }

    private void appSetup(){
        Toolbar toolbar = findViewById(R.id.tool_bar);
        ListView memoListView = findViewById(R.id.memo_list);
        ArrayList<MemoData> memoListItem = new ArrayList<>();

        toolbar.setTitle("Memo List");

        user = FirebaseAuth.getInstance().getCurrentUser();
        assert user != null;
        uid = user.getUid();

        FirebaseDatabase database = FirebaseDatabase.getInstance();
        reference = database.getReference("users").child(uid);

        customListAdapter = new CustomListAdapter(this, R.layout.custom_memo_card, memoListItem);
        memoListView.setAdapter(customListAdapter);

        databaseListener();


        FloatingActionButton addMemo = findViewById(R.id.add_new_memo);

        addMemo.setOnClickListener(v -> {
            Intent intent = new Intent(MemoListActivity.this, CreateMemoActivity.class);
            resultLauncher.launch(intent);
        });

        memoListView.setOnItemLongClickListener((parent, view, position, id) -> {
            MemoData memoData =(MemoData) memoListView.getItemAtPosition(position);
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("メモの削除")
                    .setMessage("選択したメモを削除しますか?")
                    .setPositiveButton("削除",(dialog, which) -> {
                            reference.child("Memo").child(memoData.get_memoTitle()).removeValue((error, ref) -> {
                        databaseListener();
                        Toast.makeText(MemoListActivity.this, "メモを削除しました", Toast.LENGTH_SHORT).show();
                            });
                    })
                    .setNegativeButton("やめる",(dialog, which) -> {})
                    .show();
            return false;
        });
    }

    private void logout(){
        AuthUI.getInstance()
                .signOut(this)
                .addOnCompleteListener(task -> {
                    Intent intent = new Intent(MemoListActivity.this, LoginActivity.class);
                    Toast.makeText(this, "ログアウトしました", Toast.LENGTH_SHORT).show();
                    resultLauncher.launch(intent);
                });
    }

    //画面遷移を行うためのインターフェース
    ActivityResultLauncher<Intent> resultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
        if (result.getResultCode() == LoginActivity.RESULT_CODE_LOGIN){
            Toast.makeText(this, "ログインしました", Toast.LENGTH_SHORT).show();
        }
        if (result.getResultCode() == CreateMemoActivity.RESULT_CODE_CREATE_MEMO){
            Intent intent = result.getData();
            assert intent != null;
            String[] memo = intent.getStringArrayExtra("memo");
            MemoData memoData = new MemoData(uid, memo[0], memo[1], memo[2]);

            //Realtime Databaseにデータを保存する
            reference.child("Memo").child(memo[1]).setValue(memoData).addOnCompleteListener(task -> {
                if (task.isSuccessful()){
                    databaseListener();
                    Toast.makeText(MemoListActivity.this,"メモを追加しました",Toast.LENGTH_SHORT).show();
                }else {
                    Toast.makeText(MemoListActivity.this,"メモの追加に失敗しました",Toast.LENGTH_SHORT).show();
                }
            });
        }
    });

    //ツールバーにオプションメニューを追加する
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.memo_list_tool_bar, menu);
        return true;
    }

    //ツールバーのオプションをクリックしたときの処理
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
            if (item.getItemId() == R.id.logout){
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setTitle("ログアウト確認")
                        .setMessage("ログアウトしますか?")
                        .setPositiveButton("はい", (dialog, which) -> {
                            logout();
                        })
                        .setNegativeButton("やめる", (dialog, which) -> {

                        })
                        .show();
            }
        return super.onOptionsItemSelected(item);
    }

    //データベースの更新を検出する
    private void databaseListener(){
        customListAdapter.clear();
        user = FirebaseAuth.getInstance().getCurrentUser();
        assert user != null;
        uid = user.getUid();

        FirebaseDatabase database = FirebaseDatabase.getInstance();
        reference = database.getReference("users").child(uid);
        reference.child("Memo").addChildEventListener(new ChildEventListener() {
            //データベースに追加された要素があれば実行される
            @Override
            public void onChildAdded(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
                MemoData memoData = snapshot.getValue(MemoData.class);
                customListAdapter.add(memoData);
                customListAdapter.notifyDataSetChanged();
            }

            @Override
            public void onChildChanged(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {

            }

            //データベースに削除された要素があれば実行される
            @Override
            public void onChildRemoved(@NonNull DataSnapshot snapshot) {
                MemoData memoData = snapshot.getValue(MemoData.class);
                customListAdapter.add(memoData);
                customListAdapter.notifyDataSetChanged();
            }

            @Override
            public void onChildMoved(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {

            }

            @Override
            public void onCancelled(@NonNull DatabaseError error) {

            }
        });
    }
}

メインアクティビティ的な立ち位置なので、かなり長くなってしまいました。部分的にコメントを書きましたが、一応少しだけ説明を加えます。

21行目(setSupportActionBar)

activity_memo_list.xmlファイルで追加したツールバーに、オプションメニューを表示させるためのメソッドです。

54行目(customListAdapter)

後述するクラスをインスタンス化しています。このクラスは、リスト表示をになるアダプタを継承したクラスです。そのクラスを用いることでアダプタをカスタマイズしています。

107行目(setValue(object))

Realtime databaseに値をセットします。Realtime databaseを通じて、アカウントごとにメモを保存します。すなわち、ログアウトをしたとしてもメモの内容が消えることはなく、ログインすることで再びメモを表示することができます。また、アカウントごとに保存なので、機種変更やアプリの再インストールなどでデータが消えることもなく、アカウントにログインさえすれば同じ内容のメモを表示することができます。ちなみに、Firebaseコンソールでは以下の画像のように表示されています。

f:id:PsyTouSan:20220227093013j:plain
Firebaseコンソール側の画面
121行目(inflater.inflate)

ツールバーのオプションメニューに表示する内容をメニューファイルを渡しています。このメニューファイルは次の通りです。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/logout"
        android:icon="@drawable/logout_24"
        android:title="Logout"
        app:showAsAction="ifRoom"/>
</menu>

メニューファイルの作成は、「Project」ファイル内の「res」⇒「menu」を右クリックして、「New」⇒「Menu Resource File」から作成できます。
このファイルの5行目のdrawableは、vectorファイルです。vectorファイルの作成は、Android Studioの画面の左上の「File」から「New」⇒「Vector Asset」から作成できます。また、showAsAction属性には"ifRoom"としています。これは、ツールバーにアイコンを表示する余裕がある場合は、そのアイコンを表示し、できない場合はオーバーフローメニューの中に格納するように表示するための属性です。

CreateMemoActivity.java

新たなメモを作成するアクティビティです。MemoListActivityの右下に表示されたボタンから遷移します。

package...

import...

public class CreateMemoActivity extends AppCompatActivity {

    public static final int RESULT_CODE_CREATE_MEMO = 3000;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_create_memo);

        EditText memoTitle = findViewById(R.id.memo_title);
        EditText memoContents = findViewById(R.id.memo_contents);
        Button addButton = findViewById(R.id.add_new_memo);

        addButton.setOnClickListener(v -> {
            //メモタイトルに何も入力されていない場合は、必須項目であることをユーザに知らせる。
            if (!memoTitle.getText().toString().isEmpty()){

                //コンテンツに何も入力されていない場合は「詳細なし」で登録する。
                if (memoContents.getText().toString().isEmpty()) {
                    memoContents.setText("詳細なし");
                }
                String[] memo = {getCurrentDate(), memoTitle.getText().toString(), memoContents.getText().toString()};
                Intent intent = getIntent();

                intent.putExtra("memo", memo);
                setResult(RESULT_CODE_CREATE_MEMO, intent);
                finish();

            } else {
                memoTitle.setError("必須入力の項目です");
            }
        });
    }

    //現在日時を取得するメソッド
    private String getCurrentDate(){
        @SuppressLint
                ("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm");
        Date d = new Date();
        return dateFormat.format(d);
    }
}

こんな感じです。メモの追加は、Addボタンをクリックしたときに追加され、クリック時にタイトルが未記入の場合はメモを追加できないようになっています。ただし、メモの内容が未記入の場合は「詳細なし」として格納されます。またメモの追加時に、その時点での日付を取得し、メモの作成日時としています。

LoginActivity.java

既存のアカウントにログインするためのアクティビティです。また、アカウント作成をするための画面にも遷移することができます。

package...

import...

public class LoginActivity extends AppCompatActivity {

    public static final int RESULT_CODE_LOGIN = 1000;
    private FirebaseAuth mAuth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        activitySetup();
    }

    private void activitySetup(){
        EditText email = findViewById(R.id.email);
        EditText passWord = findViewById(R.id.password);
        Button login = findViewById(R.id.loginButton);
        TextView createAccount = findViewById(R.id.createAccount);

        login.setOnClickListener(v -> {

            //入力欄が空欄でないか確認
            if (!email.toString().isEmpty()) {

                //入力欄が空欄でないか確認
                if (!passWord.toString().isEmpty()) {
                    //ログイン処理の開始
                    mAuth = FirebaseAuth.getInstance();
                    mAuth.signInWithEmailAndPassword(email.getText().toString(), passWord.getText().toString())
                            .addOnCompleteListener(this, task -> {
                                if (task.isSuccessful()) {
                                    Intent intent = getIntent();
                                    setResult(RESULT_CODE_LOGIN, intent);
                                    finish();
                                }else {
                                    Toast.makeText(LoginActivity.this, "ログインに失敗しました", Toast.LENGTH_SHORT).show();
                                }
                            });

                }else {
                    passWord.setError("必須項目です");
                }
            }else {
                email.setError("必須項目です");
            }
        });

        createAccount.setOnClickListener(v -> {
            Intent intent = new Intent(LoginActivity.this, CreateAccountActivity.class);
            resultLauncher.launch(intent);
        });
    }

    ActivityResultLauncher<Intent> resultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
        if (result.getResultCode() == CreateAccountActivity.RESULT_CODE_CREATE_ACCOUNT){
            Intent intent = new Intent(LoginActivity.this, MemoListActivity.class);
            setResult(RESULT_CODE_LOGIN, intent);
            finish();
        }
            });
}
33行目(signInWithEmailAndPassword())

ログイン処理の開始は、33行目のsignInWithEmailAndPasswordメソッドによって行われています。このメソッドの第一引数にメールアドレス第二引数にアカウントのパスワードを渡すことで、ログイン処理が実行されます。ログイン処理が完了した場合は、addOnCompleteListener内の処理が実行されます。そして、task.isSuccessfulメソッドによって、完了したログイン処理が成功したか失敗したかをboolで返してくれます。

CreateAccountActivity

既存のアカウントがない場合は、新たにアカウントを作成する必要があります。ログイン画面から遷移することができます。

package...

import...

public class CreateAccountActivity extends AppCompatActivity {

    public static final int RESULT_CODE_CREATE_ACCOUNT = 2000;

    FirebaseAuth mAuth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_create_account);

        activitySetup();
    }

    private void activitySetup(){
        EditText email = findViewById(R.id.email);
        EditText password = findViewById(R.id.password);
        EditText rePassword = findViewById(R.id.rePassword);
        Button create = findViewById(R.id.createAccount);

        mAuth = FirebaseAuth.getInstance();

        create.setOnClickListener(v -> {
            if (password.getText().toString().equals(rePassword.getText().toString())){

                //ここからアカウント作成の処理が開始される
                mAuth.createUserWithEmailAndPassword(email.getText().toString(), password.getText().toString())
                        .addOnCompleteListener(task -> {
                            if (task.isSuccessful()){
                                Toast.makeText(CreateAccountActivity.this, "アカウントを作成しました!",Toast.LENGTH_LONG).show();
                                setResult(RESULT_CODE_CREATE_ACCOUNT, getIntent());
                                finish();
                            } else {
                                Toast.makeText(CreateAccountActivity.this, "アカウント作成に失敗しました",Toast.LENGTH_LONG).show();
                            }
                        });
            }
        });

    }
}

ログインの場合と似ていますが、これでアカウントが作成することができます。

MemoData

これまでに紹介したソースコードの中にも、ちょこちょこ顔を出していたクラスです。一つのメモに格納するデータを意味しています。

package...

public class MemoData {

    public String _uid,_memoTitle,_memoContents,_date;

    public MemoData(String uid, String date, String memoTitle, String memoContents){
        this._uid = uid;
        this._date = date;
        this._memoTitle = memoTitle;
        this._memoContents = memoContents;
    }

    public MemoData(){}

    public String get_uid() {
        return _uid;
    }

    public String get_date() {
        return _date;
    }

    public String get_memoTitle() {
        return _memoTitle;
    }

    public String get_memoContents() {
        return _memoContents;
    }
    
}

見ての通り、ただのアクセサーメソッドの集まりです。コンストラクタの記述は忘れないようにしてください。
一つのメモに格納させるデータは、ユーザID、日付、メモタイトル、メモの内容の4つです。

CustomListAdapter

ようやく最後です。ArrayAdapterを継承したクラスを作成します。このクラスもこれまでのコード内に何回も顔を出しています。

package...

import...

public class CustomListAdapter extends ArrayAdapter<MemoData> {

    private final int Resource;
    private final List<MemoData>  _MemoData;

    public CustomListAdapter(@NonNull Context context, int resource, List<MemoData> memoData) {
        super(context, resource, memoData);
        Resource = resource;
        _MemoData = memoData;
    }

    @Override
    public int getCount() {
        return super.getCount();
    }

    @Nullable
    @Override
    public MemoData getItem(int position) {
        return super.getItem(position);
    }

    //リストカードに日付、メモタイトル、メモ内容を表示させる
    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {

        final ViewHolder holder = new ViewHolder();
        View v;
        if (convertView != null){
            v = convertView;
        } else{
            v = LayoutInflater.from(getContext()).inflate(Resource, null);
            holder.memoDate = v.findViewById(R.id.memo_date);
            holder.memoTitle = v.findViewById(R.id.memo_title);
            holder.memoContents = v.findViewById(R.id.memo_contents);
        }

        MemoData data = _MemoData.get(position);

        holder.memoDate = v.findViewById(R.id.memo_date);
        holder.memoDate.setText(data.get_date());

        holder.memoTitle = v.findViewById(R.id.memo_title);
        holder.memoTitle.setText(data.get_memoTitle());

        holder.memoContents = v.findViewById(R.id.memo_contents);
        holder.memoContents.setText(data.get_memoContents());

        return v;
    }

    static class ViewHolder{
        TextView memoDate,memoTitle,memoContents;
    }
}

ArrayAdapterを継承することで、独自のリストアダプタを形成することができます。10行目のクラスは、ArrayAdapterを継承している場合は必ず記述しなければなりません。

まとめ

以上で終わりです。長めの記事になってしまいました。ただ、Firebaseを活用すれば、簡単に高クオリティのアプリ開発が可能になりますから、今後も勉強していきたいと思います。今回は、ユーザ認証(Authentication)と、クラウドデータベース(Realtime Database)を利用しましたが、Firebaseはこれ以外にもかなり多くのサービスを展開しているので、それぞれ何ができるのか気になります。今後のアプリ開発次第ではお世話になるかもしれません。