본문 바로가기
안드로이드

[안드로이드 #2] 이벤트

by algosketch 2021. 3. 3.

목차

  • 이벤트
  • Toast Message
  • Activity 생명주기
  • Intent

 

 지난번 과제는 어떠셨나요? 과제에 대한 피드백은 스터디에서 하도록 하겠습니다. 오늘은 제목을 이벤트라고 지었지만 이벤트를 포함한 여러 개념들을 공부할 예정입니다.

 

 - 이벤트

 뷰를 클릭했을 때의 이벤트를 지정할 수 있는 방법은 크게 두 가지가 있습니다. 하나는 kotlin코드(혹은  java)에서 지정하는 방법이고 다른 하나는 xml코드에서 지정하는 방법입니다.

 1) xml에서 지정

 먼저 xml에서 지정하는 방법을 살펴보도록 하겠습니다. xml에서는 onClick 이라는 속성에 클릭 이벤트를 구현한 함수 이름을 넣어주면 됩니다.

xml 1
xml 2

 여기서 onClick 함수는 매개변수로 View 객체를 받아야 합니다. 예시 코드는 button Clicked! 라는 메시지를 띄우는 간단한 코드입니다.

xml 3

 

 2) 코드에서 지정

 이번에는 코틀린 코드에서 지정하는 방법입니다. 위와 동일한 결과를 내는 코드를 작성해 보겠습니다. setOnClickListener는 해당 뷰를 클릭했을 때 발생하는 이벤트를 지정하는 메서드입니다. 이 메서드의 인자로 View.OnClickListener 인터페이스를 상속 받아 구현한 클래스의 객체를 넘겨줘야 합니다.

OnClickListener 1

 위 코드는 View.OnClickListener를 상속받아 구현한 클래스입니다. 이 클래스의 객체를 setOnClickListener() 의 인자로 넘겨주면 됩니다.

button id
OnClickListener 2

 여기서 findViewById는 xml에 있는 뷰 객체를 id값을 통해서 가져오는 함수입니다. xml에서 지정한 id는 코드상에서 R.id.* 로 참조할 수 있습니다. 버튼 뷰를 가져왔고, 이 뷰에 클릭 이벤트를 연결시켜주는 메서드가 setOnClickListener입니다. 따라서 위 코드를 실행하면 아까와 동일한 결과가 나옵니다.

 그런데 말입니다. 위 코드는 축약이 가능합니다. 14, 15라인은 따로 변수에 담지 않으면 한 줄로 처리가 가능합니다. 그리고 코틀린 문법에선 오버라이딩 해야할 메서드가 하나인 인터페이스를 구현하는 클래스의 객체...(SAM 객체, 샘이라 읽음) 말이 너무 어렵네요. 쉽게 말하면 메서드가 하나인 객체는 람다로 바꿀 수 있습니다. 따라서 위 코드는 다음 코드와 동일한 결과를 가져옵니다.

 코드 OnClickListener 1 + OnClickListener 2 역할을 합니다. 람다는 언어에 익숙해지면 사용하시고 지금은 축약하는 방법을 꼭 이해하지 않으셔도 됩니다. 설명도 자세하게 안 했으니까요!

 그 밖에도 여러 가지 트릭을 쓰면 이벤트를 연결하는 더 많은 방법을 만들어낼 수 있지만, 대표적인 두 가지 방법과 람다를 사용한 코드를 살펴봤습니다.

 

 - Toast Message

 화면 하단에 잠깐 나왔다가 사라지는 메시지를 토스트 메시지라고 합니다. 토스트 메시지를 출력하려면 메시지를 만들고 -> 출력하면 됩니다.

val toast = Toast.makeText(this, "button clicked!", Toast.LENGTH_SHORT) // 메시지 만들기
toast.show() // 출력

 위처럼 메시지 만드는 것과 출력을 나눠서 작성할 수도 있지만, 위처럼 간단한 코드 한 줄로 입력하는 게 더 깔끔해 보입니다.

Toast.makeText(this, "button clicked!", Toast.LENGTH_SHORT).show()

 Toast의 makeText 메서드는 각각 Context, String, duration 타입의 인자를 받습니다. String은 출력할 메시지, duration은 토스트 메시지가 화면에 노출되는 시간을 결정합니다. 보통 Toast.LENGTH_SHORT와 Toast.LENGTH_LONG를 인자로 넘겨줍니다.
 Context는 맥락이라는 뜻이고, 이 객체는 사실 저도 정확히 이해하고 사용하는 것이 아닙니다.

