iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Mobile Development

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

【Day9】AddInvitationFragment(上)

好的,接下來我們要新增邀約的Fragment,好讓使用者可以上去PO出自己的邀約,以及讓不同的使用者可以看到目前有的邀約。那我們開始吧! 今天會完成上傳圖片/選單

頁面長相長這樣!

Day9.first

1.新增Invitation的DataClass

data class Invitation(
        val id: String = "",
        val user_id: String = "",
        val pet_image: String = "",
        val pet_type: String = "",
        val pet_type_description: String = "",
        val area: String = "",
        val date_place: String = "",
        val date_time: String = "",
        val note: String = ""
)

2.新增AddInvitationFragment

一樣繼承 BaseFragment以及databinding

3.layout

按照慣例先dimen

    <dimen name="image_height">250dp</dimen>
    <dimen name="icon_camera_margin_bottom_end">10dp</dimen>
    <dimen name="add_invitation_input_layout_margin_top_bottom"

string

	<string name="toolbar_title_add_invitation">新增邀約</string>
    <string name="hint_enter_pet_type">請選擇寵物種類</string>
    <string name="hint_enter_pet_type_description">請輸入寵物品種</string>
    <string name="hint_enter_date_place">請輸入邀約地點</string>
    <string name="hint_enter_date_time">請輸入邀約時間</string>
    <string name="hint_enter_date_note">請輸入注意事項(如寵物害怕物品..)</string>
    <string name="update_pet_image_successful">上傳圖片成功!</string>
    <string name="hint_enter_area">請輸入邀約區域</string>

layout

為了達到視覺效果統一,layout大部分會是用material.textfield.TextInputLayout來包。然後因為這次的填寫的內容比較多,所以我們會用Scroll View來包。

<?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.fragment.AddInvitationFragment">


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

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="@string/toolbar_title_add_invitation"
                android:textColor="@color/white"
                android:textSize="@dimen/toolbar_textSize"
                android:textStyle="bold">

            </TextView>

        </androidx.appcompat.widget.Toolbar>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:orientation="vertical"
            android:fillViewport="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@id/toolbar_add_invitation_fragment">

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


                <FrameLayout
                    android:id="@+id/fl_add_invitation_image"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/image_height"
                    android:background="@color/grey_light"
                    app:layout_constraintTop_toTopOf="parent">

                    <ImageView
                        android:id="@+id/iv_add_invitation_pet_image"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:scaleType="fitXY">

                    </ImageView>

                    <ImageView
                        android:id="@+id/iv_add_invitation_camera"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="bottom|end"
                        android:layout_marginEnd="@dimen/icon_camera_margin_bottom_end"
                        android:layout_marginBottom="@dimen/icon_camera_margin_bottom_end"
                        android:src="@drawable/ic_baseline_photo_camera_24">

                    </ImageView>

                </FrameLayout>


                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_add_invitation_fragment_spinner_pet_type"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="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"
                    app:layout_constraintTop_toBottomOf="@id/fl_add_invitation_image"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">

                    <AutoCompleteTextView
                        android:id="@+id/spinner_pet_type"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="none"
                        android:hint="@string/hint_enter_pet_type"
                        android:textSize="@dimen/edText_textSize"
                        android:padding="@dimen/edText_padding"/>

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


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

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

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


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

                    <AutoCompleteTextView
                        android:id="@+id/spinner_area"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="none"
                        android:hint="@string/hint_enter_area"
                        android:textSize="@dimen/edText_textSize"
                        android:padding="@dimen/edText_padding"/>

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


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

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

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

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

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

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

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

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

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


                <com.example.petsmatchingapp.utils.JFButton
                    android:id="@+id/btn_add_invitation_fragment_submit"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_note"
                    android:layout_marginTop="@dimen/button_margin_top_bottom"
                    android:layout_marginStart="@dimen/tip_margin_start_end"
                    android:layout_marginEnd="@dimen/tip_margin_start_end"
                    android:background="@drawable/button_background"
                    android:foreground="?attr/selectableItemBackground"
                    android:textColor="@color/white"
                    android:text="@string/submit"/>


            </androidx.constraintlayout.widget.ConstraintLayout>


        </ScrollView>

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

