项目实战——欧瑞天气App

到现在为止,我们已经学习了绝大多数Kotlin的核心技术以及如何用Kotlin开发Android App,也编写过大量的程序,但还没有设计过一款完整的App,本章将满足读者的这个愿望,设计一款可以访问网络的Android App:欧瑞天气。

16.1 项目概述

这款App用于从服务端获取天气预报信息,并显示在窗口区域。这款App会首先列出省级及其所辖城市和县区信息,如图16-1所示。

▲图16-1 列出省级及其所辖城市和县区信息

当单击某个城市或县区名称时,会在窗口上显示该城市或县区的天气情况,如图16-2所示。

▲图16-2 显示天气情况

这款App使用前面章节介绍的UI技术、网络技术,并且使用Kotlin语言编写。其中有一些Library使用了Java编写,实际上,这款App是Kotlin和Java的结合体。

16.2 添加依赖

在App中使用了大量的第三方Library,如gson、okhttp3、glide等,这些Library需要在app/build.gradle文件中的dependencies部分指定,如下所示:

dependencies {compile fileTree(include: ['*.jar'], dir: 'libs')androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {exclude group: 'com.android.support', module: 'support-annotations'})compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"compile 'com.android.support:appcompat-v7:25.1.1'testCompile 'junit:junit:4.12'compile 'com.android.support.constraint:constraint-layout:1.0.2'implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"implementation 'com.google.code.gson:gson:2.8.1'implementation 'com.squareup.okhttp3:okhttp:3.8.1'implementation 'com.github.bumptech.glide:glide:4.0.0-RC1'implementation 'com.android.support.constraint:constraint-layout:1.0.2'
}

16.3 实现主窗口

主窗口类是MainActivity,这是该App第一个要启动的窗口。该窗口类的实现代码如下:

Kotlin代码(主窗口类)

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val prefs = PreferenceManager.getDefaultSharedPreferences(this)if (prefs.getString("weather", null) != null) {val intent = Intent(this, WeatherActivity::class.java)startActivity(intent)finish()}}
}

我们可以看到,MainActivity类的实现代码并不复杂,其中利用SharedPreferences对象读取了配置信息weather,这个配置信息用于指明是否曾经查询过某个城市的天气,如果查询过,直接显示该城市的天气信息。这里面涉及一个WeatherActivity类,这是专门用于显示天气信息的窗口。

下面看一下MainActivity使用的布局文件(activity_main.xml)。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent">   <fragment
      android:id="@+id/choose_area_fragment"
      android:name="com.oriweather.fragment.ChooseAreaFragment"
      android:layout_width="match_parent"
      android:layout_height="match_parent" /></FrameLayout>

在布局文件中,使用<fragment>标签引用了一个ChooseAreaFragment类,这是什么呢?实际上,Fragment是从Android 3.0开始加入的类,相当于一个透明的Panel,用于封装逻辑和UI,可以作为一个组件使用。ChooseAreaFragment的作用就是实现城市和县区列表,以便单击可以显示相应地区的天气情况。

16.4 显示地区列表

ChooseAreaFragment封装了显示地区列表的逻辑,但是只有ChooseAreaFragment类还不够,还需要很多辅助类来完成相应的工作。例如,地区列表是从服务端获取的JSON数据,因此,需要有相应的类来完成从网络上获取数据的工作,而且获取的是JSON格式的数据。因此,在使用这些数据之前,需要先将其转换为Kotlin类。本节除了实现ChooseAreaFragment类外,还会讲解如何实现这些辅助类。

16.4.1 描述城市信息的数据类

从服务端获取的地区信息有3个级别:省、市和县区。这3个级别分别需要一个数据类描述。

Kotlin代码(数据类)

//  描述省信息的数据类
data class Province(var id:Int = 0, var provinceName:String, var proinceCode:String)
//  描述市信息的数据类
data class City(var id:Int = 0, var cityName:String, var cityCode:String, var provinceCode:String)
//  描述县区信息的数据类
data class County(var id:Int = 0, var countyName:String, var countyCode:String, var cityCode:String)

16.4.2 处理JSON格式的城市列表信息

当JSON格式的数据从服务端获取后,需要对这些数据进行解析。这个工作是由Utility对象完成的。

Kotlin代码(解析JSON格式的数据)

