Chapter 6. 제트팩 라이브러리
'깡샘의 안드로이드 앱 프로그래밍 with 코틀린' 11장 학습 내용
0절. ART
1절. 제트팩
2절. androidx
3절. appcompat
4절. 프래그먼트
5절. 리사이클러 뷰
6절. 뷰 페이저2
7절. 드로어 레이아웃
8절. 제트팩 화면 구성
프로젝트 바로가기
0절. ART
플랫폼 API

1절. 제트팩
제트팩(JetPack)
- 구글 안드로이드 앱 개발용 라이브러리
- androidx로 시작하는 패키지명
- 앱을 개발 시 필요 권장 아키텍처 제공
- API 레벨 호환성 문제 해결
- 플랫폼 API에서 제공하지 않는 기능 제공
2절. androidx
androidx 라이브러리
| 라이브러리 종류 |
설명 |
| androidx.appcompat |
앱 API 레벨 호환성 해결 |
| androidx.recyclerview |
목록 화면 구성 |
| androidx.viewpager2 |
스와이프로 넘기는 화면 구성 |
| androidx.fragment |
액티비티처럼 동작하는 뷰 제공 |
| androidx.drawerlayout |
옆에서 서랍처럼 열리는 화면 구성 |
3절. appcompat
appcompat 라이브러리
- 안드로이드 앱 화면 구성 액티비티 생성
- API 레벨 호환성 문제 해결
// appcompat 라이브러리 선언
implementation(lib.androidx.appcompat)
// appcompat 라이브러리 사용
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
}
문자열 출력 구성
| 타이틀 문자열 출력 구성 방법 |
| 액션바 이용 |
| 툴바 이용 |


<!-- themes.xml 테마 스타일 선언 -->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- 기본 테마 -->
<style name="Base.Theme.AndroidLab" parent="Theme.Material3.DayNight.NoActionBar">
<!-- light 테마 지정 -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.AndroidLab" parent="Base.Theme.AndroidLab" />
</resources>
<!-- AndroidManifest.xml 적용 내용 확인 -->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidLab" >
<!-- 생략 -->
</application>
툴바
- activity.xml에 androidx.appcompat.widtet.Toolbar 선언
- 액션바의 내용이 툴바에 적용되도록 지정
<!-- 레이아웃 XML에 툴바 적용 -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:titleTextColor="#FFFFFF" />

// 액션바 내용 툴바 적용
setSupportActionBar(binding.toolbar)
| 구성 요소 |
| 타이틀 문자열 |
| 내비게이션 아이콘 |
| 액션 아이템 |
| 오버플로 메뉴 |

툴바 색상 설정
| 속성 |
설명 |
| colorPrimary |
툴바 · 버튼 배경색 |
| colorAccent |
활성모드 상태 표현 |
| statusBarColor |
상태바 배경색 |
<!-- 테마 색상 지정 -->
<style name="Base.Theme.AndroidLab" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">#FF0000</item>
<item name="android:statusBarColor">#CC0000</item>
<item name="colorAccent">#00FF00</item>
</style>

업 버튼 설정
- 액션바 왼쪽에 이전 화면으로 되돌아가는 화살표 모양(←) 업 버튼

| 등록 방법 |
| AndroidManifest.xml 등록 |
| 액티비티 코드 등록 |
<!-- AndroidManifest.xml에서 업버튼 설정 -->
<activity
android:name=".test1.TwoActivity"
android:exported="false"
android:parentActivityName=".MainActivity"/>
// 툴바에 업버튼 설정
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- onSupportNavigateUp() 함수 재정의
- 업버튼 실행 시 특별한 로직 실행
- AndroidManifest.xml 설정 없이 코드에서 업버튼 설정 희망
// 사용자가 업버튼 실행 시 함수 자동 호출되는 onSupportNavigateUp() 함수 재정의
override fun onSupportNavigateUp(): Boolean{
Log.d("Yunseo", "onSupportNavigateUp")
return super.onSupportNavigateUp()
}
메뉴 구성
- 메뉴
- 툴바의 중요한 구성 요소
- 액티비티 화면에서 사용자 이벤트 사용 허용
| 액티비티 메뉴 추가 함수 |
설명 |
| onCreateOptionsMenu() |
액티비티 첫 1회만 호출 |
| onPrepareOptionsMenu() |
액티비티 첫 실행 한 번 호출 후 오버플로 메뉴가 나타날 때마다 반복 호출 |