Toast

 이 코드처럼 Activity 클래스 내부에서 호출할 경우 this를 통해 context를 불러올 수 있다는 것만 알아두세요. 만약 클래스 등이 중첩되어 this가 액티비티가 아닌 inner class를 가리키는 경우 this@MainActivity를 통해 가져올 수 있습니다. this로 Context를 못 받는 경우에는, 클릭 이벤트가 받은 View인자를 통해서도 Context를 얻을 수 있습니다.

context 1
context 2

 람다에서 it은 단일 매개변수를 가리킵니다.

 

 - Activity 생명주기

 액티비티는 특정 상황에서 자동으로 메서드를 호출합니다. 액티비티가 만들어질 때, 화면을 출력할 때, 일시 정지할 때, 완전히 종료될 때 등이 있습니다.

액티비티 생명주기 1

 이제 보니 우리가 작업하던 코드도 onCreate라는 오버라이드 메서드 내부에서 동작했었네요.

액티비티 생명주기 2

 당연하게도 액티비티 생명주기와 관련된 7가지 메서드 모두 오버라이딩할 수 있습니다. 무언가를 상속 받는 클래스 내부에서 오버라이딩 가능한 메서드 목록은 단축키 'Ctrl+O'로 확인할 수 있습니다. 

액티비티 생명주기 3
액티비티 생명주기 4

 액티비티의 메서드 목록이 어마무시하게 많은데 별도로 검색하는 란이 없어도 키보드 자판을 누르면 검색할 수 있습니다. 생명주기와 관련된 메서드들을 모두 오버라이딩 해서 로그를 찍어보도록 하겠습니다.

액티비티 생명주기 5
액티비티 생명주기 6

 위 로그는 다음과 같이 행동한 결과입니다.

1. 앱 실행
 onCreate()
 onStart()
 onResume()

2. 탭을 눌러 다른 앱 화면으로 이동
 onPause()
 onStop()

3. 다시 원래 앱으로 돌아옴
 onStart()
 onResume()

4. 뒤로가기 버튼으로 앱 종료
 onPause()
 onStop()
 onDestroy()

 각각의 상황에 대해서 어떠한 메서드가 호출되는지만 간단하게 이해하고 넘어가시면 됩니다. 알아둬야 할 것은 onPause(), onStop() 메서드의 경우 액티비티의 데이터가 유지되고 onDestroy() 메서드는 완전히 종료되기 때문에 데이터가 소실된다는 것입니다. 그런데 여기서 중요한 사실이 있습니다.

액티비티 생명주기 7

위 로그는 앱 실행 후 화면회전한 결과입니다. 화면 회전시 onDestroy() 메서드가 호출되기 때문에 액티비티의 데이터가 소실됩니다. 만약 블로그 앱에서 글을 쓰다가 화면을 회전시키면 쓰던 글이 날아가는 상황이 되는 거죠. 화면 회전과 같은 상황에서 데이터를 보존할 수 있는 방법은 OnCreate 메서드의 매개변수를 이용하는 것입니다.

액티비티 생명주기 8

 savedInstanceState라는 인자를 통해 액티비티 데이터를 저장할 수 있습니다. 이 강의에서는 자세히 다루지 않겠습니다.

 지금까지 액티비티 생명주기에 대해 살펴보았습니다. 뷰 또한 생명주기가 있습니다. 액티비티와 뷰는 생명주기가 다른데요, 뷰 중에서 액티비티의 생명주기를 갖는 Fragment라는 뷰가 있습니다. 이 내용은 내용이 많으므로 별도의 강의에서 다루도록 하겠습니다. 액티비티 생명주기에 대한 자세한 내용은 이 링크에서 확인하실 수 있습니다.

 

 - Intent (새 화면을 띄우는 방법)

 정확히 말하면 Intent에 대해 다루는 건 아니고 새 액티비티를 띄우는 방법에 대해서 다룹니다. Intent는 액티비티 뿐만 아니라 컴포넌트 사이의 통신을 위해 사용됩니다. 액티비티 또한 컴포넌트이므로 메인 액티비티와 서브 액티비티간의 통신을 위해 Intent 객체를 사용합니다.

 우선 서브 액티비티를 띄우기 위해선 새 액티비티를 만들어야 합니다. 상단 탭의 File에서도 찾을 수 있고 app 디렉터리나 적당해 보이는 디렉터리 아무곳이든 우클릭하면 새 액티비티를 만들 수 있습니다.

