반응형

앱을 개발할 때 클릭과 같은 이벤트 처리를 위해 익명클래스를 만드는 경우가 많습니다.

익명클래스는 작성 코드가 다소 지저분합니다. 클래스의 중괄호 안에 메소드 오버라이드를 위한 중괄호가 또 다시 존재하여 중첩구조가 되어 가독성도 떨어집니다. 그래서 람다표현으로 축약하여 작성하는 경우가 많습니다. 코틀린은 이 람다표현식을 적용할 때 보다 간결하게 작성해주는 SAM 변환 문법을 제공합니다.

SAM Conversion

  • SAM은 Single Abstract Method의 약자로 코틀린의 함수 리터럴을 자동으로 자바의 함수형 인터페이스로 교체해줌.
  • 즉, SAM Conversion은 자바로 작성한 Functioncal Interface에서만 동작하며 코틀린으로 작성시에는 사용하지 못함.
  • 이 기능은 자바와 상호 운영성 측면에서 나왔으며 코틀린의 경우 함수형 인터페이스가 아닌 함수타입으로 선언이 가능함.

함수형 인터페이스 Functional Interface

  • 추상메소드가 1개만 있는 인터페이스
  • default method 와 static method 는 여러개 존재해도 됨
  • 함수형 인터페이스만 람다 표현식으로 작성 가능. 즉, 추상메소드가 1개인 경우에만 람다 표현식이 가능함
  • @FunctionalInterface 어노테이션을 통해 함수형 인터페이스인지 검증 할 수 있음.

 

예제를 만들어보면서 SAM변환에 대해 알아보겠습니다.

 

Project Type : Empty Views Activity

  • Name : KotlinSamConversion
  • Language : Kotlin
  • Mininum SDK : API 26
  • Build configuration language : Kotlin DSL

 

버튼 클릭 처리에 방법에 대해 알아보기 위해 레이아웃 xml 파일에 버튼 하나를 배치하도록 하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="button"/>

</LinearLayout>

 

늦은 초기화를 이용하여 버튼을 참조하겠습니다.

class MainActivity : AppCompatActivity() {

    // 늦은 초기화로 버튼 참조하기
    lateinit var btn: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        //lateinit var 변수 초기화
        btn= findViewById(R.id.btn)
        
    }
}

 

 

버튼 클릭이벤트에 리스너를 등록하는 3가지 방법에 대해 알아보겠습니다. 기본적인 익명클래스 부터 람다식, SAM 변환으로 조금씩 코드를 축약하여 작성하는 모습을 통해 SAM 변환에 대한 효용성을 인식하도록 해보겠습니다. 

 

1) 기본적인 익명클래스로 객체를 생성하여 리스너로 등록

View.OnClickListener 는 인터페이스 이기에 곧바로 객체를 생성할 수 없습니다. 안에 onClick 이라는 추상메소드를 구현하는 별도의 클래스를 정의하고 객체로 생성해야 합니다. 하지만 btn 에만 사용되기에 굳이 별도의 class 를 만들어 재사용 할 필요가 없기에 보통 객체를 생성하면서 클래스의 내용을 구현하는 익명클래스를 이용하여 클릭이벤트를 처리합니다. 코틀린에서는 object 키워드를 이용하여 익명클래스를 구현합니다. 클릭했을 때 동작은 간단하게 Toast 를 보여주도록 하겠습니다. 주의할 점은 Toast를 보여줄 때의 첫번째 파라미터로 필요한 Context 로 보통 액티비티를 지정해 주는데 익명 클래스 안에서 this는 익명클래스 본인을 의미하기에 이를 감싸는 아웃터 클래스인 MainActivity 클래스를 의미하고자 한다면 @Label 문법을 사용하여 this@MainActivity 로 지정해야 합니다. 자바에서도 많이 사용했던 문법인데 자바에서는 MainActivity.this 로 사용했던 문법이었습니다.

//버튼 클릭이벤트 리스너 등록 3가지 방법 [ 익명객체 만들어 적용하는 3가지 방법 ]
//1. 기본적인 익명객체 문법
btn.setOnClickListener(object : View.OnClickListener{
    override fun onClick(v: View?) {
        //익명 클래스안에서 아웃터클래스(MainActivity)의 this를 호출할때 @Label 사용
        Toast.makeText(this@MainActivity, "clicked", Toast.LENGTH_SHORT).show()
    }
})

 