// 메뉴 구성 함수
override fun onCreateOptionsMenu(menu: Menu?): Boolean{
val menuItem1: MenuItem? = menu?.add(0, 0, 0, "menu1")
val menuItem2: MenuItem? = menu?.add(0, 1, 0, "menu2")
return super.onCreateOptionsMenu(menu)
}

이벤트 처리
- onOptionsItemSelected() : 사용자가 메뉴 선택 시 이벤트 처리
- MenuItem : 함수 매개 변수이자 이벤트가 발생한 메뉴 객체
// 메뉴 선택 시 이벤트 처리
override fun onOptionsItemSelected(item: MenuItem): Boolean = when(item.itemId){
0 -> {
Log.d("Yunseo", "menu1 click")
true
}
1 -> {
Log.d("Yunseo", "menu2 click")
true
}
else -> super.onOptionsItemSelected(item)
}
리소스 메뉴 구현
- res/menu 디렉토리에 메뉴 구성 XML 파일 생성

<!-- menu_main.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu1"
android:title="menu1" />
<item
android:id="@+id/menu2"
android:icon="@android:drawable/ic_menu_add"
android:title="menu2"
app:showAsAction="always" />
<item
android:id="@+id/menu3"
android:icon="@android:drawable/ic_menu_search"
android:title="menu3"
app:showAsAction="iffRoom" />
</menu>
showAsActions 속성
| 속성값 종류 |
설명 |
| never(기본) |
항상 오버플로 메뉴로 출력 |
| ifRoom |
액션바 공간 있으면 액션 아이템, 없으면 오버플로 메뉴 출력 |
| always |
항상 액션 아이템으로 출력 |
// 액티비티 코드 메뉴 XML 적용
override fun onCreateOptionsMenu(menu: Menu?): Boolean{
menuInflater.inflate(R.menu.menu_main, menu)
return super.onCreateOptionsMenu(menu)
}

액션 뷰 이용
- 특별한 기능 제공 : androidx.appcompat.widget.SearchView
<!-- 서치 뷰 사용 -->
<item android:id="@+id/menu_search"
android:title="search"
app:showAsAction="always"
app:actionViewClass="androidx.appcompat.widget.SearchView" />

서치 뷰 이용
- MenuItem 객체를 얻은 후 객체에 등록된 SearchView 객체 획득
// 서치 뷰 검색 기능 구현
override fun onCreateOptionsMenu(menu: Menu): Boolean{
val inflater = menuInflater
inflater.inflate(R.menu.menu_main, menu)
val menuItem = menu?.findItem(R.id.menu_search)
val searchView = menuItem?.actionView as SearchView
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextChange(newText: String?): Boolean{
// 검색어 변경 이벤트
return true
}
override fun onQueryTextSubmit(query: String?): Boolean{
// 키보드 검색 버튼 클릭한 순간의 이벤트
return true
}
})
return true
}
4절. 프래그먼트
프래그먼트
- 액티비티처럼 동작하는 뷰
- 액티비티에 구현되는 모든 내용을 프래그먼트 클래스에 작성

프래그먼트 구현
- Fragment 상속 후 작성하는 클래스
- onCreateView() : 작성 함수가 자동 호출되며 반환한 View 객체가 화면에 출력
// 프래그먼트 구현
// (...생략...)
import androidx.fragment.app.Fragment
class OneFragment : Fragment() {
lateinit var binding: FragmentOneBinding
override fun onCreateView(
inflater: LayoutInflater, containter: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentOneBinding.inflate(inflater, container, false)
return binding.root
}
}
프래그먼트 출력
<!-- 프래그먼트 출력 -->
<fragment
android:name="com.example.test11.OneFragment"
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- \<fragment> 태그
- activity.xml 에 등록
- name 속성 : 프래그먼트 클래스 지정
- 화면에 프래그먼트 출력
- 프래그먼트 동적 제어(추가, 제거 등) 시 FragmentManager의 FragmentTransaction 클래스 함수 이용
| FragmentTransaction 클래스 함수 종류 |
설명 |
| add(int containerViewId, Fragment fragment) |
새로운 프래그먼트 화면에 추가 |
| replace(int containerViewId, Fragment fragment) |
추가된 프래그먼트 대체 |
| remove(Fragment fragment) |
추가된 프래그먼트 제거 |
| commit() |
화면 적용 |
// 프래그먼트 동적 제어
val fragmentManager: FragmentManager = supportFragmentManager
val transaction: FragmentTransaction = fragmentManager.beginTransaction()
val fragment = OneFragment()
transaction.add(R.id.fragment_content, fragment)
transaction.commit()
프래그먼트 생명주기
- 액티비티의 생명주기 함수
- 아래의 함수를 가지며 호출 시점도 액티비티와 동일
| 생명 주기 함수 종류 |
| onCreate() |
| onStart() |
| onResume() |
| onPause() |
| onStop() |

