Realize the weather forecast App by hand -- switch the city to manual update + automatic refresh of background service

Manual update and background service automatic refresh and switch cities

Update weather manually

It is realized by pull-down refresh

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/swipe_refresh">
    <ScrollView
        android:id="@+id/weather_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none"
        android:overScrollMode="never">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:fitsSystemWindows="true">
            <include layout="@layout/title"/>
            <include layout="@layout/now"/>
            <include layout="@layout/forecast"/>
            <include layout="@layout/aqi"/>
            <include layout="@layout/suggestion"/>

        </LinearLayout>
    </ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

Use SwipeRefreshLayout to wrap the contents of ScrollView to realize drop-down refresh, and then need to change the logic in WeatherActivity. In this part, it is found that there are some problems in the code in the book, which leads to the weather information in the cache for the first time after switching cities and then refreshing. Therefore, modify the code to fix the bug here. It is mainly solved by defining WeatherId as a global variable private String mWeatherId.

private String mWeatherId;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (Build.VERSION.SDK_INT >= 21) {
        View decorView = getWindow().getDecorView();
        decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        getWindow().setStatusBarColor(Color.TRANSPARENT);
    }
    setContentView(R.layout.activity_weather);
    // Initialize each control
    ...
    swipeRefresh = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
    swipeRefresh.setColorSchemeResources(R.color.colorPrimary);
    drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
    navButton = (Button) findViewById(R.id.nav_button);
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    String weatherString = prefs.getString("weather", null);
    if (weatherString != null) {
        // Directly parse weather data when there is cache
        Weather weather = Utility.handleWeatherResponse(weatherString);
        mWeatherId = weather.basic.weatherId;
        showWeatherInfo(weather);
    } else {
        // When there is no cache, go to the server to query the weather
        mWeatherId = getIntent().getStringExtra("weather_id");
        weatherLayout.setVisibility(View.INVISIBLE);
        requestWeather(mWeatherId);
    }
    swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            requestWeather(mWeatherId);
        }
    });

    navButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            drawerLayout.openDrawer(GravityCompat.START);
        }
    });
    String bingPic = prefs.getString("bing_pic", null);
    if (bingPic != null) {
        Glide.with(this).load(bingPic).into(bingPicImg);
    } else {
        loadBingPic();
    }
}

Not much code has been modified. First, get an instance of SwipeRefreshLayout in the onCreate() method, and then call the setcolorschemeresources() method to set the color of the drop-down refresh progress bar. Here, we use the colorPrimary in the topic as the color of the progress bar.

Then call the setOnRefreshListener() method to set a drop-down refresh listener. When the drop-down refresh operation is triggered, the onRefresh() method of the listener will be called back. Here, we can call the requestweather() method to request weather information.

    /**
     * Request city weather information according to weather id.
     */
    public void requestWeather(final String weatherId) {
        String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherId + "&key=bc0418b57b2d4918819d3974ac1285d9";
        HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String responseText = response.body().string();
                final Weather weather = Utility.handleWeatherResponse(responseText);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
               ...
                        swipeRefresh.setRefreshing(false);
                    }
                });
            }

            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(WeatherActivity.this, "Failed to get weather information", Toast.LENGTH_SHORT).show();
                        swipeRefresh.setRefreshing(false);
                    }
                });
            }
        });
        loadBingPic();
    }

In addition, don't forget that when the request ends, you also need to call the setRefreshing() method of SwipeRefreshLayout and pass in false to indicate that the refresh event ends and hide the refresh progress bar. At this time, the update progress bar will disappear automatically.

Switch City

The idea here is to embed the fragments of the previously written city list into the weather information in the form of sliding menu. When switching is needed, slide out of the city list and click to switch.

First add a button to indicate that you can switch.

title.xml

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize">
    <Button
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:id="@+id/nav_button"
        android:layout_marginLeft="10dp"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:background="@drawable/ic_home"
        />
    ...
</RelativeLayout>

Then modify the interface layout file of weather information and add the function of sliding menu.

   <androidx.drawerlayout.widget.DrawerLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/drawer_layout"
        >
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/swipe_refresh">
    ...
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/choose_area_fragment"
        android:name="com.wz.myweatherapp.ChooseAreaFragment"
        android:layout_gravity="start"
        />
    </androidx.drawerlayout.widget.DrawerLayout>

