iT邦幫忙

2021 iThome 鐵人賽

DAY 4
0
Mobile Development

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

【Day4】Navigation導航X註冊畫面X Firebase Auth

好的,中秋節連假第二天,大家是吃烤肉吃的不要不要的阿? 那我們今天主要要做的就是關於登入頁面。今天會用到的就是透過Navigation來導航Fragment之間的移動。以及透過Firebase來登入帳號啦!

零.來弄Navigation!

1.首先來新增 Implementation

    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
    implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"

2.接下來到 res右鍵→new→ Android Resource File,來新增一個nav的 xml,並取名account_nav

由於Fragment是依賴在Activity上面,而我們的架構上是分AccountActivity跟主要的Activity,所以我們的Navigation也會有兩個,一個是負責AccountActivity內Fragment的跳轉,另一個則負責主要活動的Fragment跳轉。

https://ithelp.ithome.com.tw/upload/images/20210919/20138017RLbJ8QooJi.png

3.綁定account_nav到 AccountActivity

我們可以把Activity當作一個容器,然後上面會有許多Fragment來顯示,而在預設,Fragment是透過replace來替換的,如果想要用 show或是hide,就要自定義。
我們可以參考這篇文章:https://ithelp.ithome.com.tw/articles/10226275

好的,那接下來我們要到activity_account.xml 的 ConstraintLayout裡面,新增以下

<fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:defaultNavHost="true"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/account_nav" />

★ 我們把這個NavHost用來展示destanation的容器,可以把它想像成最上層,fragment都會是在這個上面來展示,通常會加上app:defaultNaHost="true" 以攔截系統的返回事件。
★app:navGraph="@navigation/account_nav":這個則丟入剛創立好的nav檔案。

一.註冊頁面

1.先創立一個 Fragment,並命名為 LoginFragment

既然我們都已經知道Activity有可能會有許多Fragment,並我們的AccountActivity也會有LoginFragment/RegisterFragment/ForgotAccountFragment這三個Fragment,那要怎麼確定彼此之間的關係以及最開始的起始Fragment呢?! 這邊就是Navigation最厲害的地方啦!!

2.設定loginFragment為起始畫面

Navigation提供了一個很直觀的畫面,可以讓我們直接透過連連看來設定彼此的跳轉關係。

我們直接去剛剛創立好的 account_nav,並且毫不猶豫地點了Design,然後點選紅色圖示中紅色的標示,這邊會顯示你剛剛新增好的LoginFragment,直接點下去,就會自動幫你新增啦!

https://ithelp.ithome.com.tw/upload/images/20210919/20138017XcdNbmywdt.jpg

當你新增完後,你可以點旁邊的code,來確定是否LoginFragment是此Navigation的起始Fragment。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/account_nav"
    app:startDestination="@id/loginFragment">

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.example.petsmatchingapp.ui.fragment.LoginFragment"
        android:label="LoginFragment" />
</navigation>

★我們注意到, app:startDestination 後面目前是loginFragment, 這也很合理,因為目前你只有一個Fragment。同理,之後你若有其他Fragment,只要想要設定該Fragment為初始Fragment,就把這一行的Fragement改成想要起始的即可。

3.設定layout

我們總共會有幾個過程
3-1.建立 dimen/string/color ,方便之後的素材
3-2.創立layout
3-3.設計logo

dimen

<dimen name="login_banner_height">200dp</dimen>
<dimen name="login_logo_width">120dp</dimen>
<dimen name="login_logo_height">120dp</dimen>
<dimen name="login_logo_chinese_textSize">22sp</dimen>
<dimen name="login_logo_chinese_marginTop">50dp</dimen>

<dimen name="edText_padding">16dp</dimen>
<dimen name="tip_margin_start_end">16dp</dimen>
<dimen name="edText_textSize">16sp</dimen>
<dimen name="hint_word_textSize">14sp</dimen>
<dimen name="tip_margin_top_bottom">35dp</dimen>

string

    <string name="login">登入</string>
    <string name="hint_enter_your_email">請輸入信箱</string>
    <string name="hint_enter_your_password">請輸入密碼</string>
    <string name="login_forgot_account">忘記密碼</string>
    <string name="login_don_have_account">還沒有帳號嗎?</string>
    <string name="register">註冊</string>
    <string name="msg_login_successful">登入成功</string>

color

<color name="light_pewter_blue">#8ac0b5</color>
<color name="pewter_blue">#56c0b2</color>

3-2.來建立layout

一樣是要按照我們之前客製字型方式,我們要先製作Edtext跟button這兩個class

button

class JFButton(context: Context, attributeSet: AttributeSet): AppCompatButton(context, attributeSet) {

init{
applyFont()
    }


private fun applyFont(){
        val typeface: Typeface = Typeface.createFromAsset(context.assets,"jfopenhuninn.ttf")
        setTypeface(typeface)
    }

}

edText

