^(코딩캣)^ = @"코딩"하는 고양이;
[단편 Android 개발 정리] 룸(Room) 사용하기 (2021년 버전)
단편 Android 개발 정리 이전 게시글: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전) 룸(Room) 사용하기 (2021년 버전) 안드로이드 앱에서 SQLite는 로컬 데이터베이스로 쓰이고 있다. SQLite를 통해 데이터베이스에 접근하기 위해 이전까지는 DatabaseHelper 클래스를 파생 및 구현시키는 방법을 써 왔으나, 룸(Room)이라는 것이 새로 제공되어 이를 정리해 두고자 한다. 결론부터 말하면 Helper 클래스를 구현하면서 메소드별로, 테이블별로 일일이 커서를 읽고 형변환하고 예외처리하는 등의 귀찮은 작업은 이제 컴파일러가 알아서 도맡을테니 메타 데이터로 껍데기만 알려주면 되는 편리한 기능이다. 컴파일러 의존성 설정하기 2021년 기준 최신 Android Stud..
API/Android
2021. 7. 8. 22:32

[단편 Android 개발 정리] 룸(Room) 사용하기 (2021년 버전)

API/Android
2021. 7. 8. 22:32

단편 Android 개발 정리

이전 게시글: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

 

룸(Room) 사용하기 (2021년 버전)

안드로이드 앱에서 SQLite는 로컬 데이터베이스로 쓰이고 있다. SQLite를 통해 데이터베이스에 접근하기 위해 이전까지는 DatabaseHelper 클래스를 파생 및 구현시키는 방법을 써 왔으나, 룸(Room)이라는 것이 새로 제공되어 이를 정리해 두고자 한다. 결론부터 말하면 Helper 클래스를 구현하면서 메소드별로, 테이블별로 일일이 커서를 읽고 형변환하고 예외처리하는 등의 귀찮은 작업은 이제 컴파일러가 알아서 도맡을테니 메타 데이터로 껍데기만 알려주면 되는 편리한 기능이다.

 

컴파일러 의존성 설정하기

2021년 기준 최신 Android Studio에서 Room을 사용하고자 한다면 다음과 같이 gradle에 의존성을 추가하면 된다. 이 역시 나중에 Android Studio가 버전 상승하면 할 필요가 없어지려나?

