Android 开发中使用 Room 快速构建 SQLite 数据库

技术更新换代快,成本温馨提示,请注意文章的更新时间。
本文最后更新于:2021年10月14日

Room 是 Google 提供的一个 ORM (Object Relational Mapping) 库,可以在 Android 开发中快速流畅地进行数据库访问。Room 提供了一个访问 SQLite 的抽象层,通过解析注解内容自动生成对应代码,大大提高了开发的效率。

Room 包含 3 个主要组件:

  • 数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。
  • DAO:包含用于访问数据库的方法。
  • Entity:表示数据库中的表。

room_architecture.png

具体的各个组件的说明请查阅官方文档(地址)。下面将讲解如何使用 Room 快速构建一个数据库并配合 RecyclerView 输出数据库内容,实现一个简单的购物清单。

文中代码重复度较高的地方会进行省略,如果需要完整代码请到文末 GitHub 仓库获取

新建工程

在 Android Studio 中创建一个新工程,这里我选用 Basic Activity 模板,你可以根据自己的需求选不同的模板来创建工程。

引入依赖

在应用或模块的 build.gradle 文件中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dependencies {
// 设置 Room 版本,此处可能不是最新版,请自行选择是否更新到最新版
def room_version = "2.2.5"

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// For Kotlin use kapt instead of annotationProcessor
// 对于 Kotlin 用户请使用 kapt 代替 annotationProcessor

// optional - Kotlin Extensions and Coroutines support for Room
// 可选 - Kotlin 扩展和 Coroutines 支持
implementation "androidx.room:room-ktx:$room_version"

// optional - RxJava support for Room
// 可选 - RxJava 支持
implementation "androidx.room:room-rxjava2:$room_version"

// optional - Guava support for Room, including Optional and ListenableFuture
// 可选 - Guava 支持
implementation "androidx.room:room-guava:$room_version"

// Test helpers
// 测试工具
testImplementation "androidx.room:room-testing:$room_version"
}

添加完成后记得点击右上角的 Sync Now 同步一下依赖。

创建数据库

创建 Entity

新建一个名为 ListItem.java 的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Entity(tableName = "items") // 自定义表名
public class ListItem {
@PrimaryKey(autoGenerate = true) // 注明此元素为主键并自动生成
public int ID;

@ColumnInfo(name = "ItemName") // 自定义列名
public String Name;

@ColumnInfo(name = "ItemNumber", defaultValue = "1") // 设置默认值
public String Number;

@ColumnInfo(name = "ItemStatus", defaultValue = "false")
public boolean Status;

// 需要手动创建一个空的构造函数,否则编译时会报错
public ListItem() {
}

// 这个是为了创建对象时方便的构造函数
public ListItem(String name, String number) {
Name = name;
Number = number;
}

// 必须为所有的列创建 getter 和 setter 以便进行访问
public int getID() {
return ID;
}

public void setID(int ID) {
this.ID = ID;
}

...

}

实现 DAO

DAO(Data Access Object, 中文为数据访问对象) 是一个面向对象的数据库接口,所以我们新建一个 interface 名为 ListItemDAO.java ,实现数据操作的一些定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Dao
public interface ListItemDAO {
@Insert // 插入操作
void insert(ListItem... items); // 对象类后加三个点表示多个对象,添加时使用逗号分隔

@Delete // 删除操作
void delete(ListItem... items);

@Update // 更新操作
void update(ListItem... items);

@Query("SELECT * FROM items") // 查询全部数据,可根据需求添加查询条件
LiveData<List<ListItem>> getAllItems();

}

创建 AppDatabase

这一步我们来创建整个 App 的数据库。新建一个文件名为 AppDatabase.java ,设置其为 abstract 并继承于 RoomDatabase

如果您的应用在单个进程中运行,则在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Database(entities = {ListItem.class}, version = 1, exportSchema = false)
// 注解中设置好需要使用的实体 entities ,多个请用逗号隔开
// 版本号 version 必须要写,以便后期升级迁移数据库
// exportSchema 如果有需求请打开并实现相应内容,如没实现且没关闭,编译将会报错
public abstract class AppDatabase extends RoomDatabase {
// 实现单例模式
private static AppDatabase INSTANCE;

public static synchronized AppDatabase getAppDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "AppDatabase").build();
}
return INSTANCE;
}

// 添加对应的 Dao
public abstract ListItemDAO getListItemDAO();
}

至此,整个数据库已经创建完毕,可以通过 DAO 对数据库进行操作:

1
2
3
4
5
// 插入数据
ListItem example = new ListItem("牙刷", 2);
AppDatabase appDatabase = AppDatabase.getAppDatabase(context);
ListItemDAO listItemDAO = appDatabase.getListItemDAO();
listItemDAO.insert(example);

但是这样操作存在一定问题:

  • 直接调用的话,对数据库的操作在主线程上执行,可能会因数据过大而造成 UI 卡顿
  • 每次都要获取 AppDatabase 实例和 ListItemDAO ,过程繁琐,代码重复

所以我们来新建一个仓库,用来简化代码,并实现多线程来进行数据库操作,避免卡顿。

创建操作仓库

新建名为 ListItemRepository.java 的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ListItemRepository {
private ListItemDAO listItemDAO;

public ListItemRepository(Context context) {
// 获取 AppDatabase 并获取对应 DAO
AppDatabase appDatabase = AppDatabase.getAppDatabase(context);
listItemDAO = appDatabase.getListItemDAO();
}

// LiveData 类型会自动在进行多线程操作,无需手动新建多线程异步操作任务
public LiveData<List<ListItem>> getAllItems() {
return listItemDAO.getAllItems();
}

// 调用多线程异步操作任务
public void addListItem(ListItem... items) {
new insertAsyncTask(listItemDAO).execute(items);
}

...

// 创建多线程异步操作任务
static class insertAsyncTask extends AsyncTask<ListItem, Void, Void> {
private ListItemDAO listItemDAO;

insertAsyncTask(ListItemDAO listItemDAO) {
this.listItemDAO = listItemDAO;
}

@Override
protected Void doInBackground(ListItem... items) {
listItemDAO.insert(items);
return null;
}
}

...

}

