建站四部曲之移動端篇(Android+上線)

本系列分爲四篇:

零、前言

本系列爲了總結一下手上的知識,致敬個人2018
本篇的重點在於:後端數據在移動端的展示
本篇總結的技術點:
材料設計串燒Retrofit+RxJava訪問請求Retrofit提交表單Retrofit緩存的實現(簡)
搜索功能的實現MVP模式的思考單元測試(簡)App的混淆打包將App上傳到服務器,提供下載地址css


1、材料設計的綜合使用:

1.佈局概覽

最外層是一個DrawerLayout並和Toolbar相關聯
DrawerLayout主要分爲左和中間兩塊,核心的是中間,左邊順帶用一下NavigationView
中間主頁面由AppBarLayout+CollapsingToolbarLayout+Toolbar祖孫三人打頭陣
中間主題由RecyclerView驍勇殺敵,最底下由BottomNavigationBar收尾
另外FloatingActionButton+bottom_sheet補刀,bottom_sheet中藏着搜索功能前端

佈局概覽.png


2.效果圖一覽

整體來講和網頁端風格保持一致java

Android原生版 網頁版手機端

3.佈局與材料設計的控件使用

佈局就不貼了,挺多的,也沒什麼技術含量,有興趣的看源碼吧
有關材料設計,我寫過一個系列:詳見--Android材料設計Material Design 開篇前言react

3.1:BottomNavigationBar的使用:

爲了方便起見,我寫了一個IconItem類,並定義了一個常量數組:android

------------------
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攻堅戰ios


2、MVP的思路

1.概述:
藍色白斜字是接口
橙色虛線是類方法的引線
藍色虛線是流程線
天藍色的是普通類
複製代碼
左中右分別是MPV,模型層(M)負責數據的獲取,經過Callback回調在控制層(P)使用
控制層(P)注意進行模型層(M)和視圖層(V)的粘合,經過邏輯進行不一樣的視圖展示
也就是說我在寫P的實現類中,管你MV怎麼實現的麼,你家老子(M,V的接口)在我手上,我還怕什麼
在寫視圖層(V)時,V手裏也有控制層的老子(P的接口),因此V也是怎麼想的

因此不管寫視圖層,數據層,控制層,只要把接口定義好,即可以分工去寫,互不影響  
這也就是面相接口編程的有點,有些人視圖很是棒,能夠專門作視圖層,
網絡、數據庫強的能夠專門作模型層等等...

就像找1個全才和找3個精通某一門的人去作同一件事同樣,理論上來講,後者作的會更周到,更輕鬆。
分工明確有助於思路的清晰和方法的複用
複製代碼

MVP思路.png


2.接口先搞起來

把ILoadingView直接放到INoteView也能夠,看我的喜愛吧git

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.錯誤類型枚舉

能夠自定義錯誤類型,以便以後根據不一樣錯誤顯示不一樣界面github

/**
 * 做者:張風捷特烈<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獲取數據的方式spring

//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);
複製代碼


3、相關操做

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();
    }
複製代碼

4、混淆打包和上線

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-github 2018-12-15 建站四部曲之移動端篇(Android+上線)
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
個人github 個人簡書 個人掘金 我的網站
3.聲明

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持


icon_wx_200.png
相關文章
相關標籤/搜索