2) 람다( Lambda) 표기법

익명클래스의 추상메소드가 1개일때 사용이 가능한 문법으로서 코드에 대한 축약표현문법입니다. 람다표현은 추상메소드의 오버라이드 코드를 생략하여  가독성도 좋고 코드도 간결해 집니다. 또한 람다의 { } 영역은 익명클래스가 아닌 일반 함수의 {} 영역처럼 되기에 이 영역안에서는 this 가 람다를 가진 MainActivity를 의미하기에 @Label 이 필요하지 않습니다.

//2. 람다(Lambda)표기법 [ 익명객체의 추상메소드가 1개일때 사용가능한 기술 - 축약표현문법 : 추상메소드의 오버라이드 코드를 생략 -람다표기법 사용 ] {}는 익명클래스가 아니기에 this 가 MainActivity를 의미함.
btn.setOnClickListener( View.OnClickListener { v-> Toast.makeText(this, "SAM conversion clicked", Toast.LENGTH_SHORT).show() }  )

 

2.1) 파라미터가 1개일 때는 파라미터의 생략도 가능합니다. 그래서 v-> 를 생략하면 코드가 더 간결해집니다.

//2.1 파라미터가 1개일때는 파라미터도 생략가능
btn.setOnClickListener( View.OnClickListener { Toast.makeText(this, "SAM conversion clicked!", Toast.LENGTH_SHORT).show() }  )

 

3) SAM 변환 

람다표현에서 setOnClickListener()메소드의 리스너에 다른 클래스를 사용하는 것은 어차피 불가능 하기에 View.OnClickListener 인터페이스이름 조차도 생략하고 함수의 소괄호()까지 제거하여 클릭 했을 때 수행할 내용만 신경쓰며 작성할 수 있도록 더 축약형으로 작성하는 것이 가능합니다. 이를 SAM 변환(Conversion) 이라고 합니다. 이름 그대로 추상메소드가 1개인 경우에만 적용이 가능한 변환 표기법 입니다. 

//3. SAM 변환 - setOnClickListener()메소드가 어차피 다른 클래스는 사용이 불가능 하기에 OnClickListener 인터페이스이름 조차도 생략하여 더 축약형으로..
btn.setOnClickListener { v-> Toast.makeText(this, "완전 SAM 적용", Toast.LENGTH_SHORT).show() }

 

3.1) 파라미터가 1개일때 생략 가능

당연하게 SAM 변환도 파라미터가 1개일때는 파라미터 표기를 생략하는 것이 가능합니다. 계속 Toast 만 보여주었으니 이번에는 사용자 데이터를 입력받아 보여주는 코드를 작성해 보겠습니다.

사용자가 글씨를 입력하고 버튼을 클릭하면 입력된 글씨를 그대로 보여주는 간단한 기능을 구현하겠습니다. EditText와 TextView를 추가하겠습니다. 버튼을 클릭했을 때 EditText의 글씨를 가져와서 TextView에 설정하도록 하겠습니다.

먼저 레이아웃 activity_main. xml 파일에 뷰를 추가하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="input text"
        android:inputType="text"/>
    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="button"/>
    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:text="RESULT"
        android:textColor="@color/black"/>

</LinearLayout>

 

MainActivity 코틀린 파일에서 뷰 참조변수 2개를 추가하고 참조하도록 하겠습니다. 위 Button 객체의 참조변수 btn 은  lateinit 늦은 초기화를 사용했으니 이번에는 다른 방법의 늦은 초기화로 참조하도록 하겠습니다.

class MainActivity : AppCompatActivity() {

    // 늦은 초기화로 버튼 참조하기
    lateinit var btn: Button

    //다른 방법으로 늦은 초기화
    val et: EditText by lazy { findViewById(R.id.et) }
    val tv by lazy { findViewById<TextView>(R.id.tv) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        .....

        //lateinit var 변수 초기화
        btn= findViewById(R.id.btn)
        
        .....
        
    }
}

 

