This series is divided into four parts:
  • Site construction tetralogy of back-end interface (SpringBoot+ online)
  • Python Data Crawler (Selenium)
  • Site construction tetralogy before the display (React+ online)
  • Mobile Terminal part of tetralogy of website construction (Android+ online)

Zero, preface,

This series is to summarize the knowledge at hand and pay tribute to my 2018

The key points of this paper are: Back-end data display on mobile terminal Material design collation, Retrofit+RxJava access request, Retrofit submission form, Retrofit cache implementation (simple), search function implementation, MVP mode thinking, unit testing (simple), App confusion packaging, App upload to the server, provide download address,


1. Comprehensive use of material design:

1. Layout overview

The outermost layer is a DrawerLayout associated with the Toolbar

DrawerLayout is mainly divided into the left and the middle, and the core is the middle. In the left NavigationView the main page is led by AppBarLayout+CollapsingToolbarLayout+Toolbar and the middle theme is created by RecyclerView In addition, bottom_sheet + FloatingActionButton. Bottom_sheet contains search function


2. Take in the renderings

Generally speaking, it is consistent with the style of the web side

The Android native version Web version of mobile

3. Use of layout and material design controls

Layout is not posted, a lot of, there is no technical content, interested in the source code

I have written a series about Material Design: see the introduction of Material Design for Android

BottomNavigationBar = BottomNavigationBar

For convenience, I write an IconItem class and define a constant array:

------------------ public class IconItem { private int color; private int iconId; private String info; // Other ellipses... } ------------------ 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, Color4SpringBoot), new IconItem("React", R.dor rawable. Icon_react, R.Dor. Color4SpringBoot), new IconItem("React", R.dor. Color4Note), new IconItem(" series article ", R.rawable. Icon_code, R.Color. Color4Ser),}; -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- use: - IconItem [] the items = Cons. BNB_ITEM; for (IconItem item : items) { mIdBnb.addItem(new BottomNavigationItem(item.getIconId(), item.getInfo()) .setActiveColorResource(item.getColor())); } mIdBnb.initialise();Copy the code

Use SwipeRefreshLayout:
// For each turn, In a different color mIdSrl. SetColorSchemeColors (red xfff3b913 0 0 xfff60c0c, / /, / / orange 0 xffe7f716, / / yellow 0 xff3df30b, / / green 0 xff0df6ef, / / blue 0xff0829FB,// blue 0xffB709F4// purple); MIdSrl. SetOnRefreshListener (() - > {} / / TODO refresh logic).Copy the code

3.3: Combination of DrawerLayout and 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(); // Add this to the cool button changes}Copy the code

3.4: Combination of BottomSheet and FloatingActionButton
mBottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet); mIdFab.setOnClickListener(v -> { if (isOpen) { mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } else { mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); } isOpen = ! isOpen; });Copy the code

4. Behavior accompanying movement

Removed from the move
FloatingActionButton Accompanying animation :FabFollowListBehavior
/** * Author: Zhang Feng Jiete Lie <br/> * Time: 2018/11/300030:14:34 <br/> * Email: [email protected]<br/> * Description: FloatingActionButton with animation * / public class FabFollowListBehavior extends CoordinatorLayout. Behaviors < FloatingActionButton > { private static final int MIN_DY = 30; public FabFollowListBehavior(Context context, AttributeSet attributeSet) { super(context, attributeSet); } /** ** */ @override public Boolean onStartNestedScroll(@nonnull CoordinatorLayout) @NonNull FloatingActionButton child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { return true; } /** * @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); ShowOrNot (coordinatorLayout, child, false).start(); } else if (dyConsumed < -min_dy) {// Slide: show showOrNot(coordinatorLayout, Child, true).start(); } / / disappear just slide / / if (dyConsumed > MIN_DY | | dyConsumed < - MIN_DY) {/ / slide: disappear / / showOrNot (child). The start (); // } } private Animator showOrNot(CoordinatorLayout coordinatorLayout, final View fab, Boolean show) {/ / get the height of the overall overhead 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) {// Obtain the height of the fab head. ValueAnimator Animator = valueAnimator. ofFloat(0, 1); animator.addUpdateListener(animation -> { fab.setScaleX((Float) animation.getAnimatedValue()); fab.setScaleY((Float) animation.getAnimatedValue()); }); return animator; }}Copy the code
BottomNavigationBar Behavior: BnbFollowListBehavior
/** * Author: Zhang Feng Jiete Lie <br/> * Time: 2018/11/3000309:35 <br/> * Email: [email protected]<br/> * Description: BottomNavigationBar Behavior */ public class BnbFollowListBehavior extends BottomVerticalScrollBehavior<BottomNavigationBar> { public BnbFollowListBehavior(Context context, AttributeSet attributeSet) { super(); }}Copy the code

