[Android/Kotlin]Firebase(2)

최지원·2024년 2월 4일
0

[Android/Kotlin]

목록 보기
8/9
post-thumbnail

레이아웃 구성에 이어서 액티비티 구현을 실습해보겠습니다.
Authentication으로 회원가입, 이메일/패스워드 로그인
Realtime Database에 데이터를 저장, Storage에 이미지를 업로드 하는 예제입니다.

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding // View binding을 사용하여 layout의 View에 접근하기 위한 변수
    private lateinit var auth: FirebaseAuth // Firebase 인증 객체
    private lateinit var authStateListener: FirebaseAuth.AuthStateListener // Firebase 인증 상태 리스너

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater) // View binding 초기화
        setContentView(binding.root) // Activity에 루트 View 설정

        FirebaseApp.initializeApp(this) // Firebase 초기화
        auth = FirebaseAuth.getInstance() // FirebaseAuth 인스턴스 초기화

        binding.join.setOnClickListener {
            val email = binding.email.text.toString() // 이메일 입력란에서 이메일 가져오기
            val password = binding.password.text.toString() // 비밀번호 입력란에서 비밀번호 가져오기
            if (email.isNotEmpty() && password.isNotEmpty()){ // 이메일과 비밀번호가 비어 있지 않은지 확인
                registerUser(email, password) // 사용자 등록 메서드 호출
            }
        }

        // FirebaseAuth의 인증 상태 변화를 듣는 리스너 설정
        authStateListener = FirebaseAuth.AuthStateListener { auth ->
            val user = auth.currentUser // 현재 인증된 사용자 가져오기
            if (user != null){ // 사용자가 로그인되어 있는 경우
                binding.login.text = "로그아웃" // 로그인 버튼 텍스트를 "로그아웃"으로 변경
                binding.login.setOnClickListener {
                    logoutUser() // 로그아웃 메서드 호출
                }
            }else{ // 사용자가 로그아웃된 경우
                binding.login.text = "로그인" // 로그인 버튼 텍스트를 "로그인"으로 변경
                binding.login.setOnClickListener {
                    val email = binding.email.text.toString() // 이메일 입력란에서 이메일 가져오기
                    val password = binding.password.text.toString() // 비밀번호 입력란에서 비밀번호 가져오기
                    if (email.isNotEmpty() && password.isNotEmpty()) {
                        loginUser(email, password) // 사용자 로그인 메서드 호출
                    }
                }
            }
        }

        auth.addAuthStateListener(authStateListener) // FirebaseAuth 상태 리스너 추가
    }

    private fun registerUser(email: String, password:String){
        // 이메일과 비밀번호를 사용하여 사용자 등록
        auth.createUserWithEmailAndPassword(email,password)
            .addOnCompleteListener(this) { task ->
                if (task.isSuccessful) Toast.makeText(this, "회원가입 성공!", Toast.LENGTH_SHORT).show() // 회원가입 성공 토스트 메시지 표시
                else Toast.makeText(this, "회원가입 실패!", Toast.LENGTH_SHORT).show() // 회원가입 실패 토스트 메시지 표시
            }
        binding.email.text = null // 이메일 입력란 초기화
        binding.password.text = null // 비밀번호 입력란 초기화
    }

    private fun loginUser(email: String, password:String){
        val intent = Intent(this, SaveActivity::class.java) // SaveActivity로 이동하는 Intent 생성
        // 이메일과 비밀번호를 사용하여 사용자 로그인
        auth.signInWithEmailAndPassword(email, password)
            .addOnCompleteListener(this){ task ->
                if (task.isSuccessful) {
                    Toast.makeText(this, "로그인 성공!", Toast.LENGTH_SHORT).show() // 로그인 성공 토스트 메시지 표시
                    startActivity(intent) // SaveActivity로 이동
                }
                else Toast.makeText(this, "회원가입 또는 아이디, 비밀번호를 확인해주세요!", Toast.LENGTH_SHORT).show() // 로그인 실패 토스트 메시지 표시
            }
        binding.email.text = null // 이메일 입력란 초기화
        binding.password.text = null // 비밀번호 입력란 초기화
    }

    private fun logoutUser(){
        auth.signOut() // 사용자 로그아웃
        Toast.makeText(this, "로그아웃!", Toast.LENGTH_SHORT).show() // 로그아웃 토스트 메시지 표시
        binding.email.text = null // 이메일 입력란 초기화
        binding.password.text = null // 비밀번호 입력란 초기화
    }

    override fun onStart() {
        super.onStart()
        auth.addAuthStateListener(authStateListener) // 액티비티 시작 시 FirebaseAuth 상태 리스너 추가
        auth.signOut() // 사용자 로그아웃
    }

    override fun onStop() {
        super.onStop()
        auth.removeAuthStateListener(authStateListener) // 액티비티 종료 시 FirebaseAuth 상태 리스너 제거
    }
}

SaveAntivity.kt

class SaveActivity: AppCompatActivity() {

