iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Mobile Development

30天建立寵物約散App-Android新手篇系列 第 5

【Day5】註冊畫面 X Firestore Database

昨天我們已經把登入畫面做好了,大家有沒有覺得萬事起頭難呢? 既然我們已經有登入畫面了,當然要有註冊畫面啦,否則我們永遠登不進去畫面~ 那麼就開始啦!

先給大家看長相

https://ithelp.ithome.com.tw/upload/images/20210921/20138017ozUyUkh27p.png

註冊畫面

1.首先我們要建立一個Fragment,並且命名為 RegisterFragment,繼承BaseFragment

2.建立 layout

2.1 先建立 dimen/string
2.2 去 layout

dimen

<dimen name="toolbar_textSize">18sp</dimen>

string

    <string name="toolbar_title_register_account">註冊帳號</string>
    <string name="hint_enter_your_name">請輸入您的姓名</string>
    <string name="hint_enter_again_password">請再次輸入您的密碼</string>
    <string name="hint_do_not_enter_same_password">請再確認密碼是否一致</string>
    <string name="register_already_have_account">我已經註冊囉!</string>
    <string name="register_pick_me_to_login">點我登入</string>
	<string name="register_success">恭喜您註冊成功!</string>

2.2 直接貼Code

<?xml version="1.0" encoding="utf-8"?>


<layout 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">


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.fragment.RegisterFragment">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar_register_fragment"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:background="@color/light_pewter_blue"
            app:layout_constraintTop_toTopOf="parent">


            <com.example.petsmatchingapp.utils.JFTextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:textColor="@color/white"
                android:text="@string/toolbar_title_register_account"
                android:textStyle="bold"
                android:gravity="center"
                android:textSize="@dimen/toolbar_textSize"/>
        </androidx.appcompat.widget.Toolbar>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toBottomOf="@id/toolbar_register_fragment"
            app:layout_constraintBottom_toBottomOf="parent">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">



                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_register_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    android:layout_marginStart="@dimen/tip_margin_start_end"
                    android:layout_marginEnd="@dimen/tip_margin_start_end"
                    android:layout_marginTop="@dimen/tip_margin_top_bottom">


                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_register_name"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:padding="@dimen/edText_padding"
                        android:inputType="textPersonName"
                        android:hint="@string/hint_enter_your_name"
                        android:textSize="@dimen/edText_textSize"/>

                </com.google.android.material.textfield.TextInputLayout>

                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_register_email"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/tip_register_name"
                    android:layout_marginStart="@dimen/tip_margin_start_end"
                    android:layout_marginEnd="@dimen/tip_margin_start_end"
                    android:layout_marginTop="@dimen/tip_margin_top_bottom">


                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_register_email"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:padding="@dimen/edText_padding"
                        android:inputType="textEmailAddress"
                        android:hint="@string/hint_enter_your_email"
                        android:textSize="@dimen/edText_textSize"/>

                </com.google.android.material.textfield.TextInputLayout>


                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_register_password"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/tip_register_email"
                    android:layout_marginStart="@dimen/tip_margin_start_end"
                    android:layout_marginEnd="@dimen/tip_margin_start_end"
                    android:layout_marginTop="@dimen/tip_margin_top_bottom">


                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_register_password"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:padding="@dimen/edText_padding"
                        android:inputType="numberPassword"
                        android:hint="@string/hint_enter_your_password"
                        android:textSize="@dimen/edText_textSize"/>

                </com.google.android.material.textfield.TextInputLayout>


                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_register_again_password"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/tip_register_password"
                    android:layout_marginStart="@dimen/tip_margin_start_end"
                    android:layout_marginEnd="@dimen/tip_margin_start_end"
                    android:layout_marginTop="@dimen/tip_margin_top_bottom">


                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_register_password_again"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:padding="@dimen/edText_padding"
                        android:inputType="numberPassword"
                        android:hint="@string/hint_enter_again_password"
                        android:textSize="16sp"/>

                </com.google.android.material.textfield.TextInputLayout>



                <com.example.petsmatchingapp.utils.JFButton
                    android:id="@+id/btn_register"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="16dp"
                    android:layout_marginEnd="16dp"
                    app:layout_constraintTop_toBottomOf="@id/tip_register_again_password"
                    android:layout_marginTop="@dimen/tip_margin_top_bottom"
                    android:text="@string/register"
                    android:background="@drawable/button_background"
                    android:foreground="?attr/selectableItemBackground"
                    android:textColor="@color/white"/>


                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    app:layout_constraintTop_toBottomOf="@id/btn_register"
                    android:layout_marginTop="20dp"
                    android:gravity="center"
                    android:orientation="horizontal"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent">

                    <com.example.petsmatchingapp.utils.JFTextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:textSize="@dimen/hint_word_textSize"
                        android:text="@string/register_already_have_account"/>

                    <com.example.petsmatchingapp.utils.JFTextView
                        android:id="@+id/tv_register_login"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:textSize="@dimen/hint_word_textSize"
                        android:textStyle="bold"
                        android:text="@string/register_pick_me_to_login"/>


                </LinearLayout>


            </androidx.constraintlayout.widget.ConstraintLayout>

        </ScrollView>


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3.回到 RegisterFragment

