반응형

이제까지 코틀린 언어의 문법을 대략적으로 알아보았으니 실제 앱 개발을 해봐야 겠죠.

안드로이드 앱 개발에서 가장 기본적인 기술인 액티비티 화면 전환과 리사이클러뷰를 통한 대량의 데이터를 보여주는 앱을 만들어 보겠습니다. 아마 이 예제를 만들어보면 코틀린을 이용한 앱 개발이 어떤 건지 대략적인 감이 올 겁니다. 더불어 자바언어로 개발했을 때에 비해 얼마나 효과적인지 느껴보실 수 있을 겁니다.

액티비티 전환 방법과 리사이클러뷰에 대한 기술 소개는 다른 글에서 소개했으니 이번에는 코틀린언어의 적용 모습에 포커스를 두고 살펴보시길 바랍니다. 


실습을 위한 프로젝트를 생성하겠습니다. 

 

Project Template : Empty Views Activity

 

Name : KotlinRecyclerViewApp

Language : Kotlin

Minimum SDK : API 26

Build configuration language : Kotlin DSL


 

 

대부분의 앱은 1개 이상의 화면을 보유하고 있는 만큼 화면을 담당하는 액티비티 전환을 코틀린으로 구현해보기 위해 화면 구성은 총 3개를 만들어 볼 예정입니다.

 

화면(Activity) 3개

1. IntroActivity  : 진입 화면 - 화면이동 버튼

2. MainActivity : 리사이클러뷰를 보여주는 화면

3. ItemActivity : 아이템 상세화면 ( 리사이클러뷰의 아이템뷰 클릭시에 이동될 아이템 상세화면 ~ 화면전환 애니메이션 적용 )

 

 

♣ 새로운 Activity 생성 [ 코틀린 클래스 파일.kt, 레이아웃 파일.xml, AndroidManifest.xml 에 등록 ]

New > Activity > Empty Views Activity

 

이렇게 안드로이드 스튜디오의 Activity 생성 메뉴를 통해 새로운 액티비티를 생성하면 Activity를 상속한 코틀린 클래스 파일과 이 액티비티에서 보여줄 화면을 구성하는 레이아웃 xml 파일을 자동으로 생성해 줍니다. 또한 앱의 구성요소(Component)를 등록하는 앱의 시작파일인 AndroidManifest.xml 파일에 등록까지 해줍니다.

 

 

1. 인트로 화면 - 진입화면

앱의 진입 화면으로 사용하는 액티비티 이기에 생성할 때 [ Launcher Activity ] 체크박스를 체크해 주시기 바랍니다.

 

런처 액티비티로 생성하였기에 AndroidManifest.xml 에 등록될 때 <Intent-Filter> 가 추가되어 있습니다. 사실 이 상태 그대로 코드를 작성해도 되지만 기존에 런처 액티비티였던 MainActivity는 더이상 진입화면으로 사용하지 않을 것이기에 인텐트필터를 제거하고 외부에서 호출할 수 없도록 exported 속성을 false 로 변경하겠습니다.

변경 전 변경 후

<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<activity
    android:name=".MainActivity"
    android:exported="false">
</activity>

 

 

먼저, 인트로 화면의 레이아웃 xml 파일을 작성해 보겠습니다. 실제 앱을 구현하는게 목적이 아니고 단지, 액티비티 전환을 코틀린으로 구현하는 것이 목적인 만큼 별도의 디자인 없이 Button 만 배치 하여 MainActivity로 전환할 수 있도록 하겠습니다.

버튼의 클릭이벤트에 반응하는 리스너 설정은 우선 익명클래스로 구현해 보겠습니다.

activity_intro.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context=".IntroActivity">

    <!-- 익명클래스로 리스너 처리   -->
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="go to Main"
        android:layout_centerInParent="true"/>

</RelativeLayout>

 

이제 액티비티 클래스 코드를 작성해 보겠습니다.

우선 필요한 코드에 집중하기 위해 불필요한 코드는 제거하고 시작하겠습니다. 안드로이드 컴포즈가 등장하면서 Edge-To-Edge UI 를 기본적으로 구현된 상태로 액티비티가 만들어집니다.

 

♣ Edge-To-Edge UI 

액티비티는 기본적으로 디스플레이 화면 전체를 사용하지 않습니다. 상태표시줄(Status bar)과 네비게이션바(Navigation bar) 영역을 제외한 부분만을 사용합니다. 예전에는 화면전체를 사용하려면 WindowManager 를 통해 Flag 설정을 직접 해야 했습니다. 하지만 안드로이드 버전이 변경되면 기존 Flag 설정으로 적용되지 않는 등의 문제가 있어 개발자들을 조금 불편하게 했습니다. 

