动手实现天气预报App(三)——切换城市手动更新+后台服务自动刷新

手动更新和后台服务自动刷新及切换城市

手动更新天气

采用下拉刷新的来实现

<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>

使用SwipeRefreshLayout将ScrollView内容包裹住,实现下拉刷新,然后需要更改WeatherActivity中的逻辑,这部分发现书中代码有些许问题,导致切换城市再刷新后仍然显示第一次缓存中的天气信息,故修改代码修复此处bug。主要是通过将WeatherId定义为一个全局变量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);
    // 初始化各控件
    ...
    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) {
    
    
        // 有缓存时直接解析天气数据
        Weather weather = Utility.handleWeatherResponse(weatherString);
        mWeatherId = weather.basic.weatherId;
        showWeatherInfo(weather);
    } else {
    
    
        // 无缓存时去服务器查询天气
        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();
    }
}

修改的代码并不算多,首先在 onCreate()方法中获取到了 SwipeRefreshLayout的实例,然后调用setcolorschemeresources()方法来设置下拉刷新进度条的颜色,这里我们就使用主题中的 colorPrimary作为进度条的颜色了。

然后调用 setOnRefreshListener()方法来设置一个下拉刷新的监听器,当触发了下拉刷新操作的时候,就会回调这个监听器的 onRefresh()方法,我们在这里去调用 requestweather()方法请求天气信息就可以了。


    /**
     * 根据天气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, "获取天气信息失败", Toast.LENGTH_SHORT).show();
                        swipeRefresh.setRefreshing(false);
                    }
                });
            }
        });
        loadBingPic();
    }

另外不要忘记,当请求结束后,还需要调用 SwipeRefreshLayout的 setRefreshing()方法,并传入false,用于表示刷新事件结束,并隐藏刷新进度条,此时更新完进度条会自动消失。

切换城市

在这里的思路是将前面写好的城市列表碎片以滑动菜单的形式嵌入天气信息,当需要切换时,滑出城市列表进行点选切换。

先加入一个按钮表示可以切换。

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>

接着修改天气信息的界面布局文件,添加滑动菜单功能。

   <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包裹原布局,其中主要包含两个部分,一个是天气界面,一个是城市信息列表的碎片

DrawerLayout中第一个控件用来显示主屏幕内容,第二个控件作为滑动菜单中的内容。

接下来介入滑动菜单额逻辑处理:

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

很简单,首先在 onCreate()方法中获取到新增的 Drawerlayout和 Button的实例,然后在Button的点击事件中调用 DrawerLayout的 openDrawer方法来打开滑动菜单就可以

不过现在还没有结束,因为这仅仅是打开了滑动菜单而已,我们还需要处理切换城市后的逻辑才行。这个工作就必须要在 ChooseAreaFragment中进行了,因为之前选中了某个城市后是跳转到 WeatherActivity的,而现在由于我们本来就是在 WeatherActivity当的,因此并不需要跳转只是去请求新选择城市的天气信息就可以了。
那么很显然这里我们需要根据 ChooseAreaFragment的不同状态来进行不同的逻辑处理,修改 ChooseAreaFragment中的代码,如下所示

mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    
    
    //可以看到,我们使用 setonItemClicklistener()方法为 Listview注册了一个监听器,当
    //用户点击了 Listview中的任何一个子项时,就会回调 onItemclick()方法。在这个方法中可以
    //通过 position参数判断出用户点击的是哪一个子项,然后获取到相应的类信息,并通过Toast显示
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int pos, long idl) {
    
    
        //当你点击了某个item的时候会进入到 List view的 onItemclick()方法中,这个时候会根据当
        //前的级别来判断是去调用 querycities()方法还是 query Counties()方法, queryCities()方
        //法是去査询市级数据,而 queryCounties()方法是去查询县级数据,这两个方法内部的流程和
        //queryProvinces()方法基本相同
        if (currentLevel == LEVEL_PROVINCE){
    
    
            selectedProvince = provinceList.get(pos);
            queryCity();
        }else if (currentLevel == LEVEL_CITY){
    
    
            selectedCity = cityList.get(pos);
            queryCounty();
        }else if (currentLevel == LEVEL_COUNTY){
    
    
            //非常简单,这里在 onitemclick()方法中加入了一个if判断,如果当前级别是 LEVEL
            //COUNTY,就启动 WeatherActivity,并把当前选中县的天气i传递过去。
            String weatherId = countyList.get(pos).getWeatherId();
            //这里使用了一个Java中的小技巧, instanceof关键字可以用来判断一个对象是否属于某
            //个类的实例。我们在碎片中调用 getActivity()方法,然后配合 instanceof关键字,就能轻
            //松判断出该碎片是在 MainActivity当中,还是在 WeatherActivity当中。如果是在 MainActivity当
            //中,那么处理逻辑不变。如果是在 WeatherActivity当中,那么就关闭滑动菜单,显示下拉刷新进
            //度条,然后请求新城市的天气信息
            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);
            }
        }
    }
});

然后我们就可以切换其他城市了。选中城市之后滑动菜单会自动关闭,并且主界面上的天气信息也会更新成你选择的那个城市。

后台自动更新

创建服务来实现后台的自动刷新天气信息的功能。
在这里插入图片描述

可以看到,在 onstartcommand()方法中先是调用了 updateweather()方法来更新天气然后调用了 updateBingPic()方法来更新背景图片。这里我们将更新后的数据直接存储到SharedPreferences文件中就可以了,因为打开WeatherActivity的时候都会优先从 SharedPreferences缓存中读取数据。

之后就是创建定时任务的技巧了,为了保证软件不会消耗过多的流量,这里将时间间隔设置为8小时,8小时后 AutoUpdateReceiver的 onstart command()方法就会重新执行,这样也就实现后台定时更新的功能了。


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){
    
    
            //有缓存时 直接解析天气数据
            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();
                            }
                }
            });
        }
    }


}

关于服务的使用,官网文档中介绍如下:

如要创建服务,您必须创建 Service 的子类(或使用它的一个现有子类)。在实现中,您必须重写一些回调方法,从而处理服务生命周期的某些关键方面,并提供一种机制将组件绑定到服务(如适用)。以下是您应重写的最重要的回调方法:

  • onStartCommand()

    当另一个组件(如 Activity)请求启动服务时,系统会通过调用 startService() 来调用此方法。执行此方法时,服务即会启动并可在后台无限期运行。如果您实现此方法,则在服务工作完成后,您需负责通过调用 stopSelf()stopService() 来停止服务。(如果您只想提供绑定,则无需实现此方法。)

  • onBind()

    当另一个组件想要与服务绑定(例如执行 RPC)时,系统会通过调用 bindService() 来调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,以供客户端用来与服务进行通信。请务必实现此方法;但是,如果您并不希望允许绑定,则应返回 null。

  • onCreate()

    首次创建服务时,系统会(在调用 onStartCommand()onBind() 之前)调用此方法来执行一次性设置程序。如果服务已在运行,则不会调用此方法。

  • onDestroy()

    当不再使用服务且准备将其销毁时,系统会调用此方法。服务应通过实现此方法来清理任何资源,如线程、注册的侦听器、接收器等。这是服务接收的最后一个调用。

如果组件通过调用 startService() 启动服务(这会引起对 onStartCommand() 的调用),则服务会一直运行,直到其使用 stopSelf() 自行停止运行,或由其他组件通过调用 stopService() 将其停止为止。

如果组件通过调用 bindService() 来创建服务,且调用 onStartCommand(),则服务只会在该组件与其绑定时运行。当该服务与其所有组件取消绑定后,系统便会将其销毁。

只有在内存过低且必须回收系统资源以供拥有用户焦点的 Activity 使用时,Android 系统才会停止服务。如果将服务绑定到拥有用户焦点的 Activity,则它其不太可能会终止;如果将服务声明为在前台运行,则其几乎永远不会终止。如果服务已启动并长时间运行,则系统逐渐降低其在后台任务列表中的位置,而服务被终止的概率也会大幅提升—如果服务是启动服务,则您必须将其设计为能够妥善处理系统执行的重启。如果系统终止服务,则其会在资源可用时立即重启服务,但这还取决于您从 onStartCommand() 返回的值。如需了解有关系统会在何时销毁服务的详细信息,请参阅进程和线程文档。

下文将介绍如何创建 startService()bindService() 服务方法,以及如何通过其他应用组件使用这些方法。

1.使用清单文件声明服务

如同对 Activity 及其他组件的操作一样,您必须在应用的清单文件中声明所有服务。

如要声明服务,请添加 service 元素作为 application 元素的子元素。下面是示例:

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

您还可在 service 元素中加入其他属性,以定义一些特性,如启动服务及其运行时所在进程需要的权限。android:name 属性是唯一必需的属性,用于指定服务的类名。发布应用后,请保此类名不变,以避免因依赖显式 Intent 来启动或绑定服务而破坏代码的风险。

注意:为确保应用的安全性,在启动 Service 时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务会响应 Intent,而用户也无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),则系统会抛出异常。

您可以通过添加 android:exported 属性并将其设置为 false,确保服务仅适用于您的应用。这可以有效阻止其他应用启动您的服务,即便在使用显式 Intent 时也如此。

注意:用户可以查看其设备上正在运行的服务。如果他们发现自己无法识别或信任的服务,则可以停止该服务。为避免用户意外停止您的服务,您需要在应用清单的 `` 元素中添加 android:description。请在描述中用一个短句解释服务的作用及其提供的好处。

2.创建启动服务

启动服务由另一个组件通过调用 startService() 启动,这会导致调用服务的 onStartCommand() 方法。

服务启动后,其生命周期即独立于启动它的组件。即使系统已销毁启动服务的组件,该服务仍可在后台无限期地运行。因此,服务应在其工作完成时通过调用 stopSelf() 来自行停止运行,或者由另一个组件通过调用 stopService() 来将其停止。

应用组件(如 Activity)可通过调用 startService() 方法并传递 Intent 对象(指定服务并包含待使用服务的所有数据)来启动服务。服务会在 onStartCommand() 方法接收此 Intent

例如,假设某 Activity 需要将一些数据保存到在线数据库中。该 Activity 可以启动一个协同服务,并通过向 startService() 传递一个 Intent,为该服务提供要保存的数据。服务会通过 onStartCommand() 接收 Intent,连接到互联网并执行数据库事务。事务完成后,服务将自行停止并销毁。

**注意:**默认情况下,服务与服务声明所在的应用运行于同一进程,并且运行于该应用的主线程中。如果服务在用户与来自同一应用的 Activity 进行交互时执行密集型或阻止性操作,则会降低 Activity 性能。为避免影响应用性能,请在服务内启动新线程。

通常,您可以扩展两个类来创建启动服务:

  • Service

    这是适用于所有服务的基类。扩展此类时,您必须创建用于执行所有服务工作的新线程,因为服务默认使用应用的主线程,这会降低应用正在运行的任何 Activity 的性能。

  • IntentService

    这是 Service 的子类,其使用工作线程逐一处理所有启动请求。如果您不要求服务同时处理多个请求,此类为最佳选择。实现 onHandleIntent(),该方法会接收每个启动请求的 Intent,以便您执行后台工作。

3.启动服务

您可以通过将 Intent 传递给 startService()startForegroundService(),从 Activity 或其他应用组件启动服务。Android 系统会调用服务的 onStartCommand() 方法,并向其传递 Intent,从而指定要启动的服务。

例如,Activity 可以结合使用显式 Intent 与 startService(),从而启动上文中的示例服务 (HelloService):

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

startService() 方法会立即返回,并且 Android 系统会调用服务的 onStartCommand() 方法。如果服务尚未运行,则系统首先会调用 onCreate(),然后调用 onStartCommand()

如果服务亦未提供绑定,则应用组件与服务间的唯一通信模式便是使用 startService() 传递的 Intent。但是,如果您希望服务返回结果,则启动服务的客户端可以为广播(通过 getBroadcast() 获得)创建一个 PendingIntent,并将其传递给启动服务的 Intent 中的服务。然后,服务便可使用广播传递结果。

多个服务启动请求会导致多次对服务的 onStartCommand() 进行相应的调用。但是,如要停止服务,只需一个服务停止请求(使用 stopSelf()stopService())即可。

在WeatherActivity中添加代码

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

4.创建绑定服务

绑定服务允许应用组件通过调用 bindService() 与其绑定,从而创建长期连接。此服务通常不允许组件通过调用 startService()启动它。

如需与 Activity 和其他应用组件中的服务进行交互,或需要通过进程间通信 (IPC) 向其他应用公开某些应用功能,则应创建绑定服务。

如要创建绑定服务,您需通过实现 onBind() 回调方法返回 IBinder,从而定义与服务进行通信的接口。然后,其他应用组件可通过调用 bindService() 来检索该接口,并开始调用与服务相关的方法。服务只用于与其绑定的应用组件,因此若没有组件与该服务绑定,则系统会销毁该服务。您不必像通过 onStartCommand() 启动的服务那样,以相同方式停止绑定服务。

如要创建绑定服务,您必须定义指定客户端如何与服务进行通信的接口。服务与客户端之间的这个接口必须是 IBinder 的实现,并且服务必须从 onBind() 回调方法返回该接口。收到 IBinder 后,客户端便可开始通过该接口与服务进行交互。

多个客户端可以同时绑定到服务。完成与服务的交互后,客户端会通过调用 unbindService() 来取消绑定。如果没有绑定到服务的客户端,则系统会销毁该服务。

实现绑定服务有多种方法,并且此实现比启动服务更为复杂。出于这些原因,请参阅另一份文档绑定服务,了解关于绑定服务的详细介绍。

可以看到,这里在 showweather()方法的最后加入启动 AutoUpdateService这个服务的代码
这样只要一旦选中了某个城市并成功更新天气之后, AutoUpdateService就会一直在后台运行,并保证每8小时更新一次天气。

测试

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/kilotwo/article/details/108429755