이제 버튼 클릭이벤트 처리를 SAM 변환으로 하겠습니다. 또한 onClick 함수의 파라미터가 v 한개뿐이기에 생략하도록 하겠습니다.

코틀린은 getXXX(), setXXX()을 사용하지 않고 곧바로 property(멤버변수)를 제어하는 방식을 선호합니다. 이를 참고하여 코드를 보시기 바랍니다.

//3.1 당연히 파라미터 생략가능 [ EditText의 글씨를 가져와서 TextView에 설정 - 코틀린의 getXXX(), setXXX()을 사용하지 않고 곧바로 property(멤버변수)를 제어하는 방식을 선호 ]
btn.setOnClickListener { tv.text= et.text.toString() }

 

3.2) 만약 생략한 파라미터 v 를 사용하고 싶다면 it 키워드 사용 가능

클릭된 버튼의 id 프로퍼티에 접근 할 때 it 키워드를 사용해 보았습니다.

//3.2 만약 파라미터를 생략했는데 {}안에서 파라미터 v를 사용하고 싶다면 특별한 키워드 it 사용
btn.setOnClickListener {
    when(it.id){
        //익명객체를 생략했기에 Context파라미터에 전달한 this가 자연스럽게 MainActivity가 되어 그냥 사용할 수 있음.
        R.id.btn-> Toast.makeText(this, "선택한 버튼의 ID 값 : " + it.id, Toast.LENGTH_SHORT).show()
    }
}

 


 

SAM은 리스너중에서 추상메소드가 1개인 모든 곳에서 사용이 가능합니다.

롱클릭, 체크박스, 라디오그룹, 레이팅바 등 에서도 사용이 가능합니다. 하나씩 살펴보면서 SAM 변환에 대해 익숙해저 보겠습니다.

 

1) LongClickListenr

주의깊게 보실 것은 return 키워드를 생략한 부분입니다. LongClickListener 는 ClickListener의 중간 과정이라고 볼 수 있기에 롱클릭이 종료되면 이어서 클릭이벤트가 발동합니다. 즉, 롱클릭 처리를 한 후에 원치않아도 클릭이벤트가 발동되기에 이벤트를 이 곳에서 멈추기위해 이벤트를 소비했다는 의미에서 true 를 리턴해줘야 합니다. 이 때 SAM 변환을 한 중괄호 { } 영역은 별도의 함수 영역이 아니기에 리턴을 위해 return 키워드를 작성하면 LongClickListener 가 아닌 이 코드가 작성되고 있는 onCreate method 영역의 return 으로 인식됩니다. 그래서 SAM 변환 영역 안에서 리턴을 하려면 return 키워드를 생략해야만 합니다. 만약, 명시적으로 return 키워드를 명시하고 싶다면 @Label 을 통해 return 하는 주체가 setOnLongClickListener 임을 명시적으로 알려줘야만 합니다.

//1) LongClickListener
btn.setOnLongClickListener {
    Toast.makeText(this, "long click", Toast.LENGTH_SHORT).show()

    //return this //error - 여기서 return 을 하면 onCreate Method 의 return 으로 인식함.
    //SAM 변환에서는 return 키워드 사용하지 않음.
    true
    // return 키워드를 명시적으로 하고싶다면.. @Label을 통해 누구의 return 인지 명시..
    //return@setOnLongClickListener true   //리턴값이 있을때의 코드
}

 

 

2) 복합버튼(CompoundButton - CheckBox, RadioButton, Switch) 의 체크상태 변경 리스너 OnCheckedChangeListener

먼저 복합버튼 중에서 가장 많이 사용되는 CheckBox 를 레이아웃 xml 에 추가하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    ........

    <!-- SAM 연습용 복합버튼.   -->
    <CheckBox
        android:id="@+id/cb"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="checkbox"/>

</LinearLayout>

 

이제 체크박스의 변경에 반응하는 리스너를 설정하겠습니다. SAM 변환과정에서 필요한 파라미터를 확인하기 위해 한번은 전통적인 익명클래스로 설정하고 이를 다시 SAM 변환으로 리스너를 설정해보겠습니다.

 