object Utility {//  解析和处理服务器返回的省级数据fun handleProvinceResponse(response: String): List<Province> {var provinces = mutableListOf<Province>()if (!TextUtils.isEmpty(response)) {try {//  将JSON数组转换为Kotlin数组形式      val allProvinces = JSONArray(response)//  对数组循环处理,每一次循环都会创建一个Province对象for (i in 0..allProvinces.length() - 1) {val provinceObject = allProvinces.getJSONObject(i)val province = Province(provinceName = provinceObject.getString("name"),proinceCode = provinceObje  ct.getString("id"))provinces.add(provinces.size, province)}} catch (e: JSONException) {e.printStackTrace()}}return provinces}//  解析和处理服务器返回的市级数据fun handleCityResponse(response: String, provinceCode: String): List<City> {var cities = mutableListOf<City>()if (!TextUtils.isEmpty(response)) {try {val allCities = JSONArray(response)for (i in 0..allCities.length() - 1) {val cityObject = allCities.getJSONObject(i)val city = City(cityName = cityObject.getString("name"),cityCode  = cityObject.getString("id"),provinceCode = provinceCode)cities.add(city)}} catch (e: JSONException) {e.printStackTrace()}}return cities}//  解析和处理服务器返回的县区级数据fun handleCountyResponse(response: String, cityCode: String): List<County> {var counties = mutableListOf<County>()if (!TextUtils.isEmpty(response)) {try {val allCounties = JSONArray(response)for (i in 0..allCounties.length() - 1) {val countyObject = allCounties.getJSONObject(i)val county = County(countyName = countyObject.getString("name"), countyCode = countyObject.getString("id"),cityCode = cityCode)counties.add(county)}} catch (e: JSONException) {e.printStackTrace()}}return counties}//  将返回的JSON数据解析成Weather实体类fun handleWeatherResponse(response: String): Weather? {try {val jsonObject = JSONObject(response)val jsonArray = jsonObject.getJSONArray("HeWeather")val weatherContent = jsonArray.getJSONObject(0).toString()return Gson().fromJson(weatherContent, Weather::class.java)} catch (e: Exception) {e.printStackTrace()}return null}}

在Utility对象中有4个方法,其中前3个方法用于分析省、市和县区级JSON格式数据,并将这些数据转换为相应的对象。第4个方法用于分析描述天气信息的JSON数据,而且未使用Android SDK标准的API进行分析,而是使用了gson开源库对JSON数据进行分析,并返回一个Weather对象,Weather类与其他相关类的定义需要符合gson标准,这些内容会在下一节介绍。

16.4.3 天气信息描述类

为了演示Kotlin与Java混合开发,描述天气信息的类用Java编写。其中Weather是用于描述天气的信息的主类,还有一些相关的类一同描述整个天气信息,如Basic、AQI、Now等。总之,这些类是由服务端返回的JSON格式天气信息决定的。获取天气信息的URL格式如下:

https://geekori.com/api/weather/?id=weather_id

这里的weather_id就是地区编码,如沈阳市和平区的编码是210102。获取该地区天气信息的URL如下:

https://geekori.com/api/weather/?id=210102

Weather以及相关类的实现代码如下:

Java代码(Weather类)

public class Weather {public String status;public Basic basic;public AQI aqi;public Now now;public Suggestion suggestion;@SerializedName("daily_forecast")public List<Forecast> forecastList;
}

Java代码(Basic类)

public class Basic {@SerializedName("city")public String cityName;@SerializedName("id")public String weatherId;public Update update;public class Update {@SerializedName("loc")public String updateTime;}
}

Java代码(AQI类)

public class AQI {public AQICity city;public class AQICity {public String aqi;public String pm25;}
}

Java代码(Now类)

public class Now
{@SerializedName("tmp")public String temperature;@SerializedName("cond")public More more;public class More {@SerializedName("txt")public String info;}
}

Java代码(Suggestion类)

public class Suggestion {@SerializedName("comf")public Comfort comfort;@SerializedName("cw")public CarWash carWash;public Sport sport;public class Comfort {@SerializedName("txt")public String info;}public class CarWash {@SerializedName("txt")public String info;}public class Sport {@SerializedName("txt")public String info;}}

16.4.4 获取城市信息的对象

如果在Java中,获取城市信息通常会使用静态方法,这样在任何地方都能调用。不过Kotlin中没有静态方法,取而代之的是对象,因此,为了封装这些功能,先要定义一个DataSupport对象。该对象主要封装了3个方法:getProvinces、getCities和getCounties。分别用于从服务端获取省、市和县区的信息。

获取省信息的URL如下:

https://geekori.com/api/china

在浏览器中查看这个URL指向的页面,会显示如下JSON格式的省信息。

[{“id”:”110000”,”name”:”北京市”},{“id”:”120000”,”name”:”天津市”},{“id”:”130000”, “name”:”河北省”},{“id”:”140000”,”name”:”山西省”},{“id”:”150000”,”name”:”内蒙古自治区”},{“id”:”210000”,”name”:”辽宁省”},{“id”:”220000”,”name”:”吉林省”},{“id”:”230000”,”name”:”黑龙江省”},{“id”:”310000”,”name”:”上海市”},{“id”:”320000”,”name”:”江苏省”},{“id”:”330000”, “name”:”浙江省”},{“id”:”340000”,”name”:”安徽省”},{“id”:”350000”,”name”:”福建省”},{“id”: “360000”,”name”:”江西省”},{“id”:”370000”,”name”:”山东省”},{“id”:”410000”,”name”:”河南省”},{“id”:”420000”,”name”:”湖北省”},{“id”:”430000”,”name”:”湖南省”},{“id”:”440000”,”name”:”广东省”},{“id”:”450000”,”name”:”广西壮族自治区”},{“id”:”460000”,”name”:”海南省”},{“id”:”500000”,”name”:”重庆市”},{“id”:”510000”,”name”:”四川省”},{“id”:”520000”,”name”:”贵州省”},{“id”:”530000”,”name”:”云南省”},{“id”:”540000”,”name”:”西藏自治区”},{“id”:”610000”,”name”:”陕西省”},{“id”:”620000”,”name”:”甘肃省”},{“id”:”630000”,”name”:”青海省”},{“id”:”640000”,”name”:”宁夏回族自治区”},{“id”:”650000”,”name”:”新疆维吾尔自治区”},{“id”:”810000”,”name”:”香港特别行政区”},{“id”:”820000”,”name”:”澳门特别行政区”}]

我们可以看到,这是一个JSON格式的数组,每一个数组元素是一个对象,表示一个省(直辖市、自治区或特别行政区)的信息,包括id和name,分别对应Province类的provinceCode和provinceName属性。

获取每一个省的城市列表的URL格式如下:

https://geekori.com/api/china/${provinceCode}

其中${provinceCode}表示省的代码,如辽宁省是210000。因此,获取辽宁省所有城市列表的URL如下:

https://geekori.com/api/china/210000

在浏览器中查看这个URL指向的页面,会显示如下内容。

[{“id”:”210100”,”name”:”沈阳市”},{“id”:”210200”,”name”:”大连市”},{“id”:”210300”,”name”: “鞍山市”},{“id”:”210400”,”name”:”抚顺市”},{“id”:”210500”,”name”:”本溪市”},{“id”:”210600”,”name”:”丹东市”},{“id”:”210700”,”name”:”锦州市”},{“id”:”210800”,”name”:”营口市”},{“id”:”210900”,”name”:”阜新市”},{“id”:”211000”,”name”:”辽阳市”},{“id”:”211100”,”name”:”盘锦市”},{“id”:”211200”,”name”:”铁岭市”},{“id”:”211300”,”name”:”朝阳市”},{“id”:”211400”,”name”:”葫芦岛市”}]

返回的仍然是JSON格式的数组,每一个数组元素是一个对象,对象的属性仍然有两个:id和name,分别对应City类的cityCode和cityName属性。

获取某一个城市的县区列表的URL格式如下:

https://geekori.com/api/china/provinceCode/{provinceCode}/{cityCode}

其中cityCode表示城市编码。例如,获取沈阳市所辖县区列表的URL如下:

https://geekori.com/api/china/210000/210100

在浏览器中查看这个URL指向的页面,会显示如下内容。

[{“id”:”210101”,”name”:”市辖区”},{“id”:”210102”,”name”:”和平区”},{“id”:”210103”,”name”:”沈河区”},{“id”:”210104”,”name”:”大东区”},{“id”:”210105”,”name”:”皇姑区”},{“id”:”210106”,”name”:”铁西区”},{“id”:”210111”,”name”:”苏家屯区”},{“id”:”210112”,”name”:”东陵区”},{“id”:”210113”,”name”:”新城子区”},{“id”:”210114”,”name”:”于洪区”},{“id”:”210122”,”name”:”辽中县”},{“id”:”210123”,”name”:”康平县”},{“id”:”210124”,”name”:”法库县”},{“id”:”210181”,”name”:”新民市”}]

现在我们已经了解了获取省、市和县区3级地区信息的URL格式,然后可以编写DataSupport类了,实现代码如下:

Kotlin代码(从服务端获取数据的对象)

object DataSupport
{//  从InputStream对象读取数据,并转换为ByteArrayprivate fun getBytesByInputStream(content: InputStream): ByteArray {var bytes: ByteArray? = nullval bis = BufferedInputStream(content)val baos = ByteArrayOutputStream()val bos = BufferedOutputStream(baos)val buffer = ByteArray(1024 * 8)var length = 0try {while (true) {length = bis.read(buffer)if(length < 0)breakbos.write(buffer, 0, length)}bos.flush()bytes = baos.toByteArray()} catch (e: IOException) {e.printStackTrace()} finally {try {bos.close()} catch (e: IOException) {e.printStackTrace()}try {bis.close()} catch (e: IOException) {e.printStackTrace()}}return bytes!!  }//  从服务端获取数据,并以字符串形式返回获取的数据private fun getServerContent(urlStr:String):String{var url = URL(urlStr)var conn = url.openConnection() as HttpURLConnection//HttpURLConnection默认就是用GET发送请求,下面的setRequestMethod可以省略conn.setRequestMethod("GET")//HttpURLConnection默认也支持从服务端读取结果流,下面的setDoInput也可以省略conn.setDoInput(true)//禁用网络缓存conn.setUseCaches(false)val content = conn.getInputStream()//将InputStream转换成byte数组,getBytesByInputStream会关闭输入流var responseBody = getBytesByInputStream(content)//  将字节流以utf-8格式转换为字符串var str = kotlin.text.String(responseBody, Charset.forName("utf-8"))return str}//  获取省列表fun getProvinces(provinces:(List<Province>)->Unit){Thread(){           var content = getServerContent("https://geekori.com/api/china")
           //  将省JSON数据转换为List<Province>对象并返回
           var provinces = Utility.handleProvinceResponse(content)provinces(provinces)}.start()}//  根据省获取城市列表fun getCities(provinceCode:String, cities:(List<City>)->Unit){Thread(){var content = getServerContent("https://geekori.com/api/china/${provinceCode}")//  将城市JSON数据转换为List<City>对象并返回
          var cities= Utility.handleCityResponse(content,provinceCode)cities(cities)}.start()}//  根据城市获取县区列表fun getCounties(provinceCode: String,cityCode:String, counties:(List<County>)->Unit){Thread(){var content = getServerContent("https://geekori.com/api/china/${provinceCode}/$  {cityCode}")//  将县区JSON数据转换为List<County>对象并返回var counties = Utility.handleCountyResponse(content,cityCode)counties(counties)}.start()}
}

