용어 정리
✔ 콜백 (Callback) : 이벤트가 발생하면 특정 메소드를 호출해 알려준다.
✔ 리스너 (Listener) : 이벤트가 발생하면 연결된 리스너(핸들러)들에게 이벤트를 전달한다. 하지만 안드로이드에서는 리스너를 1개 밖에 등록하지 못한다는 점에서 콜백이랑 개념이 크게 다르지 않다.
✔ 옵저버 (Observer) : 데이터나 속성의 변경을 감지하여 구독자에게 변경사항을 전달한다.
원리
콜백 패턴이 구체적으로 어떻게 동작하는지 알아보자.
▫ 개발을 하다 보면 다른 클래스의 함수나 변수에 접근해야 할 일이 종종 생긴다.
▫ 위의 그림처럼 A 클래스에서는 동작할 수 없고, B 클래스에서만 동작 가능한 메서드 M을 A 클래스에서 사용하려면 어떻게 해야 할까?
▫ A 클래스에서 인터페이스를 선언하고 B 클래스에서 인터페이스를 구현한 것을 A 클래스에 넘겨줌으로써 A 클래스에서도 M 메서드를 실행시킬 수 있다.
▫ 메서드 뿐만 아니라 B가 액티비티 혹은 프래그먼트일 시, A에서 B의 UI에 접근하려는 상황과 동일하다.
예제
이 상황을 실제 예제를 통하여 살펴보자.
우리가 만들어 볼 예제는 디바이스의 볼륨이 변할 때마다 MainActivity의 TextView에 볼륨값을 텍스트로 출력하는 것이다.
✔ 우선, 볼륨 값의 변화를 감지하는 옵저버 클래스인 ContentObserver 클래스를 상속받는 VolumeObserver 클래스를 생성한다.
✔ 위의 원리를 기반으로 만들어 볼 구조는 MainActivity가 아닌 외부의 클래스에서 MainActivity의 UI에 접근하는 방법을 콜백 패턴으로 구현해 볼 것이다.
✔ 다른 클래스의 요소에 접근이 필요한 클래스에서 리스너(인터페이스)를 만들어준다.
fun interface VolumeChangeListener {
fun onVolumeChanged(volume: String)
}
▫ interface 앞에 fun을 붙인 이유는 코틀린에서 추상 메소드를 하나만 가지고 있는 인터페이스를 Single Abstract Method (SAM)라고 부르며, 람다 식을 제공하기 때문이다.
✔ 인터페이스를 구현한 리스너를 넘겨받기 위해 인터페이스를 구현한 클래스에서 setter 메서드를 구현해준다.
private lateinit var listener: VolumeChangeLister
fun setVolumeChangeListener(listener: VolumeChangeListener) {
this.listener = listener
}
✔ 이제 접근하고자 하는 MainActivity에서 인터페이스를 구현해준다.
class MainActivity : AppCompatActivity(), VolumeObserver.VolumeChangeListener {
override fun onVolumeChanged(volume: String) {
binding.tvMain.text = volume
}
}
▫ MainActivity에서 VolumeChangeListener를 구현하였다면 MainActivity로 만들어진 객체는 MainActivity 타입이기도 하지만, VolumeChangeListener이기도 하다.
✔ 그리고 구현한 리스너를 VolumeObserver 클래스의 리스너 setter에 이어준다.
private val volumeObserver by lazy { VolumeObserver(this, Handler()) }
volumeObserver.setVolumeChangeListener(this)
▫ 이로써 VolumeObserver 클래스에서 MainActivity의 UI에 접근하는데 성공하였다.
전체 코드
VolumeObserver.kt
class VolumeObserver(context: Context, handler: Handler?) : ContentObserver(handler) {
private var audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private lateinit var listener: VolumeChangeListener
override fun onChange(selfChange: Boolean) {
val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
LogUtil.i(TAG, "volume : $volume")
listener.onVolumeChanged(volume.toString())
super.onChange(selfChange)
}
fun interface VolumeChangeListener {
fun onVolumeChanged(volume: String)
}
fun setVolumeChangeListener(listener: VolumeChangeListener) {
this.listener = listener
}
companion object {
private const val TAG = "VolumeObserver"
}
}
MainActivity.kt
class MainActivity : AppCompatActivity(), VolumeObserver.VolumeChangeListener {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val volumeObserver by lazy { VolumeObserver(this, Handler()) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
contentResolver.registerContentObserver(
Settings.System.CONTENT_URI,
true,
volumeObserver
)
volumeObserver.setVolumeChangeListener(this)
}
override fun onVolumeChanged(volume: String) {
binding.tvMain.text = volume
}
companion object {
private const val TAG = "MainActivity"
}
}
다른 구현 방법
✔ MainActivity에서 직접 인터페이스를 구현하지 않고, 무명 클래스로 listener 객체 자체를 만들어서 생성자로 넘겨주는 방법
VolumeObserver.kt
class VolumeObserver(context: Context,
handler: Handler?,
private val listener: VolumeChangeListener)
: ContentObserver(handler) {
private var audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
override fun onChange(selfChange: Boolean) {
val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
LogUtil.i(TAG, "volume : $volume")
listener.onVolumeChanged(volume.toString())
super.onChange(selfChange)
}
fun interface VolumeChangeListener {
fun onVolumeChanged(volume: String)
}
companion object {
private const val TAG = "VolumeObserver"
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val volumeObserver by lazy { VolumeObserver(this, Handler(), listener) }
private val listener =
VolumeObserver.VolumeChangeListener { volume -> binding.tvMain.text = volume }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
contentResolver.registerContentObserver(
Settings.System.CONTENT_URI,
true,
volumeObserver
)
}
companion object {
private const val TAG = "MainActivity"
}
}
추가
✔ MainActivity의 companion object에 context를 포함한 instance를 생성한 후, 다른 클래스에서 사용할 시에는 MainActivity의 public 멤버 및 메서드만 접근할 수 있다.
참조
https://tmdfyd0807.tistory.com/44?category=1006364
[Android] [Kotlin] 객체 인스턴스
액티비티 클래스에서 일반 클래스를 참조 요약 ✔ 다른 클래스에 있는 멤버변수와 멤버함수에 접근하기 위해 인스턴스를 생성하는 방법 ✔ 다른 클래스 객체 인스턴스를 멤버 변수에 넣고 lazy i
tmdfyd0807.tistory.com
✔ 하지만 인터페이스를 통한 콜백 패턴을 적용 시, MainActivity 내의 메서드를 정의하는 것이기 때문에 모든 멤버와 메서드를 사용할 수 있다는 차이점이 있다.
전체 코드
MainActivity.kt
class MainActivity : AppCompatActivity(), VolumeObserver.VolumeChangeListener {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val volumeObserver by lazy { VolumeObserver(this, Handler()) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
contentResolver.registerContentObserver(
Settings.System.CONTENT_URI,
true,
volumeObserver
)
volumeObserver.setVolumeChangeListener(this)
}
override fun onVolumeChanged(volume: String) {
binding.tvMain.text = volume
privateMethod()
publicMethod()
}
init {
instance = this
}
private fun privateMethod() {
LogUtil.d(TAG, "Private Method")
}
fun publicMethod() {
LogUtil.d(TAG, "Public Method")
}
companion object {
private const val TAG = "MainActivity"
private var instance: MainActivity? = null
fun instance(): MainActivity? { return instance }
}
}
VolumeObserver.kt
class VolumeObserver(context: Context, handler: Handler?) : ContentObserver(handler) {
private var audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private lateinit var listener: VolumeChangeListener
override fun onChange(selfChange: Boolean) {
val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
LogUtil.i(TAG, "volume : $volume")
listener.onVolumeChanged(volume.toString())
instance()?.publicMethod()
// instance()?.privateMethod()
super.onChange(selfChange)
}
fun interface VolumeChangeListener {
fun onVolumeChanged(volume: String)
}
fun setVolumeChangeListener(listener: VolumeChangeListener) {
this.listener = listener
}
companion object {
private const val TAG = "VolumeObserver"
}
}
깃허브
https://github.com/tmdfyd2020/Android-Sources/tree/main/CustomListener
GitHub - tmdfyd2020/Android-Sources: Android Toy Project Codes
Android Toy Project Codes. Contribute to tmdfyd2020/Android-Sources development by creating an account on GitHub.
github.com
'Android > Code' 카테고리의 다른 글
[ Android ] [ Kotlin ] 프래그먼트 onClick (0) | 2021.11.16 |
---|---|
[Android] [Kotlin] 객체 인스턴스 (0) | 2021.11.05 |
[Project] [Android] [Kotlin] Log Window (0) | 2021.10.18 |
[Android] [Kotlin] Google Map (0) | 2021.09.24 |
[Android] [Kotlin] EditText 자동 포커싱 및 키보드 (0) | 2021.09.23 |