本系列分为四篇:
零、前言
本系列为了总结一下手上的知识,致敬我的2018
本篇的重点在于:后端数据在移动端的展现
本篇总结的技术点:
材料设计串烧
、Retrofit+RxJava访问请求
、Retrofit提交表单
、Retrofit缓存的实现(简)
、
搜索功能的实现
、MVP模式的思考
、单元测试(简)
、
App的混淆打包
、将App上传到服务器,提供下载地址
、
一、材料设计的综合使用:
1.布局概览
布局概览.png最外层是一个DrawerLayout并和Toolbar相关联
DrawerLayout主要分为左和中间两块,核心的是中间,左边顺带用一下NavigationView
中间主页面由AppBarLayout+CollapsingToolbarLayout+Toolbar祖孙三人打头阵
中间主题由RecyclerView骁勇杀敌,最底下由BottomNavigationBar收尾
另外FloatingActionButton+bottom_sheet补刀,bottom_sheet中藏着搜索功能
2.效果图一览
总体来说和网页端风格保持一致
Android原生版 | 网页版手机端 |
---|---|
3.布局与材料设计的控件使用
布局就不贴了,挺多的,也没什么技术含量,有兴趣的看源码吧
有关材料设计,我写过一个系列:详见--Android材料设计Material Design 开篇前言
3.1:BottomNavigationBar的使用:
为了方便起见,我写了一个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();
3.2:SwipeRefreshLayout的使用:
//每转一圈,换一种颜色
mIdSrl.setColorSchemeColors(
0xffF60C0C,//红
0xffF3B913,//橙
0xffE7F716,//黄
0xff3DF30B,//绿
0xff0DF6EF,//青
0xff0829FB,//蓝
0xffB709F4//紫
);
mIdSrl.setOnRefreshListener(() -> {
//TODO刷新逻辑
});
3.3:DrawerLayout与Toolbar的结合
------------------------------
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();//加了这个才有酷炫的按钮变化
}
3.4:BottomSheet与FloatingActionButton的结合
mBottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
mIdFab.setOnClickListener(v -> {
if (isOpen) {
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
isOpen = !isOpen;
});
4.伴随移动的Behavior
祖孙三头.gif移出 | 移入 |
---|---|
FloatingActionButton伴随动画:FabFollowListBehavior
/**
* 作者:张风捷特烈<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;
}
}
BottomNavigationBar伴随列表显隐的Behavior:BnbFollowListBehavior
/**
* 作者:张风捷特烈<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.xml里,方便修改
<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攻坚战
二、MVP的思路
1.概述:
蓝色白斜字是接口
橙色虚线是类方法的引线
蓝色虚线是流程线
天蓝色的是普通类
左中右分别是MPV,模型层(M)负责数据的获取,通过Callback回调在控制层(P)使用
控制层(P)注意进行模型层(M)和视图层(V)的粘合,通过逻辑进行不同的视图展现
也就是说我在写P的实现类中,管你MV怎么实现的么,你家老子(M,V的接口)在我手上,我还怕什么
在写视图层(V)时,V手里也有控制层的老子(P的接口),所以V也是怎么想的
所以无论写视图层,数据层,控制层,只要把接口定义好,便可以分工去写,互不影响
这也就是面相接口编程的有点,有些人视图非常棒,可以专门做视图层,
网络、数据库强的可以专门做模型层等等...
就像找1个全才和找3个精通某一门的人去做同一件事一样,理论上来说,后者做的会更周到,更轻松。
分工明确有助于思路的清晰和方法的复用
MVP思路.png
2.接口先搞起来
把ILoadingView直接放到INoteView也可以,看个人喜好吧
2.1.视图层核心
/**
* 作者:张风捷特烈<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);
}
2.2.控制层:
/**
* 作者:张风捷特烈<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);
}
2.3.模型层
/**
* 作者:张风捷特烈<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);
}
2.4.错误类型枚举
可以自定义错误类型,以便之后根据不同错误显示不同界面
/**
* 作者:张风捷特烈<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;
}
}
3.模型层的实现
数据是核心,先把数据拿在手上,心理才踏实,使用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);
}
3.2:ResultBean和NoteBean实体类
这个和后端的实体类保持一直,你可以直接用AS的插件直接生成
也可以把后端的实体类拿来用,挺长的,不贴了,没有技术含量,详见源码
3.3:获取数据核心逻辑
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() {
}
});
}
}
3.4:测试接口(单元测试)
这里做一些单元测试,因为还没有实现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,测试通过,去视图层吧
4.视图层的实现:HomePagerView.java
findViewByid就不写了...,loading使用SwipeRefreshLayout
4.1:方法的实现
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);
}
4.2:RecyclerView的适配器
为了方便,这里用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);
}
}
}
5.控制层
前两层实现之后,这层就简单了
/**
* 作者:张风捷特烈<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);
}
}
6.运作:HomePagerView里,两句话
mPagerPresenter = new PagerPresenter(this);
mPagerPresenter.updateByArea("A", 0, 12);
三、相关操作
1.下拉刷新和点击切换:
1.1:效果一览
下拉刷新 | 点击切换 |
---|---|
1.2:下拉刷新
就这么简单
mIdSrl.setOnRefreshListener(() -> {
mPagerPresenter.updateByArea(area, 0, 1000);
});
1.3:点击切换
也就是根据点击出判断类型,根据类型使用控制层刷新视图
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) {
}
);
2.添加和搜索功能
添加功能 | 搜索功能 |
---|---|
2.1:搜索功能:
也就是根据名称匹配输入字符,再去查询,
点击是str是输入框字符串,执行mPagerPresenter的updateByName
mPagerPresenter.updateByName(str, 0, 1000);
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
isOpen = false;
2.2:添加操作
这个稍微有点麻烦,需要一个视图对话框
//接口---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();
}
四、混淆打包和上线
1.混淆:
-----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目录
2.签名打包
签名.png ttt.png混淆打包后,差不多比debug的包小一半,感觉还不错,亲测可用
3.上线
好吧,不是上传到各大市场,毕竟现在个人app很难上去
在前端界面上提供下载地址,很简单,拷到服务器上就行了,然后访问就能下载了
下载.png
4.前端React稍微修改:
下载3.png 下载2.png这样点击时就能下载了
基本上的点都讲到了,虽然不是面面俱到,整体hold住就差不多了
源码在最后,有兴趣的可以看看,总结以下,到此为止,用了五天的时间做了以下事:
1.使用SpringBoot结合Mybatis搭建了一个Restful接口的线上服务端
2.使用Python的selenium库爬取简书主页的文章信息并用java将数据通过网络请求插入数据库
3.使用React搭建前端显示界面,scss的样式使用和axios的网络请求以及移动端的网页适配
4.使用Java基于Android构建一个材料设计风格的移动端应用,以及上线
5.写了这四篇长文,总的来说还是很有手绘的,最起码知识串起来了
后记:捷文规范
1.本文成长记录及勘误表
项目源码 | 日期 | 备注 |
---|---|---|
V0.1-github | 2018-12-15 | 建站四部曲之移动端篇(Android+上线) |
2.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的掘金 | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
icon_wx_200.png
网友评论