上一篇文章已經介紹了網路API處理的部分,接著本篇文章將介紹天氣資訊畫面的設計。 首先,在UI資料夾下面中,先製作了三個畫面用的資料夾「about」,「areaList」,「weatherDetail」。
https://github.com/scobin/Android_WeatherApp/tree/feature/weatherData
開啟 app 資料夾下的buid.gradle檔案,加入以下程式碼。
apply plugin: 'kotlin-kapt'
dataBinding {
enabled = true
}
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
apply plugin: 'kotlin-kapt'
android {
...
defaultConfig {
...
}
buildTypes {
}
// add the following code
dataBinding {
enabled = true
}
}
dependencies {
...
// material design
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
}
在WeatherDetailFragment的佈局檔案(weather_detail_fragment.xml)中使用了CollapsingToolbarLayout跟RecyclerView。 大致上的構造如下: CoordinatorLayout
因為使用DataBinding功能,所以加上<layout>
以及<data>
。
整個佈局檔案內容如下。
xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewModel"
type="bruntho.com.tennki.ui.weatherDetail.WeatherDetailViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_bright"
>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/transparent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingtoolbarlayout"
android:minHeight="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:expandedTitleGravity="top"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<TextView
android:id="@+id/weather_description"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="center"
android:gravity="center"
android:textSize="36sp"
android:textColor="@android:color/background_light"
tools:text="17"
app:layout_collapseMode="pin"
/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
tools:title="Title"
app:title="@{viewModel.cityName}"
app:layout_collapseMode="pin">
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/weather_detail_item"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
設計列表內的項目來呈現所要顯示的資料。 繼承RecyclerView.Adapter,然後實作onCreateViewHolder(),getItemCount(),onBindViewHolder()三個函數。
onCreateViewHolder():設定並選擇項目的設計圖,返回設計樣式。
onBindViewHolder():得到onCreateViewHolder()所返回的樣式,並放上對應的資料。
getItemCount():返回列表的項目個數,這個數字等同於這個列表的最大項目數。
另外,當資料變更時,列表項目也想跟著做更新時,需要透過adapter來執行更新,因此加入以下自定義的函數。 updateData():執行畫面更新時使用。
class WeatherDetailRecyclerAdapter(
var data: MutableList<X>
): RecyclerView.Adapter<WeatherDetailRecyclerAdapter.WeatherDetailRecyclerHolder>() {
class WeatherDetailRecyclerHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeatherDetailRecyclerHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.weather_detail_item, parent, false)
return WeatherDetailRecyclerHolder(view)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: WeatherDetailRecyclerHolder, position: Int) {
val weather = data[position]
holder.itemView.temperature.text = weather.main.getTempC().toString() + "℃"
val date = Date(weather.dt * 1000)
val localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault())
holder.itemView.time.text = "${localDateTime.hour}時"
if (localDateTime.hour == 0) {
holder.itemView.date.text = "${localDateTime.month.value}/${localDateTime.dayOfMonth}"
} else {
holder.itemView.date.text = ""
}
}
fun updateData(response: WeatherResponse?) {
response?.let {
data = response.list as MutableList<X>
notifyDataSetChanged()
}
}
}
Fragment這裡主要執行三件事。
由於在還沒從網路上取得天氣資料時,列表上是沒有資料顯示在畫面上,而當資料取得成功時,再將資料呈現在畫面的列表中。
這一連串的處理是使用LiveData的功能來完成,因為設定為LiveData的資料可以被監視觀察,當資料有變化時可以執行定義好的處理。
所以在ViewModel裡將weather設定成LiveData然後在Fragment中做監測與資料更新處理。
class WeatherDetailFragment : Fragment() {
companion object {
fun newInstance() = WeatherDetailFragment()
}
private lateinit var viewModel: WeatherDetailViewModel
private lateinit var binding: WeatherDetailFragmentBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.weather_detail_fragment, container, false)
binding.lifecycleOwner = this
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(WeatherDetailViewModel::class.java)
binding.viewModel = viewModel
val adapter = WeatherDetailRecyclerAdapter(arrayListOf())
binding.list.apply {
this.adapter = adapter
}
binding.collapsingtoolbarlayout.setCollapsedTitleTextColor(resources.getColor(R.color.white, null))
binding.collapsingtoolbarlayout.setExpandedTitleColor(resources.getColor(R.color.white, null))
viewModel.weather.observe(viewLifecycleOwner, Observer {
adapter.updateData(it)
if (it.list.isNotEmpty()) {
binding.weatherDescription.text = "${it.list[0].main.getTempC()}℃ ${it.list[0].weather[0].description}"
}
})
}
}
將透過網路請求得到的天氣資料weather設置為LiveData,以方便做畫面更新。
class WeatherDetailViewModel(
cityId: String? = "7280291",
val cityName: String? = "Taiwan"
) : ViewModel() {
private val weatherRepo = WeatherRepo()
val weather: LiveData<WeatherResponse> get() = _weather
private var _weather = MutableLiveData<WeatherResponse>()
init {
cityId?.let {
viewModelScope.launch {
weatherRepo.loadWeather(it)?.let {
_weather.value = it
}
}
}
}
}
在WeatherDetailFragment,WeatherDetailRecyclerAdapter中的程式碼可以看到,設定溫度的部分使用了getTempC()這樣的函數,這是因為從API上取得的氣溫含有小數部分,在畫面上我希望只顯示整數部分即可,所以我在Main資料物件(model/Main.kt)這個檔案里加上getTempC()函數。
data class Main(
val grnd_level: Int,
val humidity: Int,
val pressure: Int,
val sea_level: Double,
val temp: Double,
val temp_kf: Double,
val temp_max: Double,
val temp_min: Double
) {
fun getTempC(): Int = temp.toInt()
}
若是程式執行後,在以下的部分出現錯誤時,先檢查一下 X物件 的資料的型態定義。
val date = Date(weather.dt * 1000)
如果class X(在model/X.kt中定義的資料物件)的dt被定義為Int的話需要改成Long。 dt是表示TimeStamp的數值,用來計算時間。
data class X(
val clouds: Clouds,
val dt: Long, // <-- Here
val dt_txt: String,
val main: Main,
val snow: Snow,
val sys: Sys,
val weather: List<Weather>,
val wind: Wind
)
以上就是這次WeatherDetailFragment相關的程式碼。 可以修改一下MainActivity的佈局文件讓程式執行時顯示WeatherDetailFragment來看看成果。
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/weather_detail_fragment"
android:name="bruntho.com.tennki.ui.weatherDetail.WeatherDetailFragment"/>
</androidx.constraintlayout.widget.ConstraintLayout>