dependencies {
// ... 기본적으로 생성되는 코드들은 생략하고 끝에 이런 내용들을 추가한다.

// 2021년 현재 2.3.0이 최신 버전이나 2.2.5를 써 보겠다.
def $room_version = "2.2.5"

// 클래스나 매개변수 앞에 붙는 annotation을 처리하기 위함
implementation 'androidx.room:room-runtime:$room_version'
annotationProcessor 'androidx.room:room-compiler:$room_version'

// 아래 내용이 없으면 나중에 실행할 때
// 클래스명_Impl does not exists라는 오류가 발생함
apply plugin: 'kotlin-kapt'
kapt 'androidx.room:room-compiler:$room_version'

물론 다음과 같이 버전을 하드코딩해도 된다. 나중에 룸의 버전이 올라갔을 때 불편할 수는 있겠다.

dependencies {
// ... 기본적으로 생성되는 코드들은 생략하고 끝에 이런 내용들을 추가한다.

apply plugin: 'kotlin-kapt'
kapt 'androidx.room:room-compiler:2.2.5'

implementation 'androidx.room:room-runtime:2.2.5'
annotationProcessor 'androidx.room:room-compiler:2.2.5'

kapt라는 것이 등장하는데, 이것은 kotlin 컴파일러에 부착되어서 돌아가는 플러그인 같은 것이다.

 

엔티티를 나타내는 클래스 선언하기

엔티티(entity), 행(row), 레코드(record)... 라고 불리는 것을 클래스 형태로 선언할 차례다. 예를 들어 SQL로 다음과 같이 생성된 테이블에 접근하고자 한다.

CREATE TABLE "Entity" (
	"id"	INTEGER NOT NULL UNIQUE,
	"title"	TEXT NOT NULL,
	"content"	TEXT,
	"data"	BLOB,
	PRIMARY KEY("id")
);
CREATE INDEX "IndexEntity" ON "Entity" (
	"id"	ASC
);

id는 정수형 값이고 NULL을 허용하지 않으며 테이블 내에서 유일해야 하고 인덱스까지 되어 있다고 가정한다. titleNULL을 허용하지 않는 문자열이다. content도 문자열이지만 NULL을 허용한다. data는 이진(binary) 데이터이다.

이 테이블에 보관되는 각 행을 표현하는 kotlin 클래스는 다음과 같이 적을 수 있다.

@Entity(tableName = "Entity", primaryKeys = ["id"], indicies = [Index(name = "IndexEntity", value = "id")]
class Entity(@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER) 
             var id: Int, 
             @ColumnInfo(name = "title", typeAffinity = ColumnInfo.TEXT)
             var title: String,
             @ColumnInfo(name = "content", typeAffinity = ColumnInfo.TEXT)
             var content: String?,
             @ColumnInfo(name = "data", typeAffinity = ColumnInfo.BLOB)
             var data: ByteArray?) {
	// ...
}

titlecontent의 차이에 주목한다. 하나는 NULL을 허용하지 않고 다른 하나는 허용한다. 이 차이는 annotation에서 드러나지 않는다. 둘 다 타입이 ColumnInfo.TEXT로 되어 있기 때문이다. 대신 매개변수의 형식을 본다. 하나는 String이고 다른 하나는 nullable인 String?이다.

SQLite에서 BLOB으로 선언한 필드의 경우 kotlin과 연계 시 그 자료형은 ByteArray로 될 수 있다.

테이블을 생성할 때 사용한 SQL 구문의 내용과 코틀린 클래스의 annotation으로 지정한 내용에 차이가 있을 경우 런타임 시 테이블 선언이 서로 다르다는 오류를 뱉을 수 있다.

 

엔티티에 대해 할 수 있는 조회, 삽입 및 삭제 작업 선언하기

이전까지는 Helper 클래스를 작성하면서 이를 직접 구현하였다. SQL 구문을 구성해서 쿼리 수행하고, 결과를 읽어야 한다면 커서(Cursor)를 열고 필드 순번으로써 해당 필드를 접근하고 예외 발생 시 오류처리 등등... 이것이 룸(Room)을 사용하면 단순히 인터페이스 선언만으로 축약된다. 인터페이스를 구현하는 작업은 앞서 플러그인의 버프를 받는 컴파일러가 수행한다. 즉, 인터페이스에 대한 클래스 구현은 컴파일러가 알아서 해 준다. 이와 같이 테이블의 행들을 가지고 작업할 수 있는 객체를 DAO(Database Access Object)라 한다.

@Dao
interface IEntityAccess {
	@Query("SELECT * FROM Entity")
    fun select(): List<Entity>
    @Query("SELECT * FROM Entity WHERE title LIKE :condition")
    fun select(condition: String?): List<Entity>
}

이런 식으로 쿼리를 작성한다. 메소드의 반환형은 대체로 리스트면 무난할 것 같다. 매개변수는 쿼리 안에서 콜론을 붙이고 매개변수 이름을 그대로 적으면 된다.

UPDATE, INSERT, UPDATE 등의 코드는 annotation만 작성하면 된다. 쿼리는 알아서 작성해준다. 이 때 기본 키를 가지고 특정 행을 식별하는데, 기본키에 해당하는 필드 자체를 수정할 수는 없다. 기본키 필드에 담긴 값 자체를 수정하려면 위와 같이 쿼리를 직접 작성해야 한다.

@Dao
interface IEntry2Access {
	@Insert
    fun insert(entry: Entry2)
    @Update
    fun update(entry: Entry2) // 수정할 행의 기본 키와 새로 수정할 내용이 담긴 객체
    @Delete
    fun delete(entry: Entry2)
}

 

데이터베이스 클래스 작성하기

엔티티를 정의했고, 그 엔티티에 대해 수행하는 작업을 정의했다. 그러한 엔티티들을 포함하는 단위인 데이터베이스도 클래스로 작성할 차례이다.

@Database(entities = [Entity::class, Entity1::class, Entity2::class], version = 1, exportSchema = false)
abstract class Database : RoomDatabase() {
    abstract fun accessEntity(): IEntityAccess
    abstract fun accessEntity1(): IEntityAccess
    companion object {
    	private val instance: Database? = null
    	@Synchronized
        fun getInstance(context: Context): Database {
        	if (instance == null) {
            	synchronized(Database::class) {
                    instance = Room.databaseBuilder(
                        context,
                        Database::class.java,
                        파일경로).build()
                }
            }
            
            return instance!!
        }
    }
}

asset에 이미 구촉된 데이터베이스가 있다면 이것을 앱 로컬 경로로 복사하는 식으로 생성한다.

instance = Room.databaseBuilder(
	context,
	Database::class.java,
	파일경로).createFromAsset(이름).build()

 

카테고리 “API/Android”
more...
썸네일 이미지
[단편 Android 개발 정리] 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)
단편 Android 개발 정리 이전 게시글: ActionBar 대신 Toolbar로 대체하기(2021년 버전) 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전) 이전 게시글에서 ActionBar를 Toolbar로 대체한 다음 햄버거 버튼까지 추가해 보았다. 본 게시글에서는 햄버거 버튼을 클릭하면 사이드 메뉴가 나타나는 것까지 구현해 보겠다. 안드로이드에서는 햄버거 버튼을 클릭했을 때 측면에서 스르륵 나타나는 사이드 메뉴를 DrawerLayout이라고 부른다. 이전 게시글의 레이아웃에서 이를 덧붙여보겠다. 이전 게시글에서 햄버거 버튼까지 반영했을 때의 레이아웃은 다음과 같다. 그리고 액티비티에 대한 코틀린 클래스는 다음과 같다. import는 생략한다. class MainActivity :..
API/Android
2021. 7. 8. 21:22