It is recommended to write in the string. XML, which is easy to modify
<string name="followListBehavior">com.toly1994.mycode.app.behavior.BnbFollowListBehavior</string>
<string name="behavior_fab_follow">com.toly1994.mycode.app.behavior.FabFollowListBehavior</string>
Copy the code

FloatingActionButton Accompanying animation is defined inside the label of the FloatingActionButton accompanying animation button

The BottomNavigationBar is used in RecyclerView and RecyclerView. The RecyclerView is used in RecyclerView and RecyclerView


The MVP mentality

1. Overview:
The blue and white slashes are interfaces. The orange dashed lines are leads for class methods. The blue dashed lines are flow linesCopy the code
The model layer (M) is responsible for retrieving data, and the control layer (P) is used by the control layer (P). Note that the model layer (M) and the view layer (V) are bound together with the control layer (P) through the Callback. Your Lao tze interface (M, n) in my hand, I also afraid of When writing the view layer (V), V have control layer at the hands of the Lao tze interface (P), so V is how to think So whether writing view layer, data layer, control layer, as long as the interface definition is good, can be division of labor to write, mutual influence This is faces a little programming interface, Some people view is very good, can be specialized view layer, network, database strong can be specialized model layer and so on... Just like finding one generalist to do the same thing as finding three people who are proficient in a subject, the latter will, in theory, do it more thoughtfully and easily. Clear division of labor is helpful for clear thinking and method reuseCopy the code


2. Work on the interface first

You can put the ILoadingView directly into the INoteView, if you like

2.1. View layer core
/** * Author: Zhang Feng Jiete Lie <br/> * Time: 2018/12/140014:7:49 <br/> * Email: [email protected]<br/> * Description: Public interface ILoadingView {/** * loading */ void loading(); / / void loaded(); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - / * * * the author: packer fierce < br / > * time: jet 2018/12/14 0014: "< br / > * email address: [email protected]<br/> * Public interface INoteView<T> extends ILoadingView {/** * page render data * @param dataList */ void reader(List<T>) dataList); /** * Page processing error * @param e */ void error(ErrorEnum e); }Copy the code

2.2. Control layer:
/** * Author: Zhang Feng Jiete: <br/> * Time: 2018/12/140014:20:27 <br/> * Email: [email protected]<br/> * Description: Control layer */ public interface IPresenter<T> {/** * Update view based on the area ** @param Area range * @param offset * @param count Number of entries */  void updateByArea(String area, int offset, int count); /** * updateByName(String name, String name, String name, String name) ** @param offset * @param count */ void updateByName(String name, String name, String name) int offset, int count); }Copy the code

2.3. The model layer
/** * Author: Zhang Feng Jiete Lay <br/> * Time: 2018/12/140014:13:43 <br/> * Email: [email protected]<br/> * Description: Data Model layer */ public interface INoteModel<T> {/** * Query all * @param callback * @param offset * @param Page */ void getData(Callback<T> callback, int offset, int page); /** * Querying data by area * @param callback * @param Area range * @param offset Offset * @param Page Number of items to query */ void getDataByArea(Callback<T> callback, String area, int offset, int page); /** * Query data by name (search) * @param callback callback * @param name range * @param offset Offset * @param Page Number of queries */ void getDataByName(Callback<T> callback, String name, int offset, int page); /** * insertModel * @param params */ void insertModel(Map<String, String> params); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- model layer data callback interface -- -- -- -- - / * * * the author: packer fierce < br / > * time: jet 2018/12/14 0014: shine forth < br / > * email address: [email protected]<br/> */ public interface Callback<T> {/** * start loading */ void onStartLoad(); /** * Success * @param dataList data */ void onSuccess(List<T> dataList); /** * error * @param e error */ void onError(ErrorEnum e); }Copy the code

2.4. Enumeration of error types

You can customize the error type so that different interfaces are displayed later based on different errors

/** * Author: Zhang Feng Jiete Lie <br/> * Time: 2018/12/140014:7:58 <br/> * Email: [email protected]<br/> * Description: Error type */ public enum ErrorEnum {EXCEPTION(500, "server "), NOT_FOUND(102," unknown ID "), I/O(1, "I/O EXCEPTION "), NO_NET(2, "no network "), NET_LINK(3, "Abnormal network connection "); 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; }}Copy the code