稍微看了一下我們的layout後會發現,我們會有好幾個editText,代表我們要確認使用者是否輸入為空,太棒了,我們就可以用之前在LoginFragment的方式來check

private fun validDataForm(): Boolean{

        return when{

            TextUtils.isEmpty(binding.edRegisterName.text.toString().trim()) -> {
                showSnackBar("請輸入名稱",true)
                return false
            }
            TextUtils.isEmpty(binding.edRegisterEmail.text.toString().trim()) -> {
                showSnackBar("請輸入有效的Email",true)
                return false
            }
            TextUtils.isEmpty(binding.edRegisterPassword.text.toString().trim()) -> {
                showSnackBar("請輸入有效的密碼",true)
                return false
            }
            TextUtils.isEmpty(binding.edRegisterPasswordAgain.text.toString().trim()) -> {
                showSnackBar("請輸入有效的確認密碼",true)
                return false
            }

            binding.edRegisterPasswordAgain.text.toString().trim() != binding.edRegisterPassword.text.toString().trim() ->{
                showSnackBar("請再確認密碼是否一致",true)
                return false
            }

            else -> true
            
        }
    }

這邊需要跟大家稍微說明一下,我們 sign in 的時候帳號資訊會在Authentication裡面,而每一個帳號也可以去設定userName跟photoUri,但是因為我們除了這些外,我們還會需要有其他user資訊。所以我們這次只在auth儲存 email+password,其他的資訊都儲存在 Firestore database。

首先我們需要來創造一個data class,並且命名為 User

//每個都設預設值,好讓我們在創立 Object的時候,可以不用全部都指定
data class User(
	val id: String = "",
    val name: String = "",
    val email: String = "",
    val password: String = "",
    val image: String = "",
    val gender: String = "",
    val profileCompleted: Boolean = false
)

在RegisterFragment一樣去繼承 View.OnClickListener,並且override onClick

override fun onClick(v: View?) {

//透過 when,來設定當user點擊時的回饋方式

when(v){

binding.btnRegister ->{

//如果validDataForm 回饋的為 true
if(validDataForm()){

showDialog(resources.getString(R.string.please_wait))

//實例化 user,並且把 edText拿到的資料給它。
val user = User(
										name = binding.edRegisterName.text.toString().trim(),
                    email = binding.edRegisterEmail.text.toString().trim(),
                    password = binding.edRegisterPassword.text.toString().trim(),
)

//並把 fragment + user 丟給  viewModel
accountViewModel.registerWithEmailAndPassword(this,user)
            }
        }
    }
}

一樣,因為要叫出 AccountViewModel,所以我們要在最上面新增

private val accountViewModel: AccountViewModel by sharedViewModel()

以及因為我們用override OnClick,所以我們也要在 onCreateView裡面設定

binding.btnRegister.setOnClickListener(this)

好啦,我們要回到 AccountViewModel


//把 RegisterFragment跟 user傳進去

fun registerWithEmailAndPassword(fragment: RegisterFragment, user: User){

//直接拿 auth的instance,並且透過 email跟 password來辦帳號
FirebaseAuth.getInstance().createUserWithEmailAndPassword(user.email,user.password)
.addOnSuccessListener{
												
		val newUser = User(
                            name = user.name,
                            email = user.email,
                            password = user.password,
                            id =it.user!!.uid
                    )

addUserDetailsToFireStore(fragment,newUser)

        }
.addOnFailureListener{
    fragment.registerFail(it.toString())
        }
}

★ 注意,我們在createUserWithEmailAndPassword的funtion後面新增的 addOnSuccessListener,它會傳回一個 AuthResult,我們可以直接透過它去拿到該帳號的使用者id,我們再透過這個id,去設定firestore database的 document id,我們把兩個id設為一樣,可以幫助我們找尋資料。

好的,寫完上面的 Code後會發現,我們有紅字,那我們現在要解決的就是在 Firebase Auth創號帳號後,要再把其他資訊在Firestore database 儲存資料。

private fun addUserDetailsToFireStore(fragment: RegisterFragment,user: User){

//這邊創立 Firestore的 instance,並且collection內指定集合為user,集合裡面填string,我們用Constant來確保每次呼叫都不會拼錯字

 FirebaseFirestore.getInstance().collection(Constant.USER)
//這邊指定 document的id為剛剛auth回傳的 uid
.document(user.id)
.set(user, SetOptions.merge())
.addOnSuccessListener{
fragment.registerSuccessful()

}
.addOnFailureListener{
fragment.registerFail(it.toString())
	}
}