구글은 컴포즈 개발방법을 도입하면서 UI 적으로 화면 전체를 사용하게 함으로서 사용자에게 몰입감을 주고자 화면을 상단 끝(Edge)에서 하단 끝(Edge) 까지 사용하는 것을 권장합니다. 그래서 화면 전체 사용을 편하게 적용하게 하기 위해 enableEdgeToEdge() 기능을 제공하며 기본 보일러플레이트 코드(미리 작성되어 있는 코드)로 추가하였습니다. Android 15버전 부터는 모든 앱에 기본적으로 적용된다고 합니다.

 

class IntroActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()    // 화면전체 사용 ON
        setContentView(R.layout.activity_intro)  // 액티비티가 보여줄 뷰를 설정
        
        //Edge-To-Edge 일때 하단 네비게시션바 사이즈 만큼 패딩 적용
        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
        }
    }
}

 

enableEdgeToEdge() 기능을 사용하면 상태표시줄 뿐만 아니라 하단의 네비게이션바 영역까지 액티비티가 확장됩니다. 하지만 네비게이션바는 시스템바 이기에 없어진 것은 아니고 액티비티 위에 오버레이 되어 그려집니다. 그러다 보니 액티비티의 가장 아래쪽은 네비게이션바에 의해 콘텐츠가 일부 가려지는 문제가 발생할 수 있습니다. 하여 네비게이션바 사이즈를 얻어와서 그 만큼 Window 의 안쪽 패딩을 적용하는 코드까지 추가되었습니다. 네비게이션 바의 종류(제스처, 2버튼, 3버튼)나 디바이스의 종류에 따라 사이즈가 다르기에 리스너를 통해 시스템바의 사이즈를 동적으로 얻어와서 패딩을 적용하는 코드입니다. 덕분에 코드가 지저분해 보이기도 합니다. 

 

이번 예제에서는 코틀린을 이용한 앱 개발 코드에 집중하기 위해 EdgeToEdge 관련 코드는 모두 제거하고 액티비티가 보여줄 뷰를 설정하는 setContentView()만 남겨두고 설명을 이어가겠습니다.

import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class IntroActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro) //액티비티가 보여줄 뷰 설정
        
    }//onCreate method....
    
}//MainActivity class...

 

예전버전에서 액티비티를 만들었을 때의 기본 코드와 같습니다.

 

이제 레이아웃 xml 파일에서 만든 Button 을 참조하기 위한 참조변수를 만들어보겠습니다. 보통 액티비티가 보여주는 뷰들은 액티비티 전체에서 사용될 가능성이 많기에 멤버변수(프로퍼티)로 선언하는 것을 권장합니다. 우선 값 변경이 가능한 var 키워드로 변수를 만들어 보겠습니다.

class IntroActivity : AppCompatActivity() {

    var btn:Button   //ERROR - 코틀린은 초기화 없이 멤버변수를 만들 수 없음.
    
    ....
    
 }

 

다만, 코틀린은 멤버변수를 선언할 때 초기화를 하지 않으면 문법적 에러가 발생합니다. 

 

그렇다고 뷰들 찾아와서 객체의 참조값을 리턴해주는 findViewById() 를 호출하여 초기화하면 문법적 에러는 아니지만 런타임(실행 중) 예외가 발생합니다. 이미 자바를 통해 앱을 구현해 보신 분들은 잘 알고 있듯이 xml에 작성한 레이아웃용 뷰들은 액티비티 객체가 생성된 후 자동으로 발동하는 라이프사이클 콜백 메소드인 onCreate()메소드 안에서 setContentView()를 통해 액티비티에 뷰로서 설정되면서 객체로 만들어 집니다. 하지만 멤버변수 btn 은 액티비티가 객체로 생성될 때 초기화를 위해 Button 객체의 참조값을 요구합니다. 즉, Button 객체가 아직 만들어 지기 전에 객체를 찾아오는 findViewById()를 호출하여 참조변수 btn 에 초기화를 하려 하기에 결국 찾지 못하여 null 값이 리턴됩니다. 

class IntroActivity : AppCompatActivity() {