Intent 1
Intent 2
Intent 3

 프로젝트를 처음 만들 때와 동일하게 Empty Activity로 생성한 뒤 이름을 정하고 Finish를 눌러주세요. 그리고 activity_sub.xml 파일을 열어 TextView만 추가해 줍니다.

Intent 4

 그리고 MainActivity.kt 로 돌아와 위 코드를 작성하면 서브 액티비티가 실행됩니다. 뒤로가기를 누르시면 메인 액티비티로 돌아옵니다.
 서브 액티비티를 실행하는 코드는 startActivity()로 Intent 객체를 인자로 받습니다. 그리고 Intent는 서브 액티비티에 대한 정보를 갖고 있어야 합니다. 그래서 Intent 객체를 미리 만들어 주었는데, Intent의 생성자는 Context와 서브 액티비티 class자체를 받습니다. 서브액티비티 클래스는 액티비티를 만들면서 자동으로 추가됩니다.

Intent 5

 이번에는 메인 액티비티에서 입력한 아이디 값을 서브 액티비티에 출력해보도록 하겠습니다. 아이디를 입력하고 로그인 버튼을 누르면 서브 액티비티에서 출력되는 구조입니다. 이제부터 약간의 어려움을 느낄 수 있겠지만 똑똑한 여러분이라면 충분히 따라오실 수 있을 거라 믿습니다!!

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.btn_login).setOnClickListener {
            val intent = Intent(this, SubActivity::class.java)

            val idText = findViewById<EditText>(R.id.input_id).text.toString()
            intent.putExtra("id", idText) // 데이터 전송
            
            startActivity(intent)
        }
    }
}

 

 코드를 살펴보면, 중간에 intent.putExtra() 가 있습니다. 이 메서드가 intent 객체에 데이터를 저장하는 역할을 합니다. 매개변수 매개변수 타입은 (String, DataType)으로 Key-Value 구조입니다. 가져올 때는 getExtra(key)와 같은 형태로 가져올 수 있습니다.

val idText = findViewById<TextView>(R.id.input_id).text.toString()

 이 코드는 EditText에 있는 문자열을 가져오는 코드입니다. 뷰의 text 속성을 통해 가져온 뒤 타입을 String으로 변환시켜 줍니다.

class SubActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sub)

        val idText = intent.getStringExtra("id")
        findViewById<TextView>(R.id.sub_title).text = idText
    }
}

 서브 액티비티에서는 이렇게 코드를 준비합니다. (java의 경우 getIntent같은 메서드가 있을 겁니다.)메인 액티비티에서 받은 문자열을 idText 변수에 저장한 수 서브 액티비티에 있는 TextView에 저장합니다.

Intent 6
Intent 7

 정상적으로 작동하는 것을 볼 수 있습니다. 마지막으로 서브 액티비티에서 메인 액티비티로 데이터를 전송하는 방법을 살펴보겠습니다. 만약 어렵게 느껴진다면 그냥 넘어가셔도 됩니다.
 이번에는 메인 액티비티에서 입력된 아이디를 서브 액티비티로 보내면 서브 액티비티에서는 아이디에 " is Good!"을 붙여서 메인 액티비티로 전송해 보도록 하겠습니다.

class MainActivity : AppCompatActivity() {
    var tv : EditText? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tv = findViewById(R.id.input_id) // EditText 초기화

