本系列为了总结一下手上的知识,致敬我的2018 本篇的重点在于:后端数据在移动端的展现 本篇总结的技术点:
材料设计串烧
、Retrofit+RxJava访问请求
、Retrofit提交表单
、Retrofit缓存的实现(简)
、搜索功能的实现
、MVP模式的思考
、单元测试(简)
、App的混淆打包
、将App上传到服务器,提供下载地址
、
最外层是一个DrawerLayout并和Toolbar相关联 DrawerLayout主要分为左和中间两块,核心的是中间,左边顺带用一下NavigationView 中间主页面由AppBarLayout+CollapsingToolbarLayout+Toolbar祖孙三人打头阵 中间主题由RecyclerView骁勇杀敌,最底下由BottomNavigationBar收尾 另外FloatingActionButton+bottom_sheet补刀,bottom_sheet中藏着搜索功能
布局概览.png
总体来说和网页端风格保持一致
Android原生版 | 网页版手机端 |
---|---|
| |
布局就不贴了,挺多的,也没什么技术含量,有兴趣的看源码吧
有关材料设计,我写过一个系列:详见--Android材料设计Material Design 开篇前言
为了方便起见,我写了一个IconItem类,并定义了一个常量数组:
------------------
public class IconItem {
private int color;
private int iconId;
private String info;
//其他省略...
}
------------------
public static final IconItem[] BNB_ITEM = new IconItem[]{
new IconItem("Android", R.drawable.icon_android, R.color.color4Android),
new IconItem("Spring", R.drawable.icon_spring_boot, R.color.color4SpringBoot),
new IconItem("React", R.drawable.icon_react, R.color.color4React),
new IconItem("编程随笔", R.drawable.icon_note, R.color.color4Note),
new IconItem("系列文章", R.drawable.icon_code, R.color.color4Ser),
};
------------------使用:---
IconItem[] items = Cons.BNB_ITEM;
for (IconItem item : items) {
mIdBnb.addItem(new BottomNavigationItem(item.getIconId(), item.getInfo())
.setActiveColorResource(item.getColor()));
}
mIdBnb.initialise();
//每转一圈,换一种颜色
mIdSrl.setColorSchemeColors(
0xffF60C0C,//红
0xffF3B913,//橙
0xffE7F716,//黄
0xff3DF30B,//绿
0xff0DF6EF,//青
0xff0829FB,//蓝
0xffB709F4//紫
);
mIdSrl.setOnRefreshListener(() -> {
//TODO刷新逻辑
});
------------------------------
mABDT = new ActionBarDrawerToggle(
this, mIdDlRoot, mToolbar, R.string.str_open, R.string.str_close);
mIdDlRoot.addDrawerListener(mABDT);
------------------------------
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
mABDT.syncState();//加了这个才有酷炫的按钮变化
}
mBottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
mIdFab.setOnClickListener(v -> {
if (isOpen) {
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
isOpen = !isOpen;
});
祖孙三头.gif
移出 | 移入 |
---|---|
| |
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/30 0030:14:34<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:FloatingActionButton伴随动画
*/
public class FabFollowListBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
private static final int MIN_DY = 30;
public FabFollowListBehavior(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
/**
* 初始时不调用,滑动时调用---一次滑动过程,之调用一次
*/
@Override
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull FloatingActionButton child,
@NonNull View directTargetChild,
@NonNull View target, int axes, int type) {
return true;
}
/**
* @param dyConsumed 每次回调前后的Y差值
*/
@Override
public void onNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull FloatingActionButton child,
@NonNull View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
//平移隐现
if (dyConsumed > MIN_DY) {//上滑:消失
showOrNot(coordinatorLayout, child, false).start();
} else if (dyConsumed < -MIN_DY) {//下滑滑:显示
showOrNot(coordinatorLayout, child, true).start();
}
//仅滑动时消失
// if (dyConsumed > MIN_DY || dyConsumed < -MIN_DY) {//上滑:消失
// showOrNot(child).start();
// }
}
private Animator showOrNot(CoordinatorLayout coordinatorLayout, final View fab, boolean show) {
//获取fab头顶的高度
int hatHeight = coordinatorLayout.getBottom() - fab.getBottom() + fab.getHeight();
int end = show ? 0 : hatHeight;
float start = fab.getTranslationY();
ValueAnimator animator = ValueAnimator.ofFloat(start, end);
animator.addUpdateListener(animation ->
fab.setTranslationY((Float) animation.getAnimatedValue()));
return animator;
}
private Animator showOrNot(final View fab) {
//获取fab头顶的高度
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.addUpdateListener(animation -> {
fab.setScaleX((Float) animation.getAnimatedValue());
fab.setScaleY((Float) animation.getAnimatedValue());
});
return animator;
}
}
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/30 0030:9:35<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:BottomNavigationBar伴随列表显隐的Behavior
*/
public class BnbFollowListBehavior extends BottomVerticalScrollBehavior<BottomNavigationBar> {
public BnbFollowListBehavior(Context context, AttributeSet attributeSet) {
super();
}
}
<string name="followListBehavior">com.toly1994.mycode.app.behavior.BnbFollowListBehavior</string>
<string name="behavior_fab_follow">com.toly1994.mycode.app.behavior.FabFollowListBehavior</string>
FloatingActionButton伴随动画定义在FloatingActionButton伴随动画按钮的标签内 BottomNavigationBar伴随列表显隐的Behavior 写在RecyclerView标签内 Behavior的详细介绍可见:Android材料设计之Behavior攻坚战
蓝色白斜字是接口
橙色虚线是类方法的引线
蓝色虚线是流程线
天蓝色的是普通类
左中右分别是MPV,模型层(M)负责数据的获取,通过Callback回调在控制层(P)使用
控制层(P)注意进行模型层(M)和视图层(V)的粘合,通过逻辑进行不同的视图展现
也就是说我在写P的实现类中,管你MV怎么实现的么,你家老子(M,V的接口)在我手上,我还怕什么
在写视图层(V)时,V手里也有控制层的老子(P的接口),所以V也是怎么想的
所以无论写视图层,数据层,控制层,只要把接口定义好,便可以分工去写,互不影响
这也就是面相接口编程的有点,有些人视图非常棒,可以专门做视图层,
网络、数据库强的可以专门做模型层等等...
就像找1个全才和找3个精通某一门的人去做同一件事一样,理论上来说,后者做的会更周到,更轻松。
分工明确有助于思路的清晰和方法的复用
MVP思路.png
把ILoadingView直接放到INoteView也可以,看个人喜好吧
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/14 0014:7:49<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:加载和加载完毕的视图
*/
public interface ILoadingView {
/**
* 正在加载
*/
void loading();
/**
* 加载完毕
*/
void loaded();
}
----------------------------------------
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/14 0014:7:48<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:视图层核心
*/
public interface INoteView<T> extends ILoadingView {
/**
* 页面渲染数据
* @param dataList
*/
void reader(List<T> dataList);
/**
* 页面处理错误
* @param e
*/
void error(ErrorEnum e);
}
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/14 0014:20:27<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:控制层
*/
public interface IPresenter<T> {
/**
* 根据所属区域更新视图
*
* @param area 范围
* @param offset 查询偏移值
* @param count 查询条数
*/
void updateByArea(String area, int offset, int count);
/**
* 根据查询名称更新视图
*
* @param name 范围
* @param offset 查询偏移值
* @param count 查询条数
*/
void updateByName(String name, int offset, int count);
}
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/14 0014:13:43<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:数据模型层
*/
public interface INoteModel<T> {
/**
* 查询所有
* @param callback 回调
* @param offset 查询偏移值
* @param page 查询条数
*/
void getData(Callback<T> callback, int offset, int page);
/**
* 根据所属区域查询数据
* @param callback 回调
* @param area 范围
* @param offset 查询偏移值
* @param page 查询条数
*/
void getDataByArea(Callback<T> callback, String area, int offset, int page);
/**
* 根据名称查询数据(搜索)
* @param callback 回调
* @param name 范围
* @param offset 查询偏移值
* @param page 查询条数
*/
void getDataByName(Callback<T> callback, String name, int offset, int page);
/**
* 插入模型
* @param params
*/
void insertModel(Map<String, String> params);
}
----------------------------模型层数据回调接口-----
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/14 0014:13:43<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:模型层数据回调接口
*/
public interface Callback<T> {
/**
* 开始加载
*/
void onStartLoad();
/**
* 成功
* @param dataList 数据
*/
void onSuccess(List<T> dataList);
/**
* 错误
* @param e 错误
*/
void onError(ErrorEnum e);
}
可以自定义错误类型,以便之后根据不同错误显示不同界面
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/14 0014:7:58<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:错误类型
*/
public enum ErrorEnum {
EXCEPTION(500, "服务器"),
NOT_FOUND(102, "未知id"),
IO(1, "IO异常"),
NO_NET(2, "无网络"),
NET_LINK(3, "网络连接异常");
private int code;
private String msg;
ErrorEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
数据是核心,先把数据拿在手上,心理才踏实,使用Retrofit+RxJava 下图是最简单的Retrofit+RxJava获取数据的方式
//rxjava2
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.4.0'//核心库
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'//json转换器
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'//配合Rxjava 使用
RX+Ret.png
3.1:接口先行:NoteApi.java
在此之前回顾一下服务器的接口
----查询所有:http://192.168.43.60:8089/api/android/note
----查询偏移12条,查询12条(即12条为一页的第2页):
http://192.168.43.60:8089/api/android/note/12/12
----按区域查询(A为Android数据,SB为SpringBoot数据,Re为React数据)
http://192.168.43.60:8089/api/android/note/area/A
http://192.168.43.60:8089/api/android/note/area/A/12/12
----按部分名称查询
http://192.168.43.60:8089/api/android/note/name/材料
http://192.168.43.60:8089/api/android/note/name/材料/2/2
----按类型名称查询(类型定义表见第一篇)
http://192.168.43.60:8089/api/android/note/name/ABCS
http://192.168.43.60:8089/api/android/note/name/ABCS/2/2
----按id名称查:http://192.168.43.60:8089/api/android/note/12
添-POST请求:http://192.168.43.60:8089/api/android/note
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/13 0013:19:48<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:API接口
*/
public interface NoteApi {
/**
* 查询所有操作
*/
@GET("api/android/note/{offset}/{page}")
Observable<ResultBean> findAll(@Path("offset") int offset, @Path("page") int page);
/**
* 根据范围查询
*/
@GET("api/android/note/area/{op}/{offset}/{page}")
Observable<ResultBean> findByArea(@Path("op") String op, @Path("offset") int offset, @Path("page") int page);
/**
* 根据类型查询
*/
@GET("api/android/note/type/{type}/{offset}/{page}")
Observable<ResultBean> findByType(@Path("type") String op, @Path("offset") int offset, @Path("page") int page);
/**
* 根据名字查询
*/
@GET("api/android/note/name/{type}/{offset}/{page}")
Observable<ResultBean> findByName(@Path("type") String type, @Path("offset") int offset, @Path("page") int page);
/**
* 插入操作
*/
@FormUrlEncoded
@POST("api/android/note")
Observable<ResultBean> insert(@FieldMap Map<String, String> params);
}
这个和后端的实体类保持一直,你可以直接用AS的插件直接生成
也可以把后端的实体类拿来用,挺长的,不贴了,没有技术含量,详见源码
public class NoteModel implements INoteModel<ResultBean.NoteBean> {
private static final String TAG = "NoteModel";
private NoteApi mNoteApi;
public NoteModel() {
mNoteApi = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())//json转换成JavaBean
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(BASE_URL)
.build().create(NoteApi.class);
}
@Override
public void getData(Callback<ResultBean.NoteBean> callback, int offset, int page) {
callback.onStartLoad();
doSubscribe(callback, mNoteApi.findAll(offset, page));
}
@Override
public void getDataByArea(Callback<ResultBean.NoteBean> callback, String area, int offset, int page) {
callback.onStartLoad();
doSubscribe(callback, mNoteApi.findByArea(area, offset, page));
}
@Override
public void getDataByName(Callback<ResultBean.NoteBean> callback, String name, int offset, int page) {
callback.onStartLoad();
doSubscribe(callback, mNoteApi.findByName(name, offset, page));
}
/**
* 执行api返回的Observable
*
* @param callback 回调函数
* @param apiAll Observable
*/
private void doSubscribe(Callback<ResultBean.NoteBean> callback, Observable<ResultBean> apiAll) {
apiAll.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ResultBean>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(ResultBean resultBean) {
callback.onSuccess(resultBean.getData());
}
@Override
public void onError(Throwable e) {
callback.onError(ErrorEnum.NET_LINK);
}
@Override
public void onComplete() {
}
});
}
}
这里做一些单元测试,因为还没有实现P和V,看模型层是否正确,最后的方法就是单元测试 安卓里的单元测试很简单,这里获取数据比对一下条数,通过则说明数据是对的
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void getAllData() {
NoteModel model = new NoteModel();
model.getData(new Callback<ResultBean.NoteBean>() {
@Override
public void onStartLoad() {
}
@Override
public void onSuccess(List<ResultBean.NoteBean> dataList) {
assertEquals(12, dataList.size());
}
@Override
public void onError(ErrorEnum e) {
}
}, 0, 12);
}
@Test
public void getDataByName() {
NoteModel model = new NoteModel();
model.getDataByName(new Callback<ResultBean.NoteBean>() {
@Override
public void onStartLoad() {
}
@Override
public void onSuccess(List<ResultBean.NoteBean> dataList) {
assertEquals(12, dataList.size());
}
@Override
public void onError(ErrorEnum e) {
}
}, "A", 0, 12);
}
}
单元测试.png
ok,测试通过,去视图层吧
HomePagerView.java
findViewByid就不写了...,loading使用SwipeRefreshLayout
private RecyclerView mHomeRv;//RecyclerView
private SwipeRefreshLayout mIdSrl;//下拉刷新
private IPresenter<ResultBean.NoteBean> mPagerPresenter;//控制层
@Override
public void reader(List<ResultBean.NoteBean> dataList) {
HomeAdapter ListAdapter = new HomeAdapter(dataList);
mHomeRv.setAdapter(ListAdapter);
LinearLayoutManager llm = new LinearLayoutManager(this);
GridLayoutManager gm = new GridLayoutManager(this, 2);
mHomeRv.setLayoutManager(gm);
}
@Override
public void loading() {
mIdSrl.setRefreshing(true);
}
@Override
public void loaded() {
mIdSrl.setRefreshing(false);
}
为了方便,这里用Picasso加载网络图片,自带缓存功能
public class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder> {
private Context mContext;
private List<ResultBean.NoteBean> mData;
public HomeAdapter(List<ResultBean.NoteBean> data) {
mData = data;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
mContext = parent.getContext();
View view = LayoutInflater.from(mContext).inflate(R.layout.item_a_card, parent, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
ResultBean.NoteBean note = mData.get(position);
if (note.getName().equals(mData.get(0).getName())) {
holder.mIdNewTag.setVisibility(View.VISIBLE);
} else {
holder.mIdNewTag.setVisibility(View.GONE);
}
Picasso.get()
.load(note.getImgUrl())
.into(holder.mIvCover);
holder.mIvTvTitle.setText(note.getName());
holder.mIdTvType.setText(note.getType());
}
@Override
public int getItemCount() {
return mData.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
public View mIdNewTag;
public TextView mIvTvTitle;
public ImageView mIvCover;
public TextView mIdTvType;
public MyViewHolder(View itemView) {
super(itemView);
mIvTvTitle = itemView.findViewById(R.id.iv_tv_title);
mIvCover = itemView.findViewById(R.id.iv_cover);
mIdTvType = itemView.findViewById(R.id.id_tv_type);
mIdNewTag = itemView.findViewById(R.id.id_new_tag);
}
}
}
前两层实现之后,这层就简单了
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/14 0014:13:57<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:控制层
*/
public class PagerPresenter extends BasePresenter implements IPresenter<ResultBean.NoteBean> {
private INoteView<ResultBean.NoteBean> mNoteView;
private INoteModel<ResultBean.NoteBean> mModel;
private Callback<ResultBean.NoteBean> mCallback;
public PagerPresenter(INoteView<ResultBean.NoteBean> noteView) {
mNoteView = noteView;
mModel = new NoteModel();
initCallBack();
}
private void initCallBack() {//初始化回调函数
mCallback = new Callback<ResultBean.NoteBean>() {
@Override
public void onStartLoad() {
mNoteView.loading();
}
@Override
public void onSuccess(List<ResultBean.NoteBean> dataList) {
mNoteView.reader(dataList);
mNoteView.loaded();
}
@Override
public void onError(ErrorEnum e) {
mNoteView.error(e);
mNoteView.loaded();
}
};
}
@Override
public void updateByArea(String area, int offset, int count) {
mModel.getDataByArea(mCallback, area, offset, count);
}
@Override
public void updateByName(String name, int offset, int count) {
mModel.getDataByName(mCallback, name, offset, count);
}
}
HomePagerView里,两句话
mPagerPresenter = new PagerPresenter(this);
mPagerPresenter.updateByArea("A", 0, 12);
下拉刷新 | 点击切换 |
---|---|
| |
就这么简单
mIdSrl.setOnRefreshListener(() -> {
mPagerPresenter.updateByArea(area, 0, 1000);
});
也就是根据点击出判断类型,根据类型使用控制层刷新视图
private String area = "A";
------------------------------------------
mIdBnb.setTabSelectedListener(new BottomNavigationBar.OnTabSelectedListener() {
@Override
public void onTabSelected(int position) {
switch (position) {
case 0:
area = "A";
mIdCtlBar.setTitle("Android技术栈");
mIdIvHead.setImageResource(R.mipmap.bg_android);
break;
case 1:
area = "SB";
mIdCtlBar.setTitle("SpringBoot技术栈");
mIdIvHead.setImageResource(R.mipmap.bg_springboot);
break;
case 2:
area = "Re";
mIdCtlBar.setTitle("React技术栈");
mIdIvHead.setImageResource(R.mipmap.bg_react);
break;
case 3:
area = "Note";
mIdCtlBar.setTitle("随笔编程杂谈录");
mIdIvHead.setImageResource(R.mipmap.menu_bg);
break;
case 4:
area = "A";
mIdCtlBar.setTitle("系列文章");
break;
}
mPagerPresenter.updateByArea(area, 0, 1000);
}
@Override
public void onTabUnselected(int position) {
}
@Override
public void onTabReselected(int position) {
}
);
添加功能 | 搜索功能 |
---|---|
| |
也就是根据名称匹配输入字符,再去查询, 点击是str是输入框字符串,执行mPagerPresenter的updateByName
mPagerPresenter.updateByName(str, 0, 1000);
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
isOpen = false;
这个稍微有点麻烦,需要一个视图对话框
//接口---NoteApi
@FormUrlEncoded
@POST("api/android/note")
Observable<ResultBean> insert(@FieldMap Map<String, String> params);
//模型层---NoteModel
@Override
public void insertModel(Map<String, String> params) {
doSubscribe(null, mNoteApi.insert(params));
}
//控制层---PagerPresenter
@Override
public void addItem(Map<String, String> params) {
mModel.insertModel(params);
}
//视图层:HomePagerView
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.tab_add:
doAdd(this)
break;
}
return super.onOptionsItemSelected(item);
}
public static void doAdd(Context context) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_add, null);
EditText title = dialogView.findViewById(R.id.et_upload_title);
EditText url = dialogView.findViewById(R.id.et_upload_path);
DatePicker cost_date = dialogView.findViewById(R.id.cost_date);
builder.setTitle("添加文章");
builder.setView(dialogView);
builder.setPositiveButton("确定", (dialog, which) -> {
String createTime = cost_date.getYear() + "-" + (cost_date.getMonth() + 1) + "-" + cost_date.getDayOfMonth();
ResultBean.NoteBean noteBean = new ResultBean.NoteBean();
String name = title.getText().toString();
String jianshuUrl = url.getText().toString();
String imgUrl = "8a11d27d58f4c1fa4488cf39fdf68e76.png";
noteBean.setImgUrl(imgUrl);
Map<String, String> hashMap = new HashMap<>();
hashMap.put("type","C");
hashMap.put("name",name);
hashMap.put("jianshuUrl",jianshuUrl);
hashMap.put("juejinUrl","---");
hashMap.put("imgUrl",imgUrl);
hashMap.put("createTime",createTime);
hashMap.put("info","hh");
hashMap.put("area","A");
hashMap.put("localPath","---");
mPagerPresenter.addItem(params);
});
builder.setNegativeButton("取消", null);
builder.create().show();
}
-----app/build.gradle------开启混淆
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
----app/proguard-rules.pro------混淆配置
-ignorewarnings#忽略警告
# Retrofit
-dontnote retrofit2.Platform
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
-dontwarn retrofit2.Platform$Java8
-keepattributes Signature
-keepattributes Exceptions
# okhttp
-dontwarn okio.**
# Gson
-keep class com.toly1994.mycode.bean.**{*;} # 自定义数据模型的bean目录
混淆打包后,差不多比debug的包小一半,感觉还不错,亲测可用
签名.png
ttt.png
好吧,不是上传到各大市场,毕竟现在个人app很难上去 在前端界面上提供下载地址,很简单,拷到服务器上就行了,然后访问就能下载了
下载.png
这样点击时就能下载了
下载3.png
下载2.png
基本上的点都讲到了,虽然不是面面俱到,整体hold住就差不多了 源码在最后,有兴趣的可以看看,总结以下,到此为止,用了五天的时间做了以下事:
1.使用SpringBoot结合Mybatis搭建了一个Restful接口的线上服务端
2.使用Python的selenium库爬取简书主页的文章信息并用java将数据通过网络请求插入数据库
3.使用React搭建前端显示界面,scss的样式使用和axios的网络请求以及移动端的网页适配
4.使用Java基于Android构建一个材料设计风格的移动端应用,以及上线
5.写了这四篇长文,总的来说还是很有收获的,最起码知识串起来了