建站四部曲之移动端篇(Android+上线)

2,877 阅读12分钟
本系列分为四篇:

零、前言

本系列为了总结一下手上的知识,致敬我的2018
本篇的重点在于:后端数据在移动端的展现
本篇总结的技术点:
材料设计串烧Retrofit+RxJava访问请求Retrofit提交表单Retrofit缓存的实现(简)
搜索功能的实现MVP模式的思考单元测试(简)App的混淆打包将App上传到服务器,提供下载地址


一、材料设计的综合使用:

1.布局概览

最外层是一个DrawerLayout并和Toolbar相关联
DrawerLayout主要分为左和中间两块,核心的是中间,左边顺带用一下NavigationView
中间主页面由AppBarLayout+CollapsingToolbarLayout+Toolbar祖孙三人打头阵
中间主题由RecyclerView骁勇杀敌,最底下由BottomNavigationBar收尾
另外FloatingActionButton+bottom_sheet补刀,bottom_sheet中藏着搜索功能

布局概览.png


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数据,SBSpringBoot数据,ReReact数据)
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.签名打包

混淆打包后,差不多比debug的包小一半,感觉还不错,亲测可用

签名.png

ttt.png


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-github2018-12-15建站四部曲之移动端篇(Android+上线)
2.更多关于我
笔名QQ微信爱好
张风捷特烈1981462002zdl1994328语言
我的github我的简书我的掘金个人网站
3.声明

1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持


icon_wx_200.png