목차
9 - Widget And Listener
코드에서 View를 참조하는 법
- setContentView 함수 호출 이후 xml에 선언된 View의 객체를 코드에서 참조 가능
- 전통적인 방법
- findViewById(아이디) 함수로 View의 객체를 얻는 법
- 새로운 방법
- viewBinding
HTML의 DOM구조와 비슷하다고 볼 수 있다.
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.calc)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.calc)) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(insets.left, insets.top, insets.right, insets.bottom)
windowInsets
}
val textViewTitle = findViewById<TextView>(R.id.textView) // 1
textViewTitle.text = "Hello, World!" // 2
}
}
- 1 - Code에서 제어하고자 하는 모든 View에 대하여 findViewById 함수를 호출해야 한다.
- 2 - 객체에 대한 참조를 얻게되면 코드 상에서 적용한 내용들이 앱이 화면에 반영된다.
- > 문제점
단점 : MainActivity에 findViewById를 사용할만큼 그 갯수만큼 작성해줘야한다.
단점 : 새로운 화면이 로딩되면 메모리가 로딩되면서 특징 : 다른 화면(xml) 이라면 Id명이 겹쳐도 문제가 없다 → 새로운 화면이 로딩되면 R - 우리 프로젝트의 res 관리하는 클래스. 우리 프로젝트에 1개 생성된다.
activtiy_main.xml → @+id/textViewMain findViewById(R.id.textViewSecond)
activity_second.xml → @+id/textViewSecond findViewById(R.id.textViewSecond)
main에는 textViewSecond가 없는 상황이다. 위젯은 파일마다 따로 있는데 ID는 R 클래스안에 모든 파일의 Id를 통째로 관리 되기 때문에 IDE 상에서는 에러가 발생하지 않고, 빌드를 할 때 그때서야 null exception이 뜬다. → null safety 보장 못함
View Binding
- Module:app 의 build.gradle.kts에 다음과 같은 내용을 추가하면 layout 폴더 안에 있는 파일에 대한 클래스가 자동으로 생성되며 이를 이용해 view 객체를 참조할 수 있다.