16.4.5 在ListView组件中显示地区列表

现在一切准备工作都完成了,接下来实现ChooseAreaFragment类,该类是Fragment的子类,用于显示地区列表。地区列表显示在一个ListView组件中。这个组件在与ChooseAreaFragment对应的布局文件choose_area.xml中定义,如下所示。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#fff"android:fitsSystemWindows="true"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="?attr/actionBarSize"android:background="?attr/colorPrimary"><TextViewandroid:id="@+id/title_text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:textColor="#fff"android:textSize="20sp"/><Buttonandroid:id="@+id/back_button"android:layout_width="25dp"android:layout_height="25dp"android:layout_marginLeft="10dp"android:layout_alignParentLeft="true"android:layout_centerVertical="true"android:background="@drawable/ic_back"/></RelativeLayout>
   <ListView
      android:id="@+id/list_view"
      android:layout_width="match_parent"
      android:layout_height="match_parent"/></LinearLayout>

在上面的布局文件中,除了定义一个ListView组件,还定义了一个TextView组件和一个Button组件,其中TextView组件用于显示当前列表上一级的文本,如当前列表是辽宁省中的市,那么这个TextView组件显示的是“辽宁省”。这个Button组件是一个回退按钮,单击可以回退到上一个级别。

下面我们简单说一下这个ListView组件,这个组件是Android SDK提供的一个列表组件。这个组件采用MVC模式管理数据,也就是数据和视图分离。在显示数据时,需要提供Adapter对象,这个在MVC中称为Controller,用于衔接数据和视图。ChooseAreaFragment使用ListView组件显示地区列表的原理就是首先显示省列表,然后单击某一个省,就会重新设置数据源,显示当前省中的所有市,显示县区列表也类似。

