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

張風捷特烈發表於2018-12-15
本系列分為四篇:

零、前言

本系列為了總結一下手上的知識,致敬我的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原生版 網頁版手機端
建站四部曲之移動端篇(Android+上線)
建站四部曲之移動端篇(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

移出 移入
建站四部曲之移動端篇(Android+上線)
建站四部曲之移動端篇(Android+上線)
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);
複製程式碼

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


三、相關操作

1.下拉重新整理和點選切換:
1.1:效果一覽
下拉重新整理 點選切換
建站四部曲之移動端篇(Android+上線)
建站四部曲之移動端篇(Android+上線)

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

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章