- 클래스 이름은 파일 이름을 이용해 자동으로 생성된다.
- activity_main.xml ==> ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
// 또는 private lateinit var binding:ActivityCalcBinding 사용 가능
// lateinit을 사용하면 onCreate 함수 안에서 binding에 할당을 해주어야 함.
// binding = ActivityMainBinding.inflate(layoutInflater)
// 레이아웃 파일에 해당하는 객체를 생성
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(binding.root) // 최상위 레이아웃 설정
// binding이 초기화가 안되어져 있다가, binding을 사용하면서 그때서야 초기화가 되어진다.
// 그래서 lazy를 사용해서 초기화를 늦게 해준다.
// !!!!! setContentView의 파라미터를 생성된 객체로 변경된다.
binding.textView.text = "Hello, World!" // binding.아이디 형식으로 xml 파일의 View를 바로 참조 가능
}
}
> 개념
- setContentView는 레이아웃 리소스 ID를 받아서 해당 레이아웃을 화면에 표시합니다. 이때 레이아웃 파일을 해석하여 뷰의 트리 구조를 생성합니다.
- setContentView는 파일을 주면 파일을 해석하고, View를 주면 만들어진 구조의 View를 바로 사용한다.
- binding은 파일을 해석하여 트리 구조를 미리 만들어 놓는다. 그래서 setContentView에 binding.root를 사용하면 binding에서 미리 만들어 놓은 그 구조를 바로 사용하지만 만약 setContentView에 Activity_Calc 를 작성하면 setContentView가 새로 해석하여 트리 구조를 만들고 그 구조를 사용하여 결국에는 밑에 쓰여진 textView.text = “Hello, World!”가 반영이 되지 않는다.
binding
은 레이아웃 파일을 해석하여 뷰의 트리 구조를 미리 만들어 놓습니다. 따라서setContentView
에binding.root
를 사용하면binding
에서 미리 만들어 놓은 그 구조를 바로 사용합니다. 이렇게 하면 뷰 바인딩을 통해 뷰를 직접 참조할 수 있으므로findViewById
를 사용하지 않아도 되고, 뷰의 ID를 잘못 참조하여 발생하는 오류를 줄일 수 있습니다.- 하지만
setContentView
에 레이아웃 리소스 ID를 직접 작성하면setContentView
가 새로 레이아웃 파일을 해석하여 뷰의 트리 구조를 만들게 됩니다. 이 경우binding
에서 미리 만들어 놓은 구조와는 별개의 새로운 구조가 생성되므로,binding
을 통해 설정한 뷰의 속성이 반영되지 않게 됩니다. 따라서binding
을 사용할 때는 항상setContentView
에binding.root
를 사용해야 합니다. 이렇게 하면binding
에서 설정한 뷰의 속성이 정상적으로 반영됩니다.
Event and Listener
- 사용자의 터치, 키 입력 등을 Event라고 한다.
- Event를 처리하는 방법
- Polling: 일정 주기마다 Event 발생을 확인하는 방법. Application에서는 적합하지 않으며 Arduino 등의 펌웨어에서 주로 사용하는 방법
- Callback 또는 Listener: 특정 이벤트에 대한 처리 함수를 지정해두면 해당 이벤트 발생 시 시스템이 지정된 함수를 호출해 주는 방법.
- 시스템이 호출해 주므로 콜백 함수의 형식이 중요하다. -> 주로 Interface로 정의 되어 있다.
Interface로 정의되어 있는 이유 : 이벤트 처리 함수 이름과 콜백 함수의 형식을 최소한의 규칙으로 제한 하기 위하여
Listener를 만드는 방법
- Activity가 직접 implement 하는 방법. - 계산기의 숫자 키패드 같은 데서 사용
- Event가 특정 View에 속한 것이 아닐 때(키 입력 등)
- View는 달라도 비슷한 코드를 실행할 때
- 이벤트를 수신할 View에 interface의 구현체 또는 람다식을 전달
- 추상 함수 하나만 정의된 interface의 경우 SAM 표기법으로도 전달 가능
- Interface를 구현한 별도의 클래스를 구현하고 그 객체를 전달
실습
- Hello world 내의 버튼 4가지의 리스너를 다양한 방법으로 작성해본다.
• Add: Activity가 직접 구현
• Sub: 객체를 생성하여 변수로 전달
• Mul: 익명 객체 전달
• Div: 람다식
Activity가 interface를 구현
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this) // Add 버튼에 리스너를 달아줌
}
override fun onClick(v: View?) { // View.OnClickListener의 추상 메서드 구현
try{
val n1 = binding.editTextNumber1.text.toString().toDouble()
val n2 = binding.editTextNumber2.text.toString().toDouble()
binding.textViewResult.text="${n1+n2}"
}catch(e:NumberFormatException){
return
// lambda, no return
}
}
}
객체를 변수로 전달
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
private val listener = View.OnClickListener {// 객체를 변수로 전달
try{
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text="${n1-n2}"
}catch(e:NumberFormatException){
// lambda, no return
}
}
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this) // interface 구현
binding.minusBtn.setOnClickListener(listener) // 객체를 변수로 전달
}
override fun onClick(v: View?) { // View.OnClickListener 인터페이스(추상메소드) 구현 }
}
- View.OnClickListener 는 SAM 이므로 이와같이 람다식으로 객체를 만든다. 일반적인 경우 object:View.OnClickListener { } 형식으로 정의해야 한다.
익명 객체(클래스) 전달
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
private val listener = View.OnClickListener {// 객체를 변수로 전달 }
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this) // interface 구현
binding.minusBtn.setOnClickListener(listener) // 객체를 변수로 전달
binding.mulBtn.setOnClickListener(object:View.OnClickListener { // 익명 클래스 전달
override fun onClick(v: View?) {
try{
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text="${n1*n2}"
}catch(e:NumberFormatException){
return
}
}
})
}
override fun onClick(v: View?) { // View.OnClickListener 인터페이스(추상메소드) 구현 }
}
람다식
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
private val listener = View.OnClickListener {// 객체를 변수로 전달 }
override fun onCreate(savedInstanceState: Bundle?) {
binding.plusBtn.setOnClickListener(this) // interface 구현
binding.minusBtn.setOnClickListener(listener) // 객체를 변수로 전달
binding.mulBtn.setOnClickListener(object:View.OnClickListener { // 익명 클래스 전달
override fun onClick(v: View?) {
try{
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text="${n1*n2}"
}catch(e:NumberFormatException){
return
}
}
})
binding.divBtn.setOnClickListener { // 람다식
try {
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text = if(n2 != 0) "${n1 / n2}" else "0으로 나눌 수 없습니다."
} catch (e:NumberFormatException) {}
}
}
override fun onClick(v: View?) { // View.OnClickListener 인터페이스(추상메소드) 구현}
}
- interface에 정의 된 추상 함수가 하나뿐인 경우 람다식 사용 가능
Refactoring
- 현재 코드는 중복되는 코드가 매우 많은 상태
- 현재 기능을 유지하면서 하나의 함수에서 처리하도록 리팩토링 해본다.
- 각 리스너에 전달되는 View 객체(it, view 등의 변수) 의 id를 이용하면 어떤 버튼이 클릭되었는지 확인 할 수 있다.

Activity의 onClick으로 통합한 경우
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this)
binding.minusBtn.setOnClickListener(this)
binding.mulBtn.setOnClickListener(this)
binding.divBtn.setOnClickListener(this)
}
override fun onClick(v: View?) {
try {
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
val result = when(v?.id) {
R.id.plusBtn -> n1 + n2
R.id.minusBtn -> n1 - n2
R.id.mulBtn -> n1 * n2
else -> if(n2!= 0) n1 / n2 else 0
}
binding.textViewResult.text="$result"
} catch (e: NumberFormatException) {
// lambda, no return
}
}
9 - Widget And Listener
코드에서 View를 참조하는 법
- setContentView 함수 호출 이후 xml에 선언된 View의 객체를 코드에서 참조 가능
- 전통적인 방법
- findViewById(아이디) 함수로 View의 객체를 얻는 법
- 새로운 방법
- viewBinding
HTML의 DOM구조와 비슷하다고 볼 수 있다.
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.calc)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.calc)) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(insets.left, insets.top, insets.right, insets.bottom)
windowInsets
}
val textViewTitle = findViewById<TextView>(R.id.textView) // 1
textViewTitle.text = "Hello, World!" // 2
}
}
- 1 - Code에서 제어하고자 하는 모든 View에 대하여 findViewById 함수를 호출해야 한다.
- 2 - 객체에 대한 참조를 얻게되면 코드 상에서 적용한 내용들이 앱이 화면에 반영된다.
- > 문제점
단점 : MainActivity에 findViewById를 사용할만큼 그 갯수만큼 작성해줘야한다.
단점 : 새로운 화면이 로딩되면 메모리가 로딩되면서 특징 : 다른 화면(xml) 이라면 Id명이 겹쳐도 문제가 없다 → 새로운 화면이 로딩되면 R - 우리 프로젝트의 res 관리하는 클래스. 우리 프로젝트에 1개 생성된다.
activtiy_main.xml → @+id/textViewMain findViewById(R.id.textViewSecond)
activity_second.xml → @+id/textViewSecond findViewById(R.id.textViewSecond)
main에는 textViewSecond가 없는 상황이다. 위젯은 파일마다 따로 있는데 ID는 R 클래스안에 모든 파일의 Id를 통째로 관리 되기 때문에 IDE 상에서는 에러가 발생하지 않고, 빌드를 할 때 그때서야 null exception이 뜬다. → null safety 보장 못함
View Binding
- Module:app 의 build.gradle.kts에 다음과 같은 내용을 추가하면 layout 폴더 안에 있는 파일에 대한 클래스가 자동으로 생성되며 이를 이용해 view 객체를 참조할 수 있다.