[단편 Android 개발 정리] 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

API/Android
2021. 7. 8. 21:22

단편 Android 개발 정리

이전 게시글: ActionBar 대신 Toolbar로 대체하기(2021년 버전)

 

사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

이전 게시글에서 ActionBar를 Toolbar로 대체한 다음 햄버거 버튼까지 추가해 보았다. 본 게시글에서는 햄버거 버튼을 클릭하면 사이드 메뉴가 나타나는 것까지 구현해 보겠다. 안드로이드에서는 햄버거 버튼을 클릭했을 때 측면에서 스르륵 나타나는 사이드 메뉴를 DrawerLayout이라고 부른다. 이전 게시글의 레이아웃에서 이를 덧붙여보겠다.

이전 게시글에서 햄버거 버튼까지 반영했을 때의 레이아웃은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:menu="@menu/menu_toolbar">
    </androidx.appcompat.widget.Toolbar>
</androidx.constraintlayout.widget.ConstraintLayout>

그리고 액티비티에 대한 코틀린 클래스는 다음과 같다. import는 생략한다.

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

		val toolbar = findViewById<Toolbar>(R.id.toolbar)
		setSupportActionBar(toolbar)

		supportActionBar?.setDisplayHomeAsUpEnabled(true)
		supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
	}

	override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    	// ...
		return super.onCreateOptionsMenu(menu)
	}

	override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
    	// ...
		return super.onPrepareOptionsMenu(menu)
	}

	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
        	// ...
		}

		return result;
	}
}

ConstraintLayout을 기준으로 설명하면, 맨 위에 Toolbar가 있고 그 아래에 붙여서 DrawerLayout을 배치할 것이다. 그러면 레이아웃은 다음과 같이 작성할 수 있다.

<androidx.constraintlayout.widget.ConstraintLayout ...>
	<androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" ...>
	</androidx.appcompat.widget.Toolbar>
	<androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/layout_drawer"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="이 곳은 메인 영역이다." />
        <com.google.android.material.navigation.NavigationView
            android:id="@+id/navigation_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:fitsSystemWindows="true">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="이 곳은 사이드 영역이다." />
        </com.google.android.material.navigation.NavigationView>
    </androidx.drawerlayout.widget.DrawerLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

