Android学习笔记八(JAVA):数据库与Room持久性库,菜单栏,数据绑定
本篇笔记实现如下所示的功能。在NoteListFragment页面增加了菜单栏,菜单栏中有New Note选项,点击它跳转到新建Note页面。输入TITLE和CONTENT后,点击CREATE按钮,会在数据库中添加一条数据。返回NoteListFragment,可以看见这条数据。
1.实现菜单栏
先来一个菜单栏的布局文件,在res文件夹处右键,New->Android Resource File,在Resource type中选择Menu,File name为fragment_note_list.xml。可见,在res下面多了一个menu文件夹。现在我们有两个名为fragment_note_list的布局文件,不过没问题,它们在不同的文件夹中。strings.xml中新增了一个字符串:
<string name="btn_create_note">Create</string>
menu/fragment_note_list.xml代码清单:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/new_note" android:title="@string/new_note_menu" app:showAsAction="withText"/> </menu>
注意showAsAction的属性现在给的是“withText"。
现在回到NoteListFragment.java文件,在这里把刚才新建的菜单栏布局添加进去。《Android权威编程指南》(不管是第四版还是第五版)里面用的都是在onCreate()方法中添加setHasOptionsMenu(true)和重写onCreateOptionsMenu()的方式添加菜单栏。但是这种方式已经显示为废弃了,如下图所示。嗯,就是这样,本来用的很习惯的方法,突然间就废弃了。不过废弃也表示进步,肯定是有更好的方法了。(在此想表达,有问题一定要以官方文档为主,也要实时关注官方文档)
根据文档,现在可以使用MenuProvider来给activity提供Menu。来看看这个MenuProvider是啥。哦,原来是个接口。
那就让NoteListFragment.java实现MenuProvider接口吧:
public class NoteListFragment extends Fragment implements MenuProvider{...}
这时会有提示,实现MenuProvider接口需要实现onCreateMenu(Menu,MenuInflater)方法。可以按照提示,按下alt+enter,Android Studio会自动填充这个方法。(如果发现IDE提示没有MenuProvider,并且import也不行的时候。看看gradle文件,要更新一下androidx.appcompat:appcompat的版本,因为好像是在1.4版本之后ComponentActivity才实现了MenuHost接口)
@Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { menuInflater.inflate(R.menu.fragment_note_list,menu); } @Override public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { switch (menuItem.getItemId()){ case R.id.new_note: jumpToCreateNote(); return true; default: return true; } }
jumpToCreateNote()就是要跳转到新建Note的Activity。现在我们还没有办法实现这个方法,因为还没有新建Note的Activity呢。那么现在可以先把jumpToCreateNote()注释掉,Run app,看看屏幕上是不是已经有菜单栏了呢。答案是没有,因为还没有addMenuProvider()。Holy crap!
回到官方文档,看看咋回事。哦,想要添加菜单栏,需要让MenuHost托管MenuProvider,而Fragment并没有实现MenuHost接口,Component Activity才实现了MenuHost接口。OK了,在NoteListFragment.java的onCreateView()中让Fragment的托管Activity托管MenuProvider就可以了。
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ super.onCreateView(inflater,container,savedInstanceState); View view=inflater.inflate(R.layout.fragment_note_list,container,false); ActionBar actionBar=((AppCompatActivity) getActivity()).getSupportActionBar(); actionBar.setTitle(TAG);
getActivity().addMenuProvider(this);
mNoteRecyclerView=(RecyclerView) view.findViewById(R.id.note_recycler_view); mNoteRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); return view; }
现在Run app,再看看,哇,菜单栏出现了。还记得吗,menu/fragment_note_list.xml中showAsAction属性值是“withText",现在把它换成"ifRoom"和”ifRoom|withText"再看看菜单栏是什么样的。
2.使用数据绑定实现CreateNoteActivity
笔记三中提到了视图绑定,笔记五中提到了如何创建并启动新的Activity。现在来看一下数据绑定(Data Binding)(ps:本人之前用WPF的时候,真的超爱数据绑定,尤其是双向数据绑定,不要太方便哦,个人认为,双向数据绑定是MVVM最大的特点)。与使用视图绑定一样,使用数据绑定,需要先在build.gradle(module)中添加:
dataBinding{
enabled=true
}
新建一个名为CreateNoteActivity的empty activity。所谓数据绑定,简单点理解就是把模型的数据直接绑定在视图布局上,像下面这样。<layout>节点下面有一个<data>子节点,指定了视图布局中需要的数据的名称(note)和类型(Note类)。<LinearLayout>节点也放在<layout>节点下。其中的两个<EditText>子节点中的android:text属性值分别绑定为Note类型的title属性和content属性(虽然我们在Note.java中用getter方法获取title和content的值的,但是这里可以不用写note.getTitle(),直接用note.title就好)。activity_create_note.xml中需要的这个note与CreateNoteActivity.java中的一个note是绑定的关系。这样的话,CreateNoteActivity.java中的那个note的title和content值就可以给到activity_create_note.xml了。
activity_create_note.xml代码清单:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="note" type="com.larissa.android.note.Note"/> </data> <LinearLayout android:orientation="vertical" android:layout_margin="16dp" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textStyle="bold" android:text="@string/title_label"/> <EditText android:id="@+id/note_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Enter a title of the note" android:text="@{note.title}"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/content_label" android:textStyle="bold"/> <EditText android:id="@+id/note_content" android:layout_width="match_parent" android:layout_height="260dp" android:text="@{note.content}" android:inputType="textMultiLine" /> <Button android:id="@+id/btn_create_note" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/btn_create_note"/> </LinearLayout> </layout>
下面就在CreateNoteActivity.java中指定一个note,与activity_create_note.xml中的绑定。CreateNoteActivity.java代码清单:
public class CreateNoteActivity extends AppCompatActivity { private static final String TAG="CreateNoteActivity";private ActivityCreateNoteBinding mActivityCreateNoteBinding; private Note mNote; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mActivityCreateNoteBinding=ActivityCreateNoteBinding.inflate(getLayoutInflater()); setContentView(mActivityCreateNoteBinding.getRoot()); mNote=new Note(); mNote.setTitle("Data Binding"); mNote.setContent("implements data binding."); mActivityCreateNoteBinding.setNote(mNote); } }
在CreateNoteActivity.java中添加了mNote,并通过setNote()方法把它绑定给了activity_create_note.xml视图。为了验证数据绑定,我们先把CreateNoteActivity设置为主Activity。AndroidManifest.xml代码清单如下:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.larissa.android.note"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Note" tools:targetApi="31"> <activity android:name=".CreateNoteActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MainActivity" android:exported="false"> </activity> </application> </manifest>
Run app,应该会出现下方界面了。CreateNoteActivity.java中的mNote的属性值在视图中显示出来了。
数据绑定最大的作用是双向绑定,比如<EditText>是可编辑的Text,用户对于视图中的EditText的Text属性值的编辑,需要能被Activity知道才行。双向绑定只需要把XML文件中的@{}改成@={}就可以了。activity_create_note.xml代码清单:

<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="note" type="com.larissa.android.note.Note"/> </data> <LinearLayout android:orientation="vertical" android:layout_margin="16dp" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textStyle="bold" android:text="@string/title_label"/> <EditText android:id="@+id/note_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Enter a title of the note" android:text="@={note.title}"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/content_label" android:textStyle="bold"/> <EditText android:id="@+id/note_content" android:layout_width="match_parent" android:layout_height="260dp" android:text="@={note.content}" android:inputType="textMultiLine" /> <Button android:id="@+id/btn_create_note" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/btn_create_note"/> </LinearLayout> </layout>
完善CreateNoteActivity,我们给它配个ViewModel,以备后用。然后再实现从NoteListFragment中跳转到CreateNoteActivity。新建CreateNoteViewModel.java文件,继承ViewModel类。在NoteListFragment.java中实现jumpToCreateNote()方法。
private void jumpToCreateNote(){ Intent intent=new Intent(getActivity(),CreateNoteActivity.class); startActivity(intent); }
别忘了把AndroidManifest.xml中的主Activity改回来。Run app,看看是不是能从NoteListFragment的菜单栏跳转到CreateNoteActivity了。
3.数据库与Room库
数据库用于存储note数据。Android使用SQLite数据库。这是一款小巧的嵌入式数据库,使用方便、开发简单。如MySQL和SQLServer一样,它们都使用SQL语言(Structured Query Language,结构化查询语言),也许每种数据库的SQL语法会有少许不同,不过大部分都差不多。而且SQL语句很好懂。如果之前完全没有使用过SQL语言的经验,可以先参见我之前的文章:https://www.cnblogs.com/larissa-0464/category/1989319.html
比起直接使用SQLite API,Android Jetpack提供了Room持久性库,它在SQLite上提供了一个抽象层,在充分利用SQLite的强大功能的同时,能够流畅地访问数据库。具体来说,Room具有以下优势:
- 针对SQL查询的编译时验证
- 可最大限度减少重复和容易出错的样板代码的方便注解
- 简化了数据库迁移路径
如果有.NET CORE相关开发经验的话,我感觉Room就相当于是EntityFrameworkCore(EFcore),起到了ORM(object-relational mapping,对象关系映射)的作用。
使用Room,需要添加相应的依赖,在build.gradle(module)中的dependencies节点中输入下面的代码:
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
Room有三个主要的组件:
- 数据实体,用于表示应用的数据库中的表;
- 数据访问对象(DAO, data access object),提供CRUD方法(create, retrieve, update, delete)。
- 数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点;
现在开始吧,添加数据库。
第一步,把Note类变成数据实体。很简单,给它加个注解:@Entity。如果表名不想用Note的话,就指定一个:@Entity(tableName=your table name)。表需要一个主键(primary key),一般都是id当主键。此外,用@ColumnInfo(name=*)把column的名称改了。变身后的Note.java代码清单如下:

package com.larissa.android.note; import androidx.annotation.NonNull; import androidx.databinding.BaseObservable; import androidx.databinding.Bindable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; import java.util.Date; import java.util.UUID; @Entity public class Note extends BaseObservable { @NonNull @PrimaryKey @ColumnInfo(name="id") private UUID mId; @ColumnInfo(name="title") private String mTitle; @ColumnInfo(name="content") private String mContent; @ColumnInfo(name="date") private Date mDate; @ColumnInfo(name="status") private String mStatus; public Date getDate() { return mDate; } public void setDate(Date date) { mDate = date; } public String getStatus() { return mStatus; } public void setStatus(String status) { mStatus = status; } public String getNoteLog(){ String noteLog=String.format("%s at %s",mStatus,mDate); return noteLog; } public void setNoteLog(String status){ mDate=new Date(); mStatus=status; } public UUID getId() { return mId; } public void setId(UUID id) { mId = id; } @Bindable public String getTitle() { return mTitle; } public void setTitle(String title) { mTitle = title; notifyPropertyChanged(BR.title); } @Bindable public String getContent() { return mContent; } public void setContent(String content) { mContent = content; notifyPropertyChanged(BR.content); } }
更多地注解请点击(例如忽略一个field,双主键等):https://developer.android.google.cn/training/data-storage/room/defining-data?hl=zh-cn
第二步,定义Note类的数据访问对象。先新建一个名为database的package。在database中,新建名为NoteDao的接口。NoteDao.java代码清单:

package com.larissa.android.note.database; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import com.larissa.android.note.Note; import java.util.List; @Dao public interface NoteDao { @Query("SELECT * FROM Note") LiveData<List<Note>>getAllNotes(); @Insert void addNote(Note note); @Update void updateNote(Note note); }
用@Dao注解表明这是一个DAO接口。另外目前NoteDao中定义了CRUD中的C,R,U,还没有D。更多请点击:https://developer.android.google.cn/training/data-storage/room/accessing-data?hl=zh-cn
第三步,实现数据库类。不过在此之前,先来创建一个类型转换器。为啥呢,因为Note中的UUID和Date类型,SQL的数据库不认识阿,所以要把这两个JAVA才有的类型转成数据库认识的String和Long。在database中新建NoteTypeConverters.java。代码清单如下:

package com.larissa.android.note.database; import androidx.room.TypeConverter; import java.util.Date; import java.util.UUID; public class NoteTypeConverters { @TypeConverter public Long fromDate(Date date){ return date.getTime(); } @TypeConverter public Date toDate(Long millisSinceEpoch){ return new Date(millisSinceEpoch); } @TypeConverter public String fromUUID(UUID uuid){ return uuid.toString(); } @TypeConverter public UUID toUUID(String uuid){ return UUID.fromString(uuid); } }
新建NoteDatabase.java文件,代码清单如下:

package com.larissa.android.note.database; import androidx.room.Database; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; import com.larissa.android.note.Note; @Database(entities = {Note.class},version=1,exportSchema = false) @TypeConverters({NoteTypeConverters.class}) public abstract class NoteDatabase extends RoomDatabase { public abstract NoteDao noteDao(); }
它需要添加@Database注解,并继承RoomDatabase类。
OK,数据库弄好了。下一步就是使用它了。还记得吗,笔记六中生命周期感知型组件那里提到,ViewModel类不负责获取数据,它调用相应的组件来获取数据。一般干这个活的是repository(仓储)。可以这样理解,repository从database中获取数据,不同的viewmodel都过来问repository要数据和更改数据。按照这个需求分析,每个viewmodel并不需要自己实例化一个repository,它们可以share同一个repository。那么repository就可以实现为单例模式了。(更多关于单例模式的内容,请自行百度。另外乍一看单例模式跟静态类有点像,不过还是不同的,它们之间的区别也请自行百度)。
还需要注意的是,为防止查询阻止界面,Room不允许在主线程上访问数据库。线程是一个单一执行序列。单个线程中的代码会逐步执行。所有Android应用的运行都是从主线程开始的。然而,主线程并不是像线程那样的预定执行序列。它处于一个无限循环的运行状态,等着用户或系统触发事件。一旦有事件触发,主线程便执行代码做出响应。主线程上运行着所有更新UI的代码,其中包括响应activity的启动、按钮的点击等不同UI相关事件的代码。事件处理循环让主线程总是按顺序执行。这样,事件就能一件件处理,同时代码也能快速执行,及时响应。目前,我们的代码都在主线程中执行。但是读取数据库是很费时的,并不能立即完成。如果查询数据库在主线程上运行,那么在此期间,除了等待,啥也干不了。因此,Room不允许在主线程上执行任何数据库操作,而是由后台线程完成,后台线程查询完数据库,再告知主线程。这就是异步操作。
具体到Android上,使用JAVA语言的情况下,可以使用RxJava框架的专用返回类型编写异步DAO方法,或者用Jetpack库中的LiveData封装容器类编写异步可观察查询。那么写入数据库呢,可以用Executor,相当于创建一个新线程,在新线程上面运行指定的任务。了解了之后,那就开始吧。
新建NoteRepository.java文件。这里面要有一个数据库类对象,一个DAO对象,一个用于写入数据库的executor。然后它还要是单例模式。NoteRepository.java代码清单:

package com.larissa.android.note; import android.content.Context; import androidx.lifecycle.LiveData; import androidx.room.Room; import com.larissa.android.note.database.NoteDao; import com.larissa.android.note.database.NoteDatabase; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; public class NoteRepository { private static final String DATABASE_NAME="note-database"; private static NoteRepository sNoteRepository; private NoteDatabase noteDatabase; private NoteDao noteDao; private Executor executor= Executors.newSingleThreadExecutor(); private NoteRepository(Context context){ noteDatabase= Room.databaseBuilder(context,NoteDatabase.class,DATABASE_NAME).build(); noteDao=noteDatabase.noteDao(); } public static NoteRepository getInstance(){ if(sNoteRepository==null) throw new IllegalStateException("NoteRepository must be initialized"); return sNoteRepository; } public static void initialize(Context context){ if(sNoteRepository==null) sNoteRepository=new NoteRepository(context); } public LiveData<List<Note>>getAllNotes(){ return noteDao.getAllNotes(); } public void updateNote(Note note){ executor.execute(new Runnable() { @Override public void run() { noteDao.updateNote(note); } }); } public void addNote(Note note){ executor.execute(new Runnable() { @Override public void run() { noteDao.addNote(note); } }); } }
其中,下面这些代码用于初始化NoteRepository。NoteRepository类初始化的时候,使用Room.databaseBuilder().build()方法创建数据库实例。
public class NoteRepository {private NoteDatabase noteDatabase; private NoteDao noteDao;private NoteRepository(Context context){ noteDatabase= Room.databaseBuilder(context,NoteDatabase.class,DATABASE_NAME).build(); noteDao=noteDatabase.noteDao(); } }
下面这些则是用来实现单例模式的。这样ViewModel使用NoteRepository的时候,只需要用它的getInstance()静态方法就好了。不过前提是需要先调用initialize()静态方法初始化NoteRepository。
public class NoteRepository {private static NoteRepository sNoteRepository;public static NoteRepository getInstance(){ if(sNoteRepository==null) throw new IllegalStateException("NoteRepository must be initialized"); return sNoteRepository; } public static void initialize(Context context){ if(sNoteRepository==null) sNoteRepository=new NoteRepository(context); } }
下面这些是用来封装数据库DAO的。可见,对于查询操作,返回的结果是LiveData<List<Note>>类的。对于插入和更新操作,使用executor的execute方法完成的。executor则是从Executors工厂中选择了newSingleThreadExecutor()。
public class NoteRepository {private Executor executor= Executors.newSingleThreadExecutor(); public LiveData<List<Note>>getAllNotes(){ return noteDao.getAllNotes(); } public void updateNote(Note note){ executor.execute(new Runnable() { @Override public void run() { noteDao.updateNote(note); } }); } public void addNote(Note note){ executor.execute(new Runnable() { @Override public void run() { noteDao.addNote(note); } }); } }
弄好了repository,下一步就应该更新ViewModel了。NoteListViewModel.java代码清单:
public class NoteListViewModel extends ViewModel { private static final String TAG="NoteListViewModel"; private NoteRepository noteRepository; List<Note> mNotes=new ArrayList<>(); LiveData<List<Note>>notesLiveData; public NoteListViewModel(){ for(int i=0;i<50;i++){ Note note=new Note(); note.setTitle(String.format("Note No.%s",i)); if(i%2==0) note.setNoteLog("Created"); else note.setNoteLog("Modified"); note.setContent(String.format("This is the content of Note No.%s",i)); mNotes.add(note); } noteRepository=NoteRepository.getInstance(); notesLiveData= noteRepository.getAllNotes(); } }
CreateNoteViewModel.java代码清单:
public class CreateNoteViewModel extends ViewModel { private NoteRepository noteRepository=NoteRepository.getInstance(); public void createNote(Note note){ note.setDate(new Date()); note.setStatus("Created"); note.setId(UUID.randomUUID()); noteRepository.addNote(note); } }
增加了创建Note的方法createNote(),现在把这个方法绑定给activity_create_note.xml中的CREATE按钮。CreateNoteActivity.java代码清单:
public class CreateNoteActivity extends AppCompatActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { ... mActivityCreateNoteBinding.btnCreateNote.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mCreateNoteViewModel.createNote(mNote); } }); } }
OK,run app,看看是不是大功告成啦。其实这里还有个bug,在NoteListFragment页面导航到NoteFragment页面,再按返回键,返回到NoteListFragment页面后,菜单栏上出现了两个New Node。Holy crap。没办法阿,因为每次调用onCreateView()的时候都添加了addMenuProvider(this)。再看一下官方文档,哦,MenuHost里面还有个removeMenuProvider()的方法。哦了,把这个方法放在onDestroyView()里。这回再试试呢~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端