    private lateinit var binding: ActivitySaveBinding // View binding을 사용하여 layout의 View에 접근하기 위한 변수
    private var selectedUri: Uri? = null // 선택된 이미지의 URI를 저장하는 변수

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySaveBinding.inflate(layoutInflater) // View binding 초기화
        setContentView(binding.root) // Activity에 루트 View 설정

        binding.selectImage.setOnClickListener {
            when{
                ContextCompat.checkSelfPermission(
                    this@SaveActivity,
                    android.Manifest.permission.READ_MEDIA_IMAGES
                ) == PackageManager.PERMISSION_GRANTED -> {
                    startContentProvider() // 이미지 선택을 시작하는 메서드 호출
                }

                shouldShowRequestPermissionRationale(android.Manifest.permission.READ_MEDIA_IMAGES) -> {
                    showPermissionContextPopup() // 권한에 대한 이유를 설명하는 팝업 표시
                }

                else -> {
                    ActivityCompat.requestPermissions(
                        this@SaveActivity,
                        arrayOf(android.Manifest.permission.READ_MEDIA_IMAGES),
                        PERMISSION_REQUEST_CODE
                    ) // 권한 요청 다이얼로그 표시
                }
            }
        }

        binding.save.setOnClickListener {
            val title = binding.text1.text.toString() // 제목 입력란에서 제목 가져오기
            val description = binding.text2.text.toString() // 설명 입력란에서 설명 가져오기
            selectedUri?.let { uri ->
                uploadImage(title, description, uri) // 이미지 업로드 메서드 호출
            } ?: kotlin.run {
                uploadImage(title, description, null) // 이미지 없이 업로드 메서드 호출
            }
            binding.text1.text = null // 제목 입력란 초기화
            binding.text2.text = null // 설명 입력란 초기화
            binding.imageView.setImageResource(0) // 이미지 뷰 초기화
        }

        binding.my.setOnClickListener {
            val intent = Intent(this, MyActivity::class.java) // MyActivity로 이동하는 Intent 생성
            startActivity(intent) // MyActivity로 이동
        }
    }

    private fun uploadImage(title: String, description: String, selectedUri: Uri?){
        val fileName = "images/${System.currentTimeMillis()}"
        val storageReference = FirebaseStorage.getInstance().getReference(fileName)

        if (selectedUri != null){
            showProgress() // 업로드 진행 상태를 표시하는 메서드 호출
            storageReference.putFile(selectedUri).addOnSuccessListener { taskSnapshot ->
                taskSnapshot.metadata?.reference?.downloadUrl?.addOnSuccessListener { downloadUrl ->
                    uploadFirebase(title, description, downloadUrl.toString()) // Firebase에 이미지 업로드
                }
            }
        }else{
            uploadFirebase(title, description, "") // 이미지가 없는 경우 Firebase에 업로드
        }
    }

    private fun uploadFirebase(title: String, description: String, Uri: String) {
        val model = Model(
            title,
            description,
            Uri
        )
        val communityDB = FirebaseDatabase.getInstance().reference.child("save")
        communityDB.push().setValue(model) // Firebase Database에 데이터 저장
        Toast.makeText(this, "저장 성공!", Toast.LENGTH_SHORT).show() // 성공 메시지 표시
        hideProgress() // 업로드 진행 상태를 숨기는 메서드 호출
    }

    @Deprecated("Deprecated in Java")
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_PICK_IMAGE -> {
                    data?.data?.let { uri ->
                        // 사용자가 선택한 이미지의 URI에 대한 지속적인 접근 권한을 요청
                        try {
                            contentResolver.takePersistableUriPermission(
                                uri,
                                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                            )
                        } catch (e: SecurityException) {
                            // 권한 요청 실패 처리
                            Toast.makeText(this, "권한 요청 실패", Toast.LENGTH_SHORT).show()
                        }

                        // 이미지를 화면에 표시
                        binding.imageView.setImageURI(uri)
                        selectedUri = uri
                    } ?: run {
                        Toast.makeText(this, "사진을 가져오지 못했습니다.", Toast.LENGTH_SHORT).show()
                    }
                }
                // ... 기타 다른 requestCode 처리 ...
            }
        } else {
            Toast.makeText(this, "사진을 가져오지 못했습니다.", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onRequestPermissionsResult( // 권한 체크 시 그 결과를 확인하는 함수
        requestCode: Int,               // 요청할 때 보낸 코드
        permissions: Array<out String>,
        grantResults: IntArray          // 요청에 ok했을 때의 정보를 갖음.
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) { // 요청할 때 보낸 코드가 1010이면
            PERMISSION_REQUEST_CODE ->
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 요청 결과에 ok가 있다면
                    startContentProvider() // 갤러리 실행
                } else { // 요청 결과에 ok가 없다면
                    Toast.makeText(this, "권한을 거부하셨습니다.", Toast.LENGTH_SHORT).show()
                }
        }
    }

    private fun startContentProvider() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "image/*"
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
        }
        startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE)
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun showPermissionContextPopup() { // 권한 동의x 를 누른 후 띄워지는 팝업
        AlertDialog.Builder(this)
            .setTitle("권한이 필요합니다.")
            .setMessage("사진을 가져오기 위해 필요합니다.")
            .setPositiveButton("동의") { _, _ ->
                ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.READ_MEDIA_IMAGES), PERMISSION_REQUEST_CODE)
            }
            .create()
            .show()
    }

    private fun showProgress() { // 로딩창 o
        binding.loading.isVisible = true
    }

    private fun hideProgress() { // 로딩창 x
        binding.loading.isGone = true
    }

    companion object{
        const val PERMISSION_REQUEST_CODE = 1010 // 권한 요청 코드
        private const val REQUEST_CODE_PICK_IMAGE = 2020 // 이미지 선택 요청 코드

MyActivity.kt

class MyActivity: AppCompatActivity() {

    private lateinit var binding: ActivityMyBinding // View binding을 사용하여 layout의 View에 접근하기 위한 변수
    private lateinit var adapter: Adapter // RecyclerView 어댑터

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMyBinding.inflate(layoutInflater) // View binding 초기화
        setContentView(binding.root) // Activity에 루트 View 설정

        adapter = Adapter() // 어댑터 초기화
        binding.recyclerView.layoutManager = LinearLayoutManager(this) // RecyclerView에 LinearLayoutManager 설정
        binding.recyclerView.adapter = adapter // RecyclerView에 어댑터 설정

        val saveList = mutableListOf<Model>() // Model 객체의 MutableList 생성
        // Firebase Database에 있는 "save" 데이터베이스의 변경 사항을 듣는 리스너 추가
        FirebaseDatabase.getInstance().reference.child("save").addChildEventListener(object : ChildEventListener{
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                // 데이터 추가 시
                val model = snapshot.getValue(Model::class.java) // 데이터를 Model 객체로 변환
                model ?: return // 모델이 null이면 함수 종료
                saveList.add(model) // 모델을 리스트에 추가
                adapter.submitList(saveList) // 어댑터에 리스트를 제출하여 UI 업데이트
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                // 데이터 변경 시
                // 변경 사항 처리 코드를 추가할 수 있음
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {
                // 데이터 삭제 시
                // 삭제된 데이터를 리스트에서 제거하는 코드를 추가할 수 있음
            }

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
                // 자식 노드가 이동되었을 시
                // 이동된 자식 노드 처리 코드를 추가할 수 있음
            }

            override fun onCancelled(error: DatabaseError) {
                // 작업 취소 시
                // 작업 취소 시의 처리 코드를 추가할 수 있음
            }

        })
    }
}