4.上傳照片

知道我們架構的小夥伴們都知道,我們主要有分兩個viewModel,一個是負責我們的帳號相關的AccountViewModel,另外一個則是主要放Mathcing資料的viewModel,所以我們接下來前置工作就是要新增viewModel,跟設定koin

一樣先建立MatchingViewModel,並繼承viewModel,別忘了去MyApp的startKoin funtion新增

val viewModelModule = module {
            viewModel { AccountViewModel() }
			//新增這個
            viewModel { MatchingViewModel() }
        }

4.1.新增權限check

雖然權限我們之前在ProfileFragment設定過了,但是也有可能那時候使用者拒絕,所以我們在這邊還要再問一次!

private fun checkPermission(){
		//確認是否有權限,如果有權限則開啟相簿
        if (ContextCompat.checkSelfPermission(requireActivity(),android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
            resultLauncher.launch(
 Intent(Intent.ACTION_PICK,MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
            )
        }else{
			//沒有權限則要求權限
            requestPermission()
        }
    }

這邊出現紅字,我們來新增要求權限

private fun requestPermission(){
        ActivityCompat.requestPermissions(requireActivity(), arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),Constant.REQUEST_CODE_READ)
    }

如果沒有跟到前面幾個文章的小夥伴們,也要記得在manifest新增權限喔!

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

4.2.拿到Result,並傳去viewModel

private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ uri ->
        if (uri.resultCode == Activity.RESULT_OK){

			//一樣透過 data.data去拿uri
            val selectedUri = uri.data?.data
            if (selectedUri != null){
				//有result的時候,就先把他用Glide顯示進去。
                Constant.loadPetImage(selectedUri,binding.ivAddInvitationPetImage)
				//再來傳去storage
       matchingViewModel.saveImageToFireStorage(requireActivity(),this,selectedUri)

            }
        }
    }

因為我們在loadUserImage的時候,我們為了讓層次更加明顯,以及筆者在做update使用者圖片時,剛好是中秋節,為了讓人比月嬌,所以我們把它改成圓角,但是寵物就不需要了,我們希望寵物邀約的時候,能更無死角,更全面性的看,所以我們要修改一下 Glide的地方,一樣去Constant新增以下

我們只要把圓角拿掉就好

fun loadPetImage(url: Any, v:ImageView){

        Glide.with(v)
            .load(url)
            .placeholder(R.drawable.placeholder)
            .into(v)
    }

別忘了要先宣告matchingViewModel喔!

我們在傳送邀約的時候,也會需要po出這個訊息的人的ID(這也是為什麼Invitation這個dataClass會需要有 user_id的資訊),這樣我們對這個邀約的操控性就可以更高,譬如說之後我們可能不希望在所有約散的Fragment時,看到自己的PO出的訊息等...

private val matchingViewModel: MatchingViewModel by sharedViewModel()
private val accountViewModel: AccountViewModel by sharedViewModel()

4.3.上傳至Storage並回傳可下載的uri

到 MatchingViewModel新增上傳到 Storage的funtion啦!

原則上跟上傳user的頭貼一樣,我們只要把child名稱改變就好,以便之後我們好辨識