    var btn:Button= findViewById(R.id.btn)    //초기화를 안하면 문법에러 라고 지금 여기서 find를 하면 에러.. 멤버변수가 만들어진 후 onCreate()가 호출되기에 아직 뷰가 만들어지지 않았음.
   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro) //이때 xml 레이아웃파일의 뷰 객체들이 생성됨.
        
    }
}

 

해결 방법은 여러가지가 있기에 앞으로 하나씩 새로운 방법으로 소개하겠습니다.

첫번째 방법으로 자바언어를 사용했을 때 처럼 일단 null 값으로 초기화를 하는 btn 참조변수를 만들고 onCreate() 메소드 안에서 Button 객체를 찾아와서 참조하도록 하겠습니다. 단, 코틀린은 null 값을 저장할 수 있는 변수는 ? 가 표시된 nullable 변수로 만들어야 합니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

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

        //btn 참조하기
        btn= findViewById(R.id.btn)
        
    }//onCreate method....
}//IntroActivity class........

 

자바로 개발 할 때와 같은 구조 입니다.

이제 버튼 클릭이벤트에 반응하는 리스너를 설정하겠습니다. 클릭에 반응하는 OnClickListener 는 interface 이기에 곧바로 객체를 만들 수 없습니다. 별도의 class 를 만들어 구현(implement)하거나, 객체를 생성하면서 인터페이스를 구현하는 익명클래스를 이용할 수 있습니다.  이 버튼의 클릭에만 반응하는 전용 리스너인 만큼 익명클래스로 구현해 보겠습니다.

자바와코틀린은 익명클래스를 만드는 문법이 완전히 다릅니다. 코틀린에서는 키워드 자체에서 '나 객체야.' 라는 것을 직관적으로 알수 있도록 object 라는 키워드를 사용하여 익명클래스를 만듭니다. 근데 그냥 object 라고만 쓰면 객체이긴 한데 어떤 인터페이스를 구현했는지 알수 없으니 implements 문법으로 사용하는 : 기호를 붙이고 OnClickListener 를 구현하면 됩니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

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

        //btn 참조하기
        btn= findViewById(R.id.btn)
        
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                
            }
        })
        
    }//onCreate method....
}//IntroActivity class........

 

btn 변수가 nullable 타입이기에 null 안전 연산자인 ?. 을 이용하여 NPE 에 대응해야만 합니다. nullable 변수를 사용할 때 다소 불편한 점 중에 하나입니다.

문법적 표기법의 차이만 다소 있을 뿐 클릭이벤트 처리 기법을 동일하고 모양도 비슷하여 크게 어렵지 않게 보실 수 있을 겁니다.

 

이제, 버튼의 기능을 onClick()메소드 안에 추가하겠습니다. 이 버튼은 MainActivity 로 전환을 해주는 버튼입니다. 액티비티를 전환하기 위해 Intent 객체를 생성하고 실행하고자 하는 MainActivity의 클래스 정보를 기입하겠습니다. 

 

자바와 다른점

1) new 키워드를 사용하지 않고 Intent 객체 생성

2) 익명클래스 안에서 아웃터클래스(IntroActivity) 객체를 지칭하는 아웃터클래스.thisthis@아웃터클래스명 으로 표기법 변경

 

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

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

        //btn 참조하기
        btn= findViewById(R.id.btn)
        
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                //MainActivity로 전환
                val intent: Intent = Intent(this@IntroActivity, MainActivity::class.java)
                startActivity(intent)
                
            }
        })
        
    }//onCreate method....
}//IntroActivity class........

 

 

[ IntroActivity -> MainActivity 액티비티 전환 실행화면 사진 & GIF ]

IntroActivit -> MainActivity 로 전환

 

 

기능적으로는 특별히 어려운 내용은 없습니다. 단지 코틀린에서 달라진 문법적 표기법을 살펴보았습니다.

 

위에서 살펴본 익명클래스를 이용한 버튼 클릭 리스너 처리를 코드가 다소 지저분 합니다. 익명 클래스의 영역 중괄호{} 안에 추상메소드를 구현하기 위한 onClick() 메소드의 중괄호가 중첩으로 구성되어 가독성이 떨어집니다. 이 함수 처리를 간결하게 해주는 문법이 람다표기법입니다. 자바에도 존재하지요. 코틀린에도 람다 표현이 가능합니다. 

 

이번에는 람다 표현을 이용한 버튼 클릭이벤트 처리를 실습해보기 위해 IntroActivity에 [액티비티 종료] 버튼을 하나 더 추가해 보겠습니다. 화면의 우상단 위치에 배치하기 위해 activity_main.xml 레이아웃 파일에 버튼을 하나 더 추가하겠습니다.