3. Implementation of model layer

Data is the core, the first data in hand, psychological just steadfast, use Retrofit+RxJava

The following figure shows the simplest way to get data from Retrofit+RxJava

/ / rxjava2 implementation 'IO. Reactivex. Rxjava2: rxandroid: 2.1.0' / / retrofit implementation 'com. Squareup. Retrofit2: retrofit: 2.4.0' / / core library implementation 'com. Squareup. Retrofit2: converter - gson: 2.4.0' / / json converter Implementation 'com. Jakewharton. Retrofit: retrofit2 - rxjava2 - adapter: 1.0.0' / / cooperate Rxjava useCopy the code


3.1: Interface first: Noteapi.java

Before I do that, review the server interface

- all query: http://192.168.43.60:8089/api/android/note - 12 query migration, query 12 (that is, article 12 to a page of page 2) : http://192.168.43.60:8089/api/android/note/12/12 - by region query (A data for Android, SB for SpringBoot data, Re the React data) as http://192.168.43.60:8089/api/android/note/area/A http://192.168.43.60:8089/api/android/note/area/A/12/12 - according to the part name query http://192.168.43.60:8089/api/android/note/name/ http://192.168.43.60:8089/api/android/note/name/ materials / 2/2 - according to the type name query (see first type definition table) http://192.168.43.60:8089/api/android/note/name/ABCS http://192.168.43.60:8089/api/android/note/name/ABCS/2/2 - according to the id name check: Add - POST request: http://192.168.43.60:8089/api/android/note/12 http://192.168.43.60:8089/api/android/noteCopy the code
/** * Author: Zhang Feng Jiete Lie <br/> * Time: 2018/12/130013:19:48 <br/> * Email: [email protected]<br/> * Description: */ public interface NoteApi {/** * Query all operations */ @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); /** * Query from Observable */ @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); /** * Insert operation */ @formurlencoded @post (" API /android/note") Observable<ResultBean> insert(@fieldMap Map<String, String> params); }Copy the code

3.2: ResultBean and NoteBean entity classes
This and the backend entity class has been maintained, you can directly use AS plug-ins directly generated can also be used to use the backend entity class, quite long, not paste, no technical content, see the source codeCopy the code