TextView는 영역 사이의 구분을 명확히 하기 위하여 임의로 넣은 요소이다. 아무튼 레이아웃을 저렇게 작성한다. DrawerLayout 내부에 사이드 메뉴가 보여질 NavigationView가 있다는 것이다. 특히 주목할 것은 NavigationView가 갖는 android:layout_gravity="start" 속성이다. 이 속성은 사이드 메뉴가 왼쪽에서 나타났다가 왼쪽으로 사라짐을 의미한다. 오른쪽 사이드 메뉴는 android:layout_gravity="end"로 하면 된다.

그 다음 이전 포스트에서 햄버거 메뉴를 클릭할 때 호출되는 이벤트는 다음과 같이 처리한다. 사이드 메뉴가 이미 떠 있다면(isDrawerOpen) 그것을 감추고(closeDrawer), 사이드 메뉴가 감춰져 있다면 그것을 드러낸다(openDrawer).

class MainActivity : AppCompatActivity() {
	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
			android.R.id.home -> {
				val layoutDrawer = findViewById<DrawerLayout>(R.id.layout_drawer)
				val gravity = GravityCompat.START

				if (layoutDrawer.isDrawerOpen(gravity)) {
					layoutDrawer.closeDrawer(gravity)
				} else {
					layoutDrawer.openDrawer(gravity)
				}
			}
		}

		return result;
	}
}

실행 결과는 다음과 같다.

햄버거 버튼을 누르기 전
햄버거 버튼을 누른 후

디자인적 요소는 최대한 배제한 채, 최소한 접고 펼치는 기능적인 구현은 이것으로 끝.

 

카테고리 “API/Android”
more...
썸네일 이미지
[단편 Android 개발 정리] ActionBar 대신 Toolbar로 대체하기(2021년 버전)
단편 Android 개발 정리 다음 포스트: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전) ActionBar 대신 Toolbar로 대체하기 2021년 기준 최신 버전의 Android Studio에서 앱의 ActionBar 대신 Toolbar로 대체하는 방법을 단계별로 정리해보겠다. 1 단계. Empty Project 생성하기 Android Studio를 열고 아무 액티비티도 갖지 않는 빈 프로젝트를 하나 생성한다. 프로젝트의 이름와 도메인은 적절하게 입력한다. 그 다음 /res/values/themes/themes.xml를 열면 style 태그가 ActionBar로부터 파생되었음을 지정하는 내용이 있을 것이다. 일단 확인만 하고 놔 둔다. 2 단계. 빈 액티비티 추가하기 빈 액티비티를..
API/Android
2021. 7. 7. 13:39

[단편 Android 개발 정리] ActionBar 대신 Toolbar로 대체하기(2021년 버전)

API/Android
2021. 7. 7. 13:39

단편 Android 개발 정리

다음 포스트: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)

 

ActionBar 대신 Toolbar로 대체하기

2021년 기준 최신 버전의 Android Studio에서 앱의 ActionBar 대신 Toolbar로 대체하는 방법을 단계별로 정리해보겠다.

 

1 단계. Empty Project 생성하기

Android Studio를 열고 아무 액티비티도 갖지 않는 빈 프로젝트를 하나 생성한다. 프로젝트의 이름와 도메인은 적절하게 입력한다.

아무것도 Activity도 포함하지 않는 새 프로젝트를 선택한다.

그 다음 /res/values/themes/themes.xml를 열면 style 태그가 ActionBar로부터 파생되었음을 지정하는 내용이 있을 것이다. 일단 확인만 하고 놔 둔다.

기본 스타일이 ActionBar로부터 파생되고 있다.

 

2 단계. 빈 액티비티 추가하기

빈 액티비티를 하나 추가한다. 이름은 적절히 부여한다.

activity_main.xml

아무것도 수정하지 않은 맨 처음 상태의 빈 액티비티라면 이렇게 생겼을 것이다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
	android:layout_height="match_parent"
	tools:context=".MainActivity">

</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity.kt

또한 비하인드 코드는 이렇게 생겼다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

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

 

3 단계. ActionBar를 지우기

