Goal: To achieve paper Plane App – with MVP architecture, it is a comprehensive reading client integrating Zhihu Daily, Guokai Selection and Douban Moment. The effect picture is as follows:





PaperPlane

This tutorial is divided into 7 days, covering:

  • Day one, prepare
    • The functional requirements
    • Feasibility analysis
    • For other
  • The next day, UI
    • Choose the right UI
  • Day three, overall architecture
  • Day four, home page list
    • Interface to write
    • Entity class
    • Display the data
    • Cache content
  • Day 5, details page with others
    • Interface to write
    • Entity class
    • Display the data
    • Settings and About
  • Day six, advanced functions
    • Night mode
    • Version of the adapter
  • Day seven, release and Open Source
    • Launch on Google Play
    • In making open source
    • thinking

All right, let’s cut the crap. Start now.

DAY 1

As the saying goes, all things are difficult before they are easy. If you make good preparations, you can get twice the result with half the effort. Sharpening knives is not the fault of cutting wood.

Day 1, functional requirements

Before we start formal coding, we still have to list the functions to be implemented one by one, and then the implementation will have a direction. I think we need to implement the following functions:

  • Get the message list correctly and display it
  • Ability to get historical messages
  • Display Details
  • The background automatically caches content details, which is convenient for users to view when there is no network connection
  • Bookmark specific messages
  • Night mode

There are 6 big requirements in total, which are not many, but after a careful study, these 6 requirements actually involve network, UI, data storage, background services and other contents. I believe it is not difficult for you to be smart. Now let’s study the feasibility.

Day 1, Feasibility analysis

The first question to consider is: Where does the data come from? Thanks for open source. Izzyleung on GitHub analyzed zhihu daily’s API and opened it. Zhihu Daily’s API analysis is very detailed. In the early stage of the paper Plane project, before version 3.0, only this API was used, and after 3.0, guokui Selection and Douban Moment API were also used. If you want to show more, you can go here: Awsome_API, a collection of commonly used apis at home and abroad.

Let’s take a quick look at the data. Get a list of zhihu Daily news on January 22, 2017:

http://news-at.zhihu.com/api/4/news/before/20170122Copy the code

The server returns us something in JSON format:

{
  "date": "20170121"."stories": [{"images": [
        "http://pic1.zhimg.com/ffcca2b2853f2af791310e6a6d694e80.jpg"]."type": 0."id": 9165434."ga_prefix": "012121"."title": "Who says ordinary people can't have interesting lives?"},... ] }Copy the code

OK, once we get the list, we can get the details. For example, if we get the content with id 9165434, we just need to concatenate the id to http://news-at.zhihu.com/api/4/news/ :

http://news-at.zhihu.com/api/4/news/9165434Copy the code

The obtained content is:

{
  "body": "Content in HTML format"."image_source": "Paterson."."title": "Who says ordinary people can't have interesting lives?"."image": "http://pic4.zhimg.com/e39083107b7324c6dbb725da83b1d7fb.jpg"."share_url": "http://daily.zhihu.com/story/9165434"."js": []."ga_prefix": "012121"."section": {
    "thumbnail": "http://pic1.zhimg.com/ffcca2b2853f2af791310e6a6d694e80.jpg"."id": 28."name": "Projector"
  },
  "images": [
    "http://pic1.zhimg.com/ffcca2b2853f2af791310e6a6d694e80.jpg"]."type": 0."id": 9165434."css": [
    "http://news-at.zhihu.com/css/news_qa.auto.css?v=4b3e3"]}Copy the code

Inside the body field are details of the content in HTML format, which we can display using the WebView. Of course, zhihu Daily has more than two API interfaces, you can click the link above to check it out. To get the content of Nutshell Selection and Douban Moment, you can view the file Api directly in my project.

Day 1, other preparations

To do a good job, he must sharpen his tools. It’s always good to have the tools ready.

  • A computer, I don’t know. Without it, it’s hard to do development work. We can’t write code with stone tools.
  • Software:
    • Android Studio standard
    • Chrome programmers with 360 browsers, Baidu browsers and so on always feel a little geeky.
    • Postman is a powerful Chrome plugin for web page debugging and sending HTTP requests, we do network request analysis needs to use.
    • Genymotion if you don’t like the simulator that comes with AS, try this one.
    • Git version control, command line knock up fried chicken with feeling oh.
  • Ideally, have an Android phone.
  • Stay online scientifically and ensure access to Google and StackOverFlow. To hell with Baidu.

Ok, the first day of work is almost so much, familiarize yourself with the API, have a good job, clear up the mood, prepare for tomorrow’s work.

DAY 2

Today I mainly finished the UI design. That’s what designers do, you might ask. However, in the process of developing the paper plane, I did not shoot chicken wet such a creature, THE UI was done by myself. I believe that most programmers are not so good at art.

Of course, students with art and related knowledge can try Sketch or PS to draw the prototype. For children’s shoes without art knowledge, the easiest way is of course to imitate the existing APP. Of course, you can also find suitable design drawings on the following websites:

  • Dribbble
  • UpLabs
  • The UI China
  • Standing cool ZCOOL