| 생명 주기 단계 |
| 초기화(initialized) |
| 생성(created) |
| 시작(started) |
| 재개(resumed) |
| 소멸(destroyed) |
백 스택
- 프래그먼트가 화면에 보이지 않는 순간 제거하지 않고 저장 후 다시 이용하는 기능
| 사용 여부 |
설명 |
| 미사용 |
프래그먼트 교체 시 기존 프래그먼트는 onDestroy까지 호출 후 제거 |
| 사용 |
프래그먼트 제거없이 onDestoryView 함수까지 호출 |
// 백 스택 사용 설정
transaction.addToBackStack(null)
5절. 리사이클러 뷰
리사이클러 뷰

| 구성 요소 |
설명 |
| ViewHolder(필수) |
항목에 필요한 뷰 객체 보유 |
| Adapter(필수) |
항목 구성 |
| LayoutManager(필수) |
항목 배치 |
| ItemDecoration(옵션) |
항목 꾸미기 |
뷰 홀더 준비
- 각 항목에 해당하는 뷰 객체 소유
- RecyclerView.ViewHolder 상속
// 뷰 홀더 준비
class MyViewHolder(val binding: ItemMainBinding): RecyclerView.ViewHolder(binding.root)
어댑터
| 어댑터 함수 종류 |
설명 |
| getItemCount() |
항목 개수 판단을 위해 자동 호출 |
| onCreateViewHolder() |
항목 뷰를 가지는 뷰 홀더 준비를 위해 자동 호출 |
| onBindViewHolder() |
뷰 홀더 뷰에 데이터 출력을 위해 자동 호출 |
// 어댑터 준비
class MyAdapter(val datas: MutableList<String>):
RecyclerView.Adapter<RecyclerView.ViewHolder>(){
override fun getItemCount(): Int{
TODO("Not yet implemented")
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
RecyclerView.ViewHolder{
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int){
TODO("Not yet implemented")
}
}
// 항목 개수 구하기
override fun getItemCount(): Int = datas.size
// 항목 구성에 필요한 뷰 홀더 객체 준비
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
MyViewHolder(ItemMainBinding.inflate(LayoutInflater.from(parent.context),
parent, false))
// 뷰에 데이터 출력
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int){
Log.d("yunseo", "onBindViewHolder : $position")
val binding = (holder as MyViewHolder).binding
// 뷰에 데이터 출력
binding.itemData.text = datas[position]
// 뷰에 이벤트 추가
binding.itemRoot.setOnClickListener {
Log.d("yunseo", "item root click : $position")
}
}
리사이클러 뷰 출력
// 리사이클러 뷰 액티비티
class RecyclerViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
val binding = ActivityRecyclerViewBinding.inflate(layoutInflater)
setContentView(binding.root)
val datas = mutableListOf<String>()
for(i in 1..10){
datas.add("Item $i")
}
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = MyAdapter(datas)
binding.recyclerView.addItemDecoration(DividerItemDecoration(this,
LinearLayoutManager.VERTICAL))
}
}
notifyDataSetChanged()
- 항목 구성 데이터에 새로운 항목을 동적으로 추가·제거 후 호출
// 항목 추가
datas.add("new data")
adapter.notifyDataSetChanged()
레이아웃 매니저
| 항목 종류 |
설명 |
| LinearLayoutManager |
항목 가로·세로 방향 배치 |
| GridLayoutManager |
항목 그리드 배치 |
| StaggeredGridLayoutManager |
항목을 높이가 불규칙한 그리드 배치 |
LinearLayoutManager
- 항목 가로·세로 배치
- 가로 배치 : LinearLayoutManager.HORIZONTAL
- 세로 배치 : LinearLayoutManager.VERTICAL or 생략