下面看一下ChooseAreaFragment类的完整实现。

Kotlin代码(显示地区列表)

class ChooseAreaFragment : Fragment() {private var progressDialog: ProgressDialog? = nullprivate var titleText: TextView? = nullprivate var backButton: Button? = nullprivate var listView: ListView? = null//  用于为ListView提供数据源的Adapter对象,数据源是数组     private var adapter: ArrayAdapter<String>? = nullprivate var handler = MyHandler()//  ListView的数据源 private val dataList = ArrayList<String>()// 省列表private var provinceList: List<Province>? = null// 市列表private var cityList: List<City>? = null// 县区列表private var countyList: List<County>? = null//  当前被选中的省private var selectedProvince: Province? = null//  当前被选中的市private var selectedCity: City? = null//  当前被选中的级别private var currentLevel: Int = 0//  级别伴随对象companion object {val LEVEL_PROVINCE = 0val LEVEL_CITY = 1val LEVEL_COUNTY = 2}//  Fragment的初始化方法,类似于Activity的onCreate方法override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {val view = inflater!!.inflate(R.layout.choose_area, container, false)titleText = view.findViewById(R.id.title_text) as TextViewbackButton = view.findViewById(R.id.back_button) as ButtonlistView = view.findViewById(R.id.list_view) as ListViewadapter = ArrayAdapter(context, android.R.layout.simple_list_item_1,   dataList)//  将Adapter与ListView组件绑定,这样就可以在ListView组件中显示数据了 listView!!.adapter = adapterreturn view}//  当包含Fragment的Activity被创建时调用override fun onActivityCreated(savedInstanceState: Bundle?) {super.onActivityCreated(savedInstanceState)//  设置ListView对象的Item单击事件listView!!.onItemClickListener = AdapterView.OnItemClickListener   { parent, view, position, id ->//  选择省if (currentLevel == LEVEL_PROVINCE) {selectedProvince = provinceList!![position]queryCities()     //  查询选择省中所有的城市,并显示在ListView组件中} //  选择市else if (currentLevel == LEVEL_CITY) {selectedCity = cityList!![position]queryCounties()  //  查询选择市中所有的县区,并显示在ListView组件中} //  选择县区else if (currentLevel == LEVEL_COUNTY) {val countyName = countyList!![position].countyName// 选择县区后,如果Fragment处于打开状态,则隐藏Fragment,然后显示当前县  区的// 天气情况if (activity is MainActivity) {val intent = Intent(activity, WeatherActivity::class.java)intent.putExtra("name", countyName)startActivity(intent)activity.finish()} else if (activity is WeatherActivity) {val activity = activity as WeatherActivityactivity.drawerLayout?.closeDrawers()activity.swipeRefresh?.setRefreshing(true)activity.requestWeather(countyName)}}}// 回退按钮的单击事件backButton!!.setOnClickListener {//  当前处于县区级,回退到市级 if (currentLevel == LEVEL_COUNTY) {queryCities()} //  当前处于市级,回退到省级else if (currentLevel == LEVEL_CITY) {queryProvinces()}}//  默认显示省列表queryProvinces()}//  用于更新ListView组件class MyHandler : Handler() {override fun handleMessage(msg: Message?) {var activity = msg?.obj as ChooseAreaFragmentwhen (msg?.arg1) {//  在ListView组件中显示省列表ChooseAreaFragment.LEVEL_PROVINCE -> {if (activity.provinceList!!.size > 0) {activity.dataList.clear()for (province in activity.provinceList!!) {activity.dataList.add(province.provinceName)}activity.adapter!!.notifyDataSetChanged()activity.listView!!.setSelection(0)activity.currentLevel = LEVEL_PROVINCE}}// 在ListView组件中显示市列表ChooseAreaFragment.LEVEL_CITY -> {if (activity.cityList!!.size > 0) {activity.dataList.clear()for (city in activity.cityList!!) {activity.dataList.add(city.cityName)}activity.adapter!!.notifyDataSetChanged()activity.listView!!.setSelection(0)activity.currentLevel = LEVEL_CITY}}//  在ListView组件中显示县区列表ChooseAreaFragment.LEVEL_COUNTY->{if (activity.countyList!!.size > 0) {activity.dataList.clear()for (county in activity.countyList!!) {activity.dataList.add(county.countyName)}activity.adapter!!.notifyDataSetChanged()activity.listView!!.setSelection(0)activity.currentLevel = LEVEL_COUNTY}}}}}//  查询所有的省private fun queryProvinces() {titleText!!.text = "中国"backButton!!.visibility = View.GONEDataSupport.getProvinces {provinceList = itvar msg = Message()msg.obj = thismsg.arg1 = LEVEL_PROVINCEhandler.sendMessage(msg)}}//  根据选择的省查询城市private fun queryCities() {titleText!!.setText(selectedProvince!!.provinceName)backButton!!.visibility = View.VISIBLEDataSupport.getCities(selectedProvince!!.proinceCode) {cityList = itvar msg = Message()msg.obj = thismsg.arg1 = LEVEL_CITYhandler.sendMessage(msg)}}//  根据选择的城市,查询县区private fun queryCounties() {titleText!!.setText(selectedCity!!.cityName)backButton!!.visibility = View.VISIBLEDataSupport.getCounties(selectedProvince!!.proinceCode, selectedCity!!  .cityCode){countyList = itvar msg = Message()msg.obj = thismsg.arg1 = LEVEL_COUNTYhandler.sendMessage(msg)}}}

在上面的代码中,调用了以前实现的DataSupport对象中的相应方法获取省、市和县区列表,并利用Adapter显示在ListView组件中。

16.5 显示天气信息

最后需要实现的就是在WeatherActivity中显示天气信息。在获取地区信息时使用的是HttpURLConnection,而这次,我们使用OkHttp组件来获取天气信息。在HttpUtil对象中封装了一个sendOkHttpRequest方法,用于通过OkHttp从服务端获取数据。

Kotlin代码(通过OkHttp从服务端获取数据)

object HttpUtil
{fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {val client = OkHttpClient()val request = Request.Builder().url(address).build()client.newCall(request).enqueue(callback)}
}

下面先看一下WeatherActivity使用的布局文件(activity_weather.xml)。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
   xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/colorPrimary"><ImageView
      android:id="@+id/bing_pic_img"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop" /><android.support.v4.widget.DrawerLayout
      android:id="@+id/drawer_layout"android:layout_width="match_parent"android:layout_height="match_parent"><android.support.v4.widget.SwipeRefreshLayout
         android:id="@+id/swipe_refresh"android:layout_width="match_parent"android:layout_height="match_parent"><ScrollView
             android:id="@+id/weather_layout"android:layout_width="match_parent"android:layout_height="match_parent"android:scrollbars="none"android:overScrollMode="never"><LinearLayout
                 android:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content"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></android.support.v4.widget.SwipeRefreshLayout><fragment
         android:id="@+id/choose_area_fragment"android:name="com.oriweather.fragment.ChooseAreaFragment"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="start"/></android.support.v4.widget.DrawerLayout>
</FrameLayout>

在这段布局文件中,最后放置了一个<fragment>,引用了在前面实现的ChooseArea Fragment。可能我们还记得,在前面讲activity_main.xml时,也通过<fragment>标签引用了ChooseAreaFragment,那么这是怎么回事呢?ChooseAreaFragment为什么被引用了两次呢?其实,ChooseAreaFragment被重用了两次。第一次是在activity_main.xml中,当第一次运行App时,还没有显示过任何地区的天气信息,那么ChooseAreaFragment是全屏显示在主窗口(MainActivity)上。第二次是在activity_weather.xml中,我们看到,包括<fragment>在内,所有的组件都放在了DrawerLayout中。这是一个抽屉布局,也就是可以像抽屉一样拉出和显示,移动版QQ就有这样的效果,读者可以去体验。放在抽屉布局中的ChooseAreaFragment会随着抽屉的拉开而显示,随着抽屉的关闭而隐藏。

最后,我们看一下WeatherActivity类的完整实现代码。

Kotlin代码(显示指定地区的天气信息)

class WeatherActivity : AppCompatActivity() {var drawerLayout: DrawerLayout? = null            //  定义抽屉布局类型变量var swipeRefresh: SwipeRefreshLayout? = null      //  用于刷新的布局组件private var weatherLayout: ScrollView? = nullprivate var navButton: Button? = nullprivate var titleCity: TextView? = nullprivate var titleUpdateTime: TextView? = nullprivate var degreeText: TextView? = nullprivate var weatherInfoText: TextView? = nullprivate var forecastLayout: LinearLayout? = nullprivate var aqiText: TextView? = nullprivate var pm25Text: TextView? = nullprivate var comfortText: TextView? = nullprivate var carWashText: TextView? = nullprivate var sportText: TextView? = nullprivate var bingPicImg: ImageView? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)if (Build.VERSION.SDK_INT >= 21) {val decorView = window.decorViewdecorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN  or View.SYSTEM_UI_FLAG_LAYOUT_STABLEwindow.statusBarColor = Color.TRANSPARENT}setContentView(R.layout.activity_weather)// 初始化各组件… …navButton = findViewById(R.id.nav_button) as Buttonval prefs = PreferenceManager.getDefaultSharedPreferences(this)val weatherString = prefs.getString("weather", null)val weatherId: String?if (weatherString != null) {// 有缓存时直接解析天气数据val weather = Utility.handleWeatherResponse(weatherString)weatherId = weather?.basic?.weatherIdshowWeatherInfo(weather)} else {// 无缓存时去服务器查询天气weatherId = intent.getStringExtra("weather_id")weatherLayout!!.visibility = View.INVISIBLErequestWeather(weatherId)}//  设置刷新时的监听事件swipeRefresh!!.setOnRefreshListener { requestWeather(weatherId) }navButton!!.setOnClickListener { drawerLayout?.openDrawer(GravityCompat.  START) }val bingPic = prefs.getString("bing_pic", null)if (bingPic != null) {//  装载显示天气时的背景图Glide.with(this).load(bingPic).into(bingPicImg)} else {loadBingPic()}}// 根据天气id请求城市天气信息。fun requestWeather(id: String?) {val weatherUrl = "https://geekori.com/api/weather?id=${id}"HttpUtil.sendOkHttpRequest(weatherUrl, object : Callback {@Throws(IOException::class)override fun onResponse(call: Call, response: Response) {val responseText = response.body()!!.string()val weather = Utility.handleWeatherResponse(responseText)runOnUiThread {if (weather != null && "ok" == weather!!.status) {valeditor = PreferenceManager.getDefaultSharedPreferen  ces(this@WeatherActivity).edit()editor.putString("weather", responseText)editor.apply()showWeatherInfo(weather)} else {Toast.makeText(this@WeatherActivity, "获取天气信息失败",  Toast.LENGTH_SHORT).show()}swipeRefresh?.isRefreshing = false}}override fun onFailure(call: Call, e: IOException) {e.printStackTrace()runOnUiThread {Toast.makeText(this@WeatherActivity, "获取天气信息失败",   Toast.LENGTH_SHORT).show()swipeRefresh?.isRefreshing = false}}})loadBingPic()}//  显示背景图像private fun loadBingPic() {val requestBingPic = "https://geekori.com/api/background/pic"HttpUtil.sendOkHttpRequest(requestBingPic, object : Callback {@Throws(IOException::class)override fun onResponse(call: Call, response: Response) {val bingPic = response.body()!!.string()val editor = PreferenceManager.getDefaultSharedPreferences(this  @WeatherActivity).edit()editor.putString("bing_pic", bingPic)editor.apply()runOnUiThread{ Glide.with(this@WeatherActivity).load(bingPic).  into(bingPicImg) }}override fun onFailure(call: Call, e: IOException) {e.printStackTrace()}})}// 处理并展示Weather实体类中的数据。private fun showWeatherInfo(weather: Weather??) {val cityName = weather?.basic?.cityNameval updateTime = weather?.basic?.update?.updateTime!!.split(" ")[1]val degree = weather?.now?.temperature + "℃"val weatherInfo = weather?.now?.more?.infotitleCity!!.setText(cityName)titleUpdateTime!!.setText(updateTime)degreeText!!.setText(degree)weatherInfoText!!.setText(weatherInfo)forecastLayout!!.removeAllViews()for (forecast in weather.forecastList) {val view = LayoutInflater.from(this).inflate(R.layout.forecast_item,  forecastLayout, false)val dateText = view.findViewById(R.id.date_text) as TextViewval infoText = view.findViewById(R.id.info_text) as TextViewval maxText = view.findViewById(R.id.max_text) as TextViewval minText = view.findViewById(R.id.min_text) as TextViewdateText.setText(forecast.date)infoText.setText(forecast.more.info)maxText.setText(forecast.temperature.max)minText.setText(forecast.temperature.min)forecastLayout!!.addView(view)}if (weather.aqi != null) {aqiText!!.setText(weather.aqi.city.aqi)pm25Text!!.setText(weather.aqi.city.pm25)}val comfort = "舒适度:" + weather.suggestion.comfort.infoval carWash = "洗车指数:" + weather.suggestion.carWash.infoval sport = "运动建议:" + weather.suggestion.sport.infocomfortText!!.text = comfortcarWashText!!.text = carWashsportText!!.text = sportweatherLayout!!.visibility = View.VISIBLE}}

在WeatherActivity中使用了一个SwipeRefreshLayout类,这是用于显示刷新效果的布局。当显示天气信息后,向下滑动窗口,会显示如图16-3所示的刷新效果。松开后,会重新加载当前页面。

▲图16-3 刷新天气信息

16.6 小结

本章实现了一个Android App,尽管这个App不算大,但完全可以演示使用Kotlin开发Android App的完整过程。本章实现的App综合使用了UI、Activity、布局、网络等技术。希望读者根据本书提供的Demo源代码以及本书讲解的知识独立完成这个项目,这样会让自己的Android和Kotlin开发功力有大幅度提升。

本文摘自《Kotlin程序开发入门精要》一书

Kotlin最佳项目实战——欧瑞天气App相关推荐