class JFEditText(context: Context, attrs: AttributeSet): AppCompatEditText(context, attrs) {


init{
applyFont()
    }


private fun applyFont(){
val typeface: Typeface = Typeface.createFromAsset(context.assets,"jfopenhuninn.ttf")
setTypeface(typeface)
    }
}

LoginFragment長這樣

https://ithelp.ithome.com.tw/upload/images/20210919/20138017Uf9qryUJTc.png

layout就直接貼code,不講解囉!

主要會用到客製化的view,以及客製化button的background,跟設計logo

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



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


    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.fragment.LoginFragment">

        <FrameLayout
            android:id="@+id/fl_login_fragment"
            android:layout_width="match_parent"
            android:layout_height="@dimen/login_banner_height"
            app:layout_constraintTop_toTopOf="parent"
            android:background="@color/light_pewter_blue">


            <ImageView
                android:layout_width="120dp"
                android:layout_height="120dp"
                android:layout_gravity="center"
                android:src="@drawable/app_logo">

            </ImageView>



        </FrameLayout>

        <com.example.petsmatchingapp.utils.JFTextView
            android:id="@+id/tv_login"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="@dimen/login_logo_chinese_textSize"
            android:text="@string/login"
            app:layout_constraintTop_toBottomOf="@id/fl_login_fragment"
            android:gravity="center"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginTop="@dimen/login_logo_chinese_marginTop"
            android:textStyle="bold"/>

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

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



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

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

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

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

        <com.example.petsmatchingapp.utils.JFTextView
            android:id="@+id/tv_forgot_password"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="@dimen/tip_margin_start_end"
            android:layout_marginRight="@dimen/tip_margin_start_end"
            android:textSize="@dimen/hint_word_textSize"
            android:text="@string/login_forgot_account"
            app:layout_constraintTop_toBottomOf="@id/tip_password"/>


        <com.example.petsmatchingapp.utils.JFButton
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/tv_forgot_password"
            android:layout_marginStart="@dimen/tip_margin_start_end"
            android:layout_marginEnd="@dimen/tip_margin_start_end"
            android:foreground="?attr/selectableItemBackground"
            android:text="@string/login"
            android:textColor="@color/white"
            android:background="@drawable/button_background"
            android:layout_marginTop="30dp"/>



        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/btn_login"
            android:orientation="horizontal"
            android:gravity="center"
            android:layout_marginTop="16dp"
            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:text="@string/login_don_have_account"
                android:textSize="@dimen/hint_word_textSize"/>

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



        </LinearLayout>


    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

客製化button,我們如果要給他有圓角,以及有漸層的顏色呢?

我們首先要去 drawable,新建一個 new resource file,在 Root element原本是 selector,我們改成 shape,並命名為- button_background,若是選selector,則可以設定select 控建跟沒按下的樣式。但因為button它自己就有 selectableItemBackground,就是點擊後波紋效果了,所以我們就單純用 shape即可。

button_background 的內容如下

<shape
    xmlns:android="http://schemas.android.com/apk/res/android">


	//漸層
    <gradient
		//開始顏色
        android:startColor="@color/light_pewter_blue"
        //結束顏色
		android:endColor="@color/pewter_blue"
        //從哪個方向開始漸層,倍數是45開始,上下左右
		android:angle="360"
        //渲染的type
		android:type="linear">

    </gradient>

		//角度
    <corners
        android:radius="10dp">

    </corners>

</shape>

新增完後,我們直接去想要有這樣效果的 view,把這個 drawable指定到該background即可。

3-3.製作LOGO

大家可以到這個網頁製作自己喜歡的LOGO https://www.designevo.com/tw/

4.Firebase平台調整授權設定。

接下來我們要先搞登入畫面,那就要先回去 Firebase的平台,打開專案後,到左邊的Authentication→sign in method,這邊會有許多登入方法,如第三方登入等.. 我們這次專案先把第一個電子郵件/密碼的選項開啟。

https://ithelp.ithome.com.tw/upload/images/20210919/20138017BGtSHdTN23.png

5.回到Code

我們首先要拿到edText的資料,並且驗證是否為Empty

//如果回傳 ture,代表格式正確,false則格是錯誤。

private fun validDataForm(): Boolean{

return when{

//check email欄位是否為empty,若為空則用透過snackBar跳出訊息。
TextUtils.isEmpty(binding.edEmail.text.toString().trim())->{
showSnackBar(resources.getString(R.string.hint_enter_your_email),true)
return false
}

//check passwrod欄位是否為empty,若為空則用透過snackBar跳出訊息。
TextUtils.isEmpty(binding.edPassword.text.toString().trim())->{
showSnackBar(resources.getString(R.string.hint_enter_your_password),true)
return false
}

else -> true
	}
}

★這邊要確定我們的Fragment是有繼承BaseFragment,我們可以使用snackBar呦!

接下來我們要繼續繼承 View.OnClickListener,直接在LoginFragment後面新增