먼저 체크박스 뷰를 참조하겠습니다. 리스너 설정에 집중하기 위해 참조변수는 onCreat() method 안에서 만들고 참조하겠습니다.

class MainActivity : AppCompatActivity() {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        .....

        val cb: CheckBox = findViewById(R.id.cb)
        
    }
}

 

2.1) 익명클래스 객체를 이용하여 체크상태 변경 리스너 설정하기

cb.setOnCheckedChangeListener( object : CompoundButton.OnCheckedChangeListener{
    override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
        Toast.makeText(this@MainActivity, "체크상태 : $isChecked", Toast.LENGTH_SHORT).show()
    }
})

 

체크상태 변경 콜백메소드인 onCheckedChanged() 메소드는 체크박스 참조변수와 체크상태값을 파라미터로 받게 됩니다.

이 OnCheckedChangeListener 인터페이스도 추상메소드가 1개 뿐인 함수형 인터페이스 입니다. 그렇기에 다른 함수가 리스너에 설정될 수 없습니다. 그렇기에 굳이 인터페이스 이름과 콜백메소드를 명시하지 않고 함수 영역인 중괄호{ } 만 작성하는 것으로 간략하게 작성하는 SAM 변환이 가능합니다.

 

2.2) SAM 변환으로 체크상태 변경 리스너 설정하기

OnClickListener 와 다르게 파라미터가 2개 이기에 생략하는 것은 불가능 합니다. 즉, 아래 코드는 에러입니다.

//SAM conversion
//파라미터가 여러개면 생략할 수 없음 , 파라미터들의 변수명만 작성해도 되고. 자료형까지 명시해도 됨
cb.setOnCheckedChangeListener{ Toast.makeText(this, "aaa", Toast.LENGTH_SHORT).show() } //ERROR

 

2개의 파라미터를 작성하되 자료형은 반드시 명시할 필요는 없습니다. 물론 명시해도 상관은 없습니다.

cb.setOnCheckedChangeListener{ buttonView, isChecked:Boolean ->
    Toast.makeText(this, "체크상태 : $isChecked", Toast.LENGTH_SHORT).show()
}

 

코드가 훨씬 간결해 보이네요. 

이번에는 다중선택이 가능한 체크박스와 다르게 단일선택(Single choice) 용 RadioButton 에 대한 처리를 SAM 변환으로 해보겠습니다.

 

 

3) RadioGroup 의 체크상태 변경 리스너 RadioGroup.OnCheckedChangeListener

RadioButton 은 여러개 중 한개만 선택되는 단일 선택이기에 하나를 선택하여 체크상태가 변경되면 다른 RadioButton의 체크상태도 해제되는 변경이벤트가 발생합니다. 즉, 라디오버튼 1개를 선택하면 다른 라디오버튼의 체크가 해제됩니다. 그렇기에 다중선택 용으로 사용하는 체크박스와 다르게 버튼 개별 단위로 체크상태변경 리스너를 처리하지 않습니다.

여러 라디오 버튼을 감싸는 RadioGroup 단위로 체크상태 변경 처리하여 선택된 RadioButton 이 누구인지 식별하여 원하는 작업을 수행하도록 합니다.

 

체크박스 아래에 RadioGroup 과 RadioButton 2개를 레이아웃 파일에 추가하겠습니다. 성별(남성/여성)을 선택하는 버튼들 입니다.

.....

<RadioGroup
    android:id="@+id/rg"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <RadioButton
        android:id="@+id/rb_f"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="female"
        android:checked="true"/>
    <RadioButton
        android:id="@+id/rb_m"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="male"/>

</RadioGroup>

 

체크박스때와 마찬가지로 onCreate() method 안에 RadioGroup 참조변수를 만들고 뷰를 참조하겠습니다.

val rg: RadioGroup = findViewById(R.id.rg)

 

3.1) RadioGroup에  익명클래스로 체크상태 변경 리스너 설정하기

rg.setOnCheckedChangeListener(object : RadioGroup.OnCheckedChangeListener{
    override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) {
        val rb: RadioButton = findViewById(checkedId)
        Toast.makeText(this@MainActivity, rb.text, Toast.LENGTH_SHORT).show()
    }
})

 

