前言
对于一些常规的MVVM框架搭建也有一些了,那么对于一些小功能的开发也需要说明一下,注重实践,本文实践一下。一个功能并不一定能一篇文章就能写完。
在写之前先来看看完成后的效果图吧,如下图所示:
正文
从标题就看到了记事本的功能,这个功能还是比较有实用价值的,虽然每一个手机都自带这个功能,但依然有人去开发,因为这个功能可以考察开发者的一些基本功,从代码上业务需求上都可以考察到,很多的毕业设计就是搞一个记事本,还有我之前写的天气App和垃圾分类App也有类似的毕设,学习是好的,但要有自己的思考,写一个功能的时候要想一些细节。
我们先来定一下功能需求:创建笔记、删除笔记、修改笔记、显示笔记。
暂定这些功能点,那么我们应用要有一张表去操作笔记,主要实现功能就是增删改查。下面来实现这些功能,建议你不要直接看源码,有时候过程和思路比结果更重要。
一、记事本页面
既然要写一个记事本,那么首先要创建一个页面,我的代码依然还是写在MVVM框架中的,在activity包下新建一个NotebookActivity,对应的布局是activity_notebook.xml,下面我们先修改布局代码:
<?xml version="1.0" encoding="utf-8"?> <layout 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"> <data> <variable name="hasNotebook" type="Boolean" /> <import type="android.view.View" /> </data> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical" tools:context=".ui.activity.NotebookActivity"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/white" app:navigationIcon="@drawable/icon_back_black"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="记事本" android:textColor="@color/black" android:textSize="18sp" android:textStyle="bold" /> </androidx.appcompat.widget.Toolbar> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/toolbar" android:background="@color/gray_white" android:orientation="vertical"> <!--笔记列表--> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_notebook" android:layout_width="match_parent" android:layout_height="match_parent" android:overScrollMode="never" android:paddingStart="8dp" android:visibility="@{hasNotebook ? View.VISIBLE : View.GONE}" android:paddingEnd="8dp" android:paddingTop="8dp" /> <!--没有记录布局--> <LinearLayout android:id="@+id/lay_no_record" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:visibility="@{hasNotebook ? View.GONE : View.VISIBLE}" android:orientation="vertical"> <ImageView android:layout_width="96dp" android:layout_height="96dp" android:src="@mipmap/icon_no_record" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:text="没有记录" android:textColor="@color/dark_gray" android:textSize="16sp" /> </LinearLayout> </RelativeLayout> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab_add_notebook" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" android:layout_margin="20dp" android:onClick="toEdit" android:src="@mipmap/icon_add" app:backgroundTint="@color/white" tools:ignore="UsingOnClickInXml" /> </RelativeLayout> </layout>
这里用到的图标去我源码中拿,我贴出来就不是png了,然后进入AndroidManifest.xml中去修改页面样式,给NotebookActivity增加一个主题,如下图所示:
再修改一下代码,下面修改NotebookActivity中的代码,如下图所示:
这里继承了BaseActivity,然后使用了ViewBinding和状态栏设置,还有返回监听。运行一下:
这里现在没有数据,先不管它,看到右下角有一个按钮,这个按钮点击之后进行日记编辑,增加记事。下面来写这个功能,也就是增加。
二、编辑页面
编辑页面可用于新增笔记、查看笔记、修改笔记、删除笔记,一个页面要具备这些功能,是需要好好设计一下的,先完成简单的界面设计。这里同样要新增一个Activity,在activity包下新增一个EditActivity,对应的布局是activity_edit.xml,因为编辑页面中有两个输入框,因此我需要改一下默认的输入框光标样式。在drawable下新增一个custom_cursor.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <size android:width="1dp" /> <solid android:color="@color/purple_700" /> </shape>
然后我们修改一下activity_edit.xml中的代码:
<?xml version="1.0" encoding="utf-8"?> <layout 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"> <data> </data> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context=".ui.activity.EditActivity"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/white" app:navigationIcon="@mipmap/ic_edit_return"> <ImageView android:id="@+id/iv_ok" android:layout_width="36dp" android:layout_height="36dp" android:layout_gravity="end" android:layout_marginEnd="16dp" android:foreground="?attr/selectableItemBackground" android:padding="4dp" android:src="@mipmap/ic_black_ok" android:visibility="gone" /> </androidx.appcompat.widget.Toolbar> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/toolbar" android:orientation="vertical" android:paddingStart="12dp" android:paddingEnd="16dp"> <androidx.appcompat.widget.AppCompatEditText android:id="@+id/et_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@null" android:hint="标题" android:textColor="@color/black" android:textColorHint="@color/black" android:textCursorDrawable="@drawable/custom_cursor" android:textSize="36sp" /> <androidx.appcompat.widget.AppCompatEditText android:id="@+id/et_content" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/et_title" android:background="@null" android:cursorVisible="true" android:gravity="top" android:paddingTop="4dp" android:textCursorDrawable="@drawable/custom_cursor" /> </LinearLayout> </RelativeLayout> </layout>
这个布局里面用到的图标依然到我的源码里面去找,我就不贴了。
同时也需要改一个AndroidManifest.xml中的NotebookActivity的主题,如下图所示:
然后我们修改一下EditActivity中的代码:
public class EditActivity extends BaseActivity implements View.OnClickListener { private ActivityEditBinding binding; private InputMethodManager inputMethodManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_edit); setStatusBar(true); back(binding.toolbar); //新增时 获取焦点 showInput(); initView(); } private void initView() { //监听输入 listenInput(binding.etTitle); listenInput(binding.etContent); binding.ivOk.setOnClickListener(this); } /** * 监听输入 * @param editText 输入框 */ private void listenInput(final AppCompatEditText editText) { editText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (s.length() > 0) { binding.ivOk.setVisibility(View.VISIBLE); } else { if (binding.etTitle.getText().length() == 0 && binding.etContent.getText().length() == 0 ){ binding.ivOk.setVisibility(View.GONE); } } } }); } /** * 显示键盘 */ public void showInput() { binding.etContent.requestFocus(); inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } /** * 隐藏键盘 */ public void dismiss() { if (inputMethodManager != null) { inputMethodManager.hideSoftInputFromWindow(binding.etContent.getWindowToken(), 0); } } @Override protected void onPause() { super.onPause(); dismiss(); } @Override public void onBackPressed() { super.onBackPressed(); dismiss(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.iv_ok://提交 showMsg("提交"); break; } } }
这个页面的逻辑当前是这样的,有两个输入框,一个是标题一个是内容,当输入框有输入的时候显示一个提交按钮,当没有输入或者输入框为空的时候隐藏这个提交按钮,还有一个就是一进入当前页面,就显示内容的输入框光标,同时弹出软键盘。
这个页面也需要一个入口,也就是记事本页面点击右下角的按钮跳转过来,在activity_notebook.xml中修改浮动按钮的onClick事件。
这里是一个toEdit,然后在NotebookActivity中新增一个toEdit方法
/** * 去编辑 */ public void toEdit(View view) { jumpActivity(EditActivity.class); }
当然了,我们的NotebookActivity也需要一个入口,在我的MVVM中我就在侧滑菜单中增加入口,首先增加一个路径图标,在drawable下新增一个icon_notebook.xml,代码如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:autoMirrored="true" android:tint="#000000" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="@android:color/white" android:pathData="M14.17,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V9.83c0,-0.53 -0.21,-1.04 -0.59,-1.41l-4.83,-4.83C15.21,3.21 14.7,3 14.17,3L14.17,3zM8,15h8c0.55,0 1,0.45 1,1v0c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1v0C7,15.45 7.45,15 8,15zM8,11h8c0.55,0 1,0.45 1,1v0c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1v0C7,11.45 7.45,11 8,11zM8,7h5c0.55,0 1,0.45 1,1v0c0,0.55 -0.45,1 -1,1H8C7.45,9 7,8.55 7,8v0C7,7.45 7.45,7 8,7z" /> </vector>
然后在nav_menu.xml中新增代码:
<item android:id="@+id/item_notebook" android:icon="@drawable/icon_notebook" android:title="记事本" />
添加位置如下图所示:
最后在HomeActivity中修改一下菜单点击代码,如下图所示:
下面我们运行一下:
编辑页面写好了,需要写具体的功能了,这需要在数据库中一个笔记表。
三、增加笔记表
① Bean
首先在bean包下新增一个Notebook类,代码如下:
@Entity(tableName = "notebook") public class Notebook { @PrimaryKey(autoGenerate = true) private int uid; private String title; private String content; public int getUid() { return uid; } public void setUid(int uid) { this.uid = uid; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Ignore public Notebook(String title, String content) { this.title = title; this.content = content; } public Notebook() {} }
这目前的表中我只设置了两个主要字段,标题和内容,uid自增。
② Dao
下面就是数据表的操作接口类,在dao包下新建一个NotebookDao接口,代码如下:
@Dao public interface NotebookDao { @Query("SELECT * FROM notebook") Flowable<List<Notebook>> getAll(); @Update Completable update(Notebook notebook); @Insert(onConflict = OnConflictStrategy.REPLACE) Completable insert(Notebook notebook); @Delete Completable delete(Notebook notebook); }
这里就是增删改查,相信你已经很熟悉了,如果你是从之前的文章一路看过来的话。
③ 数据库升级迁移
打开AppDatabase首先增加表和版本升级,如下图所示:
下面增加刚才的Dao的实现,一行代码搞定,如下图所示:
public abstract NotebookDao notebookDao();
然后是数据库升级迁移,代码如下:
/** * 版本升级迁移到6 在数据库中新增一个笔记表 */ static final Migration MIGRATION_5_6 = new Migration(5, 6) { @Override public void migrate(@NonNull @NotNull SupportSQLiteDatabase database) { //创建笔记表 database.execSQL("CREATE TABLE `notebook` " + "(uid INTEGER NOT NULL, " + "title TEXT, " + "content TEXT, " + "PRIMARY KEY(`uid`))"); } };
最后添加迁移,如下图所示:
数据库搞定了,下面就是存储库了。
④ 新增存储库类
在repository包下新建一个NotebookRepository类,里面的代码如下:
public class NotebookRepository { private static final String TAG = NotebookRepository.class.getSimpleName(); @Inject NotebookRepository() {} private final MutableLiveData<Notebook> notebookLiveData = new MutableLiveData<>(); private final MutableLiveData<List<Notebook>> notebooksMutableLiveData = new MutableLiveData<>(); public final MutableLiveData<String> failed = new MutableLiveData<>(); public final List<Notebook> emptyList = new ArrayList<>(); /** * 添加笔记 */ public void saveNotebook(Notebook notebook) { //保存到数据库 Completable insert = BaseApplication.getDb().notebookDao().insert(notebook); //RxJava处理Room数据存储 CustomDisposable.addDisposable(insert, () -> Log.d(TAG, "saveNotebook: 笔记数据保存成功")); } /** * 获取所有笔记 */ public MutableLiveData<List<Notebook>> getNotebooks() { Flowable<List<Notebook>> listFlowable = BaseApplication.getDb().notebookDao().getAll(); CustomDisposable.addDisposable(listFlowable, notebooks -> { if (notebooks.size() > 0) { notebooksMutableLiveData.postValue(notebooks); } else { notebooksMutableLiveData.postValue(emptyList); failed.postValue("暂无数据"); } }); return notebooksMutableLiveData; } }
这里存储里面现在是两个方法,一个用于查询,一个用于添加。然后就是新建ViewModel去操作这个存储库。
⑤ 新增ViewModel
这里其实有两个ViewModel,一个对应EditActivity,一个对应NotebookActivity,首先在viewmodels包下创建一个EditViewModel类,代码如下:
public class EditViewModel extends BaseViewModel { private final NotebookRepository notebookRepository; @ViewModelInject EditViewModel(NotebookRepository notebookRepository){ this.notebookRepository = notebookRepository; } /** * 添加笔记 */ public void addNotebook(Notebook notebook) { failed = notebookRepository.failed; notebookRepository.saveNotebook(notebook); } }
然后同样在viewmodels包下创建NotebookViewModel类,代码如下:
public class NotebookViewModel extends BaseViewModel { private final NotebookRepository notebookRepository; public LiveData<List<Notebook>> notebooks; @ViewModelInject NotebookViewModel(NotebookRepository notebookRepository){ this.notebookRepository = notebookRepository; } public void getNotebooks() { failed = notebookRepository.failed; notebooks = notebookRepository.getNotebooks(); } }
⑥ 添加笔记
首先需要把EditViewModel与EditActivity进行绑定,如下图所示:
然后就是很简单的时候,在点击右上角按钮时,进行保存笔记,在EditActivity中修改一下代码,如下图所示:
这里我是保存了数据之后关掉当前页面,就会返回到之前的NotebookActivity,那么在这个页面就需要搜索当前数据库的表,然后通过列表加载出来。
四、显示笔记列表
既然是一个列表,那么自然就需要有一个item的布局,在layout下新建一个item_notebook.xml,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="notebook" type="com.llw.mvvm.db.bean.Notebook" /> <!--点击事件--> <variable name="onClick" type="com.llw.mvvm.ui.adapter.NotebookAdapter.ClickBinding" /> </data> <RelativeLayout android:id="@+id/detail" android:foreground="?attr/selectableItemBackground" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:background="@drawable/shape_bg_white_radius_12" android:onClick="@{() -> onClick.itemClick(notebook,detail)}" android:padding="12dp"> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" android:singleLine="true" android:text="@{notebook.title}" android:textColor="@color/black" android:textSize="16sp" /> <TextView android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/tv_title" android:layout_marginTop="4dp" android:ellipsize="end" android:maxLines="3" android:text="@{notebook.content}" android:textSize="14sp" /> </RelativeLayout> </layout>
布局很简单,就是显示标题和内容,采用databinding的方式赋值,下面创建适配器,在adapter包下新建一个NotebookAdapter类,里面的代码如下:
public class NotebookAdapter extends BaseQuickAdapter<Notebook, BaseDataBindingHolder<ItemNotebookBinding>> { public NotebookAdapter(@Nullable List<Notebook> data) { super(R.layout.item_notebook, data); } @Override protected void convert(@NotNull BaseDataBindingHolder<ItemNotebookBinding> bindingHolder, Notebook notebook) { ItemNotebookBinding binding = bindingHolder.getDataBinding(); if (binding != null) { binding.setNotebook(notebook); binding.setOnClick(new NotebookAdapter.ClickBinding()); binding.executePendingBindings(); } } public static class ClickBinding { public void itemClick(Notebook notebook, View view) { } } }
这个代码也是很简单的,就是绑定数据绑定布局,下面就是显示列表了,也很简单,回到NotebookActivity,增加三个变量并添加了一个注解,如下图所示:
修改代码,如下图所示:
首先是绑定ViewModel,然后在onResume的生命周期查询数据库中的数据,在编辑页面对数据进行修改之后会销毁掉,然后就会显示NotebookActivity,会触发onResume,再去查询一次数据。然后监听数据,有数据则加载列表,没有就显示那个空内容布局。下面来运行一下看看效果如何。
还是可以的吧,下面要做的就是修改笔记。
五、修改笔记
修改笔记的前提是要查询到要修改的笔记,通过id进行查询,然后完成修改,说起来是挺简单的,当然了,实现起来也很简单,我们来实现吧。首先是列表item的点击事件,点击之后传递uid到EditActivity,通过通过uid去得到Notebook。
在NotebookAdapater中增加如下图所示代码:
因为我们的NotebookDao中并没有通过id查询笔记的方法,因此我们在NotebookDao中新加一个,代码如下:
@Query("SELECT * FROM notebook WHERE uid=:uid") Flowable<Notebook> findById(int uid);
然后去NotebookRepository中去对方法进行实现,这里我们需要实现两个方法,一个用于通过id查询,一个用于修改,在NotebookRepository中新增如下代码:
/** * 根据id获取笔记 * @param uid id */ public MutableLiveData<Notebook> getNotebookById(int uid) { Flowable<Notebook> flowable = BaseApplication.getDb().notebookDao().findById(uid); CustomDisposable.addDisposable(flowable, notebook -> { if (notebook != null) { notebookLiveData.postValue(notebook); } else { failed.postValue("未查询到笔记"); } }); return notebookLiveData; } /** * 更新笔记 * * @param notebook */ public void updateNotebook(Notebook notebook) { Completable update = BaseApplication.getDb().notebookDao().update(notebook); CustomDisposable.addDisposable(update, () -> { Log.d(TAG, "updateNotebook: " + "更新成功"); failed.postValue("200"); }); }
存储库的方法写好了,下面就是在EditViewModel中去调用了,进入EditViewModel,新增如下代码:
public LiveData<Notebook> notebook; /** * 根据Id搜索笔记 */ public void queryById(int uid) { failed = notebookRepository.failed; notebook = notebookRepository.getNotebookById(uid); } /** * 更新笔记 */ public void updateNotebook(Notebook notebook) { failed = notebookRepository.failed; notebookRepository.updateNotebook(notebook); }
这个代码就没啥好说的,见过很多类似的了,最后就是在EditActivity。
进入EditActivity中,新增两个变量:
private int uid; private Notebook mNotebook;
首先要处理uid的问题,因为我们点击新增笔记和笔记笔记都是进入这个页面,所以要分情况处理。
这里我把showInput从移除掉了,根据现在的业务逻辑它不应该在onCreate中调用了,需要在initView方法中,下面我们看看怎么去修改。
如图所示,这里获取uid,如果为-1表示为新增,否则就是更新。是更新的话就通过查询id然后观察返回的数据变化。
这里的binding.setNotebook(mNotebook);是直接通过单向绑定对控件进行赋值,因此这里需要修改一下activity_edit.xml中的代码:
最后我们修改一下确定按钮的业务逻辑,如下图所示:
这个-1是用来做什么的我就不再多说了,这里修改数据之后,依然后关闭当前页面。我们的代码写完了,运行一下吧。
修改就完成了,下面就是删除了。
六、删除笔记
删除的方法之前就写好了,因此Dao中不需要改动了,只要在NotebookRepository中增加删除的方法即可,代码如下:
/** * 删除笔记 */ public void deleteNotebook(Notebook notebook) { Completable delete = BaseApplication.getDb().notebookDao().delete(notebook); CustomDisposable.addDisposable(delete, () -> { Log.d(TAG, "deleteNotebook: " + "删除成功"); failed.postValue("200"); }); }
然后是EditViewModel中去调用,在EditViewModel中新增代码如下所示:
/** * 删除笔记 */ public void deleteNotebook(Notebook notebook) { notebookRepository.deleteNotebook(notebook); failed = notebookRepository.failed; }
下面就是在EditActivity中去调用EditViewModel中的deleteNotebook方法,在标题哪里添加一个按钮,修改activity_edit.xml,代码如下:
<!--删除按钮--> <ImageView android:id="@+id/iv_delete" android:layout_width="36dp" android:layout_height="36dp" android:layout_gravity="end" android:layout_marginEnd="16dp" android:foreground="?attr/selectableItemBackground" android:padding="2dp" android:src="@mipmap/ic_delete" android:visibility="gone" />
注意添加的位置:
然后是修改EditActivity中的代码,主要就是注册按钮的监听,然后是调用删除的方法。
删除笔记
运行一下:
OK了,本篇文章就到这里了,下一篇可能会对记事本功能进行一个优化,主要是用户体验方面,时隔近一个月,再写文章时花费的时间依然很多,久违了的感觉。今天是周五了,周末愉快啊。