// 항목 세로 배치
binding.recyclerView.layoutManager = LinearLayoutManager(this)

// 항목 가로 배치
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
binding.recyclerView.layoutManager = layoutManager
GridLayoutManager
- 그리드 배치
- 생성자 숫자 : 그리드 열의 개수
- 방향 설정 가능
// 그리드 항목 배치
val layoutManager = GridLayoutManager(this, 2)
binding.recyclerView.layoutManager = layoutManager

- 가로 설정 : 생성자에 GridLayoutManager.HORIZONTAL 지정
// 그리드 항목 가로 배치
val layoutManager = GridLayoutManager(this, 3,
GridLayoutManager.HORIZONTAL, false)
binding.recyclerView.layoutManager = layoutManager

- 네 번째 매개변수 Boolean 값 : 왼쪽, 오른쪽 정렬
// 그리드 항목 오른쪽부터 배치
val layoutManager = GridLayoutManager(this, 3,
GridLayoutManager.HORIZONTAL, true)
binding.recyclerView.layoutManager = layoutManager

StaggeredGridLayoutManager
// 지그재그 그리드 형태 배치
val layoutManager = StaggeredGridLayoutManager(2,
StaggeredGridLayoutManager.VERTICAL)
binding.recyclerView.layoutManager = layoutManager

아이템 데코레이션
- 리사이클러 뷰를 다양하게 꾸밀 때 사용
- DividerItem Decoration : 항목의 구분선 출력
- ItemDecoration을 상속받는 개발자 클래스 생성 후 다양한 꾸미기 작업 수행
| 함수 종류 |
설명 |
| onDraw() |
항목 배치 전 호출 |
| onDrawOver() |
항목 모두 배치 후 호출 |
| getItemOffsets() |
개별 항목 꾸밀 때 호출 |
// 아이템 데코레이션 구현
class MyDecoration(val context: Context): RecyclerView.ItemDecoration(){
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State){
super.onDraw(c, parent, state)
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State){
super.onDrawOver(c, parent, state)
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
}
}
// 항목 배치 전 호출되는 onDraw()
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State){
super.onDraw(c, parent, state)
c.drawBitmap(BitmapFactory.decodeResource(context.resources,
R.drawable.stadium), 0f, 0f, null)
}
// 모든 항목 배치 후 호출하는 onDrawOver()
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State){
super.onDrawOver(c, parent, state)
// 뷰 크기 계산
val width = parent.width
val height = parent.height
// 이미지 크기 계산
val dr: Drawable? = ResourcesCompat.getDrawable(context.resources,
R.drawable.kbo, null)
val drWidth = dr?.intrinsicWidth
val drHeight = dr?.intrinsicHeight
// 이미지가 그려질 위치 계산
val left = width / 2 - drWidth?.div(2) as Int
val top = height / 2 - drHeight?.div(2) as Int
c.drawBitmap(
BitmapFactory.decodeResource(context.resources, R.drawable.kbo),
left.toFloat(),
top.toFloat(),
null
)
}
// 개별 항목 꾸미는 getItemOffsets()
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
){
super.getItemOffsets(outRect, view, parent, state)
val index = parent.getChildAdapterPosition(view) + 1
if(index % 3 == 0) outRect.set(10, 10, 10, 60) // left, top, right, bottom
else outRect.set(10, 10, 10, 0)
view.setBackgroundColor(Color.LTGRAY)
ViewCompat.setElevation(view, 20.0f)
}
addItemDecoration()
- 아이템 데코레이션 객체를 리사이클러 뷰에 적용하는 함수

// 리사이클러 뷰에 적용한 아이템 데코레이션
binding.recyclerView.addItemDecoration(MyDecoration(this))
6절. 뷰 페이저2
뷰 페이저2

