目录
什么是Jetpack?
Jetpack 是Google官方推出的一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助你遵循最佳做法、让你摆脱编写样板代码的工作并简化复杂任务,以便你将精力集中放在所需的代码上。
为什么要用Jetpack?
关于为什么要用Jetpack,我参考了许多的博客和官方文档,开阔了我对Android生态圈的理解和认识,在Jetpack推出前出现的许许多多强大的第三方框架与语言,典型代表无疑是强大的RxJava
在Jetpack仍然有许多粉丝在一些功能在用它来替代Jetpack中的一些组件。
比如Jetpack衍生出的目前十分火热的MVVM
架构(下方记事本的编写中将采用,你可以在代码耦合性中体会到它与传统MVC架构或之后出现MVP架构之间的不同),其中核心组件LiveData其实在RxJava中早已经可以实现,还有需要使用Jetpack嘛?
答案是肯定的,大量的第三方框架与应用很多时候并不是最佳实现,尤其在Google对Android进行不断更新改进的情况下。Android Jetpack
于谷歌而言,这是他们重新整理和统一安卓生态环境决心的体现,Android Jetpack
所展现的内容,也是谷歌想拓展和维护的方向。于长期苦恼于第三方库选择的广大Android开发者而言,这是谷歌为我们提供的一盏明灯。
JetPack官方架构模式
以下是Google官方推荐的架构模式
使用此架构能带来什么好处?
- UI和业务逻辑解耦。
- 有效避免生命周期组件内存泄漏。
- 提高模块可测试性。
- 提高应用稳定性,有效降低以下异常发生概率。
针对Jetpack架构中繁多的组件,下面我具体介绍一款数据库交互组件Room。其余组件在Demo中使用也会做相应说明
什么是Room?为什么要使用Room?
Room是Google提供的一个ORM库。
Room持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。
Room组件架构体系
Entity,Dao,Database为Room的3大基本组件,不同组件之间的关系如图
Database:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点
使用方法:用@Database来注解类,且使用 @Database注释的类应满足以下条件
- 是扩展RoomDatabase的抽象类。
- 在注释中添加与数据库关联的实体列表。
- 包含具有 0 个参数且返回使用 @Dao 注释的类的抽象方法。
在运行时,可以通过调用 Room.databaseBuilder()
或 Room.inMemoryDatabaseBuilder()
获取 Database
的实例。
Entity:表示数据库中的表
使用方法:用@Entit来注解实体类。
Dao:提供访问数据库的方法
使用方法:@Dao用来注解一个接口或者抽象方法。
记事本应用讲解
Room作为JetPack架构组件中关于SQLite数据库的架构组件,对应有着自己的知识体系。下面通过记事本Demo对Room本身组件结合MVVM架构所涉及的知识体系做一个总结.
由于涉及到另外一些组件,在有使用到时会做简要介绍.
记事本Demo效果图:
1.编写Room数据库
1.1 编写数据库实体类
@Data
@Entity(tableName = "note")
public class Note implements Serializable {
@PrimaryKey(autoGenerate = true)
private int id;
@ColumnInfo(name = "title")
private String title;
@ColumnInfo(name = "content")
private String content;
@ColumnInfo(name = "last_update_time")
private Date lastUpdateTime;
}
1.2 编写数据库管理类Database
注意:因为实体类中存在复杂数据类型——时间类。所以在数据库管理中需要使用
@TypeConverters
注入转换类,对复杂类型进行统一的转换处理
@Database(entities = {Note.class},version = 1,exportSchema = false)
@TypeConverters({Converters.class})
public abstract class NoteDatabase extends RoomDatabase {
private static NoteDatabase INSTANCE;
public synchronized static NoteDatabase getINSTANCE(Context context) {
if (INSTANCE==null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),NoteDatabase.class,"note_datebase")
.fallbackToDestructiveMigration()
.build();
}
return INSTANCE;
}
/**
* 在@Database中 多个entity则写多个Dao
*/
public abstract NoteDao getNoteDao();
}
1.3 编写数据库操作接口Dao
@Dao
public interface NoteDao {
@Insert
void insertNotes(Note... notes);
@Update
void updateNotes(Note... notes);
@Delete
void deleteNotes(Note... notes);
@Query("delete from note")
void deleteAllNotes();
@Query("select * from note order by last_update_time desc")
LiveData<List<Note>> queryAllNotes();
@Query("select * from note where content like :pattern order by last_update_time desc")
LiveData<List<Note>> queryNotesWithPattern(String pattern);
}
2.编写数据仓库(Repository、AsyncTask)
如果此处不引入Repository类会造成什么影响呢?为什么要引入Repository?
对数据库的操作(调用Dao)逻辑将放于ViewModel中(步骤5)。使得ViewModel中代码变得杂乱
引入仓库类,用于对数据库操作并对ViewModel暴露方法。让ViewModel专注于数据处理而非对数据库的调用,对ViewModel和Dao进一步解耦。
什么是AsyncTask?为什么需要引入AsyncTask?
- 一个
Android
已封装好的轻量级异步类,用于实现多线程、异步通信、消息传递 - 数据库的操作很重,一次读写操作花费 10~20ms 是很常见的,这样的耗时很容易造成界面的卡顿。所以通常情况下,条件允许情况下要避免在主线程中处理数据库。
public class NoteRepository {
private NoteDao noteDao;
private LiveData<List<Note>> allNoteLive;
public NoteRepository(Context context){
NoteDatabase database = NoteDatabase.getINSTANCE(context);
noteDao = database.getNoteDao();
allNoteLive = noteDao.queryAllNotes();
}
public LiveData<List<Note>> getAllWordLive() {
return allNoteLive;
}
public LiveData<List<Note>> queryNotesWithPattern(String pattern){
//模糊匹配注意百分号
return noteDao.queryNotesWithPattern("%"+pattern+"%");
}
public void insertNotes(Note... notes){
new InsertAsyncTask(noteDao).execute(notes);
}
public void updateNotes(Note... notes){
new UpdateAsyncTask(noteDao).execute(notes);
}
public void deleteNotes(Note... notes){
new DeleteAsyncTask(noteDao).execute(notes);
}
public void deleteAllNotes(){
new DeleteAllAsyncTask(noteDao).execute();
}
//创建副线程类,继承AsyncTask实现
static class InsertAsyncTask extends AsyncTask<Note, Void, Void>{
private NoteDao noteDao;
InsertAsyncTask(NoteDao noteDao) {
this.noteDao = noteDao;
}
@Override
protected Void doInBackground(Note... notes) {
noteDao.insertNotes(notes);
return null;
}
}
static class UpdateAsyncTask extends AsyncTask<Note, Void, Void>{
private NoteDao noteDao;
UpdateAsyncTask(NoteDao noteDao) {
this.noteDao = noteDao;
}
@Override
protected Void doInBackground(Note... notes) {
noteDao.updateNotes(notes);
return null;
}
}
static class DeleteAllAsyncTask extends AsyncTask<Note, Void, Void>{
private NoteDao noteDao;
DeleteAllAsyncTask(NoteDao noteDao) {
this.noteDao = noteDao;
}
@Override
protected Void doInBackground(Note... notes) {
noteDao.deleteAllNotes();
return null;
}
}
static class DeleteAsyncTask extends AsyncTask<Note, Void, Void>{
private NoteDao noteDao;
DeleteAsyncTask(NoteDao noteDao) {
this.noteDao = noteDao;
}
@Override
protected Void doInBackground(Note... notes) {
noteDao.deleteNotes(notes);
return null;
}
}
}
3.编写ViewModel+LiveData
为什么要使用ViewModel?
ViewModel
是数据与 UI 分离的中间层,提供了一个将数据转换为 UI 友好型数据的场所。其次,它也提供了多 Fragment
复用相同 ViewModel
的机制。
为什么要使用LiveData?
LiveData 是一个可以感知 Activity 、Fragment生命周期的数据容器。当 LiveData 所持有的数据改变时,它会通知相应的界面代码进行更新。
此处为了方便,数据和界面的交互放在了Activity中,读者有需要可以使用Databinding对Activity进行进一步解耦.
public class NoteViewModel extends AndroidViewModel {
/**
* 使用数据仓库处理好的数据库交互逻辑
*/
private NoteRepository repository;
public NoteViewModel(@NonNull Application application) {
super(application);
repository = new NoteRepository(application);
}
public LiveData<List<Note>> getAllNoteLive() {
return repository.getAllWordLive();
}
public LiveData<List<Note>> queryNotesWithPattern(String pattern){
return repository.queryNotesWithPattern(pattern);
}
public void insertNotes(Note... notes){
repository.insertNotes(notes);
}
public void updateNotes(Note... notes){
repository.updateNotes(notes);
}
public void deleteNotes(Note... notes){
repository.deleteNotes(notes);
}
public void deleteAllNotes(){
repository.deleteAllNotes();
}
}
4.编写界面
界面引入了RecyclerView,代替了传统ListView。如果没有使用过RecyclerView可以参照
其中涉及到矢量图的使用,如果没有使用过矢量图可以参照
4.1 编写主页的Fragment
fragment_notes.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:id="@+id/mainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".base.NotesFragment">
<!-- TODO: Update blank fragment layout -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_margin="16dp"
android:clickable="true"
app:srcCompat="@drawable/ic_add_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
cell_card.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginRight="8dp"
android:foreground="?selectableItemBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.85" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline1"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_keyboard_arrow_right_black_24dp" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:text="TextView"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/textView_time"
app:layout_constraintEnd_toStartOf="@+id/guideline1"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline1"
app:layout_constraintStart_toStartOf="@+id/textView_title"
app:layout_constraintTop_toBottomOf="@+id/textView_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
4.2 编写主界面映射Fragment界面
为什么要这样处理?
在fragment中编写好界面后,只需要在main_activity
将界面映射过来,之后页面的切换与数据传递交由Navigation作为导航管理Fragment。期间你的路径与与数据设置只需在可视化中简单操作即可。在main_activity
只需要做很少的处理即可
main_activity.xml
<?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=".MainActivity">
<fragment
android:id="@+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
5.编写RecyclerView的适配器
public class MyAdapt extends ListAdapter<Note, MyAdapt.MyViewHolder> {
public MyAdapt() {
super(new DiffUtil.ItemCallback<Note>() {
@Override
public boolean areItemsTheSame(@NonNull Note oldItem, @NonNull Note newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Note oldItem, @NonNull Note newItem) {
return oldItem.getContent().equals(newItem.getContent()) &&
oldItem.getLastUpdateTime().equals(newItem.getLastUpdateTime());
}
});
}
/**
* 在适配器中创建 ViewHolder,选择item注入
*/
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View itemView = layoutInflater.inflate(R.layout.cell_card, parent, false);
return new MyViewHolder(itemView);
}
/**
* 对每条item进行数据绑定
* 经常被呼叫,每次滚入滚出都会调用,所以监听绑定放入onCreateViewHolder中
*/
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
Note note = getItem(position);
holder.textView_title.setText(note.getTitle());
//对日期格式化再输出
@SuppressLint("SimpleDateFormat") SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
holder.textView_time.setText(simpleDateFormat.format(note.getLastUpdateTime()));
holder.itemView.setOnClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putSerializable("note", note);
//传递参数
NavController navController = Navigation.findNavController(v);
navController.navigate(R.id.action_notesFragment_to_addFragment, bundle);
});
}
/**
* 自定义 holder对应 item
* 内部类最好使用static修饰,防止内存泄漏
*/
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView_title, textView_time;
MyViewHolder(@NonNull View itemView) {
super(itemView);
textView_title = itemView.findViewById(R.id.textView_title);
textView_time = itemView.findViewById(R.id.textView_time);
}
}
}
6.编写Fragment(对不同Fragment功能进行简要介绍)
6.1 编写主界面Fragment逻辑
主界面一些逻辑处理比较复杂,涉及到一些功能如
搜索
、清空
、数据观察
、观察移除
(数据观察这块需要注意传入的环境,博主之前没传好出现一些比较奇怪的bug,注释有标明)。包括扩展功能如:撤销删除
、滑动删除
、矢量图定点绘制
(需要有一定的图形代码编写基础)
public class NotesFragment extends Fragment {
//final String TAG = "mainTag";
//视图层
private NoteViewModel noteViewModel;
private RecyclerView recyclerView;
private MyAdapt myAdapt;
//数据层
private LiveData<List<Note>> noteLive;
private FragmentActivity fragmentActivity;
//操作标识,只有更新时候才上移。更新删除保持不动
private boolean undoAction;
/**
* 实时保存数据列表,防止通过liveData时直接获取元素时因为异步获取,发生空指针异常
* 主要用于标记滑动删除中的撤销
*/
private List<Note> allNotes;
public NotesFragment() {
// 显示菜单栏目
setHasOptionsMenu(true);
}
/**
* 当复合的选项菜单被选中,其监听在此处处理。如:清空数据功能
*/
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
//多个选项菜单,根据不同菜单项的R.id进行匹配操作
if (item.getItemId() == R.id.clear_data) {//清空数据前需要弹窗确认
AlertDialog.Builder builder = new AlertDialog.Builder(fragmentActivity);
builder.setTitle("清空数据");
builder.setPositiveButton("确定", (dialog, which) -> noteViewModel.deleteAllNotes());
builder.setNegativeButton("取消", (dialog, which) -> {
});
builder.create();
builder.show();
}
return super.onOptionsItemSelected(item);
}
/**
* 初始化菜单栏,并实现显式菜单项功能show
*/
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_menu, menu);
//搜索
SearchView searchView = (SearchView) menu.findItem(R.id.app_bar_search).getActionView();
//控制搜索框长度
int maxWidth = searchView.getMaxWidth();
searchView.setMaxWidth((int) (0.5 * maxWidth));
//设置搜索框的实时监听
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
//去除多余前后空格
String pattern = newText.trim();
noteLive = noteViewModel.queryNotesWithPattern(pattern);
/*
注意:重新赋予LiveData后最好先移除之前的观察。
大坑:观察的移除和注入都必须是getViewLifecycleOwner获取的LifecycleOwner。其对应fragment的生命周期
*/
noteLive.removeObservers(getViewLifecycleOwner());
//对LiveData重新进行观察,注意Owner的生命周期,需要注入fragment的owner
noteLive.observe(getViewLifecycleOwner(), notes -> {
//备份列表
allNotes = notes;
//将观察的数据注入RecycleAdapt中
myAdapt.submitList(notes);
});
//修改为返回true后事件不会再向下传递,默认false会继续传递
return true;
}
});
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_notes, container, false);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
fragmentActivity = requireActivity();
//初始化当前页面所用ViewModel,注入activity
noteViewModel = new ViewModelProvider(fragmentActivity).get(NoteViewModel.class);
//初始化recyclerView
recyclerView = fragmentActivity.findViewById(R.id.recyclerView);
myAdapt = new MyAdapt();
//recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));//大坑
recyclerView.setLayoutManager(new LinearLayoutManager(fragmentActivity));//大坑,不设置布局不显示
recyclerView.setAdapter(myAdapt);
//观察数据列表
noteLive = noteViewModel.getAllNoteLive();
//需要注入fragment的owner
noteLive.observe(getViewLifecycleOwner(), notes -> {
//Log.d(TAG, "onChanged: " + notes);
//读取当前显示列表的个数
int temp = myAdapt.getItemCount();
//备份列表
allNotes = notes;
//如果数据变化后的元素 > 变化前的个数 说明是添加操作,进行
if (notes.size() > temp && !undoAction) {
/*
滚动到首部,增强视觉效果
注意定时任务,否则太快会定位到第二行
*/
new Timer().schedule(new TimerTask() {
public void run() {
recyclerView.smoothScrollToPosition(0);
}
}, 300);
}
//如果是撤销删除任务调用观察,撤销后需要恢复undoAction状态
if (undoAction) {
undoAction = false;
}
//将观察的数据注入RecycleAdapt中
myAdapt.submitList(notes);
});
//初始化floatingActionButton浮动按钮
FloatingActionButton floatingActionButton = fragmentActivity.findViewById(R.id.floatingActionButton);
floatingActionButton.setOnClickListener(v -> {
NavController navController = Navigation.findNavController(v);
navController.navigate(R.id.action_notesFragment_to_addFragment);
});
//滑动删除
/*
参数简介:
@param dragDirs Binary OR of direction flags in which the Views can be dragged.
(上下拖动 ItemTouchHelper.UP | ItemTouchHelper.DOWN)
@param swipeDirs Binary OR of direction flags in which the Views can be swiped.
(左右滑动 ItemTouchHelper.START | ItemTouchHelper.END)
*/
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
//处理上下拖动
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
//处理左右滑动
//通过提前备份note处理空指针异常,通过viewHolder.getAdapterPosition()定位元素位置
Note deleteNote = allNotes.get(viewHolder.getAdapterPosition());
//删除滑出元素
noteViewModel.deleteNotes(deleteNote);
//提供撤销操作,此处需要更改fragment中的布局为CoordinatorLayout,否则会被浮动按钮遮挡
Snackbar.make(fragmentActivity.findViewById(R.id.mainFragment), "删除了一个笔记", Snackbar.LENGTH_SHORT)
.setAction("撤销", v -> {
//与添加进行区分,防止添加的上移动作混入
undoAction = true;
//添加回删除的元素
noteViewModel.insertNotes(deleteNote);
}).show();
}
//在滑动的时候,画出浅灰色背景和垃圾桶图标,增强删除的视觉效果
Drawable icon = ContextCompat.getDrawable(requireActivity(), R.drawable.ic_delete_black_24dp);
Drawable background = new ColorDrawable(Color.LTGRAY);
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
View itemView = viewHolder.itemView;
int iconMargin = (itemView.getHeight() - icon.getIntrinsicHeight()) / 2;
int iconLeft, iconRight, iconTop, iconBottom;
int backTop, backBottom, backLeft, backRight;
backTop = itemView.getTop();
backBottom = itemView.getBottom();
iconTop = itemView.getTop() + (itemView.getHeight() - icon.getIntrinsicHeight()) / 2;
iconBottom = iconTop + icon.getIntrinsicHeight();
if (dX > 0) {
backLeft = itemView.getLeft();
backRight = itemView.getLeft() + (int) dX;
background.setBounds(backLeft, backTop, backRight, backBottom);
iconLeft = itemView.getLeft() + iconMargin;
iconRight = iconLeft + icon.getIntrinsicWidth();
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom);
} else if (dX < 0) {
backRight = itemView.getRight();
backLeft = itemView.getRight() + (int) dX;
background.setBounds(backLeft, backTop, backRight, backBottom);
iconRight = itemView.getRight() - iconMargin;
iconLeft = iconRight - icon.getIntrinsicWidth();
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom);
} else {
background.setBounds(0, 0, 0, 0);
icon.setBounds(0, 0, 0, 0);
}
background.draw(c);
icon.draw(c);
}
}).attachToRecyclerView(recyclerView);
}
}
6.2 添加与编辑 Fragment
该Fragmen处理了
添加与更新
的综合逻辑,主要完成实时保存、并对不同情况(添加数据被清空时、没有修改退出时等多情况)进行相应处理,详细已经给出注解
public class EditorFragment extends Fragment {
private final String TAG = "addTag";
private EditText editText;
private NoteViewModel noteViewModel;
private InputMethodManager inputMethodManager;//键盘
private Note note = null;//是否是更新
private String oldContent = null;//更新状态下记录原有的内容
public EditorFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_add, container, false);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
FragmentActivity fragmentActivity = requireActivity();
editText = fragmentActivity.findViewById(R.id.note);
//获取note,判断本次是修改还是增加.注意添加默认值,否则无法以添加方式进入编辑页面
note = (Note) (getArguments() != null ? getArguments().get("note") : null);
//如果note非空,说明本次是修改
if (note != null) {
Log.d(TAG, "onActivityCreated: " + note);
oldContent = note.getContent();
editText.setText(oldContent);
}
//初始化ViewModel
noteViewModel = new ViewModelProvider(fragmentActivity).get(NoteViewModel.class);
/*
弹出键盘
1. 需要在manifest中的activity处添加android:windowSoftInputMode="adjustNothing",防止页面被压缩
2. 大坑:必须先获取光标,再取键盘。否则先加载键盘会导致键盘无法弹出.
解决:延迟一秒等绘制界面结束后再弹出
*/
//获取光标
editText.requestFocus();
//初始化键盘
inputMethodManager = (InputMethodManager) fragmentActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
//设定光标所在位置,大坑
/*Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
inputMethodManager.showSoftInput(editText, 0);
}
}, 1000);*/
}
@Override
public void onPause() {
super.onPause();
//更新状态下记录提交前的内容
String newContent = editText.getText().toString();
String title;
Log.d(TAG, "onPause: " + newContent);
if (note != null) {
//修改状态
//1.先进行新旧内容比较
if (oldContent.equals(newContent)) {
Log.d(TAG, "onPause: 没有发生变化");
return;
}
//2.发生变化,进行空值判断
if (newContent.equals("")) {
//如果文本内容已被清空,直接删除改id
noteViewModel.deleteNotes(note);
return;
}
//3. 进行修改
//进行长度验证,选取合适部分给title赋值
if (newContent.length() <= 16) {
title = newContent;
} else {
title = newContent.substring(0, 16) + "...";
}
//设值修改
note.setTitle(title);
note.setContent(newContent);
note.setLastUpdateTime(new Date());
noteViewModel.updateNotes(note);
} else {
//添加状态
//对本次编辑内容进行验证,做出相应操作
//如果为空直接退出不进行任何操作,
if (!newContent.equals("")) {
//进行长度验证,选取合适部分给title赋值
if (newContent.length() <= 16) {
title = newContent;
} else {
title = newContent.substring(0, 16) + "...";
}
//注入数据库note对象
Note note = new Note();
note.setTitle(title);
note.setContent(newContent);
note.setLastUpdateTime(new Date());
Log.d(TAG, "onPause: " + note);
noteViewModel.insertNotes(note);
}
}
}
@Override
public void onResume() {
super.onResume();
inputMethodManager.showSoftInput(editText, 0);
}
}
对Demo进行数据迁移(Room Migration)
1.进行添加字段的数据库迁移
1.1 添加Entity中的字段
数据库实体类添加以下字段
@ColumnInfo(name = "data")
private Integer data;
1.2 修改Database
1.修改@Database注解中的version
2.定义1->2迁移策略 Migration MIGRATION_1_2
3.dataBuilder中添加.addMigrations(MIGRATION_1_2)
@Database(entities = {Note.class},version = 2,exportSchema = false)
public abstract class NoteDatabase extends RoomDatabase {
private static NoteDatabase INSTANCE;
static synchronized NoteDatabase getWordDatabase(Context context){
if (INSTANCE==null){
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),NoteDatabase.class,"note_database")
.addMigrations(MIGRATION_1_2)
.build();
}
return INSTANCE;
}
//多个Entity多个Dao
public abstract NoteDao getNoteDao();
static final Migration MIGRATION_1_2 = new Migration(1,2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE note ADD COLUMN falg INTEGER NOT NULL DEFAULT 1");
}
};
}
2.进行删除字段的数据库迁移(复杂)
由于SQLite作为轻量级数据库,并不支持像mysql、sqlserver等数据库中的drop column 。所以对于删除字段的数据库迁移的操作比较麻烦。对应2->3策略需要以下步骤:
- 按删除后剩余字段建立新表
- 将原表对应字段数据导入新表
- 删除原表
- 将新表改名为原表的名称
2.1 删除Entity中的字段
删除1.1中增加的字段data
2.2 修改Database
@Database(entities = {Note.class},version = 2,exportSchema = false)
public abstract class NoteDatabase extends RoomDatabase {
private static NoteDatabase INSTANCE;
static synchronized NoteDatabase getWordDatabase(Context context){
if (INSTANCE==null){
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),NoteDatabase.class,"note_database")
.addMigrations(MIGRATION_1_2)
.build();
}
return INSTANCE;
}
//多个Entity多个Dao
public abstract NoteDao getNoteDao();
static final Migration MIGRATION_1_2 = new Migration(1,2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE note ADD COLUMN falg INTEGER NOT NULL DEFAULT 1");
}
};
static final Migration MIGRATION_2_3 = new Migration(2,3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE note_temp (id INTEGER PRIMARY KEY NOT NULL," +
"id,INTEGER,title TEXT,content TEXT ,last_update_time INTEGER )");
database.execSQL("INSERT INTO note_temp(id,title ,content,last_update_time )" +
"SELECT id,title ,content,last_update_time FROM note");
database.execSQL("DROP TABLE note");
database.execSQL("ALTER TABLE note_temp RENAME TO note");
}
};
}
3.暴力迁移(不推荐)
可以在Entity做任意修改,而后修改Database:
static synchronized NoteDatabase getNoteDatabase(Context context){
if (INSTANCE==null){
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),NoteDatabase.class,"note_database")
// .addMigrations(MIGRATION_2_3)
.fallbackToDestructiveMigration()
.build();
}
return INSTANCE;
}
使用fallbackToDestructiveMigration替换之前的addMigrations,这种方法虽然使得程序可以正常运行但是数据库对应数据将全部丢失,显然这种方式是不友好的。
结束
对于该例子的介绍就到这里,如果有编写错误请在评论区留言。
由于篇幅有限无法详细介绍所有编码和功能,只能选取主要功能进行讲解。有兴趣深入的可以clone该工程的库进行研究(有可能进行一定的功能更新,所以建议还是仔细阅读上面代码自己写一份。当然我也会上传csdn资源,资源中的代码该版本与上面讲解版本一致)
最后放上Demo的GitHub地址:Github地址.