//傳入的fragment就要改成AddInvitationFragment
fun saveImageToFireStorage(activity: Activity, fragment: AddInvitationFragment, uri: Uri){

        val sdf: StorageReference = FirebaseStorage.getInstance().reference.child(Constant.PET_IMAGE + "_" + System.currentTimeMillis() + "_" + Constant.getFileExtension(activity, uri))
        sdf.putFile(uri)
            .addOnSuccessListener {
								//當上傳資料成功時,我們在新增Successful的listener,並且把它改成 downloadUri,並傳回fragment!	
                it.metadata?.reference?.downloadUrl
                    ?.addOnSuccessListener {
                        fragment.saveImageSuccessful(it)
                    }
                    ?.addOnFailureListener {
                        fragment.saveImageFail(it.toString())
                    }

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

    }

接下來我們發現紅字,我們來解決它,一樣回Fragment來新增以下,且因為我們要讓我們的 mUri 存活在整個class,所以我們在class新增一個 null的 mUri

private var mUri: String? = null
fun saveImageSuccessful(uri: Uri){
        mUri = uri.toString()
    }

    fun saveImageFail(e:String){
        showSnackBar(e,true)
    }

4.4.onClick事件

一樣先繼承 View.OnClickListener,並新增check權限的funtion

override fun onClick(v: View?) {
        when(v){
            binding.ivAddInvitationCamera -> {
                checkPermission()
            }
        }
    }

別忘了在 onCreateView新增

binding.ivAddInvitationCamera.setOnClickListener(this)

5.Spinner下拉式選單

我們這邊一樣透過外面在包一個 inputLayout,這樣就可以保證我們的layout長相都一樣。其中也有很多有趣的設計,可以看一下 https://material.io/components/text-fields/android

5.1.綁定adapter

我們先做一個 String List,先在class 先延遲初始化

private lateinit var petList: List<String>
private lateinit var areaList: List<String>

再來onCreateView賦值

petList = listOf("狗", "貓", "兔子", "鳥","豬","魚","其它")
areaList = listOf("基隆市","台北市","新北市","桃園市","新竹市","新竹縣","苗栗縣","彰化縣","雲林縣","南投縣","台中市","嘉義市","嘉義縣","台南市",
            "高雄市","屏東縣","宜蘭縣","花蓮縣","台東縣","澎湖縣","金門縣","連江縣","其它")

★貼心小提醒,選單要記得添加其它呦,要不然沒有看到適合自己的人就會覺得被排擠!!

private fun setSpinner(){
				
        val petAdapter = ArrayAdapter(requireContext(),R.layout.spinner_list_item,petList)
        binding.spinnerPetType.setAdapter(petAdapter)
        
        val areaAdapter = ArrayAdapter(requireContext(),R.layout.spinner_list_item,areaList)
        binding.spinnerArea.setAdapter(areaAdapter)

    }

ArrayAdapter傳入

  • context
  • 等等會做的text的Layout
  • 選單資料(list)

再來新增 layout,並命名為 spinner_list_item

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

	<TextView app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/edText_padding"
    android:ellipsize="end"
    android:maxLines="1"
    android:textSize="@dimen/edText_textSize"
    android:textAppearance="?attr/textAppearanceSubtitle1"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" />

★注意外面不用包layout喔,自己在做的時候就有手濺,自己亂加,結果就導致不行

取得資料
我們先在class下新增

	private var selectedPetType: String? = null
    private var selectedArea: String? = null

新增setOnItemClickListener就可以拿到 view,position,id 等資料囉! 但我們這邊只要 position就好了!

binding.spinnerPetType.setOnItemClickListener { _, _, position, _ ->
               
		        selectedPetType = petList[position]
               
        }

        binding.spinnerArea.setOnItemClickListener { _, _, position, _ ->

		         selectedArea = areaList[position]
		  
        }

好的,今天就先告一段落啦!! 目前還沒有弄Navigation導航到這個Fragment,我相信有認真學習的夥伴們一定可以自己做出來,所以我們就先給大家看一下小成果吧!!

大家也可以自己Log看看有沒有成功拿到資料!

day9.finish


上一篇
【Day8】EditProfileFragment X Storage上傳照片
下一篇
【Day10】AddInvitationFragment(下) X DatePickerDialog
系列文
30天建立寵物約散App-Android新手篇30

尚未有邦友留言

立即登入留言