xMuu's Blog

哈,你也在这里。

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

xMuu's Avatar 2020-04-14

  1. 1. 新建工程
  2. 2. 引入依赖
  3. 3. 创建数据库
    1. 3.1. 创建 Entity
    2. 3.2. 实现 DAO
    3. 3.3. 创建 AppDatabase
    4. 3.4. 创建操作仓库
  4. 4. 创建 RecyclerView
    1. 4.1. 创建布局文件
    2. 4.2. 创建 Adapter
    3. 4.3. 设置 RecyclerView
  5. 5. 添加按钮功能
  6. 6. 效果展示
  7. 7. 源码以及 Demo

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
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 的效果可以到这里下载体验。

本文作者 : xMuu
本文采用 CC BY-NC-SA 4.0 许可协议。转载和引用时请注意遵守协议!
本文链接 : https://xmuu.github.io/2020/04/14/android-using-room-to-build-a-sqlite-database-quickly/

本文最后更新于 天前,文中所描述的信息可能已发生改变