- 스와이프로 넘기는 화면 구성
- 2019년, viewpager 대신 viewpager2 제공
- 항목이 순서대로 나열
- 한 화면에 항목 하나 표현
| 어댑터 종류 |
| RecyclerView.Adapter(기존) |
| FragmentStateAdapter(New!) |
// 뷰 페이저2 구현 : 리사이클러 뷰 어댑터 이용
class MyPagerViewHolder(val binding: ItemPagerBinding) :
RecyclerView.ViewHolder(binding.root)
class MyPagerAdapter(val datas: MutableList<String>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>(){
override fun getItemCount(): Int{
return datas.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
RecyclerView.ViewHolder =
MyPagerViewHolder(ItemPagerBinding.inflate(LayoutInflater.from(
parent.context), parent, false))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int){
val binding = (holder as MyPagerViewHolder).binding
// 뷰에 데이터 출력
binding.itemPagerTextView.text = datas[position]
when (position % 3) {
0 -> binding.itemPagerTextView.setBackgroundColor(Color.RED)
1 -> binding.itemPagerTextView.setBackgroundColor(Color.BLUE)
2 -> binding.itemPagerTextView.setBackgroundColor(Color.GREEN)
}
}
}
- FragmentStateAdapter로 뷰 페이저2 구현

// 뷰 페이저 2 구현 - 프래그먼트 어댑터 이용
class MyFragmentPagerAdapter(activity: FragmentActivity): FragmentStateAdapter(activity){
val fragments: List<Fragment>
init{
fragments = listOf(OneFragment(), TwoFragment(), ThreeFragment())
Log.d("Yunseo", "Fragments Size : ${fragments.size}")
}
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment = fragments[position]
}
// 뷰 페이저 2 어댑터 적용
binding.viewpager.adapter = MyPagerAdapter(datas)
7절. 드로어 레이아웃
DrawerLayout
- activity.xml에서 드로어 메뉴 출력 부분의 태그를 DrawerLayout으로 선언
- DrawerLayout 아래에 2개의 뷰 선언
- 첫 번째 하위 태그: 액티비티 화면에 출력
- 두 번째 하위 태그: 평소에 안 보이고 끌려 나옴

<!-- 드로어 레이아웃 구성 -->
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout ...>
...
</LinearLayout>
<TextView
android:layout_gravity="start"/>
</android.drawerlayout.widget.DrawerLayout>
- 툴바 영역에 토글(toggle) 버튼 함께 제공
- 토글 버튼 : ActionBarDrawerToggle 클래스에서 제공

// 드로어 메뉴 토글 버튼 적용
class DrawerActivity : AppCompatActivity() {
lateinit var toggle: ActionBarDrawerToggle
override fun onCreate(savedInstanceState: Bundle?){
// (...생략...)
toggle = ActionBarDrawerToggle(this, binding.drawer, R.string.drawer_opened, R.string.drawer_closed)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toggle.syncState()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean{
// 이벤트가 토글 버튼에서 발생할 경우
if(toggle.onOptionsItemSelected(item)){
return true
}
return super.onOptionsItemSelected(item)
}
}
8장. 제트팩 화면 구성
1) 새 모듈 생성 & 그래들 설정
- Ch11_JetPack 새 모듈 생성
- 뷰 바인딩 기법 추가
viewBinding.isEnabled = true
2) 문자열 리소스 생성
- res/values/strings.xml 파일에 ActionBarDrawerToggle 생성 시 지정할 문자열 리소스 추가
3) 테마 추가
- res/values/themes.xml 파일에 액티비티 윈도우를 출력할 때 액션바 출력이 없도록 설정
4) 프래그먼트 생성
- New -> Fragment -> Fragment(Blank) 선택
- OneFragment, TwoFragment, ThreeFragment 이름의 프래그먼트 3개 추가
5) 파일 복사
- res/drawable : kbo.png 파일 복사
- res/layout : fragment_one.xml, fragment_two.xml, fragment_three.xml 파일 복사
- OneFragment.kt, TwoFragment.kt, ThreeFragment.kt : 해당 소스 영역에 복사
6) fragment_one.xml 파일 작성
- 리사이클러 뷰 구성을 위해 fragment_one.xml 파일 작성
- OneFragment.kt 파일 작성
7) 메뉴 리소스 생성
- New -> Android Resource Directory 선택
- Resource type 에서 menu 디렉토리 생성
- New -> Menu Resource File 선택[cite: 49].
- File name 에 menu_main 입력 후 파일 생성
- menu_main.xml 파일 열고 작성
8) 메인 레이아웃 XML 작성
- activity_main.xml 파일에 등록
- 드로어 레이아웃
- 툴바
- 뷰 페이저2
9) 메인 액티비티 작성
10) 앱 실행

프로젝트 바로가기