멋쟁이 사자처럼에서 진행하는 안드로이드 앱 스쿨에서 멘토링을 받고 배운 지식들을 정리하는 글입니다.
📕 질문
갤러리 관련 권한을 요청할 때 아래 코드와 같이 요청합니다. PermissionManagement를 따로 만든 이유는 여러 화면에서 갤러리를 접근할 때마다 권한이 있는지 확인하기 때문에 액티비티나 프래그먼트 마다 권한을 검사하는 코드가 완전히 중복되어 최대한 중복되는 부분을 줄이고자 만들었습니다.
제가 할 수 있는 최대한으로 만들어봤는데 여기서 다른 더 좋은 방법이 있는지 알고 싶습니다.
class PermissionManagement(private val context: Context) {
companion object{
val REQUEST_IMAGE_PERMISSIONS =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
} else {
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
val REQUEST_LOCATION_PERMISSIONS = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
}
fun isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
fun isImagePermissionGranted(): Boolean = REQUEST_IMAGE_PERMISSIONS.any { isPermissionGranted(it) }
fun isLocationPermissionGranted(): Boolean = REQUEST_LOCATION_PERMISSIONS.any { isPermissionGranted(it) }
fun showPermissionSnackBar(view: View) {
Snackbar.make(view, "권한이 거부 되었습니다. 설정(앱 정보)에서 권한을 확인해 주세요.",
Snackbar.LENGTH_INDEFINITE
).setAction("확인"){
//설정 화면으로 이동
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val packageName = context.packageName
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
context.startActivity(intent)
}.show()
}
}
📖 이에 대해 멘토링에선 다음과 같은 답변을 얻을 수 있었습니다.
현재 클래스는 REQUEST_IMAGE_PERMISSIONS 등의 상수를 사용하며 메소드만으로 이루어져 있고 프로퍼티가 없습니다. 이렇게 클래스의 인스턴스가 생성되었을 때 내부의 변동 없이 특정한 기능만을 하는 클래스는 싱글톤으로 만들면 객체 지향적 관점에서 더욱 분명한 코드가 될 수 있습니다.
Singleton Pattern : 클래스의 인스턴스를 생성할 때 단 하나의 인스턴스만 생성
코틀린에선 object Class를 통해 Singleton을 구현할 수 있습니다. 하지만 위 코드는 확장 함수를 통해서도 구현할 수 있기 때문에 코틀린에서 권장되는 패턴은 아닙니다. 확장 함수는 실제로 어떤 클래스의 실제 멤버는 아니지만 멤버인 것 마냥 호출할 수 있기 때문에 기존 클래스를 확장할 수 있어서 개방/폐쇄 디자인 원칙(Open Close Principle)을 지원할 수 있습니다. 이건 좀 더 공부를 해야 봐야겠네요 호호호🤔
위 클래스의 메소드를 보면 Context가 필요한 메소드들이 있습니다. Object Class는 매개 변수를 가질 수 없기 때문에 각 메소드에 마다 매개 변수로 전달해줘야 합니다. 하지만 저는 이 메소드들을 Activity에서 사용하기 때문에 한 가지 문제점이 있었습니다.
안드로이드에서 싱글톤, ViewModel처럼 Activity보다 오래 살아있을 수 있는 가능성이 있는 요소들은 내부 함수가 아직 종료되지 않은 상태에서 액티비티가 종료되는 경우가 발생할 수 있습니다. 이런 경우 Activity의 Context를 가진다면 Activity에 대한 참조를 메모리에 남겨두어 Memory Leak이 발생할 수 있습니다. 그래서 Acitivity의 생명 주기보다 오래 살아있거나 앱 전역에서 사용될 경우 applicationContext를 사용해야 합니다.
그래서 저는 이후 다른 클래스에서도 사용하기 위해 applicationContext를 제공하는 싱글톤 클래스를 만들었습니다.
class App: Application() {
init {
instance = this
}
companion object{
private var instance: App? = null
fun context(): Context {
return instance?.applicationContext!!
}
}
}
최종 완성된 코드입니다. 확장 함수를 사용한 방법도 연구해 봐야겠네용
object PermissionManagement {
private fun getContext(): Context = App.context()
val REQUEST_IMAGE_PERMISSIONS =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
} else {
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
val REQUEST_LOCATION_PERMISSIONS = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
fun isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(getContext(), permission) == PackageManager.PERMISSION_GRANTED
}
fun isImagePermissionGranted(): Boolean =
REQUEST_IMAGE_PERMISSIONS.any { isPermissionGranted(it) }
fun isLocationPermissionGranted(): Boolean =
REQUEST_LOCATION_PERMISSIONS.any { isPermissionGranted(it) }
fun showPermissionSnackBar(view: View) {
Snackbar.make(view, "권한이 거부 되었습니다. 설정(앱 정보)에서 권한을 확인해 주세요.",
Snackbar.LENGTH_SHORT
).setAction("확인"){
//설정 화면으로 이동
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val packageName = getContext().packageName
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
getContext().startActivity(intent)
}.show()
}
}
🚨 requireContext의 비밀
코틀린의 !! 연산자는 NullpointerException을 발생시킵니다. 이는 NullpointerException 은 어플리케이션을 크래쉬 시킨다는 문제도 있지만 가장 큰 문제점은 코틀린의 장점은 널에 유연한 대처가 가능하다는 점인데 이 의도에 어긋납니다.
그럼 이게 requireContext( ) 와 무슨 상관일까요? requireContext( ) 의 내부 구현을 한번 살펴 보겠습니다.
내부 구현을 보면 context가 null 일 때 IllegalStateException을 일으키며 앱을 크래쉬 시킵니다. !! 연산자와 비슷한 동작을 하는거죠 ! 멘토님께 이 내용을 듣고 정말 놀랐습니다. Android Studio에서 대놓고 requireContext( ) 를 써서 에러를 고치라는 문구를 보여주니까 당연히 이걸 쓰는게 맞겠거니 해서 써왔는데 !!!! 심지어 프로젝트의 Fragment의 컨텍스트가 필요한 거의 모든 부분에 requireContext( )를 사용했는데 !!! 물론 requireContext를 수행했을 때 에러 날 일이 거의 없겠지만 굉장히 놀라웠습니다
🙇♂️ 후기
정말 멘토링을 할 때마다 많은 것들을 배웁니다. 너무 소중한 배움이었습니다. 이러니 멘토링을 안 할 수가 있나~ 한 가지 아쉬웠던 점은 항상 멘토링을 하는 분들만 하는 게 주제 넘지만 좀 안타..깝다... ? 라고 느꼈습니다. 이렇게 정말 아니 진짜 진짜 진짜 엄청난걸 배워갈 수 있는데 이 좋은 기회들을 놓치고 계신 게 너무 아깝다는 생각이 들었습니다. 아뇨 ? 하지마세요 제가 다할 거예요
그리고 제가 지금껏 열심히 프로젝트를 계속 했기 때문에 코루틴이든 StateFlow든 러닝 커브가 조금 있는 기술도 깊게 물어볼 수 있었던 거 같고 또 이해를 할 수 있었던 것 같습니다. 뿐만 아니라 아키텍처적으로도 계속 관심을 가지고 한 게 멘토님께서 아키텍처적 관점으로 하시는 말씀들을 다 이해할 수 있었고 무엇보다 그런 아키텍처에 관한 얘기들이 너무 재미있더라고요. 아키텍처를 살짝 찍먹 해본 결과 이건 몇 번 외우고 공부한다고 되는 게 아닌 거 같아서 이런 중간중간 알아가는 지식들이 쌓이고 쌓여 나중에 아키텍처에 대한 공부를 하게 됐을 때 큰 도움이 될 것 같습니다.
흔히 하는 말들 중에 준비된 자가 기회를 얻는 말이 있는데 이럴 때 쓰는 말이 아닌가 싶네요. 앞으로도 멘토님들 많이 많이 괴롭혀야겠습니다. 앱 스쿨 과정이 이제 약 4개월 남았는데 뭔가 벌써 짧게 느껴지는 기분이라 하루 하루 열심히 달려보려 합니다 퐈이팅!!🔥
'TEKHIT ANDROID SCHOOL' 카테고리의 다른 글
[TEKHIT] 오늘의 멘토링 (1) | 2024.02.16 |
---|---|
[TEKHIT] 오늘의 멘토링 (0) | 2024.02.13 |
[TEKHIT] 오늘의 멘토링 (1) | 2024.02.07 |
[TECKHIT] Material3 Text fields (0) | 2024.01.22 |
[TECKHIT] Material3 Button (1) | 2024.01.21 |