2 단계에서 아무것도 건드리지 않고 그냥 실행을 해 보겠다. 그러면 다음과 같이 상단에 ActionBar를 포함하는 빈 화면이 나타난다.

기본 설정에 따라 ActionBar가 추가되어 있는 빈 액티비티

 

여기에서부터 하나씩 수정해나가겠다. 앞서 확인했던 /res/values/themes/themes.xml 파일을 다시 열고 앱이 정의하고 있는 기본 테마를 ActionBar에서 NoActionBar 계열로 바꾼다. 그러면 AndroidManifest.xmlapplication에서 themes.xml에서 정의된 테마를 참조하고 그 결과가 application의 일부인 MainActivity에도 반영된다.

NoActionBar 테마를 적용한다.

정리하자면,

themes.xmlTheme.MaterialComponents.DayNight.NoActionBar로부터 파생된 Theme.Testapp라는 이름의 새 테마를 정의한다.

AndroidManifest.xmlTheme.Testapp를 참조하여 앱의 전반적인 테마를 적용한다.

MainActivity도 여기에 영향을 받아 ActionBar가 사라진다.

NoActionBar가 적용된 앱의 모습

 

4 단계. ActionBar가 사라진 자리에 Toolbar 넣기

이전 버전의 Android Studio에서는 Toolbar의 이름이 다음과 같이 android.support.v7.widget.Toolbar였다.

<android.support.v7.widget.Toolbar
	android:id="..." />

그러나 2021년 현재 최신 Android Studio에는 이 코드가 오류가 난다. Toolbar의 이름은 다음과 같이 바뀌었다. 물론 gradle에서 의존성을 수정하고 번거로운 세팅을 하면 옛날 이름으로 쓸 수 있겠으나, 이제부터 개발하는 앱은 기본 설정을 사용하는 것으로...

<androidx.appcompat.widget.Toolbar
	android:id="..." />

다시 나타난 것은 ActionBar가 아니라 Toolbar임

이제 MainActivty의 비하인드 코드 측에도 레이아웃에 새로 추가된 Toolbar를 ActionBar처럼 사용하라고 지정해 주어야 한다. MainActivty.kt를 열고 onCreate 메서드의 맨 끝 부분에 다음의 내용을 적는다.

val toolbar = findViewById<Toolbar>(R.id.toolbar)
//setSupportActionBar(toolbar)

onCreate 메소드의 전체적인 내용은 다음과 같아야 할 것이다. super.onCreate를 호출하고 setContentView를 실행한 다음에 새로 작성하는 문장들이 등장해야 할 것이다.

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

	val toolbar = findViewById<Toolbar>(R.id.toolbar)
	setSupportActionBar(toolbar)
}

그러면 ActionBar처럼 Toolbar에도 앱의 이름이 나타나는 것을 볼 수 있다. 텍스트의 색이 조금 달라 보이는 것은 기분 탓일 것이다.

완성된 Toolbar의 모습

 

5 단계. 메뉴 추가하기

ActionBar와 달리 Toolbar는 View의 일종이기 때문에 버튼이나 메뉴 등 다양한 요소들의 기능과 위치를 제어하기가 편리하다.

 

메뉴 작성하기

프로젝트에 /res/menu 디렉토리를 하나 생성한다. 여기에 메뉴를 정의하는 XML 파일을 생성한다. 이 파일에 Toolbar에 넣을 메뉴들을 적는다.

<?xml version="1.0" encoding="utf-8"?>
<menu
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_item0"
        android:title="바나나"
        app:showAsAction="ifRoom" />
    <item android:id="@+id/action_item1"
        android:title="포도"
        app:showAsAction="never" />
    <item android:id="@+id/action_item2"
        android:title="딸기"
        app:showAsAction="never" />
    <item android:id="@+id/action_item3"
        android:title="포도"
        app:showAsAction="never" />
</menu>

여기서 app:showAsAction 속성은 메뉴 항목을 Toolbar 우측 [더 보기...] 버튼을 눌러야 나타나게 할 것인지 여부를 지정한다.

  • never는 해당 항목을 무조건 [더 보기...] 버튼을 눌러야만 나타나게 한다.
  • ifRoom은 화면의 폭이 충분할 때는 Toolbar에 직접 나타내고 좁을 때는 [더 보기...] 버튼을 통해 보여지게 한다.
  • always라고 해서 화면의 폭과 무관하게 무조건 Toolbar에 직접 나타내는 옵션이 있는데 이는 IDE에서 권장하지 않고 ifRoom을 사용하도록 유도하고 있다.