There are also some minor caveats:

  • Follow Material Design specifications – This is not mandatory, but since we are building an Android App, how can we expect the Android environment to improve if we don’t follow the Material Design specifications ourselves?
  • The correct use of BottomNavigation – BottomNavigation as a face of Google has been controversial since its birth. My personal recommendation is to use TabLayout instead of bottom navigation, which is a big faith thing. If you must, please do not use the iOS standard directly on Android, please refer to this article: Bottom Navigation in Material Design is not a license to mindlessly port the iOS Navigation mode, and I give you a look of contempt.
  • Use the right ICONS – try to use the material. IO/ICONS/ICONS from the site. If you use the ICONS from the iOS version, I give you another look of contempt.

The final design of the paper plane is as follows:





PaperPlane

On the home page, Drawer is used as the top-level navigation, Tab is used as the secondary navigation, card layout is used for list items, and FloatingActionButton is used as the date selection button. Use shrinkable toolbars, images and text for the details page. I don’t know anything else. As you will see later, I made a mistake here, the card layout is not appropriate for this. See also: material. IO/guidelines /…)

DAY 3

Now it’s time to actually write code.

Without saying anything about creating a new Android Studio project, here is my project structure:





The project structure

· ├ ─ ─ app |├─ libs store relevant JAR files, etc|├ ─ ─ the SRC|   |├─ androidTest|   |├ ─ ─ the main|   |   |├── Assets to store the original files|   |   |├ ─ ─ Java|   |   |   |├ ─ ─ com. Marktony. Zhihudaily Java packages|   |   |   |   |├── About the page|   |   |   |   |├── RecyclerView and ViewPager control adapter|   |   |   |   |├ ─ ─ app Application|   |   |   |   |├─ Beans for Entity classes|   |   |   |   |├─ Bookmarks Collection Page|   |   |   |   |├─ CustomTabs Chrome Custom Tabs|   |   |   |   |├─ DB database related|   |   |   |   |├─ Detail page|   |   |   |   |├─ homepage page|   |   |   |   |├─ InnerBrowser Built-in Browser page|   |   |   |   |├─ Interfaze Set|   |   |   |   |├─ License Open Source License Page|   |   |   |   |├─ Search search page|   |   |   |   |├─ Go to school|   |   |   |   |├─ Settings Settings page|   |   |   |   |├─ Util Tool Class Collection|   |   |   |   |├─ BasePresenter. Java Presenter Base Class|   |   |   |   |├─ BaseView. Java View Base Class|   |   |├ ─ ─ res|   |   |├─ AndroidManifest.xml manifest fileCopy the code

(As you can see, I’m subcontracting by page and feature.)

Once the package was built, we started importing third-party open source libraries to simplify code writing and achieve specific effects. Go to the app folder in your project directory, open the build.gradle file and add the following content.

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    // Use volley to simplify network requests
    compile files('libs/library - 1.0.19. Jar')
    // AppCompat compatible package
    compile 'com. Android. Support: appcompat - v7:25.1.0'
    // Material Design
    compile 'com. Android. Support: design: 25.1.0'
    // Recycler View control
    compile 'com. Android. Support: recyclerview - v7:25.1.0'
    // Preference Screen Settings and configuration about the page
    compile 'com. Android. Support: preference - v14:25.1.0'
    // Support Chrome Custom Tabs
    compile 'com. Android. Support: customtabs: 25.1.0'
    // Card view control
    compile 'com. Android. Support: cardview - v7:25.1.0'
    // Parse JSON data
    compile 'com. Google. Code. Gson: gson: 2.7'
    // Image loading
    compile 'com. Making. Bumptech. Glide: glide: 3.7.0'
    // To maintain UI consistency in earlier SDKS, the Material Data Time Picker library was introduced
    compile 'com. Wdullaer: materialdatetimepicker: 2.5.0'
    testCompile 'junit: junit: 4.12'Copy the code

Due to some legacy issues, I chose volley instead of using OkHttp as the network request package. If you have some basic knowledge, you can choose to use OkHttp.

There are two ways to import Volley:

  • Paste volley jar in app directory lib, you can download it here: volley.

  • It can also be introduced through Gradle.

compile 'com. Android. Volley: volley: 1.0.0'Copy the code

Then click Sync Project with Gradle Files.

The first is the overall structure: MVP. For an overall architecture selection and a more detailed introduction, check out this article: Refactoring! Apply Google-MVP to existing projects. Here we take a look at Todo-MVP from Google’s Android Architecture Blueprints [Beta].

  1. Start by creating the basic BaseView and BasePresenter, which are the base classes for all Views and Presenters, respectively.

     public interface BaseView<T> {
         // Set Presenter to View
         void setPresenter(T presenter);
        // Initialize the interface control
         void initViews(View view);
     }Copy the code
     public interface BasePresenter {
         // Get the data and change the display, calling OnResume() in the Fragment method in the todo-MVP project
         void start(a);
     }Copy the code
  2. Then create a contract class that manages the View and Presenter together. Here, the part of Zhihu Daily is used as an example (unless otherwise specified, the following codes all take the part of Zhihu Daily as an example. Guokurai Is similar to the code of Douban Moment, and the detailed code can be found in the Repo of GitHub).

     public interface ZhihuDailyContract {
    
         interface View extends BaseView<Presenter> {
    
             // Display loading or other types of errors
             void showError(a);
             // Display loading
             void showLoading(a);
             // Stop displaying loading
             void stopLoading(a);
             // After the data is successfully obtained, it is displayed on the interface
             void showResults(ArrayList<ZhihuDailyNews.Question> list);
             // Displays the date picker dialog used to load the specified date
             void showPickDialog(a);
    
         }
    
         interface Presenter extends BasePresenter {
             // Request data
             void loadPosts(long date, boolean clearing);
             // Refresh data
             void refresh(a);
             // Load more articles
             void loadMore(long date);
             // Display details
             void startReading(int position);
             // Just look around
             void feelLucky(a); }}Copy the code
  3. Create the corresponding subclasses View and Presenter in the subpackages you have sorted out above.

    “`java

    public class ZhihuDailyFragment extends Fragment

     implements ZhihuDailyContract.View {
    
     public ZhihuDailyFragment(a) {}
    
     public static ZhihuDailyFragment newInstance(a) {
         return new ZhihuDailyFragment();
     }Copy the code
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return null;
    }

    @Override
    public void setPresenter(ZhihuDailyContract.Presenter presenter) {

    }

    @Override
    public void initViews(View view) {

    }

    @Override
    public void showError() {

    }

    @Override
    public void showLoading() {

    }

    @Override
    public void stopLoading() {

    }

    @Override
    public void showResults(ArrayList<ZhihuDailyNews.Question> list) {

    }

    @Override
    public void showPickDialog() {

    }

}
```