  1. iOS 7最佳实践:一个天气App案例

    转自:sjpsega's Blog iOS7最佳实践:一个天气App案例(一) iOS7最佳实践:一个天气App案例(二) 注:本文译自:raywenderlich ios-7-best-practi ...

  2. Android项目实战:账本APP开发

    好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 Java项目实战:账本APP服务 ...

  3. Android项目实战:简单天气-刘桂林-专题视频课程

    Android项目实战:简单天气-11200人已学习 课程介绍         学习新的知识点,时下Android比较流行的MPAndroidChart + Retrofit2.0 + Gson! 课 ...

  4. 【Android项目实战 | 从零开始写app(十二)】实现app首页智慧服务热门推荐热门主题、新闻

    说在前面,由于各种adapter,xml布局,bean实体类,Activity,也为了让看懂,代码基本都是"简单粗暴直接不好看",没啥okhttp和util工具类之类的封装,本篇幅 ...

  5. 【Android项目实战 | 从零开始写app (六) 】用TabLayout+ViewPager搭建App 框架主页面底部导航栏

    本篇实现效果: 搭建app框架的方式有很多,本节主要用TabLayout+ViewPager搭建App框架,这种方式简单易实现,在主页中加载Fragment碎片,实现不同功能页面的切换效果图如下: 文 ...

