【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)

好啦!! 明天会是轻松的部分,如果忘记密码後怎麽办呢!!!!
大家纠期待一下啦!!!! へけ


<<:  Day 6 | 角色动画制作

>>:  Day20 霓虹灯文字

Angular 转换 API 资料格式 (Adapter)

今天的内容属於设计模式的一种。 当我们从後端接到资料後,有时後资料格式往往不是如我们所想,所以会再加...

这些日子我学到的JavaScript:Day28- AJAX 2

post — 传统表单输入介绍 这个功能常用在注册帐号时,将使用者输入的资料跟资料库做比对,检查是否...

#18 JS: Intro to function

What is function? Simple explanation: when you fin...

Day 12 Compose UI Dialog

今年的疫情蛮严重的,希望大家都过得安好,希望疫情快点过去,能回到一些线下技术聚会的时光~ 今天目标:...

Day 06-大 module 小 module,能够重复使用又好维护的就是好 module

大 module 小 module,能够重复使用又好维护的就是好 module 上一章介绍 modu...