```java
public class ZhihuDailyPresenter implements ZhihuDailyContract.Presenter {

    public ZhihuDailyPresenter(Context context, ZhihuDailyContract.View view) {

    }

    @Override
    public void loadPosts(long date, final boolean clearing) {

    }

    @Override
    public void refresh() {

    }

    @Override
    public void loadMore(long date) {

    }

    @Override
    public void startReading(int position) {

    }

    @Override
    public void feelLucky() {

    }

    @OverridePublic void start() {}} ' ';Copy the code
  1. Create a VolleySingleton, a singleton of Volley. This way, the entire application can maintain only one request queue, and it is much easier to add new network requests.

     public class VolleySingleton {
    
         private static VolleySingleton volleySingleton;
         private RequestQueue requestQueue;
    
         private VolleySingleton(Context context){
             requestQueue = Volley.newRequestQueue(context.getApplicationContext());
         }
    
         public static synchronized VolleySingleton getVolleySingleton(Context context){
             if(volleySingleton == null){
                 volleySingleton = new VolleySingleton(context);
             }
             return volleySingleton;
         }
    
         public RequestQueue getRequestQueue(a){
             return this.requestQueue;
         }
    
         public <T> void addToRequestQueue(Request<T> req){ getRequestQueue().add(req); }}Copy the code
  2. Then there is the implementation of the Model layer. With Gson, JSON conversion is much easier, so we only need to return String.

     public interface OnStringListener {
         /** * Callback * if the request succeeds@param result
          */
         void onSuccess(String result);
         /** * Callback * if the request fails@param error
          */
         void onError(VolleyError error);
     }Copy the code

    Two methods are defined as a callback when the request succeeds and when the request fails.

    We then define an implementation class for StringModel – StringModelImpl.

     public class StringModelImpl {
         private Context context;
         public StringModelImpl(Context context) {
             this.context = context;
         }
         public void load(String url, final OnStringListener listener) {
             StringRequest request = new StringRequest(url, new Response.Listener<String>() {
    
     @Override
                 public void onResponse(String s) { listener.onSuccess(s); }},new Response.ErrorListener() {
    
     @Override
                 public void onErrorResponse(VolleyError volleyError) { listener.onError(volleyError); }}); VolleySingleton.getVolleySingleton(context).addToRequestQueue(request); }}Copy the code
  3. At this point, the basic architecture is set up. Now it’s time to grab a cup of coffee and finish off the last bits of work for the day.

    Create api.java file to store all apis used by your app.

    public class Api {
    
        // Message content retrieval and offline download
        // The id obtained in the latest message can be concatenated to the NEWS to obtain the corresponding JSON format content
        public static final String ZHIHU_NEWS = "http://news-at.zhihu.com/api/4/news/";
    
        // Past information
        // To query for messages on November 18, before should be 20161118
        // The date of birth of Zhihu daily is May 19, 2013. If the number after before is less than 20130520, then only empty messages can be obtained
        public static final String ZHIHU_HISTORY = "http://news.at.zhihu.com/api/4/news/before/";
    
        // Get the list of articles selected by the shell, by combining the corresponding parameters into the full URL
        public static final String GUOKR_ARTICLES = "http://apis.guokr.com/handpick/article.json?retrieve_type=by_since&category=all&limit=25&ad=1";
    
        // Get specific information about the fruit shell article V1
        public static final String GUOKR_ARTICLE_LINK_V1 = "http://jingxuan.guokr.com/pick/";
    
        // Douban moment
        // Query the message list by date
        public static final String DOUBAN_MOMENT = "https://moment.douban.com/api/stream/date/";
    
        // Get the content of the article
        public static final String DOUBAN_ARTICLE_DETAIL = "https://moment.douban.com/api/post/";
    
    }Copy the code

    Create networkstate. Java file to determine the current network status, whether there is a network connection, WiFi or mobile data.

    public class NetworkState {
    
        // Check whether you are connected to the network
        public static boolean networkConnected(Context context){
    
            if(context ! =null){
                ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo info = manager.getActiveNetworkInfo();
                if(info ! =null)
                    return info.isAvailable();
            }
    
            return false;
        }
    
        // Check whether WiFi is connected
        public static boolean wifiConnected(Context context){
            if(context ! =null){
                ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo info = manager.getActiveNetworkInfo();
                if(info ! =null) {if (info.getType() == ConnectivityManager.TYPE_WIFI)
                        returninfo.isAvailable(); }}return false;
        }
    
        // Check whether the mobile network is connected
        public static boolean mobileDataConnected(Context context){
            if(context ! =null){
                ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo info = manager.getActiveNetworkInfo();
                if(info ! =null) {if (info.getType() == ConnectivityManager.TYPE_MOBILE)
                        return true; }}return false; }}Copy the code

    Create a dateFormatter. Java file to convert a long date to a String.

    public class DateFormatter {
    
        /** * Convert long date to String *@param date date
         * @return String date
         */
        public String ZhihuDailyDateFormat(long date) {
            String sDate;
            Date d = new Date(date + 24*60*60*1000);
            SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
            sDate = format.format(d);
    
            return sDate;
        }
    
        public String DoubanDateFormat(long date){
            String sDate;
            Date d = new Date(date);
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
            sDate = format.format(d);
    
            returnsDate; }}Copy the code

    OK, day 3 job done.

Day 4

Today’s task is to complete the home page.

Day 4, interface writing

Our home page uses the method of Activity + Fragment, that is, a MainActivity + MainFragment + BookmarksFragment. The MainActivity layout file contains the DrawerLayout, Toolbar, and Fragment container.

The layout file for MainActivity is as follows:

activity_main.xml

<?xml version="1.0" encoding="utf-8"? >
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include layout="@layout/app_bar_main" />

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/nav_header_main"
        app:menu="@menu/activity_main_drawer" />

</android.support.v4.widget.DrawerLayout>Copy the code

nav_header_main.xml

<?xml version="1.0" encoding="utf-8"? >
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/nav_header_height"
    android:background="@drawable/nav_header"
    android:gravity="bottom"
    android:orientation="vertical"
    android:theme="@style/ThemeOverlay.AppCompat.Dark">

</LinearLayout>Copy the code

Nav_header is really just a simple ImageView.

app_bar_main.xml

<?xml version="1.0" encoding="utf-8"? >
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".homepage.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:elevation="0dp"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="? attr/actionBarSize"
            android:background="@color/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/layout_fragment"
        android:layout_marginTop="? actionBarSize"/>

</android.support.design.widget.CoordinatorLayout>Copy the code

OK, the Activity layout file is complete. Then you can write Java code.

MainActivity.java

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener{

    private MainFragment mainFragment;
    private BookmarksFragment bookmarksFragment;

    private NavigationView navigationView;
    private DrawerLayout drawer;
    private Toolbar toolbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Initialize the controller
        initViews();

        // Restore the fragment state
        if(savedInstanceState ! =null) {
            mainFragment = (MainFragment) getSupportFragmentManager().getFragment(savedInstanceState, "MainFragment");
            bookmarksFragment = (BookmarksFragment) getSupportFragmentManager().getFragment(savedInstanceState, "BookmarksFragment");
        } else {
            mainFragment = MainFragment.newInstance();
            bookmarksFragment = BookmarksFragment.newInstance();
        }

        if(! mainFragment.isAdded()) { getSupportFragmentManager().beginTransaction() .add(R.id.layout_fragment, mainFragment,"MainFragment")
                    .commit();
        }

        if(! bookmarksFragment.isAdded()) { getSupportFragmentManager().beginTransaction() .add(R.id.layout_fragment, bookmarksFragment,"BookmarksFragment")
                    .commit();
        }

        // instantiate BookmarksPresenter
        new BookmarksPresenter(MainActivity.this, bookmarksFragment);

        // The home page is displayed by default
        showMainFragment();

    }

    // Initialize the controller
    private void initViews(a) {

        toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this,
                drawer,
                toolbar,
                R.string.navigation_drawer_open,
                R.string.navigation_drawer_close);
        drawer.setDrawerListener(toggle);
        toggle.syncState();

        navigationView = (NavigationView) findViewById(R.id.nav_view);
        navigationView.setNavigationItemSelectedListener(this);

    }

    // Display MainFragment and set Title
    private void showMainFragment(a) {

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.show(mainFragment);
        fragmentTransaction.hide(bookmarksFragment);
        fragmentTransaction.commit();

        toolbar.setTitle(getResources().getString(R.string.app_name));

    }

    // Display BookmarksFragment and set Title
    private void showBookmarksFragment(a) {

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.show(bookmarksFragment);
        fragmentTransaction.hide(mainFragment);
        fragmentTransaction.commit();

        toolbar.setTitle(getResources().getString(R.string.nav_bookmarks));

    }

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {

        drawer.closeDrawer(GravityCompat.START);

        int id = item.getItemId();
        if (id == R.id.nav_home) {
            showMainFragment();
        } else if (id == R.id.nav_bookmarks) {
            showBookmarksFragment();
        } else if (id == R.id.nav_change_theme) {

        } else if (id == R.id.nav_settings) {

        } else if (id == R.id.nav_about) {

        }

        return true;
    }

    // Indicates the Fragment status
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (mainFragment.isAdded()) {
            getSupportFragmentManager().putFragment(outState, "MainFragment", mainFragment);
        }

        if (bookmarksFragment.isAdded()) {
            getSupportFragmentManager().putFragment(outState, "BookmarksFragment", bookmarksFragment); }}}Copy the code

As you can see from the code,MainActivity handles the DrawerLayout click event, controlling the display or hiding of a particular Fragment. The Fragment state is also saved and restored here.

MainFragment.java

public class MainFragment extends Fragment {

    private Context context;
    private MainPagerAdapter adapter;

    private TabLayout tabLayout;

    private ZhihuDailyFragment zhihuDailyFragment;
    private GuokrFragment guokrFragment;
    private DoubanMomentFragment doubanMomentFragment;

    private ZhihuDailyPresenter zhihuDailyPresenter;
    private GuokrPresenter guokrPresenter;
    private DoubanMomentPresenter doubanMomentPresenter;

    public MainFragment(a) {}

    public static MainFragment newInstance(a) {
        return new MainFragment();
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.context = getActivity();

        // The Fragment status is restored
        if(savedInstanceState ! =null) {
            FragmentManager manager = getChildFragmentManager();
            zhihuDailyFragment = (ZhihuDailyFragment) manager.getFragment(savedInstanceState, "zhihu");
            guokrFragment = (GuokrFragment) manager.getFragment(savedInstanceState, "guokr");
            doubanMomentFragment = (DoubanMomentFragment) manager.getFragment(savedInstanceState, "douban");
        } else {
            // Create a View instance
            zhihuDailyFragment = ZhihuDailyFragment.newInstance();
            guokrFragment = GuokrFragment.newInstance();
            doubanMomentFragment = DoubanMomentFragment.newInstance();
        }

        Create an instance of Presenter
        zhihuDailyPresenter = new ZhihuDailyPresenter(context, zhihuDailyFragment);
        guokrPresenter = new GuokrPresenter(context, guokrFragment);
        doubanMomentPresenter = new DoubanMomentPresenter(context, doubanMomentFragment);

    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);

        // Initialize the controller
        initViews(view);

        // Display the menu
        setHasOptionsMenu(true);

        // Hide fab when TAB Layout position is shell selection
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab);
                if (tab.getPosition() == 1) {
                    fab.hide();
                } else{ fab.show(); }}@Override
            public void onTabUnselected(TabLayout.Tab tab) {}@Override
            public void onTabReselected(TabLayout.Tab tab) {}});return view;
    }


    // Initialize the controller
    private void initViews(View view) {

        tabLayout = (TabLayout) view.findViewById(R.id.tab_layout);
        ViewPager viewPager = (ViewPager) view.findViewById(R.id.view_pager);
        // Set the offline count to 3
        viewPager.setOffscreenPageLimit(3);

        adapter = new MainPagerAdapter(
                getChildFragmentManager(),
                context,
                zhihuDailyFragment,
                guokrFragment,
                doubanMomentFragment);

        viewPager.setAdapter(adapter);
        tabLayout.setupWithViewPager(viewPager);

    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.main, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_feel_lucky) {
            feelLucky();
        }
        return true;
    }

    // Save the state
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        FragmentManager manager = getChildFragmentManager();
        manager.putFragment(outState, "zhihu", zhihuDailyFragment);
        manager.putFragment(outState, "guokr", guokrFragment);
        manager.putFragment(outState, "douban", doubanMomentFragment);
    }

    // Just look around
    public void feelLucky(a) {
        Random random = new Random();
        int type = random.nextInt(3);
        switch (type) {
            case 0:
                zhihuDailyPresenter.feelLucky();
                break;
            case 1:
                guokrPresenter.feelLucky();
                break;
            default:
                doubanMomentPresenter.feelLucky();
                break; }}public MainPagerAdapter getAdapter(a) {
        returnadapter; }}Copy the code

The MainFragment on the home page is responsible for displaying TabLayout + ViewPager related content.

OK, finally set up the UI frame of the home page, have a cup of coffee, take a rest and calm down.

Now it’s time to implement the concrete ZhihuDailyFragment layout. Careful observation, in fact, ZhihuDailyFragment contains only a RecyclerView control, will get the content in the form of a list of display. In addition, it is not difficult to find that the layout of Guokushu.com and Douban Moment is the same as the layout of Zhihu Daily list, which can be reused.

fragment_list.xml

<?xml version="1.0" encoding="utf-8"? >
<android.support.v4.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/refreshLayout">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:focusable="true"
        android:clickable="true">

        <android.support.v7.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/recyclerView"
            android:scrollbars="vertical"
            android:scrollbarFadeDuration="1"
            android:fadeScrollbars="true"/>

    </FrameLayout>

</android.support.v4.widget.SwipeRefreshLayout>Copy the code

The layout actually includes SwipeRefreshLayout, which shows loading and manual refreshing.

The layout of the subitems of the list is as follows:

  1. Plain text only
  2. Normal text + picture
  3. The head item is used to display the subitem type (such as Zhihu Daily, used in the favorites page)
  4. Bottom item, load more etc

Home_list_item_without_image.xml – Plain text only

<?xml version="1.0" encoding="utf-8"? >
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="96dp"
    android:layout_width="match_parent"
    android:focusable="true"
    android:clickable="true"
    android:foreground="? android:attr/selectableItemBackground"
    app:cardCornerRadius="4dp"
    app:cardElevation="1dp"
    app:cardPreventCornerOverlap="true"
    android:layout_marginTop="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/textViewTitle"
        android:paddingTop="8dp"
        android:paddingBottom="8dp"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:gravity="center_vertical"
        android:maxLines="3"
        android:ellipsize="end"
        android:textSize="18sp" />

</android.support.v7.widget.CardView>Copy the code

Home_list_item_layout. XML – Plain text + image

<?xml version="1.0" encoding="utf-8"? >
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="96dp"
    android:layout_width="match_parent"
    android:focusable="true"
    android:clickable="true"
    android:foreground="? android:attr/selectableItemBackground"
    app:cardCornerRadius="4dp"
    app:cardElevation="1dp"
    app:cardPreventCornerOverlap="true"
    android:layout_marginTop="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:paddingLeft="8dp"
        android:paddingRight="8dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:id="@+id/textViewTitle"
            android:paddingTop="8dp"
            android:paddingBottom="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginEnd="8dp"
            android:gravity="center_vertical"
            android:maxLines="3"
            android:ellipsize="end"
            android:textSize="18sp" />

        <ImageView
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:id="@+id/imageViewCover"
            android:layout_gravity="center_vertical" />

    </LinearLayout>

</android.support.v7.widget.CardView>Copy the code

Bookmark_header. XML – header entry

<?xml version="1.0" encoding="utf-8"? >
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/textViewType"
    android:paddingLeft="8dp"
    android:paddingStart="8dp"
    android:paddingRight="8dp"
    android:paddingEnd="8dp"
    android:paddingTop="8dp"
    android:gravity="center_vertical"
    android:textColor="@color/colorPrimary"
    android:textAllCaps="true"/>Copy the code

List_footer.xml – Bottom item, load more

<?xml version="1.0" encoding="utf-8"? >
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:layout_marginTop="8dp"
    android:layout_marginBottom="8dp"
    android:gravity="center_horizontal"
    android:background="@color/viewBackground">

    <android.support.v4.widget.ContentLoadingProgressBar
        android:id="@+id/address_looking_up"
        style="? android:attr/progressBarStyleInverse"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:visibility="visible" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:text="@string/loading_more"
        android:layout_marginLeft="16dp"
        android:layout_marginStart="8dp"
        android:gravity="center_vertical"/>

</LinearLayout>Copy the code

The layout file is almost done here.

Day 4, entity class

We can design the entity class directly from the return data in JSON format. You can code it manually or use the Android Studio plugin GsonFormat.

Json data:

{
  "date": "20170121"."stories": [{"images": [
        "http://pic1.zhimg.com/ffcca2b2853f2af791310e6a6d694e80.jpg"]."type": 0."id": 9165434."ga_prefix": "012121"."title": "Who says ordinary people can't have interesting lives?"},... ] }Copy the code

The corresponding bean: ZhihuDailyNews. Java

public class ZhihuDailyNews {

    private String date;
    private ArrayList<Question> stories;

    public String getDate(a) {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public ArrayList<Question> getStories(a) {
        return stories;
    }

    public void setStories(ArrayList<Question> stories) {
        this.stories = stories;
    }

    public class Question {

        private ArrayList<String> images;
        private int type;
        private int id;
        private String ga_prefix;
        private String title;

        public ArrayList<String> getImages(a) {
            return images;
        }

        public void setImages(ArrayList<String> images) {
            this.images = images;
        }

        public int getType(a) {
            return type;
        }

        public void setType(int type) {
            this.type = type;
        }

        public int getId(a) {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getGa_prefix(a) {
            return ga_prefix;
        }

        public void setGa_prefix(String ga_prefix) {
            this.ga_prefix = ga_prefix;
        }

        public String getTitle(a) {
            return title;
        }

        public void setTitle(String title) {
            this.title = title; }}}Copy the code

Day 4, display data

First, we have to have an Adapter.

ZhihuDailyNewsAdapter.java

public class ZhihuDailyNewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private final Context context;
    private final LayoutInflater inflater;
    private List<ZhihuDailyNews.Question> list = new ArrayList<ZhihuDailyNews.Question>();
    private OnRecyclerViewOnClickListener mListener;

    // Text + image
    private static final int TYPE_NORMAL = 0;
    // footer, load more
    private static final int TYPE_FOOTER = 1;

    public ZhihuDailyNewsAdapter(Context context, List<ZhihuDailyNews.Question> list){
        this.context = context;
        this.list = list;
        this.inflater = LayoutInflater.from(context);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // Load different layouts according to ViewType
        switch (viewType) {
            case TYPE_NORMAL:
                return new NormalViewHolder(inflater.inflate(R.layout.home_list_item_layout, parent, false), mListener);
            case TYPE_FOOTER:
                return new FooterViewHolder(inflater.inflate(R.layout.list_footer, parent, false));
        }
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        // Do different processing for different ViewHolder
        if (holder instanceof NormalViewHolder) {

            ZhihuDailyNews.Question item = list.get(position);

            if (item.getImages().get(0) = =null){
                ((NormalViewHolder)holder).itemImg.setImageResource(R.drawable.placeholder);
            } else {
                Glide.with(context)
                        .load(item.getImages().get(0)) .asBitmap() .placeholder(R.drawable.placeholder) .diskCacheStrategy(DiskCacheStrategy.SOURCE) .error(R.drawable.placeholder) .centerCrop() .into(((NormalViewHolder)holder).itemImg); } ((NormalViewHolder)holder).tvLatestNewsTitle.setText(item.getTitle()); }}// Since it contains footer, the return value should be + 1
    @Override
    public int getItemCount(a) {
        return list.size() + 1;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == list.size()) {
            return ZhihuDailyNewsAdapter.TYPE_FOOTER;
        }
        return ZhihuDailyNewsAdapter.TYPE_NORMAL;
    }

    public void setItemClickListener(OnRecyclerViewOnClickListener listener){
        this.mListener = listener;
    }

    public class NormalViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        private ImageView itemImg;
        private TextView tvLatestNewsTitle;
        private OnRecyclerViewOnClickListener listener;

        public NormalViewHolder(View itemView, OnRecyclerViewOnClickListener listener) {
            super(itemView);
            itemImg = (ImageView) itemView.findViewById(R.id.imageViewCover);
            tvLatestNewsTitle = (TextView) itemView.findViewById(R.id.textViewTitle);
            this.listener = listener;
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            if(listener ! =null){ listener.OnItemClick(v,getLayoutPosition()); }}}public class FooterViewHolder extends RecyclerView.ViewHolder{

        public FooterViewHolder(View itemView) {
            super(itemView); }}}Copy the code

The adapter contains two constants, TYPE_NORMAL and TYPE_FOOTER, which are used to distinguish the types of items and thus load different layouts. As we all know, there is no set item RecyclerView native and click event method, all we need to define an interface – OnRecyclerViewOnClickListener.

OnRecyclerViewOnClickListener.java

package com.marktony.zhihudaily.interfaze;

import android.view.View;

public interface OnRecyclerViewOnClickListener {

    void OnItemClick(View v,int position);

}Copy the code

ZhihuDailyPresenter.java

To implement the loadPosts method in ZhihuDailyPresenter, add network access to the manifest file:

model.load(Api.ZHIHU_HISTORY + formatter.ZhihuDailyDateFormat(date), new OnStringListener() {
                @Override
                public void onSuccess(String result) {

                    try {
                        ZhihuDailyNews post = gson.fromJson(result, ZhihuDailyNews.class);

                        if (clearing) {
                            list.clear();
                        }

                        for (ZhihuDailyNews.Question item : post.getStories()) {
                            list.add(item);                          
                        }
                        view.showResults(list);

                    } catch (JsonSyntaxException e) {
                        view.showError();
                    }

                    view.stopLoading();
                }

                @Override
                public void onError(VolleyError error) { view.stopLoading(); view.showError(); }});Copy the code

With Gson, we can easily convert JSON-formatted data into Java objects.

ZhihuDailyFragment

Implement the showResults method of ZhihuDailyFragment.

@Override
public void showResults(ArrayList<ZhihuDailyNews.Question> list) {
    if (adapter == null) {
        adapter = new ZhihuDailyNewsAdapter(getContext(), list);
        adapter.setItemClickListener(new OnRecyclerViewOnClickListener() {
            @Override
            public void OnItemClick(View v, int position) { presenter.startReading(position); }}); recyclerView.setAdapter(adapter); }else{ adapter.notifyDataSetChanged(); }}Copy the code

Day 4, cache content

By completing the above code, we have only achieved normal operation in the network state, but what if the user does not have an unimpeded network connection? This is where the cache comes in handy. Once the user loads it once, they can view the content that was previously offline even without an Internet connection. We chose to use the Android native SQLite database to store the data (you can also use Realm).

The first step, of course, is to create the database (since paper Airplane has iterated through several versions, the SQL statement or other content you used to create the database should not be exactly the same as my file).

DatabaseHelper.java

public class DatabaseHelper extends SQLiteOpenHelper {


    public DatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

        db.execSQL("create table if not exists Zhihu("
                + "id integer primary key autoincrement,"
                + "zhihu_id integer not null,"
                + "zhihu_news text,"
                + "zhihu_time real,"
                + "zhihu_content text)");

        db.execSQL("alter table Zhihu add column bookmark integer default 0");

    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}}Copy the code

I believe danniu should see, this database design really not how 😂, because I database learning is really very general. Ask Daniel not to spray.

field type meaning note
id integer A primary key Since the growth
zhihu_id integer Zhihu Daily message ID Provided by Zhihu
zhihu_news text Zhihu Daily news content Corresponds to a Java entity class
zhihu_time real Zhihu Daily news release time Provided by Zhihu
zhihu_content text Zhihu Daily news details Corresponds to a Java entity class
bookmark integer Collected or not Since SQLite does not have a Boolean type, use a different value of INTEGER instead

OK, when we get the data correctly, we can store it.

ZhihuDailyPresenter.java

if ( !queryIfIDExists(item.getId())) {
    db.beginTransaction();
    try {
        DateFormat format = new SimpleDateFormat("yyyyMMdd");
        Date date = format.parse(post.getDate());
        values.put("zhihu_id", item.getId());
        values.put("zhihu_news", gson.toJson(item));
        values.put("zhihu_content"."");
        values.put("zhihu_time", date.getTime() / 1000);
        db.insert("Zhihu".null, values);
        values.clear();
        db.setTransactionSuccessful();
    } catch (Exception e) {
        e.printStackTrace();
    } finally{ db.endTransaction(); }}// Query whether the id already exists in the database table
private boolean queryIfIDExists(int id){

    Cursor cursor = db.query("Zhihu".null.null.null.null.null.null);
    if (cursor.moveToFirst()){
        do {
            if (id == cursor.getInt(cursor.getColumnIndex("zhihu_id"))) {return true; }}while (cursor.moveToNext());
    }
    cursor.close();

    return false;
}Copy the code

If you are careful, you may have noticed that there is another field in the data table — ZHIHU_content, which you did not store. This is because when we request the list of Zhihu messages, we do not return the details of the message. However, we still need to cache the details. Network requests made on the UI thread can cause ANR, so a better solution is to do it in the Service.

We’re going to send out some of the necessary data, in the form of local broadcast.

ZhihuDailyPresenter.java

Intent intent = new Intent("com.marktony.zhihudaily.LOCAL_BROADCAST");
intent.putExtra("type", CacheService.TYPE_ZHIHU);
intent.putExtra("id", item.getId());
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);Copy the code

The CacheService then receives the broadcast, gets the transmitted data, and makes network requests and data storage.

CacheService.java

public class CacheService extends Service {

    private DatabaseHelper dbHelper;
    private SQLiteDatabase db;

    private static final String TAG = CacheService.class.getSimpleName();

    public static final int TYPE_ZHIHU = 0x00;
    public static final int TYPE_GUOKR = 0x01;
    public static final int TYPE_DOUBAN = 0x02;

    @Override
    public void onCreate(a) {
        super.onCreate();
        dbHelper = new DatabaseHelper(this."History.db".null.5);
        db = dbHelper.getWritableDatabase();

        IntentFilter filter = new IntentFilter();
        filter.addAction("com.marktony.zhihudaily.LOCAL_BROADCAST");
        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
        manager.registerReceiver(new LocalReceiver(), filter);

    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }

    /** * Network requests the content body of Zhihu daily corresponding to id * When type is 0, the data in body will be stored * When type is 1, the content in share URL will be requested again and stored *@paramId Indicates the ID */ corresponding to the message content of Zhihu Daily to be obtained
    private void startZhihuCache(final int id) {

        Cursor cursor = db.query("Zhihu".null.null.null.null.null.null);
        if (cursor.moveToFirst()) {
            do {
                if ((cursor.getInt(cursor.getColumnIndex("zhihu_id")) == id)
                        && (cursor.getString(cursor.getColumnIndex("zhihu_content")).equals(""))) {
                    StringRequest request = new StringRequest(Request.Method.GET, Api.ZHIHU_NEWS + id, new Response.Listener<String>() {
                        @Override
                        public void onResponse(String s) {
                            Gson gson = new Gson();
                            ZhihuDailyStory story = gson.fromJson(s, ZhihuDailyStory.class);
                            if (story.getType() == 1) {
                                StringRequest request = new StringRequest(Request.Method.GET, story.getShare_url(), new Response.Listener<String>() {
                                    @Override
                                    public void onResponse(String s) {
                                        ContentValues values = new ContentValues();
                                        values.put("zhihu_content", s);
                                        db.update("Zhihu", values, "zhihu_id = ?".newString[] {String.valueOf(id)}); values.clear(); }},new Response.ErrorListener() {
                                    @Override
                                    public void onErrorResponse(VolleyError volleyError) {}}); request.setTag(TAG); VolleySingleton.getVolleySingleton(CacheService.this).addToRequestQueue(request);
                            } else {
                                ContentValues values = new ContentValues();
                                values.put("zhihu_content", s);
                                db.update("Zhihu", values, "zhihu_id = ?".newString[] {String.valueOf(id)}); values.clear(); }}},new Response.ErrorListener() {
                        @Override
                        public void onErrorResponse(VolleyError volleyError) {}}); request.setTag(TAG); VolleySingleton.getVolleySingleton(CacheService.this).addToRequestQueue(request); }}while (cursor.moveToNext());
        }
        cursor.close();
    }

    @Override
    public void onDestroy(a) {
        super.onDestroy();
        VolleySingleton.getVolleySingleton(this).getRequestQueue().cancelAll(TAG);
    }

    class LocalReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            int id = intent.getIntExtra("id".0);
            switch (intent.getIntExtra("type", -1)) {
                case TYPE_ZHIHU:
                    startZhihuCache(id);
                    break;
                case TYPE_GUOKR:
                    startGuokrCache(id);
                    break;
                case TYPE_DOUBAN:
                    startDoubanCache(id);
                    break;
                default:
                case -1:
                    break; }}}}Copy the code

If the specified ID in the database is not empty, we will skip it directly, which can save the user’s traffic and power.

At this point, the storage of the data is complete. But how do you read it? Ha, in fact, also simple, we judge the current network status, if the user device is not connected to the network, we directly go to the database to read, and then parse on the line.

ZhihuDailyPresenter.java

if (NetworkState.networkConnected(context)) {
    // balabala
} else {
    Cursor cursor = db.query("Zhihu".null.null.null.null.null.null);
    if (cursor.moveToFirst()) {
        do {
            ZhihuDailyNews.Question question = gson.fromJson(cursor.getString(cursor.getColumnIndex("zhihu_news")), ZhihuDailyNews.Question.class);
            list.add(question);
        } while (cursor.moveToNext());
    }
    cursor.close();
    view.stopLoading();
    view.showResults(list);
}Copy the code

So here we are, almost done for the day. Wait, did you forget something? Our Service is not started.

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initViews(); 

    // Start the service
    startService(new Intent(this, CacheService.class));

}

@Override
protected void onDestroy(a) {
    ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
        if (CacheService.class.getName().equals(service.service.getClassName())) {
            stopService(new Intent(this, CacheService.class)); }}super.onDestroy();
}Copy the code

Today’s content is the most day in a week, which may be more than the total of the previous days. You may need to work overtime to complete the content. There are still parts of Activity, Presenter and Fragment that need to be completed by yourself. But are you excited to see your App working correctly? Get some rest and get ready for tomorrow.