이 튜토리얼에서는 안드로이드 앱에서 RecyclerView와 SQLite 데이터베이스를 연동하여 연락처 목록을 관리하는 방법을 단계별로 살펴봅니다. 사용자가 연락처를 추가하고, 수정하고, 삭제하면 그 결과가 데이터베이스에 저장되고 RecyclerView에 실시간으로 반영되는 구조입니다.
1단계: 새 프로젝트 생성 및 의존성 추가
Android Studio에서 File → New Project로 이동한 뒤, 필요한 정보를 모두 입력하여 새 프로젝트를 생성합니다.
그다음 build.gradle (Module: app) 파일에 아래 의존성을 추가합니다.
implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'com.android.support:cardview-v7:28.0.0' implementation 'com.android.support:design:28.0.0'
2단계: 메인 레이아웃 작성 (activity_main.xml)
res/layout/activity_main.xml 파일에 다음 코드를 추가합니다. 이 레이아웃에는 연락처 목록을 보여줄 RecyclerView와 새 연락처를 추가하기 위한 버튼이 포함됩니다.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_marginBottom="50dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:id="@+id/myContactList" />
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:text="ADD"
android:padding="2dp"
android:layout_marginBottom="4dp"/>
</FrameLayout>3단계: 서브 레이아웃 리소스 파일 생성
연락처 입력용 다이얼로그 레이아웃과 목록 항목 레이아웃 두 개의 리소스 파일을 생성합니다.
add_contacts.xml — 연락처 입력 폼
이름과 전화번호를 입력받는 EditText 두 개로 구성된 레이아웃입니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="https://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/enterName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter Name"
android:textSize="12sp"
android:inputType="text"
android:maxLines="1"
android:layout_marginEnd="8dp"
android:padding="12dp"/>
<EditText
android:id="@+id/enterPhoneNum"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter Number"
android:layout_marginTop="10dp"
android:textSize="12sp"
android:inputType="phone"
android:maxLines="1"
android:layout_marginEnd="8dp"
android:padding="12dp"/>
</LinearLayout>contact_list_layout.xml — 목록 항목 레이아웃
각 연락처 항목을 CardView로 감싸고, 이름·전화번호 표시 영역과 수정·삭제 아이콘 버튼을 배치합니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="https://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/contactName"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:layout_below="@+id/contactName"
android:id="@+id/phoneNum"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textStyle="bold" />
</RelativeLayout>
<ImageView
android:id="@+id/editContact"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:src="@drawable/ic_edit"
android:contentDescription="TODO" />
<ImageView
android:id="@+id/deleteContact"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:src="@drawable/ic_remove"
android:contentDescription="TODO" />
</LinearLayout>
</androidx.cardview.widget.CardView>4단계: 어댑터 클래스 작성 (ContactAdapter.java)
RecyclerView의 데이터 바인딩을 담당하는 어댑터 클래스입니다. 각 항목의 수정·삭제 이벤트 처리와 이름 기반 검색 필터(Filterable) 기능도 함께 구현되어 있습니다.
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Objects;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
class ContactAdapter extends RecyclerView.Adapter<ContactViewHolder>
implements Filterable {
private Context context;
private ArrayList<Contacts> listContacts;
private ArrayList<Contacts> mArrayList;
private SqliteDatabase mDatabase;
ContactAdapter(Context context, ArrayList<Contacts> listContacts) {
this.context = context;
this.listContacts = listContacts;
this.mArrayList = listContacts;
mDatabase = new SqliteDatabase(context);
}
@Override
public ContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.contact_list_layout, parent, false);
return new ContactViewHolder(view);
}
@Override
public void onBindViewHolder(ContactViewHolder holder, int position) {
final Contacts contacts = listContacts.get(position);
holder.tvName.setText(contacts.getName());
holder.tvPhoneNum.setText(contacts.getPhno());
holder.editContact.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
editTaskDialog(contacts);
}
});
holder.deleteContact.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mDatabase.deleteContact(contacts.getId());
((Activity) context).finish();
context.startActivity(((Activity) context).getIntent());
}
});
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence charSequence) {
String charString = charSequence.toString();
if (charString.isEmpty()) {
listContacts = mArrayList;
}
else {
ArrayList<Contacts> filteredList = new ArrayList<>();
for (Contacts contacts : mArrayList) {
if (contacts.getName().toLowerCase().contains(charString)) {
filteredList.add(contacts);
}
}
listContacts = filteredList;
}
FilterResults filterResults = new FilterResults();
filterResults.values = listContacts;
return filterResults;
}
@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
listContacts = (ArrayList<Contacts>) filterResults.values;
notifyDataSetChanged();
}
};
}
@Override
public int getItemCount() {
return listContacts.size();
}
private void editTaskDialog(final Contacts contacts) {
LayoutInflater inflater = LayoutInflater.from(context);
View subView = inflater.inflate(R.layout.add_contacts, null);
final EditText nameField = subView.findViewById(R.id.enterName);
final EditText contactField = subView.findViewById(R.id.enterPhoneNum);
if (contacts != null) {
nameField.setText(contacts.getName());
contactField.setText(String.valueOf(contacts.getPhno()));
}
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Edit contact");
builder.setView(subView);
builder.create();
builder.setPositiveButton("EDIT CONTACT", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String name = nameField.getText().toString();
final String ph_no = contactField.getText().toString();
if (TextUtils.isEmpty(name)) {
Toast.makeText(context, "Something went wrong. Check your input values", Toast.LENGTH_LONG).show();
} else {
mDatabase.updateContacts(new
Contacts(Objects.requireNonNull(contacts).getId(), name, ph_no));
((Activity) context).finish();
context.startActivity(((Activity)
context).getIntent());
}
}
});
builder.setNegativeButton("CANCEL", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(context, "Task cancelled",Toast.LENGTH_LONG).show();
}
});
builder.show();
}
}5단계: 데이터 모델 클래스 작성 (Contacts.java)
연락처 하나의 정보(id, 이름, 전화번호)를 담는 모델 클래스입니다.
public class Contacts {
private int id;
private String name;
private String phoneNumber;
Contacts(String name, String phno) {
this.name = name;
this.phoneNumber = phno;
}
Contacts(int id, String name, String phno) {
this.id = id;
this.name = name;
this.phoneNumber = phno;
}
int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
String getPhno() {
return phoneNumber;
}
public void setPhno(String phno) {
this.phoneNumber = phno;
}
}6단계: 뷰홀더 클래스 작성 (ContactViewHolder.java)
목록 항목 뷰의 위젯 참조를 캐싱하여 성능을 높이는 ViewHolder 클래스입니다.
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
class ContactViewHolder extends RecyclerView.ViewHolder {
TextView tvName, tvPhoneNum;
ImageView deleteContact;
ImageView editContact;
ContactViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.contactName);
tvPhoneNum = itemView.findViewById(R.id.phoneNum);
deleteContact = itemView.findViewById(R.id.deleteContact);
editContact = itemView.findViewById(R.id.editContact);
}
}7단계: SQLite 데이터베이스 헬퍼 작성 (SqliteDatabase.java)
SQLiteOpenHelper를 상속받아 데이터베이스 생성, 조회, 삽입, 수정, 삭제(CRUD) 기능을 구현한 클래스입니다.
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
public class SqliteDatabase extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 5;
private static final String DATABASE_NAME = "Contacts";
private static final String TABLE_CONTACTS = "Contacts";
private static final String COLUMN_ID = "_id";
private static final String COLUMN_NAME = "contactName";
private static final String COLUMN_NO = "phoneNumber";
SqliteDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String CREATE_CONTACTS_TABLE = "CREATE TABLE "
+ TABLE_CONTACTS + "(" + COLUMN_ID
+ " INTEGER PRIMARY KEY,"
+ COLUMN_NAME + " TEXT,"
+ COLUMN_NO + " INTEGER" + ")";
db.execSQL(CREATE_CONTACTS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACTS);
onCreate(db);
}
ArrayList<Contacts> listContacts() {
String sql = "select * from " + TABLE_CONTACTS;
SQLiteDatabase db = this.getReadableDatabase();
ArrayList<Contacts> storeContacts = new ArrayList<>();
Cursor cursor = db.rawQuery(sql, null);
if (cursor.moveToFirst()) {
do {
int id = Integer.parseInt(cursor.getString(0));
String name = cursor.getString(1);
String phno = cursor.getString(2);
storeContacts.add(new Contacts(id, name, phno));
}
while (cursor.moveToNext());
}
cursor.close();
return storeContacts;
}
void addContacts(Contacts contacts) {
ContentValues values = new ContentValues();
values.put(COLUMN_NAME, contacts.getName());
values.put(COLUMN_NO, contacts.getPhno());
SQLiteDatabase db = this.getWritableDatabase();
db.insert(TABLE_CONTACTS, null, values);
}
void updateContacts(Contacts contacts) {
ContentValues values = new ContentValues();
values.put(COLUMN_NAME, contacts.getName());
values.put(COLUMN_NO, contacts.getPhno());
SQLiteDatabase db = this.getWritableDatabase();
db.update(TABLE_CONTACTS, values, COLUMN_ID + " = ?", new String[]{String.valueOf(contacts.getId())});
}
void deleteContact(int id) {
SQLiteDatabase db = this.getWritableDatabase();
db.delete(TABLE_CONTACTS, COLUMN_ID + " = ?", new String[]{String.valueOf(id)});
}
}8단계: 메인 액티비티 작성 (MainActivity.java)
액티비티에서는 데이터베이스에서 연락처 목록을 불러와 RecyclerView에 연결하고, ADD 버튼 클릭 시 새 연락처를 추가하는 다이얼로그를 띄웁니다. 액티비티 종료 시에는 데이터베이스 연결을 반드시 닫아주어야 합니다.
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private SqliteDatabase mDatabase;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView contactView = findViewById(R.id.myContactList);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
contactView.setLayoutManager(linearLayoutManager);
contactView.setHasFixedSize(true);
mDatabase = new SqliteDatabase(this);
ArrayList<Contacts> allContacts = mDatabase.listContacts();
if (allContacts.size() > 0) {
contactView.setVisibility(View.VISIBLE);
ContactAdapter mAdapter = new ContactAdapter(this, allContacts);
contactView.setAdapter(mAdapter);
}
else {
contactView.setVisibility(View.GONE);
Toast.makeText(this, "There is no contact in the database. Start adding now", Toast.LENGTH_LONG).show();
}
Button btnAdd = findViewById(R.id.btnAdd);
btnAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addTaskDialog();
}
});
}
private void addTaskDialog() {
LayoutInflater inflater = LayoutInflater.from(this);
View subView = inflater.inflate(R.layout.add_contacts, null);
final EditText nameField = subView.findViewById(R.id.enterName);
final EditText noField = subView.findViewById(R.id.enterPhoneNum);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Add new CONTACT");
builder.setView(subView);
builder.create();
builder.setPositiveButton("ADD CONTACT", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String name = nameField.getText().toString();
final String ph_no = noField.getText().toString();
if (TextUtils.isEmpty(name)) {
Toast.makeText(MainActivity.this, "Something went wrong. Check your input values", Toast.LENGTH_LONG).show();
}
else {
Contacts newContact = new Contacts(name, ph_no);
mDatabase.addContacts(newContact);
finish();
startActivity(getIntent());
}
}
});
builder.setNegativeButton("CANCEL", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "Task cancelled", Toast.LENGTH_LONG).show();
}
});
builder.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mDatabase != null) {
mDatabase.close();
}
}
}9단계: 매니페스트 설정 (AndroidManifest.xml)
마지막으로 AndroidManifest.xml에 MainActivity를 등록합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="app.com.sample">
<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/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>앱 실행 및 결과 확인
이제 애플리케이션을 실행해 봅시다. 실제 안드로이드 스마트폰을 컴퓨터에 연결했다고 가정합니다. Android Studio에서 프로젝트의 액티비티 파일 중 하나를 연 뒤 툴바의 Run 아이콘을 클릭하고, 실행할 모바일 기기를 선택하세요. 그러면 앱이 설치되고 아래와 같은 기본 화면이 표시됩니다.


ADD 버튼을 눌러 연락처를 추가하면 SQLite 데이터베이스에 저장되고, 목록의 수정·삭제 아이콘을 통해 해당 항목을 업데이트하거나 제거할 수 있습니다. 이렇게 RecyclerView와 SQLite 데이터베이스를 결합하면 대량의 데이터도 효율적으로 화면에 표시하고 관리할 수 있습니다.