설명보다는 직접 보고 정리하면 되겠다.

메뉴를 정의하는 xml 파일

 

레이아웃에 메뉴 적용하기

레이아웃 파일의 androidx.appcompat.widget.Toolbar로 돌아가서 다음의 속성을 추가하면 Toolbar에 메뉴가 적용된다.

app:menu="@menu/파일.xml"

"바나나" 항목은 app:showAsAction="ifRoom"으로 지정했으므로 Toolbar에 바로 나타난다.

ifRoom 값을 지정한 메뉴 항목

app:showAsAction="never"로 지정한 항목은 [더 보기...] 버튼을 눌러야 나타난다.

never 값을 지정한 메뉴 항목

 

6 단계. 메뉴 항목마다 이벤트 작성하기

Toolbar와 menu의 겉 모양은 대충 만들었으니 비하인드 코드를 작성할 차례이다. kotlin 소스 코드를 열고 액티비티에 다음과 같이 메소드를 오버라이드한다.

class MainActivity: AppCompatActivity() {
	// ...
	override fun onCreateOptionsMenu(menu: Menu?): Boolean {
		Log.d("menu", "onCreateOptionsMenu")
		menuInflater.inflate(R.menu.menu_toolbar, menu)
		return super.onCreateOptionsMenu(menu)
	}

	override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
		Log.d("menu", "onPrepareOptionsMenu")
		return super.onPrepareOptionsMenu(menu)
	}

	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
			R.id.action_item0 -> { Log.d("menu", "바나나 선택함"); result = true }
			R.id.action_item1 -> { Log.d("menu", "포도 선택함"); result = true }
			R.id.action_item2 -> { Log.d("menu", "딸기 선택함"); result = true }
		}

		return result;
	}
}

각 메소드가 언제 실행되는지 Logcat으로 찍어보면 알 수 있다

onCreateOptionsMenu는 액티비티에 딸려 있는 메뉴를 최초로 구성할 때 1회만 호출된다. 이것을 재정의하여 menuInflater.inflate를 호출해준다. menuInflater.inflate 메소드가 무엇인가 하니, 리소스로부터 메뉴 XML 파일을 불러와서 메뉴로 띄워주는 역할을 한다. 레이아웃에서 지정했던 app:menu="@menu/파일.xml"과 동일한 것이다. 다만 레이아웃에서 app:menu="@menu/파일.xml"을 굳이 적지 않고 menuInflater.inflate 메소드만 호출해도 앱 실행 시 메뉴가 나타났지만, 반대로 menuInflater.inflate 메소드를 호출하지 않고 레이아웃에게 app:menu="@menu/파일.xml"만을 지정하면 메뉴가 나타나지 않았다. 그냥 그렇다는 거다.

onPrepareOptionsMenu는 메뉴를 최초로 구성할 때는 물론이고 [더 보기...] 버튼을 클릭하여 메뉴가 펼쳐질 때마다 호출된다. 사용자에게 메뉴 항목을 보여주기 전 뭔가를 메뉴 항목을 업데이트할 때 유용할 것으로 보인다.

onOptionsItemSelected는 메뉴 항목을 클릭/터치할 때 호출된다. 어떤 메뉴가 클릭/터치되었는지는 매개변수로 넘어온 item: MenuItemmenu.itemId를 통해 식별 가능하다.

 

7 단계. 햄버거 버튼 만들기

이렇게까지 ActionBar를 굳이 ToolBar로 바꾸어서 버튼들을 추가하는 이유 중 하나는 사이드 바 메뉴, 일명 "햄버거 버튼(hamburger button)"을 만들기 위함일 것이다. 햄버거 버튼을 추가해 보겠다.

 

햄버거 이미지 준비하기