现在我们就可以用这样的方式来操作数据库了:

1
2
3
4
5
6
7
8
// 一次创建,多次使用,多线程操作,不会导致 UI 卡顿
ListItem example = new ListItem("牙刷", 2);
ListItemRepository repository = new ListItemRepository(context);
repository.addListItem(example);
repository.addListItem(example);

// 也可以直接新建一个仓库并使用
new ListItemRepository(context).updateListItem(temp);

这样代码写起来就非常方便了,不用再啰啰嗦嗦去获取 DAO 。

创建 RecyclerView

现在我们来正式使用数据库。在布局文件中添加 RecyclerView 并新建对应文件。这里以 fragment_first.xml 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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=".FirstFragment">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

创建布局文件

item_list.xml 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/list_item">

<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:clickable="false"
android:layout_margin="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>

<TextView
android:id="@+id/item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:text="@string/app_name"
android:layout_marginHorizontal="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/checkbox"/>

<TextView
android:id="@+id/item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:text="@string/app_name"
android:layout_marginHorizontal="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

创建 Adapter

ListAdapter.java 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class ListAdapter extends RecyclerView.Adapter<ListAdapter.ItemViewHolder> {

private List<ListItem> mData;

// 创建构造函数,传入数据
public ListAdapter(List<ListItem> mData) {
this.mData = mData;
}

@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ItemViewHolder(LayoutInflater.from(parent.getContext()).inflate(
R.layout.item_list, parent, false));
}

// 重写元素绑定逻辑
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, final int position) {
// 设置元素内容
holder.itemStatus.setChecked(mData.get(position).Status);
holder.itemName.setText(mData.get(position).Name);
String number = "数量:" + mData.get(position).Number;
holder.itemNumber.setText(number);
// 添加单击修改状态事件
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ListItem temp = mData.get(position);
temp.Status = !temp.Status;
new ListItemRepository(v.getContext()).updateListItem(temp);
}
});
// 添加长按删除事件
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
final ListItem temp = mData.get(position);
new ListItemRepository(v.getContext()).deleteListItem(temp);
Snackbar.make(v, "物品已删除", Snackbar.LENGTH_LONG)
.setAction("撤销", new View.OnClickListener() {
@Override
public void onClick(View v) {
new ListItemRepository(v.getContext()).addListItem(temp);
}
}).show();
return true;
}
});
}

@Override
public int getItemCount() {
return mData.size();
}

// 绑定列表项目的各个元素
static class ItemViewHolder extends RecyclerView.ViewHolder {
CheckBox itemStatus;
TextView itemName, itemNumber;
ItemViewHolder(@NonNull View itemView) {
super(itemView);
itemStatus = itemView.findViewById(R.id.checkbox);
itemName = itemView.findViewById(R.id.item_name);
itemNumber = itemView.findViewById(R.id.item_number);
}
}
}

设置 RecyclerView

在对应的位置添加上 RecyclerView 的初始化代码,这里以 FirstFragment.java 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FirstFragment extends Fragment {

private RecyclerView recyclerView;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_first, container, false);
}

public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ListItemRepository repository = new ListItemRepository(view.getContext());
// 绑定 RecyclerView
recyclerView = view.findViewById(R.id.list);
// 设置 RecyclerView 布局管理器
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
// 动态监听 ListItem 的变化并设置 RecyclerView Adapter
repository.getAllItems().observe(getViewLifecycleOwner(), new Observer<List<ListItem>>() {
@Override
public void onChanged(List<ListItem> listItems) {
recyclerView.setAdapter(new ListAdapter(listItems));
}
});
}
}

添加按钮功能

MainActivity 的浮动按钮添加一个弹出对话框的效果,用来添加新物品到数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MainActivity extends AppCompatActivity {

private ListItemRepository repository;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
repository = new ListItemRepository(getApplicationContext());

FloatingActionButton fab = findViewById(R.id.fab);
// 为浮动按钮设置点击监听
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
createNewItemDialog(view).show();
}
});
}

// 创建对话框
private AlertDialog createNewItemDialog(final View view) {
final View dialogView;
LayoutInflater inflater = getLayoutInflater();
AlertDialog.Builder builder = new AlertDialog.Builder(Objects.requireNonNull(view.getContext()));
dialogView = inflater.inflate(R.layout.dialog_layout, null);
builder.setTitle("添加新物品").setView(dialogView);
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
TextInputEditText itemNameInput = dialogView.findViewById(R.id.item_name_input);
TextInputEditText itemNumberInput = dialogView.findViewById(R.id.item_number_input);
String name = Objects.requireNonNull(itemNameInput.getText()).toString();
String number = Objects.requireNonNull(itemNumberInput.getText()).toString();
if (!name.equals("")) {
repository.addListItem(new ListItem(name, number));
Snackbar.make(view, "物品已添加", Snackbar.LENGTH_SHORT).show();
} else {
Snackbar.make(view, "物品名称不能为空", Snackbar.LENGTH_SHORT).show();
}
}
});
builder.setNegativeButton("取消", null);
return builder.create();
}
}

效果展示

布局不太好看,不要介意,看效果就好。

效果图

源码以及 Demo

整个过程的源码已经上传到 GitHub ,项目地址:点这里 ,如有错误或瑕疵,欢迎提出。

如果想试试整个 App 的效果可以到这里下载体验。