为什么要用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数据库
@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;
}
注意:因为实体类中存在复杂数据类型——时间类。所以在数据库管理中需要使用
@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;
}
/**
*/
public abstract NoteDao getNoteDao();
}
@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 queryAllNotes();
@Query(“select * from note where content like :pattern order by last_update_time desc”)
LiveData queryNotesWithPattern(String pattern);
}
2.编写数据仓库(Repository、AsyncTask)
对数据库的操作(调用Dao)逻辑将放于ViewModel中(步骤5)。使得ViewModel中代码变得杂乱
引入仓库类,用于对数据库操作并对ViewModel暴露方法。让ViewModel专注于数据处理而非对数据库的调用,对ViewModel和Dao进一步解耦。
一个Android
已封装好的轻量级异步类,用于实现多线程、异步通信、消息传递
数据库的操作很重,一次读写操作花费 10~20ms 是很常见的,这样的耗时很容易造成界面的卡顿。所以通常情况下,条件允许情况下要避免在主线程中处理数据库。
public class NoteRepository {
private NoteDao noteDao;
private LiveData allNoteLive;
public NoteRepository(Context context){
NoteDatabase database = NoteDatabase.getINSTANCE(context);
noteDao = database.getNoteDao();
allNoteLive = noteDao.queryAllNotes();
}
public LiveData getAllWordLive() {
return allNoteLive;
}
public LiveData 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
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
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
private NoteDao noteDao;
DeleteAllAsyncTask(NoteDao noteDao) {
this.noteDao = noteDao;
}
@Override
protected Void doInBackground(Note… notes) {
noteDao.deleteAllNotes();
return null;
}
}
static class DeleteAsyncTask extends AsyncTask
private NoteDao noteDao;
DeleteAsyncTask(NoteDao noteDao) {
this.noteDao = noteDao;
}
@Override
protected Void doInBackground(Note… notes) {
noteDao.deleteNotes(notes);
return null;
}
}
}
3.编写ViewModel+LiveData
ViewModel
是数据与 UI 分离的中间层,提供了一个将数据转换为 UI 友好型数据的场所。其次,它也提供了多 Fragment
复用相同 ViewModel
的机制。
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 getAllNoteLive() {
return repository.getAllWordLive();
}
public LiveData 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可以参照
Android 控件 RecyclerView
其中涉及到矢量图的使用,如果没有使用过矢量图可以参照
Android中使用矢量图(快速运用)
fragment_notes.xml
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”> android:id=“@+id/recyclerView” android:layout_width=“match_parent” android:layout_height=“match_parent” /> 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” />
cell_card.xml
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”> 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”> android:layout_width=“match_parent” android:layout_height=“match_parent”> android:id=“@+id/guideline1” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:orientation=“vertical” app:layout_constraintGuide_percent=“0.85” /> 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” /> 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” /> 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” /> 在fragment中编写好界面后,只需要在 main_activity.xml 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”> 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” /> 5.编写RecyclerView的适配器 public class MyAdapt extends ListAdapter public MyAdapt() { super(new DiffUtil.ItemCallback() { @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()); } }); } /** */ @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功能进行简要介绍) 主界面一些逻辑处理比较复杂,涉及到一些功能如 public class NotesFragment extends Fragment { //final String TAG = “mainTag”; //视图层 private NoteViewModel noteViewModel; private RecyclerView recyclerView; private MyAdapt myAdapt; //数据层 private LiveData private FragmentActivity fragmentActivity; //操作标识,只有更新时候才上移。更新删除保持不动 private boolean undoAction; /** 实时保存数据列表,防止通过liveData时直接获取元素时因为异步获取,发生空指针异常 主要用于标记滑动删除中的撤销 */ private List 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); } /** */ @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(); 目前已经更新的部分资料: 网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。 需要这份系统化学习资料的朋友,可以戳这里获取 一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长! (); 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(); 目前已经更新的部分资料: [外链图片转存中…(img-bidZ5OPN-1714282184946)] 网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。 需要这份系统化学习资料的朋友,可以戳这里获取 一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!4.2 编写主界面映射Fragment界面
为什么要这样处理?
main_activity
将界面映射过来,之后页面的切换与数据传递交由Navigation作为导航管理Fragment。期间你的路径与与数据设置只需在可视化中简单操作即可。在main_activity
只需要做很少的处理即可
6.1 编写主界面Fragment逻辑
搜索
、清空
、数据观察
、 观察移除
(数据观察这块需要注意传入的环境,博主之前没传好出现一些比较奇怪的bug,注释有标明)。包括扩展功能如:撤销删除
、滑动删除
、 矢量图定点绘制
(需要有一定的图形代码编写基础) noteLive;
最后
最后
[外链图片转存中…(img-wtdaVCN3-1714282184947)]
[外链图片转存中…(img-XhkJwcaj-1714282184947)]