在此專案,MainActivity取得的天氣資料是被觀察的目標
而二個顯示的fragment畫面就是觀察者
一樣是建立包含3種方法的interface
interface ILocationPublisher {
fun getCurrentLocationWeatherRecord(): WeatherData.Records.Location?
fun add(subscriber: () -> Unit)
fun remove(subscriber: () -> Unit)
}
interface裡的add/remove函數使用另外一種寫法
參數subscriber是() -> Unit
型態,表示須要傳入的是一個沒有回傳值的匿名函數
而這個匿名函數定義在各觀察者中
lambda是讓一段程式碼可以像物件一樣傳遞,例如:
下方的subscriber變數就是指向以lambda寫的一段程式物件
class SecondFragment : Fragment() {
...
private val subscriber: () -> Unit = {
val loc = (activity as ILocationPublisher).getCurrentLocationWeatherRecord()
loc?.let {
updateContent(it)
}
}
fun updateContent(location: WeatherData.Records.Location) {
//隔日
tv_startTime3.text = location.weatherElement[0].time[2].startTime
tv_endTime3.text =
location.weatherElement[0].time[2].endTime//.split(" ")[1]
tv_status3.text =
location.weatherElement[0].time[2].parameter.parameterName
tv_rainProbability3.text =
"降雨機率 ${location.weatherElement[1].time[2].parameter.parameterName} %"
}
}
這裡表示變數subscriber存放的就是() -> Unit
型態的函數
當呼叫ILocationPublisher.add時,就可以把subscriber傳遞過去
(activity as ILocationPublisher).add(subscriber)
其實如果不用匿名函數的話
private val subscriber: () -> Unit = {
val loc = (activity as ILocationPublisher).getCurrentLocationWeatherRecord()
loc?.let {
updateContent(it)
}
}
以上可以寫成
fun subscriber(){
val loc = (activity as ILocationPublisher).getCurrentLocationWeatherRecord()
loc?.let {
updateContent(it)
}
}
但此時這個subscriber就不是一個變數,而是函數名
當使用它傳遞,是傳遞函數的回傳值(Unit),而不是整個函數
可以看到編譯器提示型態錯誤了,所以還是須要用lambda語法
(activity as ILocationPublisher).add { subscriber() }
如此就是將整個subscriber()函數作爲引數傳入add了
MainActivity實作interface ILocationPublisher
class MainActivity : AppCompatActivity(), ILocationPublisher {
...
private var location: WeatherData.Records.Location? = null
private val locationSubscribers = mutableListOf<() -> Unit>()
private fun setQueryResult(location: WeatherData.Records.Location) {
this.location = location
locationSubscribers.forEach { it.invoke() }
}
override fun getCurrentLocationWeatherRecord(): WeatherData.Records.Location? {
return this.location
}
override fun add(subscriber: () -> Unit) {
locationSubscribers.add(subscriber)
}
override fun remove(subscriber: () -> Unit) {
locationSubscribers.remove(subscriber)
}
}
在搜尋後呼叫並將資料傳入setQueryResult
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
val searchItem = menu?.findItem(R.id.action_search)
if (searchItem != null) {
val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
if (newText!!.isNotEmpty()) {
weatherList.forEach { location ->
if (location.locationName.contains(newText)) {
setQueryResult(location)
}
}
}
return true
}
})
}
return super.onCreateOptionsMenu(menu)
}
二個fragment(因爲拿來練習的天氣資料只有預報36小時,所以改成二頁)
加入上面lambda的subscriber與updateContent()以訂閱並更新各自的畫面
要注意的是fragment的生命週期,當載入時訂閱沒有問題
但當destroy後,要記得取消訂閱,否則fragment沒有東西可更新時,卻還是收到通知
就會出錯
class SecondFragment : Fragment() {
...
override fun onDestroyView() {
super.onDestroyView()
(activity as ILocationPublisher).remove(subscriber) //取消訂閱
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
(activity as ILocationPublisher).add(subscriber) //訂閱
}
}
把需要的資料都整理一下,畫面大致如下
有發現一個問題:查詢後,今日的畫面有顯示資料,但明日預報沒有?
原來因爲當app開啓時,只有今日的fragment先被載入
明日預報尚未載入,要載入後觸發在onActivityCreated()內的訂閱才會執行更新
所以當畫面滑過去明日預報的fragment後,需要再次查詢,畫面才會獲得更新
解決方法就是使用viewPager2.offscreenPageLimit
讓非當前畫面的Second Fragment預先載入
這樣在Second Fragment的subscribe才會在app啓動時就生效
viewPager2.offscreenPageLimit = 1
app使用起來也比較正常了