본문 바로가기
활동 일지/캡스톤디자인프로젝트

[캡스톤디자인프로젝트 | 그로쓰] Kotlin & Firebase Storage

by seoyamin 2023. 4. 11.

# 목표

Kotlin을 이용한 안드로이드 어플에서 Firebase Storage로 이미지를 업로드하자 !

 

 

1. Firebase 환경 구축

1-1. Firebase 프로젝트 생성

 

1-2. 프로젝트 콘솔창에서 안드로이드 앱 생성

 

로컬에서 작업 중인 프로젝트의 패키지명을 입력하고 [앱 등록]

 

google-services.json 파일을 다운받아서 프로젝트의 app 폴더로 복붙해준다.

이때, 안드로이드 스튜디오에서 좌측 파일 목록 열람 방식을 Android → Project 방식으로 바꿔서 추가하면 된다.

 

프로젝트 파일 중 build.gradle (Project :     )  파일에 종속성을 추가한다.

 

프로젝트 파일 중 build.gradle (Module :     )  파일에 종속성을 추가한다.

 

나는 Firebase 여러 기능 중 Storage를 사용할 것이므로 build.gradle (Module :  )  파일에 아래 종속성도 추가해줬다.

implementation 'com.google.firebase:firebase-storage-ktx'

 

1-3. 안드로이드 스튜디오 설정

프로젝트로 돌아와서 Tools / Firebase를 클릭하면 우측에 다음 설정창이 생긴다.

여기서 Cloud Storage를 선택하고 (java말고 kotlin 선택) 시키는 대로 4개의 단계를 진행한다. (sync)

 

 

 

2. layout 만들기

[사진 선택] 버튼, 선택한 사진이 보일 이미지뷰를 포함한 layout을 하나 만들어준다.

나는 2개의 Fragment로 [ 사진 업로드 이전 -> 사진 업로드 이후 ] 화면을 다르게 구현하고 싶어서 Fragment용 layout에 만들었다.

그런데 두 Fragment들 가운데 훨씬 중요한 것은 사진 업로드 이전 Fragment이다. 여기서 사진 선택, 선택한 사진 보기, 사진 다시 선택하기 과정이 다 이루어지기 때문이다. (사진 업로드 이후 Fragment는 형식상..)

 

# fragment_search_before.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.search.SearchBeforeFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="17sp"
                android:gravity="center"
                android:text="검색하고 싶은 벌레 사진을 첨부해 보세요!"
                android:textSize="15sp"
                android:textColor="@color/main_grey"/>

            <LinearLayout
                android:id="@+id/Search_Before_Container_Image"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_gravity="center"
                android:gravity="center"
                android:layout_marginTop="20sp"
                android:layout_marginBottom="40sp"
                android:background="@drawable/custom_button_white_grey2"
                android:orientation="vertical">

                <LinearLayout
                    android:id="@+id/Search_Before_Container_Select"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_gravity="center"
                    android:gravity="center"
                    android:orientation="vertical">

                    <ImageButton
                        android:id="@+id/Search_Before_Btn_Select"
                        android:layout_width="200sp"
                        android:layout_height="202sp"
                        android:padding="10sp"
                        android:backgroundTint="@color/white"
                        android:src="@drawable/img_search_upload"
                        android:scaleType="fitCenter"/>

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="클릭하여 사진 첨부하기"
                        android:textSize="17sp"
                        android:textColor="@color/main_grey2"
                        android:fontFamily="@font/main_font_sb" />

                </LinearLayout>

                <ImageView
                    android:id="@+id/Search_Before_Image"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:layout_marginTop="20sp"
                    android:layout_marginBottom="40sp"
                    android:visibility="gone" />

            </LinearLayout>

            <LinearLayout
                android:id="@+id/Search_Before_Container_Edit"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="5"
                android:gravity="bottom"
                android:orientation="vertical"
                android:visibility="gone">

                <androidx.appcompat.widget.AppCompatButton
                    android:id="@+id/Search_Before_Btn_Edit"
                    android:layout_width="match_parent"
                    android:layout_height="50sp"
                    android:background="@drawable/custom_button_white_red"
                    android:text="다시 선택하기"
                    android:textSize="20sp"
                    android:textColor="@color/main_red"
                    android:fontFamily="@font/main_font_b"/>

            </LinearLayout>

        </LinearLayout>

    </LinearLayout>

</FrameLayout>

 

Fragment를 얹을 Activity의 layout은 다음과 같다.