  6. 【Android项目实战 | 从零开始写app(十三)】实现用户中心模块清除token退出登录信息修改等功能

    五一后,被ji金伤了,哇呜呜,还是得苦逼老老实实打工写代码,看下面吧 本篇实现效果: 实现登录用户名展示到用户中心页面上,并且页面有个人信息,订单列表,修改密码,意见反馈发送到服务端,前面登录后,通过 ...

  7. 【Android项目实战 | 从零开始写app一一智慧服务】完结篇系列导航篇、源代码

    目录 文章介绍 涉及知识 系列汇总 项目源代码 文章介绍 本系列小文是一个简单的Android app项目实战,对于刚入门Android 的初学者来说,基础学完了,但是怎么综合的去写一个小app,可能 ...

  8. iOS7最佳实践:一个天气App案例

    注:本文译自: raywenderlich ios-7-best-practices-part-1 ,去除了跟主题无关的寒暄部分. 欢迎转载,保持署名 在这个两部分的系列教程中,您将探索如何使用以下工 ...

  9. React项目实战之租房app项目(二)首页模块

    前言 目录 前言 一.首页路由处理 二.轮播图 2.1 轮播图效果图 2.2 拷贝轮播图组件代码 2.3 轮播图代码详解 2.4 轮播图代码重构 2.5 解决轮播图出现的BUG 三.导航菜单 3.1 ...

最新文章

