本系列分為四篇:
零、前言
本系列為了總結一下手上的知識,致敬我的2018
本篇的重點在於:後端資料在移動端的展現
本篇總結的技術點:
材料設計串燒
、Retrofit+RxJava訪問請求
、Retrofit提交表單
、Retrofit快取的實現(簡)
、
搜尋功能的實現
、MVP模式的思考
、單元測試(簡)
、App的混淆打包
、將App上傳到伺服器,提供下載地址
、
一、材料設計的綜合使用:
1.佈局概覽
最外層是一個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
移出 | 移入 |
---|---|
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個精通某一門的人去做同一件事一樣,理論上來說,後者做的會更周到,更輕鬆。
分工明確有助於思路的清晰和方法的複用
複製程式碼
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 使用
複製程式碼
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);
}
}
複製程式碼
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的包小一半,感覺還不錯,親測可用
3.上線
好吧,不是上傳到各大市場,畢竟現在個人app很難上去
在前端介面上提供下載地址,很簡單,拷到伺服器上就行了,然後訪問就能下載了
4.前端React稍微修改:
這樣點選時就能下載了
基本上的點都講到了,雖然不是面面俱到,整體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----看到這裡,我在此感謝你的喜歡與支援