4. 목록 화면 만들기¶
검색 화면의 최종 목표는 "검색어를 입력하면 장소들이 목록으로 좌르륵 나오는 것"입니다. 이번 장에서는 그 목록을 만듭니다. 다만 진짜 검색은 6장에서 연결하고, 지금은 우리가 직접 만든 연습용 데이터 세 곳으로 목록을 완성합니다. 화면부터 만들고 데이터는 나중에 갈아 끼우는 것 — 실제 개발자들이 즐겨 쓰는 순서입니다.
RecyclerView — 칸을 재활용하는 순환 열차¶
지하철 2호선은 순환선입니다. 열차 몇 대가 같은 선로를 빙글빙글 돌면서 수십만 명을 실어 나릅니다. 승객 수만큼 열차를 만들지 않습니다.
안드로이드의 목록 화면 RecyclerView 가 정확히 이 방식이다. 장소가 1,000개라도 화면에 보이는 줄은 열 개 남짓이므로, 줄(뷰) 열몇 개만 만들어 두고 화면 밖으로 사라진 줄을 재활용(recycle) 해서 새 데이터를 실어 다시 내보낸다. 그래서 아무리 긴 목록도 빠르고 가볍다.
RecyclerView 는 세 가지 부품이 협력해 움직인다.
- 데이터 — 보여 줄 내용의 목록. 우리는
Place객체의 List. - 아이템 레이아웃 — 목록 한 줄의 생김새를 정한 XML.
- 어댑터(Adapter) — 데이터를 아이템 레이아웃에 실어 나르는 통역사. "몇 줄인지", "각 줄에 무엇을 채울지"를 어댑터가 답한다.
[CH-1 아래 코드를 그대로 작성하고 실행하시오.]¶
RecyclerView 는 별도 라이브러리이므로 의존성부터 추가합니다. app/build.gradle 의 dependencies 블록 안, implementation libs.activity 아래에 두 줄을 추가합니다.
// 목록 화면 (4장)
implementation 'androidx.recyclerview:recyclerview:1.3.2'
실행 결과
Gradle files have changed since last project sync. A project sync may be
necessary for the IDE to work properly. [Sync Now]
[설명]
- build.gradle 을 고치면 안드로이드 스튜디오 위쪽에 위와 같은 노란 안내줄이 나타난다. Sync Now 를 눌러야 새 라이브러리가 실제로 내려받아진다.
- 앞으로 5·6·7장에서도 라이브러리를 추가할 때마다 같은 과정(의존성 추가 → Sync Now)을 반복한다.
장소 데이터의 모양 정하기 — Place 클래스¶
목록에 실어 나를 "장소 하나"는 이름·주소·평점·좌표 같은 정보 묶음입니다. 이런 묶음은 클래스로 정의합니다.
[CH-2 아래 코드를 그대로 작성하고 실행하시오.]¶
java/com/example/myapplication 폴더에서 마우스 오른쪽 버튼 → New → Java Class 로 Place 클래스를 만들고 아래처럼 작성합니다.
package com.example.myapplication;
/** 앱 안에서 장소 하나를 표현하는 모델. 검색 결과를 이 모양으로 바꿔 목록·상세에 넘긴다. */
public class Place {
public String id; // 장소 고유 번호 (6장에서 Places API 가 채워 준다)
public String name;
public String address;
public String rating; // 평점이 없는 장소도 있어서 문자열로 둔다
public double lat;
public double lng;
public String imageUrl; // 장소 사진 주소 (없으면 null)
public Place(String id, String name, String address, String rating,
double lat, double lng, String imageUrl) {
this.id = id;
this.name = name;
this.address = address;
this.rating = rating;
this.lat = lat;
this.lng = lng;
this.imageUrl = imageUrl;
}
}
[설명]
- 이런 클래스를 모델(model) 이라 부른다 — 앱 안에서 데이터가 어떤 모양인지 정한 설계도다.
lat(위도)·lng(경도)는 지도 위의 좌표다. 5장에서 지도가 생기면 이 좌표로 마커를 꽂는다.- 6장에서 Google 의 검색 결과를 받아 오면, 그 복잡한 응답을 전부 이
Place모양으로 바꿔서 쓴다. 바깥 세상의 데이터가 어떻게 생겼든 앱 내부는 한 가지 모양만 다루게 하는 정리 습관이다.
목록 한 줄의 생김새 — 아이템 레이아웃¶
[CH-3 아래 코드를 그대로 작성하고 실행하시오.]¶
res/layout 폴더에서 마우스 오른쪽 버튼 → New → Layout Resource File 로 item_place.xml 을 만들고 아래처럼 작성합니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/item_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="13sp" />
<TextView
android:id="@+id/item_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="13sp" />
</LinearLayout>
[설명]
- 이름(굵게) → 주소 → 평점을 세로로 쌓은, 목록 한 줄의 설계도다. 이 레이아웃이 줄 수만큼 복제되어 목록이 된다.
android:background="?attr/selectableItemBackground"는 "누르는 동안 살짝 어두워지는 효과"를 입히는 테마 속성이다. 3장에서 배운?attr/문법이 여기서도 쓰였다 — 목록 줄을 탭할 수 있다는 느낌을 주는 관용구다.
통역사 고용하기 — 어댑터¶
[CH-4 아래 코드를 그대로 작성하고 실행하시오.]¶
SearchResultAdapter 클래스를 새로 만들고 아래처럼 작성합니다. 이 장에서 가장 긴 코드이지만, 구조를 알고 보면 세 가지 질문에 답하는 문서일 뿐입니다.
package com.example.myapplication;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class SearchResultAdapter extends RecyclerView.Adapter<SearchResultAdapter.PlaceViewHolder> {
/** 목록의 한 줄을 탭했을 때 무엇을 할지는 화면(액티비티)이 정한다. */
public interface OnPlaceClickListener {
void onPlaceClick(Place place);
}
private final List<Place> places;
private final OnPlaceClickListener listener;
public SearchResultAdapter(List<Place> places, OnPlaceClickListener listener) {
this.places = places;
this.listener = listener;
}
@NonNull
@Override
public PlaceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_place, parent, false);
return new PlaceViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull PlaceViewHolder holder, int position) {
Place place = places.get(position);
holder.nameText.setText(place.name);
holder.addressText.setText(place.address);
if (place.rating == null) {
holder.ratingText.setText(R.string.detail_no_rating);
} else {
holder.ratingText.setText("⭐ " + place.rating);
}
holder.itemView.setOnClickListener(v -> listener.onPlaceClick(place));
}
@Override
public int getItemCount() {
return places.size();
}
static class PlaceViewHolder extends RecyclerView.ViewHolder {
TextView nameText;
TextView addressText;
TextView ratingText;
PlaceViewHolder(View itemView) {
super(itemView);
nameText = itemView.findViewById(R.id.item_name);
addressText = itemView.findViewById(R.id.item_address);
ratingText = itemView.findViewById(R.id.item_rating);
}
}
}
아직 R.string.detail_no_rating 이 없어서 빨간 줄이 뜹니다. 다음 CH 에서 문구를 추가하면 사라집니다.
[설명]
- 어댑터는 RecyclerView 의 세 가지 질문에 답한다.
getItemCount()— 모두 몇 줄인가?onCreateViewHolder()— 새 줄이 필요하면 어떻게 만드나?onBindViewHolder()— position 번째 줄에 무엇을 채우나? - ViewHolder 는 한 줄 안의 TextView 들을 붙잡아 두는 손잡이다. 줄을 재활용할 때마다
findViewById를 다시 하면 느려지므로, 처음 한 번만 찾아서 들고 다닌다. onBindViewHolder가 바로 재활용 현장이다 — 화면 밖으로 나간 줄(holder)이 돌아오면 새 position 의 데이터로 내용만 갈아 끼운다.- 맨 위의
OnPlaceClickListener인터페이스에 주목한다. 어댑터는 "줄이 눌렸다"는 사실만 알리고, 눌렸을 때 무엇을 할지는 화면(액티비티)이 정하게 넘긴다. 그래서 같은 어댑터를 어느 화면에서든 다르게 활용할 수 있다.
검색 화면 조립하기¶
부품이 다 모였습니다. 검색 화면에 검색창과 RecyclerView 를 배치하고, 어댑터에 연습용 데이터를 실어 연결합니다.
[CH-5 아래 코드를 그대로 작성하고 실행하시오.]¶
새 화면에 쓸 문구를 strings.xml 에 추가합니다. 전체를 아래 내용으로 맞춥니다.
<resources>
<string name="app_name">버킷맵</string>
<string name="btn_find_place">장소 찾기</string>
<string name="btn_my_places">내 장소</string>
<string name="map_placeholder">지도가 들어갈 자리</string>
<string name="btn_wish">가보고 싶어요</string>
<string name="btn_visited">다녀왔어요</string>
<string name="btn_remove">빼기</string>
<string name="detail_photo_desc">장소 사진</string>
<string name="detail_no_rating">평점 없음</string>
<string name="toast_not_ready">아직 준비 중입니다</string>
<string name="search_hint">어디로 가고 싶나요?</string>
<string name="btn_search">검색</string>
<string name="chip_wish">가볼 곳</string>
<string name="chip_visited">다녀온 곳</string>
<string name="my_places_empty">아직 저장한 장소가 없습니다</string>
</resources>
[CH-6 아래 코드를 그대로 작성하고 실행하시오.]¶
activity_search.xml 을 임시 화면에서 진짜 검색 화면으로 바꿉니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp">
<EditText
android:id="@+id/edit_keyword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/search_hint"
android:imeOptions="actionSearch"
android:inputType="text" />
<Button
android:id="@+id/btn_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/btn_search" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_results"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
[설명]
- EditText 는 글자를 입력받는 뷰다.
hint는 비어 있을 때 흐리게 보이는 안내 문구,imeOptions="actionSearch"는 키보드 엔터 키를 돋보기 모양으로 바꿔 준다. - 위 줄(검색창+버튼)은 필요한 만큼만 차지하고, RecyclerView 가
layout_weight="1"로 남은 세로 공간 전부를 가져간다. 3장에서 버튼 두 개가 가로 공간을 나눠 갖던 것과 같은 원리다. - RecyclerView 는 기본 위젯이 아니라서
androidx.recyclerview.widget.RecyclerView처럼 전체 이름을 적는다.
[CH-7 아래 코드를 그대로 작성하고 실행하시오.]¶
SearchActivity.java 를 아래처럼 바꿉니다. 임시 버튼 코드는 사라지고, 연습용 장소 세 곳이 목록에 실립니다.
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class SearchActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
RecyclerView resultList = findViewById(R.id.list_results);
resultList.setLayoutManager(new LinearLayoutManager(this));
// 진짜 검색은 6장에서 연결한다 — 지금은 연습용 더미 데이터
List<Place> dummyPlaces = new ArrayList<>();
dummyPlaces.add(new Place("dummy-1", "경복궁", "서울 종로구 사직로 161",
"4.6", 37.5796, 126.9770, null));
dummyPlaces.add(new Place("dummy-2", "N서울타워", "서울 용산구 남산공원길 105",
"4.5", 37.5512, 126.9882, null));
dummyPlaces.add(new Place("dummy-3", "국립중앙박물관", "서울 용산구 서빙고로 137",
"4.7", 37.5240, 126.9804, null));
SearchResultAdapter adapter = new SearchResultAdapter(dummyPlaces, place -> {
Intent intent = new Intent(this, PlaceDetailActivity.class);
intent.putExtra(PlaceDetailActivity.EXTRA_NAME, place.name);
intent.putExtra(PlaceDetailActivity.EXTRA_ADDRESS, place.address);
intent.putExtra(PlaceDetailActivity.EXTRA_RATING, place.rating);
startActivity(intent);
});
resultList.setAdapter(adapter);
Button searchButton = findViewById(R.id.btn_search);
searchButton.setOnClickListener(v ->
Toast.makeText(this, R.string.toast_not_ready, Toast.LENGTH_SHORT).show());
}
}
아직 PlaceDetailActivity.EXTRA_ADDRESS 가 없어서 빨간 줄이 뜹니다. 다음 CH 에서 추가합니다.
[설명]
- RecyclerView 를 쓰는 순서는 항상 같다 — ① LayoutManager 지정(줄을 세로로 쌓는
LinearLayoutManager), ② 어댑터를 만들어, ③setAdapter로 연결. - 어댑터를 만들 때 두 번째 인자로 "줄이 눌리면 할 일"을 람다로 넘겼다. 상세 화면으로 가는 Intent 에 이름·주소·평점을 실어 보낸다 — 2장에서 배운 putExtra 그대로다.
- "떡볶이 맛집"을 보내던 임시 버튼은 지웠다. 이제 목록의 모든 줄이 상세 화면으로 가는 문이 된다.
- 검색 버튼은 아직 할 일이 없어 안내 문구만 띄운다. 6장에서 진짜 검색으로 바뀐다.
[CH-8 아래 코드를 그대로 작성하고 실행하시오.]¶
상세 화면이 주소와 평점도 받을 수 있게 PlaceDetailActivity.java 를 아래처럼 바꿉니다.
package com.example.myapplication;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class PlaceDetailActivity extends AppCompatActivity {
public static final String EXTRA_NAME = "extra_name";
public static final String EXTRA_ADDRESS = "extra_address";
public static final String EXTRA_RATING = "extra_rating";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_place_detail);
TextView nameText = findViewById(R.id.text_place_name);
TextView addressText = findViewById(R.id.text_place_address);
TextView ratingText = findViewById(R.id.text_place_rating);
nameText.setText(getIntent().getStringExtra(EXTRA_NAME));
addressText.setText(getIntent().getStringExtra(EXTRA_ADDRESS));
String rating = getIntent().getStringExtra(EXTRA_RATING);
if (rating == null) {
ratingText.setText(R.string.detail_no_rating);
} else {
ratingText.setText("⭐ " + rating);
}
Button wishButton = findViewById(R.id.btn_wish);
Button visitedButton = findViewById(R.id.btn_visited);
Button removeButton = findViewById(R.id.btn_remove);
wishButton.setOnClickListener(v ->
Toast.makeText(this, R.string.toast_not_ready, Toast.LENGTH_SHORT).show());
visitedButton.setOnClickListener(v ->
Toast.makeText(this, R.string.toast_not_ready, Toast.LENGTH_SHORT).show());
removeButton.setOnClickListener(v ->
Toast.makeText(this, R.string.toast_not_ready, Toast.LENGTH_SHORT).show());
}
}
실행 결과
앱을 실행하고 장소 찾기 를 누르면 연습용 장소 세 곳이 목록으로 나타납니다.