Firestote有兩種新增資料的方法

  • add: Firebase會自動生成document 的ID
  • set:需要為document 指定ID,指定的ID如果沒有,就會新增,一般預設若指定ID跟已有的ID重複,則會覆蓋,可以透過 merge來達到合併的功能。

至於說剛剛的Constant怎麼寫呢?

在創立 class的地方,我們創立一個Object,並且命名為 Constant
並在裡面新增名為 USER的變數即可,就可以在任何地方呼叫它。

object Constant{

const val USER: String = "user"

}

接下來,我們回到 RegisterFragment,並且新增把資料上傳到Firestore成功或失敗後會跑的funtion

fun registerSuccessful(){
hideDialog()
showSnackBar(resources.getString(R.string.register_success),false)
//當成功上傳後,跳轉到 loginFragment
nav.navigate(R.id.action_registerFragment_to_loginFragment)
}

fun registerFail(e: String){
hideDialog()
//失敗則show 錯誤訊息
showSnackBar(e,true)
}

我們可以看到 nav.navigate(R.id.action_registerFragment_to_loginFragment) 是紅字。

首先我們要把nav這個解決,之前提到我們可以透過findNavController,來控制Fragment跳轉的事件~

  1. 同樣在 RegisterFragment的class下面新增延遲初始化的nav變數
  2. 並且在 onCreateView指定 nav = findNavController()

再過來要解決後面的 R.id的紅字,這邊的id都是action的id

因為我們目前在 account_nav裡面只有一個Fragment,所以我們要跟昨天交的方式,把RegisterFragment新增進去,並且用連連看的方式把它們連起來。點選圓圈圈,就可以拖移到想要轉換到的Fragment。並且因為我們會從LoginFragment透過textView轉換到RegisterFragment,以及RegisterFragment透過textView轉到LoginFragment,所以我們兩邊都要連起來。

https://ithelp.ithome.com.tw/upload/images/20210920/20138017b3AQVu4Lch.png

這樣就可以啦! 那我們是不是還忘了什麼啊??

沒錯,就是我們 layout裡面有一個 已經登入了的選項,我們當然也要把這個加入到 onClick的地方啊,並且透過 navigation 的方式轉移到 LoginFragment,在onClick新增

binding.tvRegisterLogin -> {
                nav.navigate(R.id.action_registerFragment_to_loginFragment)
            }

並在 onCreateView新增

binding.tvRegisterLogin.setOnClickListener(this)

那再來的步驟,就是我們要把我們的 LoginFragment轉到RegisterFragment
先到LoginFragment,並在 onClick的Funtion 裡面新增

binding.tvRegister -> {
                    nav.navigate(R.id.action_loginFragment_to_registerFragment)
            }

還有在class下面新增

//延遲初始化
private lateinit var nav: NavController

在onCreateView 裡面

//初始化 NavController
nav = findNavController()

//設定 onClickListener
binding.tvRegister.setOnClickListener(this)

接下來要設定在toolbar左上方的回退鍵

1.先新增 vector asset,並且選擇一個往左邊的箭頭
2.在onCreateView寫入以下Code

 
//設定剛剛的icon 
binding.toolbarRegisterFragment.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
  
//設定導航事件  
binding.toolbarRegisterFragment.setNavigationOnClickListener {
            requireActivity().onBackPressed()
        }

然後當然還有到 Firebase平台,點選自己的專案後,到達 Firestroe database,點選建立資料庫,選擇測試模式。

https://ithelp.ithome.com.tw/upload/images/20210920/20138017jVb8Kyx3p8.png

並且需要更改讀取和寫入的規則
https://ithelp.ithome.com.tw/upload/images/20210920/20138017jF2t0q0zPt.png

上面的流程透過 email+password註冊帳號後,可以從Firebase Auth 看到
https://ithelp.ithome.com.tw/upload/images/20210920/201380171Yd0dw8Dhb.png

從 Firestore database 看到
https://ithelp.ithome.com.tw/upload/images/20210920/20138017JGO61mVuDF.png

完成品出來啦!!

Register完成.gif
(請原諒這個動畫是我在還沒新增返回鍵時就拍攝的,所以時間跟完文章後,左上角會有白色的返回鍵,且可以使用 XD)

好啦!! 明天會是輕鬆的部分,如果忘記密碼後怎麼辦呢!!!!
大家糾期待一下啦!!!! へけ


上一篇
【Day4】Navigation導航X註冊畫面X Firebase Auth
下一篇
【Day6】重設密碼頁面X Firebase Auth
系列文
30天建立寵物約散App-Android新手篇30

尚未有邦友留言

立即登入留言