라디오그룹의 체크상태 변경 리스너의 onCheckedChanged() 콜백메소드도 2개의 파라미터를 가집니다. 다만, 체크박스의 콜백메소드와 파라미터 종류가 다릅니다. 첫번째는 RadioGroup 참조변수와 현재 선택된 RadioButton의 id 속성값을 파라미터로 받습니다.

이 2번째 파라미터인 선택된 라디오버튼의 id 를 이용하여 RadioButton 뷰를 참조하고 이 뷰의 글씨를 얻어와 보여줄 수 있습니다.

 

이 라디오그룹의 리스너 인터페이스도 콜백메소드가 1개뿐이기에 함수 영역 { } 만 작성하는 SAM 변환이 가능합니다.

 

3.2) SAM conversion 으로 체크상태 변경 리스너 설정하기

파라미터의 변수명만 작성해도 됩니다. 1개가 아니면 생략할 수 없습니다.

//SAM conversion
rg.setOnCheckedChangeListener { group, checkedId ->
    val rb: RadioButton = findViewById(checkedId)
    Toast.makeText(this, "선택 : ${rb.text}", Toast.LENGTH_SHORT).show()
}

 

역시 이벤트 처리 코드가 아주 간결합니다.

이렇듯 인터페이스의 추상메소드가 1개뿐이라면 SAM 변환을 사용하는 것으로 코드의 간결함이 매우 좋아지기에 많이 사용됩니다.

 

그런데 SAM 변환( Single Abstract Method ) 라는 이름이 무색하게 추상메소드가 1개가 아닌 곳에서도 SAM 변환 적용이 가능한 경우가 있습니다.

 

4) 특이하게 추상메소드가 1개가 아님에도 사용할 수 있는 경우도 있음.

SAM이라는 이름이 좀 어색합니다.

사용자 입력을 받는 EditText 의 글씨 변경 이벤트 처리는 TextWatcher 인터페이스를 이용합니다. 이 인터페이스는 글씨변경전, 변경될 때, 변경 후에 각각 발동하는 콜백용 추상메소드 3개를 가지고 있습니다. 즉, Single Abstract Method 가 아닙니다. 그럼에도 SAM 변환이 가능합니다. 이 3개 중 마지막 1개의 추상 메소드[ afterTextChanged() ]를 SAM 변환으로 처리할 수 있습니다.

 

위에서 사용했던 EditText 를 참조했던 et 참조변수를 이용하여 실습을 진행하겠습니다.

 

4.1) TextWatcher 익명 클래스를 통해 글씨변경 이벤트 처리하기

et.addTextChangedListener( object : TextWatcher {
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        //자동으로 써진 TODO 를 지우거나 앞에 주석표시 없으면 이 영역이 실행될 때 에러 발생할 수 있음.
        //TODO("Not yet implemented")
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        //TODO("Not yet implemented")
    }

    override fun afterTextChanged(s: Editable?) {
        //TODO("Not yet implemented")
    }
})

 

위 익명클래스의 코드를 보면 알 수 있듯이 3개의 추상 메소드를 가지고 있습니다. 이 중 마지막에 있는 afterTextChanged() 메소드만을 사용한다면 SAM 변환으로 간략하게 작성이 가능합니다.

 

4.2) SAM 변환으로 글씨변경 이벤트 처리하기

//위 처럼 추상메소드가 3개인 인터페이스는 원래 SAM이 안됨. 하지만, addTextChangedListener()에서는 3개중 마지막 추상메소드만 사용하는 인터페이스로 변경하여 SAM처리해줌
//즉,아래 SAM변환 메소드는 위 TextWatcher의 afterTextChanged() 추상메소드 임
et.addTextChangedListener{
    Toast.makeText(this,"글씨변경 : " + it.toString(),Toast.LENGTH_SHORT).show()
}

 

 

5) 뷰의 이벤트 처리 외에 다이얼로그의 버튼 이벤트 처리에도 SAM conversion 사용 가능

뷰의 이벤트 처리에 사용되었던 리스너 익명클래스들 외에도 추상메소드 1개를 가진 익명클래스가 사용되는 모든 곳에서 SAM 변환이 가능합니다. 대표적으로 사용되는 다이얼로그의 버튼 클릭 이벤트 처리에도 사용해 보겠습니다. 사실, 이 리스너도 뷰의 OnClickListener 와 거의 동일한 onClick() 추상메소드를 1개 가지고 있습니다. 다만, 파라미터 개수만 2개로 다를 뿐입니다.

