參考:《第一行代碼:Android》第2版——郭霖
注1:本文為原創,例子可參考郭前輩著作:《第一行代碼:Android》第2版
注2:本文不贅述android開發的基本理論,不介紹入門知識,不介紹Android Studio基本安裝,開門見山,直接使用kotlin改寫郭前輩的《第一行代碼:Android》中的部分例子,有機會的話自己做一些新例子出來!
注3:本文嘗試用Google新官推語言kotlin改寫《第一行代碼:Android》中的案例,偶爾涉及java作為對比
注4:開發基於Android Studio 3.0,並且新建項目時勾選“support kotlin”
進入實戰——開發酷歐天氣(2)
本次博文,我將嘗試使用kotlin語言對郭前輩的《第一行代碼》中的最后那個實戰項目“酷歐天氣”進行重寫
原書:14.5顯示天氣信息(p509)
上一章,我們已經把省市縣數據“爬”到了app中,接下來我們要做的是“爬”天氣預報信息,完成app剩下的功能
注冊API
先去注冊一下郭神提供的weather api(通過他獲得在線的天氣信息)
http://console.heweather.com/register
注冊完成登陸后取得key,並測試訪問一下是否能獲得天氣的json信息
http://guolin.tech/api/weather?cityid=CN101190402&key=e970db87b1f24fb8a00555c6b361e8d4
(訪問形式如上,參考原書:p522,地址中的參數cityid就是在上一章中County數據類里的weather_id)
獲得數據:
這就說明已經可以成功獲取最近的天氣數據了!
不過這個json信息量有點大啊!==|||
數據類
上一章已經“吹”過了kotlin強大的data class(數據類)了,這是一種比java pojo類更加方便精簡的類!
每一個你建立的POJO類文件最后都轉換為一行代碼而已
原作中,作者對gson實體類進行了分析(原書p509),可以使用他分析后創建的實體類(只需要轉換成kotlin data class的寫法)
不過對於這么復雜的json,我使用了在線json轉POJO,將剛剛獲得的天氣json數據先轉換為POJO類(這樣使得實體類定義更加完整並且比自己一個個手寫要塊多了,工具是個好東西!)
json轉POJO網址:http://www.bejson.com/json2javapojo/
注意從網站上得到的只是java的POJO類,不是kotlin類(反正我還沒有找到json直接轉kotlin數據類的工具==|||求推薦)
進入Android Studio 3.0,在項目中新建包:weatherapi->新建datas.kt
為什么要新建一個包?
這個包是專門存放kotlin的天氣數據類的,因為我發現郭前輩那個api獲得天氣json轉換后的數據類與之前的我的City這個數據類有沖突(那個json里也有個City),故而將這次天氣的數據類放到一個單獨的包中,保證不會沖突,另外引入時需要注意,別引入了外面那個City類!
嘗試轉換
先新建一個City.java,這是一個POJO類文件,將json在線轉換好的POJO類代碼復制進來
這是那個在線json轉換出來的City java POJO類:
package cn.cslg.weatherkotlin.weatherapi;
public class City {
private String aqi;
private String pm10;
private String pm25;
private String qlty;
public void setAqi(String aqi){
this.aqi = aqi;
}
public String getAqi(){
return this.aqi;
}
public void setPm10(String pm10){
this.pm10 = pm10;
}
public String getPm10(){
return this.pm10;
}
public void setPm25(String pm25){
this.pm25 = pm25;
}
public String getPm25(){
return this.pm25;
}
public void setQlty(String qlty){
this.qlty = qlty;
}
public String getQlty(){
return this.qlty;
}
}
使用IDE可以直接將java轉換為kotlin代碼,這是kotlin類庫自帶的功能,我們來轉這個java類為kotlin類
點擊Android Studio 3.0上面的工具欄上的Code->Convert Java File to Kotlin File(在最后一個)
IDE轉換結束后,很遺憾,這不是一個kotlin的數據類,自行修修補補,改成data class吧(求工具完全轉換!)
以下是我自行修改后:City的dataclass
data class City(val aqi: String, val pm10: String, val pm25: String, val qlty: String)
個人推薦一種辦法:
- 先把那個網站轉換的所有的類一起復制到一個java文件中,我知道這樣會報錯,java只能有一個public class
- 轉換為kotlin
- 替換所有public,internal為data
- 替換所有的{}為()
- 刪除最后一個屬性的逗號
- 所有代碼合並成一行
- Code->Reformat Code
最終的datas.kt:
package cn.cslg.weatherkotlin.weatherapi
data class City(var aqi: String, var pm10: String, var pm25: String, var qlty: String)
data class Aqi(var city: City)
data class Update(var loc: String, var utc: String)
data class Basic(var city: String, var cnty: String, var id: String, var lat: String, var lon: String, var update: Update)
data class Now(var cond: Cond, var fl: String, var hum: String, var pcpn: String, var pres: String, var tmp: String, var vis: String, var wind: Wind)
data class Air(var brf: String, var txt: String)
data class Comf(var brf: String, var txt: String)
data class Cw(var brf: String, var txt: String)
data class Drsg(var brf: String, var txt: String)
data class Flu(var brf: String, var txt: String)
data class Sport(var brf: String, var txt: String)
data class Trav(var brf: String, var txt: String)
data class Uv(var brf: String, var txt: String)
data class Suggestion(var air: Air, var comf: Comf, var cw: Cw, var drsg: Drsg, var flu: Flu, var sport: Sport, var trav: Trav, var uv: Uv)
data class Astro(var mr: String, var ms: String, var sr: String, var ss: String)
data class Tmp(var max: String, var min: String)
data class Daily_forecast(var astro: Astro, var cond: DailyCond, var date: String, var hum: String, var pcpn: String, var pop: String, var pres: String, var tmp: Tmp, var uv: String, var vis: String, var wind: Wind)
data class Cond(var code: String, var txt: String)
data class DailyCond(var code_d:String,var code_n:String,var txt_d:String,var txt_n:String)
data class Wind(var deg: String, var dir: String, var sc: String, var spd: String)
data class Hourly_forecast(var cond: Cond, var date: String, var hum: String, var pop: String, var pres: String, var tmp: String, var wind: Wind)
data class HeWeather(var aqi: Aqi, var basic: Basic, var daily_forecast: List<Daily_forecast>, var hourly_forecast: List<Hourly_forecast>, var now: Now, var status: String, var suggestion: Suggestion)
data class Weather(var HeWeather:List<HeWeather>)
注意最后一個Weather類,這是我單獨添加出來的,因為我注意到API返回的json開頭就有一個HeWeathter對象並且對應的值是HeWeather數組,Gson映射的時候是以最外層{}開始轉換為一個對象的,這個對象將會是這個Weather,而不是HeWeather!
另外注意到,那個工具網站生成的POJO類中有兩個Cond!仔細查看json數據格式,確實有兩個Cond,而且不一樣!所以我另開了一個DailyCond數據類作為daily_forecast下的cond的實體模型
可以樹形查看json格式的網站:http://www.qqe2.com/
至此數據類已經建立好了,后面可以放心的用Gson映射為實體對象了!
顯示天氣
原書:p520
接下來可以開始書寫kt代碼了,我將會把原書中的java翻譯成為簡單kotlin代碼
原書中有handleWeatherResponse,針對json進行處理,這個我們不需要,我使用Gson映射為數據類實體
WeatherActivity.kt:
package cn.cslg.weatherkotlin
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import cn.cslg.weatherkotlin.weatherapi.*
import com.google.gson.Gson
import org.jetbrains.anko.custom.async
import org.jetbrains.anko.find
import org.jetbrains.anko.uiThread
import java.net.URL
class WeatherActivity : AppCompatActivity() {
private var weatherLayout: ScrollView? = null
private var titleCity: TextView? = null
private var titleUpdateTime: TextView? = null
private var degreeText: TextView? = null
private var weatherInfoText: TextView? = null
private var forecastLayout: LinearLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
weatherLayout = find<ScrollView>(R.id.weather_layout)
titleCity = find<TextView>(R.id.title_city)
titleUpdateTime = find<TextView>(R.id.title_update_time)
degreeText = find<TextView>(R.id.degree_text)
weatherInfoText = find<TextView>(R.id.weather_info_text)
forecastLayout = find<LinearLayout>(R.id.forecast_layout)
val weatherId = intent.getStringExtra("weather_id");
weatherLayout!!.visibility = View.INVISIBLE
requestWeather(weatherId)
}
//從服務器加載天氣信息
private fun requestWeather(wid:String){
val url="http://guolin.tech/api/weather?cityid="+wid+"&key=e970db87b1f24fb8a00555c6b361e8d4"
async {
val s = URL(url).readText()
uiThread {
val weather = Gson().fromJson(s,Weather::class.java)
showWeatherInfo(weather.HeWeather[0])
}
}
}
//顯示出天氣信息
private fun showWeatherInfo(w: HeWeather){
titleCity!!.text=w.basic.city
titleUpdateTime!!.text= w.basic.update.loc
degreeText!!.text=w.now.tmp+" ℃"
weatherInfoText!!.text=w.now.cond.txt+" "+w.now.wind.dir+" "+w.now.wind.sc+"級"
weatherLayout!!.visibility=View.VISIBLE
forecastLayout!!.removeAllViews()
for( d in w.daily_forecast){
val v = LayoutInflater.from(this).inflate(R.layout.forecast_item,forecastLayout,false)
val dateText = v.find<TextView>(R.id.date_text)
val infoText = v.find<TextView>(R.id.info_text)
val maxText = v.find<TextView>(R.id.max_text)
val minText = v.find<TextView>(R.id.min_text)
dateText.text = d.date
maxText.text = d.tmp.max
minText.text = d.tmp.min
if(d.cond.code_d == d.cond.code_n){
infoText.text = d.cond.txt_d
}else{
infoText.text = d.cond.txt_d+"->"+d.cond.txt_n
}
forecastLayout!!.addView(v)
}
}
}
一點分析
overwrite的onCreate方法不多解釋,主要是獲取R中的布局轉成控件,隱藏了尚未加載的地方,還從intent中獲得了一個參數,用於選擇查詢的城市
requestWeather方法使用了async請求API的數據,傳入城市的weather_id
注意到,這里使用Gson轉換的時候並沒有上一章的TypeToken,因為這一次獲得json數據最外層不是數組,也就不需要List映射,所以直接傳入一個對象的類,kotlin使用Object::class.java即可!
showWeatherInfo方法將獲取的數據初始化到界面上,其中用到了kotlin的for( in )遍歷結構,非常簡單!
補充ChooseAreaFragment
ChooseAreaFragment.kt:
class ChooseAreaFragment : Fragment() {
......
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//列表點擊監聽事件
listView!!.setOnItemClickListener {
LEVEL_COUNTY -> {
selectedCounty = countyList[position]
val intent = Intent(activity,WeatherActivity::class.java)
intent.putExtra("weather_id", selectedCounty!!.weather_id)
startActivity(intent)
activity.finish()
.......
}
往里面添加了:點擊縣跳轉到WeatherActivity的Intent
補充布局
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">
<ScrollView
android:id="@+id/weather_layout"
android:scrollbars="none"
android:overScrollMode="never"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/title"/>
<include layout="@layout/now"/>
<include layout="@layout/forecast"/>
</LinearLayout>
</ScrollView>
</FrameLayout>
forecast.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="wrap_content"
android:layout_margin="15dp"
android:background="#8000"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:paddingTop="10sp"
android:paddingBottom="10sp"
android:text="三天預報"
android:textColor="#fff"
android:textSize="20sp"
/>
<LinearLayout
android:id="@+id/forecast_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</LinearLayout>
</LinearLayout>
forecast_item.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="wrap_content"
android:layout_margin="15dp"
android:orientation="horizontal">
<TextView
android:id="@+id/date_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:layout_gravity="center_vertical"
android:textColor="#fff"
/>
<TextView
android:id="@+id/info_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textColor="#fff"
/>
<TextView
android:id="@+id/max_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:gravity="center"
android:textColor="#fff"
/>
<TextView
android:id="@+id/min_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="right"
android:textColor="#fff"
/>
</LinearLayout>
now.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_margin="15dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/degree_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textColor="#fff"
android:textSize="60sp"
/>
<TextView
android:id="@+id/weather_info_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textColor="#fff"
android:textSize="20sp"
/>
</LinearLayout>
title.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
>
<TextView
android:id="@+id/title_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#fff"
android:textSize="20sp"
/>
<TextView
android:id="@+id/title_update_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10sp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:textColor="#fff"
android:textSize="16sp"
/>
</RelativeLayout>
效果
結尾
app的大體功能和樣式已經實現了,接下來需要對他做更多的美化和完善!