경복궁 줄을 탭하면 상세 화면이 열리고, 이번에는 주소와 평점까지 제대로 채워집니다.

[설명]
- 상수
EXTRA_ADDRESS·EXTRA_RATING이 추가되고, 받은 값을 3장에서 만들어 둔 TextView 들에 채운다. 임시 문구 "주소가 여기에 표시됩니다"가 진짜 주소로 덮이는 순간이다. - 평점은
null인지 먼저 확인한다. 세상의 모든 장소에 평점이 있는 것은 아니다 — 데이터가 없을 가능성을 항상 의심하는 습관이 오류 없는 앱을 만든다. - 뒤로 가기를 누르면 목록 화면이 그대로 남아 있다. 2장에서 배운 back 스택 덕분이다.
내 장소 화면의 뼈대¶
내 장소 화면도 목록 화면입니다. 다만 "가볼 곳"과 "다녀온 곳"을 나눠 보는 필터가 위에 붙습니다. 필터로는 알약 모양 버튼인 Chip 을 씁니다.
[CH-9 아래 코드를 그대로 작성하고 실행하시오.]¶
activity_my_places.xml 을 아래처럼 바꿉니다. (MyPlacesActivity.java 는 2장 그대로 두면 됩니다.)
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_wish"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/chip_wish" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_visited"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/chip_visited" />
</com.google.android.material.chip.ChipGroup>
<TextView
android:id="@+id/text_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="48dp"
android:text="@string/my_places_empty"
android:textSize="15sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_my_places"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
실행 결과
메인 화면에서 내 장소 를 누르면 필터 칩과 빈 목록 안내가 보입니다.