Model.kt

data class Model(
    val title: String,        // 모델의 제목
    val description: String,  // 모델의 설명
    val uri: String           // 모델의 URI(Uniform Resource Identifier)
) {
    constructor() : this("", "", "") // 기본값을 가지는 보조 생성자
}

Adapter.kt

class Adapter : ListAdapter<Model, Adapter.ViewHolder>(DiffUtil) {
    // onCreateViewHolder 함수는 RecyclerView가 ViewHolder를 처음으로 생성할 때 호출됩니다.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
        // ViewholderMyBinding을 사용하여 ViewHolder의 레이아웃을 인플레이트합니다.
        val binding = ViewholderMyBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    // onBindViewHolder 함수는 RecyclerView의 각 ViewHolder에 데이터를 바인딩합니다.
    override fun onBindViewHolder(holder: Adapter.ViewHolder, position: Int) {
        // 현재 position에 있는 아이템을 ViewHolder에 바인딩합니다.
        holder.bind(currentList[position])
    }

    // ViewHolder 클래스는 RecyclerView의 각 아이템에 대한 View를 보유합니다.
    inner class ViewHolder(private val binding: ViewholderMyBinding) : RecyclerView.ViewHolder(binding.root) {
        // bind 함수는 ViewHolder에 데이터를 바인딩합니다.
        fun bind(item: Model) {
            // 데이터 모델(Model)의 속성을 ViewHolder의 View에 설정합니다.
            binding.title.text = item.title
            binding.description.text = item.description
            Glide.with(binding.imageView)
                .load(item.uri)
                .into(binding.imageView)
        }
    }

    // DiffUtil은 RecyclerView의 아이템 갱신을 관리합니다.
    companion object {
        val DiffUtil = object : DiffUtil.ItemCallback<Model>() {
            // areItemsTheSame 함수는 두 아이템이 동일한 항목인지 확인합니다.
            override fun areItemsTheSame(oldItem: Model, newItem: Model): Boolean {
                return oldItem == newItem
            }

            // areContentsTheSame 함수는 두 아이템이 동일한 내용을 가지고 있는지 확인합니다.
            override fun areContentsTheSame(oldItem: Model, newItem: Model): Boolean {
                return oldItem == newItem
            }
        }
    }
}




Firegbase 공식 문서

profile
안드로이드, 플러터 주니어입니다

0개의 댓글