레이아웃 구성에 이어서 액티비티 구현을 실습해보겠습니다.
Authentication으로 회원가입, 이메일/패스워드 로그인
Realtime Database에 데이터를 저장, Storage에 이미지를 업로드 하는 예제입니다.
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 상태 리스너 제거
}
}
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 // 이미지 선택 요청 코드
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) {
// 작업 취소 시
// 작업 취소 시의 처리 코드를 추가할 수 있음
}
})
}
}
data class Model(
val title: String, // 모델의 제목
val description: String, // 모델의 설명
val uri: String // 모델의 URI(Uniform Resource Identifier)
) {
constructor() : this("", "", "") // 기본값을 가지는 보조 생성자
}
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
}
}
}
}