3.3: Obtain the data core logic
public class NoteModel implements INoteModel<ResultBean.NoteBean> { private static final String TAG = "NoteModel"; private NoteApi mNoteApi; public NoteModel() { mNoteApi = new Retrofit.Builder() AddConverterFactory (GsonConverterFactory. The create ()) / / json convert javabeans .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)); } /** * Execute the API return Observable ** @param API callback function * @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() { } }); }}Copy the code

3.4: Testing interfaces (unit Testing)

Do some unit testing here, because we haven’t implemented P and V yet, to see if the model layer is correct, and the last way is to do unit testing

Unit testing in Android is very simple, where you take the data and compare the number of items, and if you pass, the data is correct

@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); }}Copy the code

Ok, test passed, go to the view layer


4. Implementation of view layer:HomePagerView.java

FindViewByid, I won’t write it… , loading uses SwipeRefreshLayout

4.1: Method implementation
private RecyclerView mHomeRv; //RecyclerView private SwipeRefreshLayout mIdSrl; Private IPresenter<ResultBean.NoteBean> mPagerPresenter; / / control layerCopy the code
@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);

    }
Copy the code

4.2: RecyclerView adapter

For convenience, here you use Picasso to load online images, which comes with its own caching function

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); }}}Copy the code

5. Control layer

After the first two layers are implemented, this layer is simple

/** * Author: Zhang Feng Jiete Lie <br/> * Time: 2018/12/140014:13:57 <br/> * Email: [email protected]<br/> * Description: 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); }}Copy the code

6. Run:HomePagerView, two sentences
mPagerPresenter = new PagerPresenter(this);
mPagerPresenter.updateByArea("A", 0, 12);
Copy the code


Iii. Related operations

1. Pull down to refresh and click to switch:
1.1: Overview of effects
The drop-down refresh Click on the switch

1.2: Refresh from the drop-down list

It’s that simple

mIdSrl.setOnRefreshListener(() -> {
    mPagerPresenter.updateByArea(area, 0, 1000);
});
Copy the code

1.3: Click to switch

That is, according to the click to determine the type, according to the type using the control layer refresh view

private String area = "A"; ------------------------------------------ mIdBnb.setTabSelectedListener(new BottomNavigationBar.OnTabSelectedListener()  { @Override public void onTabSelected(int position) { switch (position) { case 0: area = "A"; Midctlbar.settitle ("Android Technology Stack "); mIdIvHead.setImageResource(R.mipmap.bg_android); break; case 1: area = "SB"; Midctlbar.settitle ("SpringBoot technology stack "); mIdIvHead.setImageResource(R.mipmap.bg_springboot); break; case 2: area = "Re"; Midctlbar.settitle ("React technology stack "); mIdIvHead.setImageResource(R.mipmap.bg_react); break; case 3: area = "Note"; Midctlbar.settitle (" Programming Essay Miscellany "); mIdIvHead.setImageResource(R.mipmap.menu_bg); break; case 4: area = "A"; Midctlbar.settitle (" Series "); break; } mPagerPresenter.updateByArea(area, 0, 1000); } @Override public void onTabUnselected(int position) { } @Override public void onTabReselected(int position) { } );Copy the code

2. Add and search functions
Add functionality The search function
2.1: Search function:

It matches the input character by name, and then it looks up,

Let’s say STR is the input string, and then execute the updateByName of the mPagerPresenter

mPagerPresenter.updateByName(str, 0, 1000);
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
isOpen = false;
Copy the code

2.2: Add operations

This is a little bit cumbersome and requires a view dialog

// API --NoteApi @formurlencoded @post (" API /android/note") Observable<ResultBean> insert(@fieldMap Map<String, // API --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, String> params); mNoteApi.insert(params)); } // Control layer --PagerPresenter @override public void addItem(Map<String, String> params) {mmodel.insertModel (params); } // View layer: 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 (" Add article "); builder.setView(dialogView); Builder. SetPositiveButton (" sure, "(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 (" cancel ", null); builder.create().show(); }Copy the code

Confuse packaging and on-line

1. The confusion:
-----app/build.gradle------ Enable obfuscation buildTypes {release {minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}} ----app/proguard-rules.pro------ Obviate configuration - ignorewarningsIgnore warning # 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.**{*; } # Customize the beans directory for the data modelCopy the code

2. Signature packaging

After obfuscating the package, it is almost half the size of the debug package, which feels good and can be tested personally


3. Online

Well, not to every market, because it’s hard to get personal apps to do that these days

In front of the interface to provide the download address, very simple, copy to the server on the line, and then access to download

4. Front-end React slightly modified:

So you can download it when you click


Basically the point has been talked about, although not everything, the whole hold almost

Source code in the end, interested can see, summarized below, so far, with five days to do the following things:

1. Use SpringBoot and Mybatis to build an online server with Restful interface 2. Use Python's Selenium library to crawl the article information from the home page of the brief book and insert the data into database 3 via a network request in Java. Use React for front-end display, SCSS style and Axios for web requests and mobile web adaptation 4. Use Java to build a material design style mobile application based on Android, and go Live 5. I have written these four long articles, on the whole, it is still very fruitful, at least the knowledge is strung togetherCopy the code

Postscript: Jie wen standard

1. Growth record and Errata of this paper
Program source code The date of note
V0.1 – making 2018-12-15 Mobile Terminal part of tetralogy of website construction (Android+ online)
2. More about me
Pen name QQ WeChat hobby
Zhang Feng Jie te Li 1981462002 zdl1994328 language
My lot My Jane books I’m the nuggets Personal website
3. The statement

1—- This article is originally written by Zhang Fengjie, please note if reproduced

2—- welcome the majority of programming enthusiasts to communicate with each other 3—- personal ability is limited, if there is something wrong welcome to criticize and testify, must be humble to correct 4—- see here, I thank you here for your love and support