[설명]
- Chip 은 Material 라이브러리가 제공하는 알약 모양 선택 버튼이다. 누르면 체크 상태가 토글되는, 작은 스위치라고 생각하면 된다.
- ChipGroup 의
singleSelection="true"는 한 번에 하나만 선택되게,selectionRequired="true"는 최소 하나는 항상 선택되어 있게 강제한다. 라디오 버튼과 같은 동작을 칩으로 만든 것이다. app:으로 시작하는 속성은 안드로이드 기본이 아니라 라이브러리가 추가한 속성이라, 맨 위에xmlns:app선언이 필요하다.- 빈 상태 안내(
text_empty)를 미리 두었다. 7장에서 저장된 장소가 생기면 이 문구를 숨기고 목록을 채운다.
정리¶
이번 장에서 배운 것을 정리합니다.
- RecyclerView 는 줄 몇 개를 재활용해 긴 목록을 그린다 — 순환 열차 방식이다.
- 어댑터는 데이터와 화면 사이의 통역사로, 줄 수·줄 생성·줄 채우기 세 질문에 답한다.
- 클릭 처리는 어댑터가 인터페이스로 화면에 위임한다 — 어댑터는 재사용 가능한 부품으로 남는다.
- 진짜 데이터가 오기 전에 더미 데이터로 화면을 먼저 완성하는 것이 효율적인 개발 순서다.
[self Test 1] - 연습용 장소를 다섯 곳으로 늘려 보시오. 우리 동네에서 가 보고 싶은 곳 두 곳을 골라 이름·주소를 넣고, 평점 하나는 일부러 null 로 넣어 "평점 없음"이 표시되는지 확인하시오. (좌표는 아직 몰라도 된다 — 0, 0 을 넣어 두고 5장에서 진짜 좌표 찾는 법을 배운다.)
[self Test 2] - 아이템 레이아웃의 이름 글자 크기를 18sp 로 키우고 실행해, 목록 전체 줄 높이가 어떻게 달라지는지 관찰하시오.