Design-loving front-end engineer
Ryong
Design-loving front-end engineer
전체 방문자
오늘
어제
    • Framework
    • React
      • Concept
      • Library
      • Hook
      • Component
      • Test
    • NodeJS
    • Android
      • Concept
      • Code
      • Sunflower
      • Etc
    • Flutter
      • Concept
      • Package
    • Web
    • Web
    • CSS
    • Language
    • JavaScript
    • TypeScript
    • Kotlin
    • Dart
    • Algorithm
    • Data Structure
    • Programmers
    • Management
    • Git
    • Editor
    • VSCode
    • Knowledge
    • Voice
Design-loving front-end engineer

Ryong

[ Project ] [ Android ] [ Kotlin ] 콜백 함수
Android/Code

[ Project ] [ Android ] [ Kotlin ] 콜백 함수

2021. 11. 16. 16:11

용어 정리

✔  콜백 (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
    'Android/Code' 카테고리의 다른 글
    • [ Android ] [ Kotlin ] 프래그먼트 onClick
    • [Android] [Kotlin] 객체 인스턴스
    • [Project] [Android] [Kotlin] Log Window
    • [Android] [Kotlin] Google Map
    Design-loving front-end engineer
    Design-loving front-end engineer
    디자인에 관심이 많은 모바일 앱 엔지니어 Ryong입니다.

    티스토리툴바