activity_intro.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context=".IntroActivity">

    <!-- 익명클래스로 리스너 처리   -->
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="go to Main"
        android:layout_centerInParent="true"/>


    <!-- (람다표현)으로 클릭이벤트 처리   -->
    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:layout_alignParentRight="true"
        android:text="EXIT"
        android:backgroundTint="#FFFF3300"/>

</RelativeLayout>

 

 

이제 이 EXIT 버튼 클릭 처리를 위해 IntroActivity의 코틀린 파일을 수정하겠습니다.

새로 추가한 버튼의 참조변수도 필요합니다. 위에서는 nullable 변수를 사용했었는데 null 안전 문법을 사용하는 것이 불편하니 초기화를 늦게 적용하는 것을 요청하는 '늦은초기화 문법 lateinit' 을 사용해 보겠습니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    //늦은 초기화 lateinit
    lateinit var btn2: Button
    
    ....
    
}//IntroActivity class.....

 

lateinit 을 통해 늦은 초기화를 하면 멤버변수의 초기화 시점이 참조변수가 처음 사용될 때 실행 됩니다. 

이제 btn2 의 초기화를 onCreate()영역 안에서 수행하겠습니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    //늦은 초기화 lateinit
    lateinit var btn2: Button

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

        //btn 참조하기
        btn= findViewById(R.id.btn)
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                val intent: Intent = Intent(this@IntroActivity, MainActivity::class.java)
                startActivity(intent)
            }
        })

        //btn2 참조하기 [lateinit - 늦은초기화 문법으로 ] 
        btn2= findViewById(R.id.btn2)  //이때 초기화 됨
        
    }//onCreate method...
    
}//IntroActivity class...

 

이제 버튼 클릭처리를 익명클래스인 object 를 사용하지 않고 람다 표현으로 간결화 하겠습니다. 이 람다 표현만으로도 이미 많이 간결해 집니다. 근데 코틀린은 이것 또한 더 줄여서 작성하는 SAM (Single Abstract Method) 변환 이란 문법을 도입했습니다. 메소드의 파라미터를 받기위한 소괄호 () 안에 람다 표현의 중괄호 {} 가 중첩으로 되어 있는 것이 지저분해 함수의 소괄호()가 생략된 문법입니다. 짧게 소개하기는 여의치 않아 별도의 글로 SAM 변환에 대해 소개할 예정입니다. 지금은 앱의 클릭이벤트 처리를 코틀린으로 구현하는 것이 목적이기에 깊게 소개하지 않고 문법적인 사용모습을 보여드리고자 하오니 대략 이렇게 사용하는 구나. 정도로 넘어가시길 바랍니다. 추후 별도로 소개하겠습니다. 얼마나 간결한지 한번 보시죠.

package com.kitesoft.kotlinrecyclerviewapp

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    //늦은 초기화 lateinit
    lateinit var btn2: Button

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

        //btn 참조하기
        btn= findViewById(R.id.btn)
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                val intent: Intent = Intent(this@IntroActivity, MainActivity::class.java)
                startActivity(intent)
            }
        })

        //btn2 참조하기 [lateinit - 늦은초기화 문법으로 ]
        btn2= findViewById(R.id.btn2)

        //리스너 설정을 람다 표기법을 이용하면 더 쉽게 코딩 가능. 단, 그 리스너 인터페이스의 추상메소드가 1개일 때[단일 추상 메소드 : SAM (Single Abstract Method]변환으로 구현 가능
        btn2.setOnClickListener {
            finish()   //액티비티 종료
        }
    }//onCreate Method...

}//MainActivity class...

 

클릭 이벤트처리가 정말 간결하네요. 아직 조금만 살펴봤지만 코틀린의 매력을 조금씩 느껴갈 수 있기를 바랍니다. 자바에 비해 분명 요즘의 트랜디한 모던 코딩 문법이어서 맘에 드실겁니다.

 

[ IntroActivity 전체 실행화면 사진 & GIF ]

IntroActivity 의 버튼 클릭에 따른 액티비티전환 및 종료 실행화면

 


 

앱 구현의 기본기능인 클릭이벤트 처리, 액티비티 전환 및 종료에 대하여 코틀린으로 구현해보았습니다.

이제 앱 개발에서 가장 많이 구현하는 기능인 대량의 데이터를 보여주는 RecyclerView 를 사용하는 MainActivity를 만들어보겠습니다.

다음 글에서 보겠습니다.

반응형

+ Recent posts