        // 버튼 클릭시 서브 액티비티를 띄워줍니다.
        findViewById<Button>(R.id.btn_login).setOnClickListener {
            val intent = Intent(this, SubActivity::class.java)
            val idText = tv!!.text.toString()
            intent.putExtra("id", idText)
            startActivityForResult(intent, 10)
        }
    }

    // 서브 액티비티 종료시 자동 호출됩니다.
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == 10 && resultCode == Activity.RESULT_OK) {
            val bundle = data?.extras
            tv?.text = bundle?.getString("returnString")
        }
    }
}

 기본적으로는 아까와 비슷한 코드인데 TextView만 MainActivity의 전역에서 사용할 수 있도록 밖으로 뺀 것입니다. 다만, 서브 액티비티를 호출하는 함수가 startActivity(intent)에서 startActivityForResult(intent, 10)로 바뀌었습니다. 이 메서드는 결과 값을 받겠다는 의미이고 요청 코드를 10으로 줬습니다. 액티비티는 서브 액티비티가 종료되면 자동으로 onActivityResult()를 호출합니다. 이 메서드는 요청 코드, 결과 코드, data를 받습니다. 요청 코드는 startActivityForResult에서 넘겨줬던 10이 돌아옵니다. 이는 어떤 서브 액티비티에서 얻은 결과인지 구분하기 위함입니다. resultCode는 서브 액티비티가 정상적으로 값을 반환했는지를 판별합니다. 마지막으로 data는 서브 액티비티에서 반환한 데이터입니다.
 데이터 전송은 Bundle이라는 객체를 이용합니다. 이 코드는 잠시 두고 서브 액티비티 코드를 살펴봅시다.

class SubActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sub)

        // 메인 액티비티에서 받은 텍스트를 화면에 출력
        val idText = intent.getStringExtra("id")
        findViewById<TextView>(R.id.sub_title).text = idText

        // 메인 액티비티로 반환할 데이터 세팅
        val resultIntent = Intent(this, MainActivity::class.java)
        val bundle = Bundle()
        bundle.putString("returnString", idText + " is Good!")
        resultIntent.putExtras(bundle)
        setResult(Activity.RESULT_OK, resultIntent)
    }
}

 이번에도 Intent를 이용합니다. Intent에 전송할 데이터 Bundle 객체를 저장하는 구조입니다. putString() 역시 먼저 살펴봤던 Key-Value 구조입니다. Value 부분에 전송할 문자열인 idText + " is Good!"를 넣어줍니다. putExtras() 를 통해 Bundle 객체는 resultIntent에 저장하고 setResult로 반환 결과를 세팅합니다. 첫 번째 인자인 Activity.RESULT_OK는 결과가 정상임을 나타냅니다. 이제 액티비티를 종료하게 되면 이 액티비티를 호출한 메인 액티비티로 데이터가 전송됩니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(requestCode == 10 && resultCode == Activity.RESULT_OK) {
        val bundle = data?.extras
        tv?.text = bundle?.getString("returnString")
    }
}

 이 코드를 다시 살펴봅시다. data?.extras 로 아까 putExtras()로 저장했던 Bundle 객체를 불러옵니다. 그리고 저장된 문자열을 불러와 TextView에 저장합니다. 결과는 아래와 같습니다.

Intent 8

 

과제

 지난 번에 만든 로그인 화면을 기반으로 로그인 버튼을 구현합니다. 단, 실제 로그인 관련 데이터는 존재하지 않으므로 구현 내용은 다음과 같습니다. 

  1. 아이디 혹은 비밀번호를 입력하지 않았을 때 : 아이디 혹은 비밀번호를 입력하라는 한다는 토스트 메시지
  2. 모두 입력 되어 있을 경우 : 아이디 또는 비밀번호가 일치하지 않습니다. 라는 텍스트를 붉은 글씨로 표시
     - 기본으로 보였던 붉은 글씨를 처음에는 안 보이게 수정해주세요.

 심화 과제 : 아이디를 입력하지 않았을 경우 아이디 입력란을, 비밀번호를 입력하지 않았을 경우 비밀번호 입력란을 자동으로 포커싱(커서 위치를 이동)해준다.

과제 예시

 

 뷰를 안 보이게 하는 방법과 포커싱하는 방법은 알려드리지 않았습니다. 따라서 이번 과제에서는 구글링이 필요합니다.

 이번에는 공부한 내용에 비해 쉬운 과제로 준비해 봤습니다. 마찬가지로 과제를 깃허브에 push하셔야 피드백이 가능합니다.

과제 예시 코드 : github.com/HamBP/NewStudy/commit/5d38b2383a70291194a63ab9a5aa0e883457342b