  1. 习题8-6 删除字符 (20 分)
  2. 益老而弥坚:软件开发哲学反思录
  3. java ancestor_java – 家谱祖先查找算法
  4. BZOJ-1005-明明的烦恼
  5. android palette组件用法,Palette颜色提取使用详解
  6. springboot日志可视化_spring boot面试问题集锦
  7. JVM007_运行时栈帧结构
  8. 初次使用Apache、ip地址、防火墙、域名、DNS、hosts文件、端口、URL介绍、Apache配置文件、配置虚拟主机、请求响应、http协议、
  9. 2 引入失败_Curse选择WE,RNG天价引援失败,上单几乎只剩一个选项
  10. linux rpm安装mysql5.7.*密码策略,访问策略等常见问题
  11. MyBatis的ResultMaps
  12. FloatingActionButton
  13. matlab计算器设计流程图_matlab计算器设计
  14. 配置中心—Consul配置管理
  15. python qq邮箱,Python使用QQ邮箱发送邮件报错smtplib.SMTPAuthenticationError
  16. 计算机网络 自顶向下方法 (一) 笔记 总结 第一章 概述
  17. 关于let你不知道的知识点——红宝石书笔记记录
  18. Java用最少代码实现五子棋-玩家对战模式-人机对战模式-电脑策略对战
  19. unity3d显示c4d材质_学习笔记分享 如何学好C4D
  20. EventBus的介绍和使用

热门文章

  1. 【游戏开发】天龙八部demo
  2. 软件测试OA办公自动化系统测试方案
  3. 仿58同城地方门户本地生活小程序源码
  4. 与计算机科学与技术相似的专业,【选专业】名称相似但实际千差万别的专业 谨防掉坑!...
  5. steam(wallpaperEngine)双屏,比较cool的桌面推荐
  6. ubuntu12.04中shibboleth布署
  7. 下载mnist手写数字数据集
  8. matlab fft计算功率,使用 FFT 获得功率频谱密度估计
  9. springboot外委员工后台管理系统 毕业设计-附源码101157
  10. 超薄手机诺基亚5310红色