- 클래스 이름은 파일 이름을 이용해 자동으로 생성된다.
- activity_main.xml ==> ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
// 또는 private lateinit var binding:ActivityCalcBinding 사용 가능
// lateinit을 사용하면 onCreate 함수 안에서 binding에 할당을 해주어야 함.
// binding = ActivityMainBinding.inflate(layoutInflater)
// 레이아웃 파일에 해당하는 객체를 생성
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(binding.root) // 최상위 레이아웃 설정
// binding이 초기화가 안되어져 있다가, binding을 사용하면서 그때서야 초기화가 되어진다.
// 그래서 lazy를 사용해서 초기화를 늦게 해준다.
// !!!!! setContentView의 파라미터를 생성된 객체로 변경된다.
binding.textView.text = "Hello, World!" // binding.아이디 형식으로 xml 파일의 View를 바로 참조 가능
}
}
> 개념
- setContentView는 레이아웃 리소스 ID를 받아서 해당 레이아웃을 화면에 표시합니다. 이때 레이아웃 파일을 해석하여 뷰의 트리 구조를 생성합니다.
- setContentView는 파일을 주면 파일을 해석하고, View를 주면 만들어진 구조의 View를 바로 사용한다.
- binding은 파일을 해석하여 트리 구조를 미리 만들어 놓는다. 그래서 setContentView에 binding.root를 사용하면 binding에서 미리 만들어 놓은 그 구조를 바로 사용하지만 만약 setContentView에 Activity_Calc 를 작성하면 setContentView가 새로 해석하여 트리 구조를 만들고 그 구조를 사용하여 결국에는 밑에 쓰여진 textView.text = “Hello, World!”가 반영이 되지 않는다.
binding
은 레이아웃 파일을 해석하여 뷰의 트리 구조를 미리 만들어 놓습니다. 따라서setContentView
에binding.root
를 사용하면binding
에서 미리 만들어 놓은 그 구조를 바로 사용합니다. 이렇게 하면 뷰 바인딩을 통해 뷰를 직접 참조할 수 있으므로findViewById
를 사용하지 않아도 되고, 뷰의 ID를 잘못 참조하여 발생하는 오류를 줄일 수 있습니다.- 하지만
setContentView
에 레이아웃 리소스 ID를 직접 작성하면setContentView
가 새로 레이아웃 파일을 해석하여 뷰의 트리 구조를 만들게 됩니다. 이 경우binding
에서 미리 만들어 놓은 구조와는 별개의 새로운 구조가 생성되므로,binding
을 통해 설정한 뷰의 속성이 반영되지 않게 됩니다. 따라서binding
을 사용할 때는 항상setContentView
에binding.root
를 사용해야 합니다. 이렇게 하면binding
에서 설정한 뷰의 속성이 정상적으로 반영됩니다.
Event and Listener
- 사용자의 터치, 키 입력 등을 Event라고 한다.
- Event를 처리하는 방법
- Polling: 일정 주기마다 Event 발생을 확인하는 방법. Application에서는 적합하지 않으며 Arduino 등의 펌웨어에서 주로 사용하는 방법
- Callback 또는 Listener: 특정 이벤트에 대한 처리 함수를 지정해두면 해당 이벤트 발생 시 시스템이 지정된 함수를 호출해 주는 방법.
- 시스템이 호출해 주므로 콜백 함수의 형식이 중요하다. -> 주로 Interface로 정의 되어 있다.
Interface로 정의되어 있는 이유 : 이벤트 처리 함수 이름과 콜백 함수의 형식을 최소한의 규칙으로 제한 하기 위하여
Listener를 만드는 방법
- Activity가 직접 implement 하는 방법. - 계산기의 숫자 키패드 같은 데서 사용
- Event가 특정 View에 속한 것이 아닐 때(키 입력 등)
- View는 달라도 비슷한 코드를 실행할 때
- 이벤트를 수신할 View에 interface의 구현체 또는 람다식을 전달
- 추상 함수 하나만 정의된 interface의 경우 SAM 표기법으로도 전달 가능
- Interface를 구현한 별도의 클래스를 구현하고 그 객체를 전달
실습
- Hello world 내의 버튼 4가지의 리스너를 다양한 방법으로 작성해본다.
• Add: Activity가 직접 구현
• Sub: 객체를 생성하여 변수로 전달
• Mul: 익명 객체 전달
• Div: 람다식
Activity가 interface를 구현
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this) // Add 버튼에 리스너를 달아줌
}
override fun onClick(v: View?) { // View.OnClickListener의 추상 메서드 구현
try{
val n1 = binding.editTextNumber1.text.toString().toDouble()
val n2 = binding.editTextNumber2.text.toString().toDouble()
binding.textViewResult.text="${n1+n2}"
}catch(e:NumberFormatException){
return
// lambda, no return
}
}
}
객체를 변수로 전달
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
private val listener = View.OnClickListener {// 객체를 변수로 전달
try{
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text="${n1-n2}"
}catch(e:NumberFormatException){
// lambda, no return
}
}
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this) // interface 구현
binding.minusBtn.setOnClickListener(listener) // 객체를 변수로 전달
}
override fun onClick(v: View?) { // View.OnClickListener 인터페이스(추상메소드) 구현 }
}
- View.OnClickListener 는 SAM 이므로 이와같이 람다식으로 객체를 만든다. 일반적인 경우 object:View.OnClickListener { } 형식으로 정의해야 한다.
익명 객체(클래스) 전달
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
private val listener = View.OnClickListener {// 객체를 변수로 전달 }
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this) // interface 구현
binding.minusBtn.setOnClickListener(listener) // 객체를 변수로 전달
binding.mulBtn.setOnClickListener(object:View.OnClickListener { // 익명 클래스 전달
override fun onClick(v: View?) {
try{
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text="${n1*n2}"
}catch(e:NumberFormatException){
return
}
}
})
}
override fun onClick(v: View?) { // View.OnClickListener 인터페이스(추상메소드) 구현 }
}
람다식
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
private val listener = View.OnClickListener {// 객체를 변수로 전달 }
override fun onCreate(savedInstanceState: Bundle?) {
binding.plusBtn.setOnClickListener(this) // interface 구현
binding.minusBtn.setOnClickListener(listener) // 객체를 변수로 전달
binding.mulBtn.setOnClickListener(object:View.OnClickListener { // 익명 클래스 전달
override fun onClick(v: View?) {
try{
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text="${n1*n2}"
}catch(e:NumberFormatException){
return
}
}
})
binding.divBtn.setOnClickListener { // 람다식
try {
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
binding.textViewResult.text = if(n2 != 0) "${n1 / n2}" else "0으로 나눌 수 없습니다."
} catch (e:NumberFormatException) {}
}
}
override fun onClick(v: View?) { // View.OnClickListener 인터페이스(추상메소드) 구현}
}
- interface에 정의 된 추상 함수가 하나뿐인 경우 람다식 사용 가능
Refactoring
- 현재 코드는 중복되는 코드가 매우 많은 상태
- 현재 기능을 유지하면서 하나의 함수에서 처리하도록 리팩토링 해본다.
- 각 리스너에 전달되는 View 객체(it, view 등의 변수) 의 id를 이용하면 어떤 버튼이 클릭되었는지 확인 할 수 있다.

Activity의 onClick으로 통합한 경우
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val binding by lazy { ActivityCalcBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
....
binding.plusBtn.setOnClickListener(this)
binding.minusBtn.setOnClickListener(this)
binding.mulBtn.setOnClickListener(this)
binding.divBtn.setOnClickListener(this)
}
override fun onClick(v: View?) {
try {
val n1 = binding.editTextNumber1.text.toString().toInt()
val n2 = binding.editTextNumber2.text.toString().toInt()
val result = when(v?.id) {
R.id.plusBtn -> n1 + n2
R.id.minusBtn -> n1 - n2
R.id.mulBtn -> n1 * n2
else -> if(n2!= 0) n1 / n2 else 0
}
binding.textViewResult.text="$result"
} catch (e: NumberFormatException) {
// lambda, no return
}
}