DrawerLayout wraps the original layout, which mainly includes two parts: one is the weather interface and the other is the fragment of the city information list

The first control in DrawerLayout is used to display the content of the main screen, and the second control is used as the content in the sliding menu.

Next, intervene in the logical processing of sliding menu:

public DrawerLayout mDrawerLayout;
private Button navButton;
...
        navButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mDrawerLayout.openDrawer(GravityCompat.START);
            }
        });

Very simply, first get the new drawerlayout and Button instances in the onCreate() method, and then call the openDrawer method of drawerlayout in the Button click event to open the sliding menu

But it's not over yet, because it just opens the sliding menu. We still need to deal with the logic after switching cities. This work must be carried out in ChooseAreaFragment, because before, after selecting a city, we jumped to WeatherActivity. Now, because we were originally working in WeatherActivity, we don't need to jump, just ask for the weather information of the newly selected city.
Obviously, we need to perform different logical processing according to different states of ChooseAreaFragment and modify the code in ChooseAreaFragment, as shown below

mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    //As you can see, we use the setonItemClicklistener() method to register a listener for Listview when
    //When the user clicks any sub item in the Listview, the onItemclick() method will be called back. In this method, you can
    //Determine which sub item the user clicks through the position parameter, and then obtain the corresponding class information and display it through Toast
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int pos, long idl) {
        //When you click an item, you will enter the onItemclick() method of List view. At this time, according to the
        //Determine whether to call the queryCities() method or the query Counties() method according to the previous level, and the queryCities() side
        //Method is to query municipal data, while queryCounties() method is to query county-level data. The internal processes and
        //The queryProvinces() method is basically the same
        if (currentLevel == LEVEL_PROVINCE){
            selectedProvince = provinceList.get(pos);
            queryCity();
        }else if (currentLevel == LEVEL_CITY){
            selectedCity = cityList.get(pos);
            queryCounty();
        }else if (currentLevel == LEVEL_COUNTY){
            //Very simple. Here, an if judgment is added to the onitemclick() method. If the current LEVEL is LEVEL
            //Country, start WeatherActivity and pass the weather i of the currently selected COUNTY.
            String weatherId = countyList.get(pos).getWeatherId();
            //Here we use a small trick in Java. The instanceof keyword can be used to judge whether an object belongs to an object
            //An instance of a class. We call the getActivity() method in the fragment, and then cooperate with the instanceof keyword to
            //Song determines whether the fragment is in MainActivity or WeatherActivity. If it is in MainActivity
            //The processing logic remains unchanged. If it is in WeatherActivity, close the sliding menu and display the new entry of the drop-down brush
            //Degree bar, and then request weather information for the new city
            if(getActivity() instanceof  MainActivity){
                Intent intent = new Intent(getActivity(),WeatherActivity.class);
                intent.putExtra("weather_id",weatherId);
                startActivity(intent);
                getActivity().finish();
            }else if (getActivity() instanceof WeatherActivity){
               WeatherActivity activity = (WeatherActivity) getActivity();
               activity.mDrawerLayout.closeDrawers();
               activity.mSwipeRefreshLayout.setRefreshing(true);
               activity.requestWeather(weatherId);
               //Log.d(TAG, "onItemClick: "+"getActivity() instanceof WeatherActivity"+weatherId);
            }
        }
    }
});

Then we can switch to other cities. After selecting a city, the sliding menu will automatically close, and the weather information on the main interface will be updated to the city you selected.

Background automatic update

Create a service to realize the function of automatically refreshing weather information in the background.

You can see that in the onstartcommand() method, the updateweather() method is called to update the weather, and then the updateBingPic() method is called to update the background image. Here, we can store the updated data directly into the SharedPreferences file, because the data will be read from the SharedPreferences cache first when opening WeatherActivity.

Then comes the skill of creating scheduled tasks. In order to ensure that the software will not consume too much traffic, set the time interval to 8 hours, and the onstart command() method of AutoUpdateReceiver will be re executed after 8 hours, so as to realize the function of background scheduled update.

