20189200余超 2018-2019-2 移动平台应用开发实践作项目代码分析
项目名称
小说阅读器
项目功能
注册登录
用户信息、用户密码、用户图像修改
书籍分类
书架
书籍搜索(作者名或书籍名)
书籍阅读(仅txt格式,暂不支持PDF等其他格式)
阅读字体、背景颜色、翻页效果等设置
意见反馈(反馈信息发送到我的邮箱)
项目简介
本项目主要是一个小说阅读软件,我所做的小说阅读软件是一个可以根据用户的阅读兴趣、爱好、来进行完成的。第一,我的项目功能里面有注册用户的,用户可以进行注册和登录。第二,用户信息功能,用户可以查看自己的信息,修改自己的密码,修改用户图像等。第三,我还根据书的特点进行了书架的分类,分为了玄幻、奇幻、武侠、仙侠、都市、职场、历史、军事等。第四,书籍搜索功能,也即是用户可以根据小说的名字或者作者来进行搜索相应的小说。第五,书籍的阅读仅限于txt格式的阅读。第六,用户自己可以设置用户的阅读字体、背景颜色、翻页效果等功能。第七,用户可以进行信息的反馈,且发送到我的邮箱里面。
项目的运行截图
使用开源库
Rx2网络封装 RxHttpUtils
6.0权限库 RxPermissions
Glide图片加载库 Glide
下拉刷新库 SmartRefreshLayout
RecyclerView简化框架 BaseRecyclerViewAdapterHelper
MD风格Dialog material-dialogs
TabLaout选择 NavigationTabStrip
数据加载动画 Android-SpinKit
展开折叠TextView ExpandTextView
流式标签 FlowLayout
数据库 greenDAO
版本更新进度条 NumberProgressBar
图片选择器 TakePhoto
项目首页- GanK -在基础上修改
代码组成部分
1-1代码模块截图
以上是本项目的整体代码结构
1-2代码模块分析
api:该包主要是用于网络接口的代码
db:该包主要是数据库连接,数据存储代码
event:该包主要是用于事件回调后的通知
Intenfces:该包主要是用于接口的编写
Model:该包主要是用于实体类的编写,如小说,用户等实体类
Util:该包主要是用于工具类代码的编写
View:该包主要是用于app界面的编写
Viewmodel:该包主要是用于界面数据显示的实体
Widget:该包用于app界面装置代码的编写
Anim:主要存放自定义按钮样式
Color:主要是用于存放颜色代码
Drawable:主要是用于存放图片等静态文件
Layout:主要是用于存放界面的代码
Mipmap-*:用于适配各种分辨的图标
Raw:存放文件 如txt
Values:存放各种文本变量
Xml:存放配置文件 如文件路径等
主要使用Android mvp开发模式进行开发,该模式有以下几个优点
1、模型与视图完全分离,我们可以修改视图而不影响模型
2、可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部
3、我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)
代码调用关系
代码调用关系
一:用户管理
0:用户注册
由VMUserRegisterInfo类中的register()对BookService中的接口进行调用,完成整个的注册功能
2:登录功能
由VMUseLoginInfo类中的login方法对BookService中的接口调用 完成登录流程
3:修改密码功能
由VMUseLoginInfo类中的updatePassword方法对BookService中的接口调用 完成修改密码流程
4:更新个人信息
由VMUseLoginInfo类中的updateUserInfo方法对BookService中的接口调用 完成更新个人信息流程
5:更新头像
由VMUseLoginInfo类中的uploadAvatar方法对BookService中的接口调用 完成更新头像流程
从网络接口获取电子书
1 获取所有的分类
由VMBookClassify类中的bookClassify方法对BookService中的接口进行调用,完成整个对分类的获取
2:获取分类下的书籍
由VMBookClassify类中的getBooks方法对BookService中的接口进行调用,完成整个对分类的获取
3:获取书籍信息
由VMBookClassify类中的bookInfo方法对BookService中的接口进行调用,完成整个对书籍信息的获取
4 获取书籍目录
由VMBookClassify类中的setBookInfo方法对BookService中的接口进行调用,完成整个对书籍目录的获取
核心代码分析
1:网络爬虫
该app的书籍信息主要来自于互联网,我们通过fider对追书神器的网络请求进行抓取,获取http请求,随后,我们在本app中使用Rxjava框架,进行对抓取的http链接进行请求,获取数据后封装显示。以下是核心代码
/**
* 获取书籍信息
*
* @param bookid
*/
public void bookInfo(String bookid) {
iBookDetail.showLoading();
RxHttpUtils.getSInstance().addHeaders(tokenMap())
.createSApi(BookService.class).bookInfo(bookid)
.compose(Transformer.switchSchedulers())
.subscribe(new RxObserver() {
@Override
protected void onError(String errorMsg) {
iBookDetail.stopLoading();
}
@Override
protected void onSuccess(BookBean bookBean) {
iBookDetail.stopLoading();
iBookDetail.getBookInfo(bookBean);
}
});
}
public void bookClassify() {
if (!NetworkUtils.isConnected()) {
if (mIBookClassify != null) {
mIBookClassify.NetWorkError();
}
return;
}
RxHttpUtils.getSInstance().addHeaders(tokenMap()).createSApi(BookService.class)
/* RxHttpUtils.createApi(BookService.class)*/
.bookClassify()
.compose(Transformer.switchSchedulers())
.subscribe(new RxObserver() {
@Override
protected void onError(String errorMsg) {
if (mIBookClassify != null) {
mIBookClassify.stopLoading();
mIBookClassify.errorData(errorMsg);
}
}
@Override
protected void onSuccess(BookClassifyBean data) {
if (mIBookClassify != null) {
mIBookClassify.stopLoading();
if (data == null) {
mIBookClassify.emptyData();
return;
}
mIBookClassify.getBookClassify(data);
}
}
@Override
public void onSubscribe(Disposable d) {
addDisposadle(d);
}
});
}
public void setBookInfo(CollBookBean collBookBean) {
LoadingHelper.getInstance().showLoading(mContext);
if (CollBookHelper.getsInstance().findBookById(collBookBean.get_id()) == null) {
RxHttpUtils.getSInstance().addHeaders(tokenMap()).createSApi(BookService.class)
.bookChapters(collBookBean.get_id())
.compose(Transformer.switchSchedulers())
.subscribe(new RxObserver() {
@Override
protected void onError(String errorMsg) {
LoadingHelper.getInstance().hideLoading();
}
@Override
protected void onSuccess(BookChaptersBean data) {
LoadingHelper.getInstance().hideLoading();
List bookChapterList = new ArrayList<>();
for (BookChaptersBean.ChatpterBean bean : data.getChapters()) {
BookChapterBean chapterBean = new BookChapterBean();
chapterBean.setBookId(data.getBook());
chapterBean.setLink(bean.getLink());
chapterBean.setTitle(bean.getTitle());
// chapterBean.setTaskName("下载");
chapterBean.setUnreadble(bean.isRead());
bookChapterList.add(chapterBean);
}
collBookBean.setBookChapters(bookChapterList);
CollBookHelper.getsInstance().saveBookWithAsync(collBookBean);
iBookShelf.bookInfo(collBookBean);
}
@Override
public void onSubscribe(Disposable d) {
addDisposadle(d);
}
});
} else {
LoadingHelper.getInstance().hideLoading();
iBookShelf.bookInfo(collBookBean);
}
}
private void getBooksByTag() {
Map map = new HashMap<>();
map.put("access-token", SharedPreUtils.getInstance().getString("token", "weyue"));
map.put("app-type", "Android");
RxHttpUtils.getSInstance().addHeaders(map).createSApi(BookService.class)
.booksByTag(tagName, page)
.compose(Transformer.switchSchedulers())
.subscribe(new RxObserver>() {
@Override
protected void onError(String errorMsg) {
mRefreshLayout.finishLoadmore();
}
@Override
protected void onSuccess(List data) {
mRefreshLayout.finishLoadmore();
mBeans.addAll(data);
if (mBeans.size() > 0) {
mBookTagsAdapter.notifyDataSetChanged();
}
}
@Override
public void onSubscribe(Disposable d) {
super.onSubscribe(d);
mDisposable = d;
}
});
}
自己实现功能分析
1 :用户管理模块
该模块主要是有用户登录,用户注册,修改密码,修改个人信息,修改头像这几个功能。
(1)用户登录
功能分析:用户在界面输入用户名和密码后,通过http请求后台接口,验证用户名和密码。完成整个登录流程
@OnClick({R.id.ctv_register, R.id.fab})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.ctv_register:
startActivityForResult(new Intent(this, RegisterActivity.class), 10000);
break;
case R.id.fab:
String username = mActvUsername.getText().toString();
String password = mEtPassword.getText().toString();
if (TextUtils.isEmpty(username)) {
ToastUtils.show("用户名不能为空");
return;
}
if (TextUtils.isEmpty(password)) {
ToastUtils.show("密码不能为空");
return;
}
mModel.login(username, password);
break;
}
}
(2)用户注册
功能分析:用户在未登录的情况下,可以查阅电子书,但是无法对喜欢的电子书进行添加到书架的操作,这时,我app会自动跳转到注册页面,提示用户注册后可以使用相映的功能,当用户输入用户名和密码后,请求后台提供的接口,如果用户名存在,则注册失败,否则,注册成功,并对用户密码进行md5加密后存入数据库
@Override
protected void initView() {
super.initView();
initThemeToolBar("用户注册");
mFab.setOnClickListener(v -> {
mUsername = mActvUsername.getText().toString();
mPassword1 = mEtPassword.getText().toString();
String password2 = mEtPasswordConfirm.getText().toString();
if (TextUtils.isEmpty(mUsername)) {
ToastUtils.show("用户名不能为空");
return;
}
if (TextUtils.isEmpty(mPassword1) || TextUtils.isEmpty(password2)) {
ToastUtils.show("密码不能为空");
return;
}
if (!mPassword1.equals(password2)) {
ToastUtils.show("两次输入密码不一样");
return;
}
mModel.register(mUsername, mPassword1);
});
}
(3)修改密码
功能分析:用户点击修改密码后,首先对原密码进行验证,验证成功后则修改成功。否则修改失败
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.fab_edit_password:
mFabMenu.toggle();
new MaterialDialog.Builder(this)
.title("修改用户密码")
.inputRange(2, 20, ThemeUtils.getThemeColor())
// .inputType(InputType.TYPE_TEXT_VARIATION_PASSWORD)
.input("请输入新密码", null, (dialog, input) -> {
dialog.dismiss();
mModel.updatePassword(input.toString());
})
.show();
break;
case R.id.fab_edit_userinfo:
mFabMenu.toggle();
startEdit();
break;
case R.id.iv_avatar:
/**
* 设置内容区域为简单列表项
*/
final String[] items = {"相册", "拍摄"};
new MaterialDialog.Builder(this)
.title("选择照片方式")
.items(items)
.itemsCallback((dialog, itemView, position, text) -> {
switch (position) {
case 0:
dialog.dismiss();
imageUri = getImageCropUri();
//从相册中选取图片并裁剪
takePhoto.onPickFromGalleryWithCrop(imageUri, cropOptions);
//从相册中选取不裁剪
//takePhoto.onPickFromGallery();
break;
case 1:
dialog.dismiss();
imageUri = getImageCropUri();
//拍照并裁剪
takePhoto.onPickFromCaptureWithCrop(imageUri, cropOptions);
//仅仅拍照不裁剪
//takePhoto.onPickFromCapture(imageUri);
break;
}
})
.show();
break;
case R.id.btn_confirm:
new MaterialDialog.Builder(this)
.title("修改用户信息")
.content("是否确认修改?")
.negativeText("取消")
.onNegative((dialog, which) -> dialog.dismiss())
.positiveText("确定")
.onPositive((dialog, which) -> {
String nickname = mEtNickName.getText().toString();
String brief = mEtBrief.getText().toString();
if (TextUtils.isEmpty(nickname)) {
ToastUtils.show("昵称不能为空");
return;
}
if (TextUtils.isEmpty(brief)) {
ToastUtils.show("我的格言不能为空");
return;
}
stopEdit();
dialog.dismiss();
mModel.updateUserInfo(nickname, brief);
})
.show();
break;
}
}
(4)修改个人信息
功能分析:用户可以再app个人系信息界面修改自己的信息,新的信息填写完成后,点击保存,则完成整个信息的修改。
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.fab_edit_password:
mFabMenu.toggle();
new MaterialDialog.Builder(this)
.title("修改用户密码")
.inputRange(2, 20, ThemeUtils.getThemeColor())
// .inputType(InputType.TYPE_TEXT_VARIATION_PASSWORD)
.input("请输入新密码", null, (dialog, input) -> {
dialog.dismiss();
mModel.updatePassword(input.toString());
})
.show();
break;
case R.id.fab_edit_userinfo:
mFabMenu.toggle();
startEdit();
break;
case R.id.iv_avatar:
/**
* 设置内容区域为简单列表项
*/
final String[] items = {"相册", "拍摄"};
new MaterialDialog.Builder(this)
.title("选择照片方式")
.items(items)
.itemsCallback((dialog, itemView, position, text) -> {
switch (position) {
case 0:
dialog.dismiss();
imageUri = getImageCropUri();
//从相册中选取图片并裁剪
takePhoto.onPickFromGalleryWithCrop(imageUri, cropOptions);
//从相册中选取不裁剪
//takePhoto.onPickFromGallery();
break;
case 1:
dialog.dismiss();
imageUri = getImageCropUri();
//拍照并裁剪
takePhoto.onPickFromCaptureWithCrop(imageUri, cropOptions);
//仅仅拍照不裁剪
//takePhoto.onPickFromCapture(imageUri);
break;
}
})
.show();
break;
case R.id.btn_confirm:
new MaterialDialog.Builder(this)
.title("修改用户信息")
.content("是否确认修改?")
.negativeText("取消")
.onNegative((dialog, which) -> dialog.dismiss())
.positiveText("确定")
.onPositive((dialog, which) -> {
String nickname = mEtNickName.getText().toString();
String brief = mEtBrief.getText().toString();
if (TextUtils.isEmpty(nickname)) {
ToastUtils.show("昵称不能为空");
return;
}
if (TextUtils.isEmpty(brief)) {
ToastUtils.show("我的格言不能为空");
return;
}
stopEdit();
dialog.dismiss();
mModel.updateUserInfo(nickname, brief);
})
.show();
break;
}
}
(5)修改头像
功能分析:用户可以选择自己喜欢的头像,点击头像后会提示用户选择新的照片作为自己的头像,提交后保存到数据库,完成整个模块的修改
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.fab_edit_password:
mFabMenu.toggle();
new MaterialDialog.Builder(this)
.title("修改用户密码")
.inputRange(2, 20, ThemeUtils.getThemeColor())
// .inputType(InputType.TYPE_TEXT_VARIATION_PASSWORD)
.input("请输入新密码", null, (dialog, input) -> {
dialog.dismiss();
mModel.updatePassword(input.toString());
})
.show();
break;
case R.id.fab_edit_userinfo:
mFabMenu.toggle();
startEdit();
break;
case R.id.iv_avatar:
/**
* 设置内容区域为简单列表项
*/
final String[] items = {"相册", "拍摄"};
new MaterialDialog.Builder(this)
.title("选择照片方式")
.items(items)
.itemsCallback((dialog, itemView, position, text) -> {
switch (position) {
case 0:
dialog.dismiss();
imageUri = getImageCropUri();
//从相册中选取图片并裁剪
takePhoto.onPickFromGalleryWithCrop(imageUri, cropOptions);
//从相册中选取不裁剪
//takePhoto.onPickFromGallery();
break;
case 1:
dialog.dismiss();
imageUri = getImageCropUri();
//拍照并裁剪
takePhoto.onPickFromCaptureWithCrop(imageUri, cropOptions);
//仅仅拍照不裁剪
//takePhoto.onPickFromCapture(imageUri);
break;
}
})
.show();
break;
case R.id.btn_confirm:
new MaterialDialog.Builder(this)
.title("修改用户信息")
.content("是否确认修改?")
.negativeText("取消")
.onNegative((dialog, which) -> dialog.dismiss())
.positiveText("确定")
.onPositive((dialog, which) -> {
String nickname = mEtNickName.getText().toString();
String brief = mEtBrief.getText().toString();
if (TextUtils.isEmpty(nickname)) {
ToastUtils.show("昵称不能为空");
return;
}
if (TextUtils.isEmpty(brief)) {
ToastUtils.show("我的格言不能为空");
return;
}
stopEdit();
dialog.dismiss();
mModel.updateUserInfo(nickname, brief);
})
.show();
break;
}
}
电子书模块
该模块主要是从互联网上获取电子书资源
(1)获取追书神器url
功能分析:该功能主要是通fiddler抓取追书神器的网络请求 由此可以获取到电子书相关的url
(2)数据解析
功能分析:该功能主要是通过http请求获取抓取到的url请求中的数据,然后封装成java对象,用户页面上内容的展示,格式为json
/**
* 1、判断本地数据库有没有收藏书籍的数据。
* 2、本地数据库没有收藏书籍数据就网络请求。否则就取本地数据
@param collBookBean
*/
public void setBookInfo(CollBookBean collBookBean) {
LoadingHelper.getInstance().showLoading(mContext);
if (CollBookHelper.getsInstance().findBookById(collBookBean.get_id()) == null) {
RxHttpUtils.getSInstance().addHeaders(tokenMap()).createSApi(BookService.class)
.bookChapters(collBookBean.get_id())
.compose(Transformer.switchSchedulers())
.subscribe(new RxObserver() {
@Override
protected void onError(String errorMsg) {
LoadingHelper.getInstance().hideLoading();
}
@Override
protected void onSuccess(BookChaptersBean data) {
LoadingHelper.getInstance().hideLoading();
List bookChapterList = new ArrayList<>();
for (BookChaptersBean.ChatpterBean bean : data.getChapters()) {
BookChapterBean chapterBean = new BookChapterBean();
chapterBean.setBookId(data.getBook());
chapterBean.setLink(bean.getLink());
chapterBean.setTitle(bean.getTitle());
// chapterBean.setTaskName("下载");
chapterBean.setUnreadble(bean.isRead());
bookChapterList.add(chapterBean);
}
collBookBean.setBookChapters(bookChapterList);
CollBookHelper.getsInstance().saveBookWithAsync(collBookBean);
iBookShelf.bookInfo(collBookBean);
}
@Override
public void onSubscribe(Disposable d) {
addDisposadle(d);
}
});
} else {
LoadingHelper.getInstance().hideLoading();
iBookShelf.bookInfo(collBookBean);
}
public void bookClassify() {
if (!NetworkUtils.isConnected()) {
if (mIBookClassify != null) {
mIBookClassify.NetWorkError();
}
return;
}
RxHttpUtils.getSInstance().addHeaders(tokenMap()).createSApi(BookService.class)
/* RxHttpUtils.createApi(BookService.class)*/
.bookClassify()
.compose(Transformer.switchSchedulers())
.subscribe(new RxObserver() {
@Override
protected void onError(String errorMsg) {
if (mIBookClassify != null) {
mIBookClassify.stopLoading();
mIBookClassify.errorData(errorMsg);
}
}
@Override
protected void onSuccess(BookClassifyBean data) {
if (mIBookClassify != null) {
mIBookClassify.stopLoading();
if (data == null) {
mIBookClassify.emptyData();
return;
}
mIBookClassify.getBookClassify(data);
}
}
@Override
public void onSubscribe(Disposable d) {
addDisposadle(d);
}
});
}