# activity_search.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"
    android:background="#FFFFFF"
    tools:context=".activity.search.SearchActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/Toolbar_Search"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:background="@color/white"
        android:elevation="5sp"
        app:titleTextColor="@color/black"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/Toolbar_Search_Text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:lineSpacingExtra="3.3sp"
            android:layout_gravity="center_horizontal"
            android:text="이 벌레 뭐지 ?"
            android:textSize="25sp"
            android:fontFamily="@font/main_font_sb"/>

    </androidx.appcompat.widget.Toolbar>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:gravity="center"
        android:orientation="vertical"
        android:layout_marginTop="70dp"
        android:paddingHorizontal="30sp">

        <FrameLayout
            android:id="@+id/Search_MainView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="8"
            android:gravity="bottom"
            android:paddingBottom="15sp"
            android:orientation="vertical">

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/Search_Btn"
                android:layout_width="match_parent"
                android:layout_height="50sp"
                android:background="@drawable/custom_button_red"
                android:text="검색하기"
                android:textSize="20sp"
                android:textColor="@color/white"
                android:fontFamily="@font/main_font_b" />

        </LinearLayout>

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

3. Activity, Fragment 코드 작성하기

나는 Fragment에 있는 [사진 선택] 버튼을 누르면, Activity에 있는 함수를 호출해서 Fragment에 선택한 사진을 보여주게끔 코드를 작성했다. 

 

# SearchBeforeFragment.kt

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.appcompat.widget.AppCompatButton
import com.zonebug.debugging.R

class SearchBeforeFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val view = inflater.inflate(R.layout.fragment_search_before, container, false)
        val selectBtn : ImageButton = view.findViewById(R.id.Search_Before_Btn_Select)
        val editBtn : AppCompatButton = view.findViewById(R.id.Search_Before_Btn_Edit)

        // 이미지 선택하기
        selectBtn.setOnClickListener {
            (activity as SearchActivity).selectImage()
        }

        // 이미지 다시 선택하기
        editBtn.setOnClickListener {
            (activity as SearchActivity).selectImage()
        }

        return view
    }
}

 

 

# SearchActivity.kt

import android.app.ProgressDialog
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import com.google.firebase.storage.FirebaseStorage
import com.zonebug.debugging.R
import com.zonebug.debugging.databinding.ActivitySearchBinding
import java.text.SimpleDateFormat
import java.util.*

class SearchActivity : AppCompatActivity() {

    private lateinit var binding : ActivitySearchBinding
    var isBefore : Boolean = true
    lateinit var imageURI : Uri

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySearchBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val transaction = supportFragmentManager.beginTransaction()
        transaction.add(R.id.Search_MainView, SearchBeforeFragment()).commit()


        // 하단 버튼 클릭
        binding.SearchBtn.setOnClickListener {
            if(isBefore) uploadImage()

            isBefore = !isBefore
            switchFragment()
        }

    }


    private fun switchFragment() {
        val transaction = supportFragmentManager.beginTransaction()

        if(isBefore) {
            transaction.replace(R.id.Search_MainView, SearchBeforeFragment())
                .addToBackStack(null)
                .commit()
            binding.SearchBtn.text = "검색하기"
        } else {
            transaction.replace(R.id.Search_MainView, SearchAfterFragment())
                .addToBackStack(null)
                .commit()
            binding.SearchBtn.text = "다른 벌레 검색하기"
        }

    }


    fun selectImage() {
        val intent = Intent()
        intent.type = "image/*"
        intent.action = Intent.ACTION_PICK

        startActivityForResult(intent, 100)
    }


    override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        val selectContainer : LinearLayout = this.findViewById(R.id.Search_Before_Container_Select)
        val editContainer : LinearLayout = this.findViewById(R.id.Search_Before_Container_Edit)
        val imageView : ImageView = this.findViewById(R.id.Search_Before_Image)

        if(requestCode == 100 && resultCode == RESULT_OK) {
            imageURI = data?.data!!
            imageView.setImageURI(imageURI)
            imageView.visibility = View.VISIBLE

            editContainer.visibility = View.VISIBLE
            selectContainer.visibility = View.GONE
        }
    }


    private fun uploadImage() {
        val progressDialog = ProgressDialog(this)
        progressDialog.setMessage("업로드중입니다")
        progressDialog.setCancelable(false)
        progressDialog.show()

        val formatter = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault())
        val now = Date()
        val fileName = formatter.format(now)
        val storageReference = FirebaseStorage.getInstance().getReference("Search/$fileName")

        val imageView : ImageView = this.findViewById(R.id.Search_Before_Image)
        storageReference.putFile(imageURI).addOnSuccessListener {
            imageView.setImageURI(null)
            Toast.makeText(this@SearchActivity, "업로드 성공", Toast.LENGTH_SHORT).show()
            if(progressDialog.isShowing) progressDialog.dismiss()

        }.addOnFailureListener {
            if(progressDialog.isShowing) progressDialog.dismiss()
            Toast.makeText(this@SearchActivity, "업로드 실패", Toast.LENGTH_SHORT).show()
        }
    }


    override fun onBackPressed() {
        finish()
    }
}

 

중요한 흐름은 selectImage( ) → onActivityResult( ) → uploadImage( ) 이다.