public class AutoUpdateService extends Service {

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

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        updateWeather();
        updateBingPic();
        AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
        int anHour = 8*60*60*1000;
        long triggerAtTimer = SystemClock.elapsedRealtime() + anHour;
        Intent i = new Intent(this,AutoUpdateService.class);
        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
        manager.cancel(pi);
        manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTimer,pi);
        return super.onStartCommand(intent, flags, startId);

    }

    private void updateBingPic() {
        String requestBingPic = "http://guolin.tech/api/bing_pic";
        HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String bingPic = response.body().string();
                SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(AutoUpdateService.this).edit();
                editor.putString("bing_pic",bingPic);
                editor.apply();
            }
        });
    }



    private void updateWeather() {
        SharedPreferences pres = PreferenceManager.getDefaultSharedPreferences(this);
        String weatherString = pres.getString("weather", null);
        if (weatherString != null){
            //Directly parse weather data when there is cache
            Weather weather = Utility.handleWeatherResponse(weatherString);
            String weatherId = weather.basic.weatherId;

            String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherId +"&key=755a053d247341699ebbe941099d994f";
            HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                        e.printStackTrace();
                }
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    final String responseText = response.body().string();
                    final Weather weather = Utility.handleWeatherResponse(responseText);

                            if (weather != null&&"ok".equals(weather.status)){

                                SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(AutoUpdateService.this).edit();
                                editor.putString("weather",responseText);
                                editor.apply();
                            }
                }
            });
        }
    }


}

The use of the service is described in the official website document as follows:

To create a Service, you must create a subclass of the Service (or use one of its existing subclasses). In the implementation, you must rewrite some callback methods to deal with some key aspects of the Service life cycle and provide a mechanism to bind components to services, if applicable. The following are the most important callback methods you should override:

  • onStartCommand()

    When another component (such as Activity) requests to start the service, the system will call this method by calling startService(). When this method is executed, the service starts and can run indefinitely in the background. If you implement this method, you are responsible for stopping the service by calling stopSelf() or stopService() after the service work is completed. (if you only want to provide bindings, you don't need to implement this method.)

  • onBind()

    When another component wants to bind to a service (such as executing RPC), the system will call this method by calling bindService(). In the implementation of this method, you must provide an interface for the client to communicate with the service by returning IBinder. Please be sure to implement this method; However, if you do not want to allow binding, you should return null.

  • onCreate()

    When the service is created for the first time, the system will call this method (before calling onStartCommand() or onBind()) to execute the one-time setup program. If the service is already running, this method will not be called.

  • onDestroy()

    This method is called when the service is no longer in use and ready to be destroyed. The service should implement this method to clean up any resources, such as threads, registered listeners, receivers, etc. This is the last call received by the service.

If a component starts a service by calling startService() (which causes a call to onStartCommand()), the service runs until it stops itself using stopSelf(), or until it is stopped by another component by calling stopService().

If a component creates a service by calling bindService() and onStartCommand() is not called, the service will only run when the component is bound to it. When the service is unbound from all its components, the system will destroy it.

The Android system will stop service only when the memory is too low and the system resources must be reclaimed for use by the Activity with user focus. If the service is bound to an Activity with user focus, it is unlikely to terminate; If the service is declared as Run in the foreground , it will almost never end. If the service has been started and run for a long time, the system will gradually reduce its position in the background task list, and the probability of service termination will increase significantly - if the service is a startup service, you must design it to properly handle the restart performed by the system. If the system terminates the service, it will restart the service as soon as resources are available, but this also depends on the value you return from onStartCommand(). For more information on when the system destroys services, see Processes and threads file.

The following describes how to create startService() and bindService() service methods and how to use these methods through other application components.

1. Use manifest file to declare service

As with activities and other components, you must declare all services in the manifest file of the application.

To declare a service, add service Element as application The child element of the element. Here is an example:

<manifest ... >
  ...
  <application ... >
      <service android:name=".ExampleService" />
      ...
  </application>
</manifest>

You can also service Add other attributes to the element to define some features, such as the permissions required to start the service and the process in which it runs. android:name Property is the only required property that specifies the class name of the service. After publishing the application, please keep the class name unchanged to avoid the risk of damaging the code by relying on explicit Intent to start or bind the service.

Note: to ensure the security of the application, always use explicit Intent when starting the Service, and do not declare Intent filter for the Service. Starting services with implicit Intent is a security risk because you cannot determine which services will respond to Intent and users cannot see which services have been started. Starting from Android 5.0 (API level 21), if bindService() is called with implicit Intent, the system will throw an exception.

You can add android:exported Property and set it to false to ensure that the service is only applicable to your application. This can effectively prevent other applications from starting your service, even when using explicit Intent.

Note: users can view the services running on their devices. If they find a service they don't recognize or trust, they can stop the service. To prevent users from accidentally stopping your service, you need to add `` Add to element android:description . Please use a short sentence in the description to explain the role of the service and the benefits it provides.

2. Create startup service

The startup service is started by another component by calling startService(), which causes the onStartCommand() method of the service to be called.

After a service is started, its life cycle is independent of the component that started it. Even if the system has destroyed the component that started the service, the service can still run indefinitely in the background. Therefore, the service should stop running from the line by calling stopSelf() when its work is completed, or another component should stop it by calling stopService().

Application components (such as Activity) can start the service by calling the startService() method and passing the Intent object (specifying the service and including all data of the service to be used). The service will receive this Intent in the onStartCommand() method.

For example, suppose an Activity needs to save some data to an online database. The Activity can start a collaboration service and provide the data to be saved for the service by passing an Intent to startService(). The service will receive Intent through onStartCommand(), connect to the Internet and execute database transactions. After the transaction is completed, the service will stop and destroy itself.

**Note: * * by default, the service and the application where the service declaration is located run in the same process and in the main thread of the application. If a service performs intensive or blocking operations when a user interacts with an Activity from the same application, the performance of the Activity will be reduced. To avoid affecting application performance, start a new thread within the service.

In general, you can extend two classes to create a startup service:

  • Service

    This is the base class for all services. When extending this class, you must create a new thread to perform all service work, because the service uses the main thread of the application by default, which will reduce the performance of any Activity the application is running.

  • IntentService

    This is a subclass of Service, which uses worker threads to process all start requests one by one. This is the best choice if you do not require the Service to process multiple requests at the same time. Implement onHandleIntent(), which receives the Intent of each startup request so that you can perform background work.

3. Start service

You can start services from activities or other application components by passing Intent to startService() or startforeroundservice(). The Android system will call the onStartCommand() method of the service and pass Intent to it to specify the service to start.

For example, Activity can use explicit Intent in combination with startService() to start the example service (HelloService) in the above:

Intent intent = new Intent(this, HelloService.class);
startService(intent);

The startService() method will return immediately, and the Android system will call the onstartcommand () method of the service. If the service is not running, the system will first call onCreate() and then onStartCommand().

If the service does not provide binding, the only communication mode between the application component and the service is the Intent passed by startService(). However, if you want the service to return results, the client that started the service can create a PendingIntent for the broadcast (obtained through getBroadcast()) and pass it to the service in the Intent that started the service. The service can then use the broadcast to deliver the results.

Multiple service startup requests will result in multiple corresponding calls to onStartCommand() of the service. However, to stop a service, you only need a service stop request (using stopSelf() or stopService()).

Add code in WeatherActivity

private void showWeatherInfo(Weather weather) {
   ...
   ...
    mWeatherLayout.setVisibility(View.VISIBLE);
    Intent intent = new Intent(this, AutoUpdateService.class);
    startService(intent);
 }

4. Create binding service

Binding service allows application components to bind to it by calling bindService(), so as to create a long-term connection. This service usually does not allow components to start it by calling startService().

If you need to interact with services in activities and other application components, or you need to expose some application functions to other applications through interprocess communication (IPC), you should create a binding service.

To create a binding service, you need to return IBinder by implementing the onBind() callback method, so as to define the interface to communicate with the service. Then, other application components can retrieve the interface by calling bindService() and start calling service related methods. The service is only used for the application components bound to it. Therefore, if no component is bound to the service, the system will destroy the service. You don't have to stop the binding service in the same way as the service started through onStartCommand().

To create a bound service, you must define an interface that specifies how the client communicates with the service. The interface between the service and the client must be the implementation of IBinder, and the service must return the interface from the onBind() callback method. After receiving the IBinder, the client can start to interact with the service through this interface.

Multiple clients can be bound to the service at the same time. After the interaction with the service is completed, the client will unbind by calling unbindService(). If there is no client bound to the service, the system will destroy the service.

There are many ways to implement a binding service, and this implementation is more complex than starting a service. For these reasons, refer to another document Binding service , learn more about binding services.

You can see that the code to start the AutoUpdateService service is added at the end of the showweather() method
In this way, once a city is selected and the weather is successfully updated, AutoUpdateService will always run in the background and ensure that the weather is updated every 8 hours.

test

Tags: Java Android

Posted by bradleybebad on Thu, 19 May 2022 13:49:32 +0300