다이얼로그의 버튼은 [긍정의 버튼, 부정의 버튼, 중립의 버튼 ] 3개를 설정할 수 있습니다.

SAM 변환의 과정을 확인하기 위해 버튼 3개의 클릭이벤트 처리를 차례로 익명클래스, 람다표현, SAM 변환으로 처리해 보겠습니다.

 

먼저, 버튼이 클릭되었을 때 다이얼로그가 보이도록 액티비티의 레이아웃 파일 activity_main.xml 에 버튼을 추가하겠습니다. 라디오 그룹 아래에 배치되도록 하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    ......

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="show AlertDialog"
        android:textAllCaps="false"/>

</LinearLayout>

 

실습의 편의를 의해 별도의 버튼 참조변수 선언 없이 MainActivity의 onCreate() 메소드 안에서 버튼을 찾아와서 곧바로 클릭이벤트를 처리하여 다이얼로그를 만들어 보여주도록 하겠습니다. 

  • 긍정의 버튼 : PositiveButton     -- 익명클래스로 처리
  • 부정의 버튼 : NegativeButton   -- 람다로 처리
  • 중립의 버튼 : NeutralButton      -- SAM conversion 으로 처리
class MainActivity : AppCompatActivity() {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        .....

        findViewById<Button>(R.id.btn2).setOnClickListener {

            val builder= AlertDialog.Builder(this)
            builder.setTitle("Dialog")
            builder.setMessage("This is Alert Dialog")
            // 익명클래스로 클릭이벤트 처리
            builder.setPositiveButton("OK", object : DialogInterface.OnClickListener{
                override fun onClick(p0: DialogInterface?, p1: Int) {
                    Toast.makeText(this@MainActivity, "click ok", Toast.LENGTH_SHORT).show()
                }
            })
            // lambda 표현식으로 리스너처리
            builder.setNegativeButton("CANCEL",{po,p1->Toast.makeText(this, "click cancel", Toast.LENGTH_SHORT).show()})
            // SAM conversion 완성 - 위에서 ()도 생략했듯.. 여기서도. ()를 생략하고 {}만 작성하고자 하였으나 "CANCEL"처럼 다른 파라미터가 있으면 그 파라미터는 그대로 ()에 넣고.. {}만 밖으로 이동
            builder.setNeutralButton("OPTION"){po,p1-> Toast.makeText(this, "click option", Toast.LENGTH_SHORT).show()}
            builder.create().show()

        } 
        
    }//onCreate method
    
}//MainActivity class

 

주의해서 보실 부분은 setNeutralButton() 의 SAM 변환 모습입니다. 그동안 소개했던 뷰의 이벤트 처리와 다르게 이 메소드는 파라미터가 리스너 이외에 버튼에 보여지는 글씨를 설정하는 String 파라미터가 존재합니다.

즉, 그동안은 리스너를 설정하는 메소드의 파라미터가 익명클래스만 파라미터로 요구했기에 메소드의 소괄호()를 생략하고 추상메소드의 기능을 실제 구현하는 메소드의 중괄호 { } 만 작성했습니다. 하지만 다이얼로그의 버튼들을 설정하는 메소드는 리스너만 파라미터로 전달하지 않기에 파라미터를 전달하는 소괄호()를 완전 생략할 수 없습니다. 그래서 SAM 변환을 하는 리스너는 소괄호() 밖에 중괄호 { } 를 작성하고 소괄호()에는 버튼에 보여지는 글씨를 설정합니다.

 

즉, 안드로이드 대부분의 리스너는 이런식으로 SAM 변환 코드로 처리가 가능합니다.

 

 

6) SAM conversion은 리스너 뿐이 아니라 안드로이드의 여러 콜백처리에도 사용될 수 있음.

추상메소드가 1개만 있는 함수형 인터페이스를 사용하는 모든 곳에 SAM 변환이 가능합니다.

 