먼저 햄버거 아이콘을 준비한다. 직접 만들 필요도 없고, 구글링 통해 굳이 직접 구할 필요도 없다. Android Studio의 클립아트에 미리 준비되어 있다. drawable 디렉토리에 새로운 Vector(Image) Asset을 추가해본다.

New 메뉴에서 Vector(Image) Asset 항목 클릭

여기서는 벡터 이미지를 넣어보겠다. 클립아트 옆의 그림을 클릭하면 미리 준비된 아이콘들의 목록이 나타난다. 이 중에서 햄버거 아이콘(이름: menu)을 찾아서 클릭하고, 색상과 크기(일단은 넉넉하게 크기를 지정해 준다)를 적절하게 넣어주면 끝.

표시된 부분을 클릭
menu라고 이름이 붙은 아이콘 선택

그리고 onCreate 메소드로 가서 홈(Home) 버튼이 Toolbar에 생겨나도록 다음과 같이 문장을 작성한다. 앞서 만들어 본 메뉴 항목과 달리 햄버거 아이콘은 대체로 화면의 왼쪽에 위치할텐데, Toolbar의 왼쪽에 생기는 아이콘을 홈 버튼이라 한다.

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

		val toolbar = findViewById<Toolbar>(R.id.toolbar)
		setSupportActionBar(toolbar)

		// 새로 추가된 두 줄
		supportActionBar?.setDisplayHomeAsUpEnabled(true)
		supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
	}
}

setDisplayHomeAsUpEnabled은 매개변수에 true를 전달함으로써 Toolbar에 홈 버튼이 나타나게 한다. setHomeAsUpIndicator은 홈 버튼으로 표시할 아이콘을 지정하는데, 위에서 클립아트를 통해 추가한 햄버거 아이콘의 아이디를 적으면 된다.

그러면 이와 같이 넉넉한 사이즈로 햄버거 아이콘이 나타난다

이런 모양을 원했던 것이 아니다. 그렇다면 화면 크기에 맞게 이미지 크기를 축소해주어야 한다. 여기서는 벡터 형식으로 아이콘을 추가했으므로 크기 조정이 매우 간편하다. 햄버거 아이콘 파일을 열고 폭(android:width)과 높이(android:height)를 ?attr/actionBarSize로 수정한다. 그러면 다음과 같이 제법 그럴듯한 아이콘이 만들어진다.

<vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="?attr/actionBarSize"
    android:tint="#FFFFFF"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="?attr/actionBarSize">
    <path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>

한결 그럴듯한 햄버거 아이콘

이것도 크다 하면 마진(margin)을 좀 더 주는 식으로 줄일 수 있다. path element를 group으로 감싼 다음 scaleXscaleY1.0보다 작은 값으로 지정한다. 그러면 path가 표현하는 햄버거 아이콘이 좀 더 축소된다. 예를 들어 scaleX="0.8" scaleY="0.8"로 하면 가로와 세로를 본래의 80%로 축소시킨다.

좀 더 보기 좋아졌으나 뭔가 아쉬운 아이콘

크기는 좀 줄었는데 위치가 애매해졌다. 아이콘을 버튼 영역의 정중앙에 오도록 하려면 pivotXpivotY를 각각 android:viewportWidthM의 절반, android:viewportHeight의 절반으로 지정하면 된다.

<vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="?attr/actionBarSize"
    android:tint="#FFFFFF"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="?attr/actionBarSize">
    <group
        android:scaleX="0.8"
        android:scaleY="0.8"
        android:pivotX="12"
        android:pivotY="12">
        <path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
    </group>
</vector>

딱 좋은 상태의 햄버거 아이콘

이 버튼을 터치했을 때의 이벤트 처리는 다음과 같이 작성한다.

class MainActivity : AppCompatActivity() {
	override fun onOptionsItemSelected(item: MenuItem): Boolean {
		var result = false

		when (item.itemId) {
			android.R.id.home -> { Log.d("menu", "햄버거 버튼"); result = true }
            // ...
		}

		return result;
	}
}

햄버거 버튼, 홈 버튼의 ID는 android.R.id.home으로 고정되어 있다.

카테고리 “API/Android”
more...

“API/Android” (3건)