就會發現它跑出錯誤,因為我們需要在 override onClick這個 funtion。

class LoginFragment : BaseFragment(),View.OnClickListener {

override fun onClick(v: View?) {
TODO("Not yet implemented")
   }
}

我們直接在 onClick裡面新增

override fun onClick(v: View?) {

//我們用 when來判斷當今天使用者點擊哪個view,後會有什麼回饋。
when(v){

binding.btnLogin ->{

        }

binding.tvForgotPassword ->{

        }

binding.tvRegister ->{

        }

    }
}

你以為onClick結束了嗎? 你還需要在 onCreateView新增以下,它才算是完成

binding.btnLogin.setOnClickListener(this)
binding.tvForgotPassword.setOnClickListener(this)
binding.tvRegister.setOnClickListener(this)
接下來我們需要把當今天 validDataForm() 回傳為 true的時候,把資料註冊到Firebase Auth。而我們Fragment只負責UI,而所以我們要把跟Firebase連接的部分放在 AccountViewModel。

我們先去AccountViewModel,新增以下的funtoin。

//參數傳入 loginFragment,以及 edText拿到的 email跟 password
fun loginWithEmailAndPassword(fragment: LoginFragment, email: String, paw: String){

//透過 FirebaseAuth.getInstance()去拿到實例後,就可以直接點出sign的 funtion了。
FirebaseAuth.getInstance().signInWithEmailAndPassword(email,paw)

//新增 SuccessListener
.addOnSuccessListener{
fragment.loginSuccessful()
}
新增 FailureListener
.addOnFailureListener{
Timber.d("Error while logging cause $it")
fragment.loginFail(it.toString())
}
}

因為我們需要當sign in 成功後,需要跳轉到主要Activity,以及當sign in 失敗後,需要顯示錯誤內容,而UI部分都是在 Fragment,所以我們等等會在 LoginFragment新增成功/失敗的 Funtion,並且在 viewModel的loginWithEmailAndPassword funtion,的參數傳入loginFragment,讓我們可以呼叫loginFragment的成功/失敗 funtion。

接下來要回去LoginFragment,新增成功/失敗的 funtion

//AccountViewModel的 sign in funtion 成功後會呼叫以下的funtion。
fun loginSuccessful(){

        hideDialog()
        showSnackBar(resources.getString(R.string.msg_login_successful),false)
        requireActivity().startActivity(Intent(requireActivity(), MatchingActivity::class.java))

        requireActivity().finish()
    }


//AccountViewModel的 sign in funtion 失敗後會呼叫以下的 funtion
    fun loginFail(message: String){
        hideDialog()
        showSnackBar(message, true)
    }

★別忘了要新增一個 MatchingActivity,讓我們當今天sign in 成功後可以透過 Intent來跳轉到該 Actvitiy。

好的,但是我們的 AccountViewModel的 loginWithEmailAndPassword() 還沒被呼叫到,我們該怎麼呼叫它呢?

歡迎回去昨天的文章看看關於 Koin文章!

如果要呼叫viewModel的話,我們直接在 class 下面新增以下,就可以呼叫囉!

private val accountViewModel: AccountViewModel by sharedViewModel()

接下來我們要在 onClick裡面設定,當今天使用者點擊 login 的按鈕時,應該要進到viewModel的funtion。

//VlidDataForm 回傳 true時,代表資料格式正確。
if(validDataForm()){

//show出 progressBar
showDialog(resources.getString(R.string.please_wait))
val email = binding.edEmail.text.toString().trim()
val paw = binding.edPassword.text.toString().trim()
//進到剛剛建立好的 accountViewModel login funtion
accountViewModel.loginWithEmailAndPassword(this,email,paw)
}

由於在login畫面的時候,我們不應該讓user看到 status欄位,為了提供user更安心,不會被打擾的畫面,所以我們一樣在onCreateView新增以下的code。

由於在login畫面的時候,我們不應該讓user看到 status欄位,為了提供user更安心,不會被打擾的畫面,所以我們一樣在onCreateView新增以下的code。

★這邊要注意,因為我們在這邊是Fragment,所以我們呼叫window的時候前面要先呼叫 requireActivity()。

並且醜醜的ActionBar我們不要,於是我們到 manifest裡面修改 AccountActivity的設定

<activity android:name=".ui.activity.AccountActivity"

		//設定 NoActionBar
    android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"
    //設定畫面只能垂直,不要讓人家水平
		android:screenOrientation="portrait"/>

★注意因為我們這邊的theme,因為我們在login_fragment的layout裡面有用到Material的 TextInputLayout,所以我們只能選擇有 Material的theme。

好啦 完成啦!!

圖片

接下來明天會是把註冊/忘記密碼頁面完成!!


上一篇
【Day3】Firebase連接XBaseFragment建立X服務定位Koin使用
下一篇
【Day5】註冊畫面 X Firestore Database
系列文
30天建立寵物約散App-Android新手篇30

尚未有邦友留言

立即登入留言