ex) 다른 액티비티를 실행 할때 결과를 받아 돌아오는 ActivityResultLauncher 객체의 콜백 처리

 

먼저, 버튼을 클릭하면 실행된 SecondActivity 를 만들겠습니다.

 

화면은 간단하게 "This is SecondActivity" 라는 글씨를 보여주는 TextView 를 배치하겠습니다.

 

이제. MainActivity 의 레이아웃 파일인 activity_main.xml 에 버튼 하나를 추가하고 이 버튼을 클릭하면 SecondActivity 로 전환하겠습니다. 이때 그냥 전환하는 것이 아니라 결과를 되돌려 받는 ActivityResultLauncher 객체를 사용하고 콜백처리를 익명클래스, 람다표현, SAM 변환 순으로 변경해 보겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    .....

    <Button
        android:id="@+id/btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="go to SecondActivity"
        android:textAllCaps="false"/>

</LinearLayout>

 

실습의 편의를 위해 별도의 버튼 참조변수 선언 없이 onCreate() 메소드 안에서 버튼을 찾아와 클릭 이벤트를 처리하겠습니다.

findViewById<Button>(R.id.btn3).setOnClickListener {
    //SecondActivity 를 실행하기 위한 인텐트 객체 생성
    val intent: Intent = Intent(this, SecondActivity::class.java)
    
}

 

결과를 받기위해 액티비티를 대신 실행해 주는 일종의 대행사 객체인 ActivityResultLauncher 는 액티비티에 등록(register) 해야 사용할 수 있습니다. 또한 이 등록은 반드시 클래스의 멤버 위치에서 수행해야 합니다. 그래서 onCreate() 메소드 밖에서 객체를 생성하겠습니다.

 

6.1) 익명 클래스로 ActivityResultCallback 객체 처리

class MainActivity : AppCompatActivity() {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        .....

        findViewById<Button>(R.id.btn3).setOnClickListener {
            //SecondActivity 를 실행하기 위한 인텐트 객체 생성
            val intent: Intent = Intent(this, SecondActivity::class.java)
            
            //결과를 받아 돌아오는 ActivityResultLauncher 실행객체로 SecondActivity 실행!
            resultLauncher.launch(intent)    
        }
        
    }//onCreate method
    
    //결과를 받아 돌아오는 Activity Result Launcher 실행객체 -- 익명클래스로 ActivityResultCallback 콜백객체 처리
    val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult(), object : ActivityResultCallback<ActivityResult> {
            override fun onActivityResult(result: ActivityResult) {
                TODO("Not yet implemented")
            }
        })
    
}//MainActivity class

 

6.2) 일부 SAM 변환하여 ActivityResultCallback 객체 처리

ActivityResultCallback 익명클래스만 SAM 변환으로 처리하였습니다. 콜백 익명클래스의 메소드명과 소괄호()를 모두 생략했습니다.

// SAM 변환을 일부 사용하기
val resultLauncher2: ActivityResultLauncher<Intent> = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult(), ActivityResultCallback {
        if(it?.resultCode== RESULT_OK){
            //.........
        }
    })

 

6.3) 완성된 SAM 변환으로 ActivityResultCallback 객체 처리

registerForActivityResult() 메소드에 콜백객체 외에 다른 파라미터인 ActivityResultContracts 객체는 소괄호() 안에 두고 콜백객체만 SAM 변환의 중괄호{ } 로 분리하여 작성하면 코드의 가독성이 조금더 향상됩니다. 

class MainActivity : AppCompatActivity() {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        .....

        findViewById<Button>(R.id.btn3).setOnClickListener {
            //SecondActivity 를 실행하기 위한 인텐트 객체 생성
            val intent: Intent = Intent(this, SecondActivity::class.java)
            
            //결과를 받아 돌아오는 ActivityResultLauncher 실행객체로 SecondActivity 실행!
            //resultLauncher.launch(intent)   
            
            resultLauncher3.launch(intent)
        }
        
    }//onCreate method


    // SAM 변환 완성
    val resultLauncher3: ActivityResultLauncher<Intent> = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()){
        if(it?.resultCode== RESULT_OK){
            //.........
        }
    }
    
}//MainActivity class

 


 

 

 

반응형

+ Recent posts