## 前言
此次會先以首次碰Widget的觀點寫,
目標是做出一個即時看板,每隔固定時間可以刷新API,更新最新的資料並顯示。
## 如何創建Widget?
### 方法1. 直接用原生自動生成
1. 直接在要創的資料夾右鍵 New -> 找widget ,就會出現下面的畫面

2. 再來說說這些在講啥
- Placement:選擇的是這個widget是在主螢幕還是鎖定螢幕上出現(鎖定螢幕有限制版本5以下才能用XD),所以基本上就是Homescreen
對應xml的 widgetCategory
- Resizable:就是放到桌面上後,他可以怎麼拉,垂直or水平or都可or不給拉
對應xml的 resizeMode (如果要Both 就是寫`horizontal|vertical`)
- 剩下就是設定最小寬高,每個cells 就是一格,直接用螢幕比對就行
- Configuration Screen:這個是讓他在放小工具時,會自動連到你設定的Activity,然後可以直接吃設定 EX:讓他顯示特定文字阿,或是自己的設定檔。
3. 右鍵創建的缺點就是... 他會爆改專案設定檔,然後各種建立資源,如果是已經有再用的專案,就要用git去看被改了那些,有點麻煩XD~
4. 好處就是按Finish後直接build就能看到Sample XD 一秒完成
### 方法2. 自己創建
1. 要自己創就要做這些步驟XD

2. 步驟摘要:
1. 創建布局:手動在res/layout下創建Widget布局文件。
2. 實現AppWidgetProvider:創建一個繼承自AppWidgetProvider的類。
3. 配置文件:在res/xml下創建Widget的配置XML文件。
4. 更新Manifest:在AndroidManifest.xml中添加`<receiver>`標籤以聲明您的Widget提供者。
5. 如果有要做Configure還要再另外加相關設定。
6. 測試Widget:確保在不同條件下測試Widget的行為。
3. 優點就專案不會被調整、也比較知道自己在幹嘛,缺點當然就是要自己一項一項查拉~
4. 配遺漏或沒想法,就請開個空白分支,然後先用右鍵自動創建,看一下需要參考的地方,然後就可以砍掉分支,或是直接Rollback
## 不自動生成的創建方式 QuickStart
當作練習,也想說讓自己熟一點Widget,所以當然要自己來呀~
不過在創建上會直接先用最陽春版本,也就是不自訂Configure~
`以下步驟只是最一開始生成而已,詳細邏輯請看後面`
### Step.1 創建布局:手動在res/layout下創建Widget布局文件。
1. 基本上就跟正常創Layout一樣,只是他有限定只有一些View可以用(詳細看最後一點)。
2. 常見的ConstraintLayout、View、RecycleView都不能用~
4. 如果你有用到不能使用的View,IDE通常會紅字提示你(在放R.layout.xxx檔案那邊),在看是什麼View不能用換掉就好
5. 如果你用自動創建,會發現他有跑出一些Style、Theme,但實際上其實都不用用到也沒差XD,直接background寫好圓角就行
6. 截至目前(最後更新於2023年4月),RemoteViews支持以下視圖(GPT提供):
```
FrameLayout
LinearLayout
RelativeLayout
GridLayout
TextView
ImageView
Button
ImageButton
ProgressBar
SeekBar
AnalogClock
Chronometer
StackView
AdapterViewFlipper
ListView
GridView
ViewFlipper
```
### Step.2 實現AppWidgetProvider:創建一個繼承自AppWidgetProvider的類。
```kotlin=
/**
* Implementation of App Widget functionality.
* App Widget Configuration implemented in [RealtimeCaseDashBoardAppWidgetConfigureActivity]
*/
class RealtimeCaseDashBoardAppWidget : AppWidgetProvider() {
/**
* 如果是使次建立,並不一定會呼叫到onUpdate唷~
* 另外因為小工具使用者可以創建多個,因此會需要每個更新都要處理
* - 這邊主要就是自己呼叫 or 系統給定的時間更新(預設是24hr)
*/
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
//更新Widget~
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
// 每個Widget被移除時觸發
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
// When the user deletes the widget, delete the preference associated with it.
for (appWidgetId in appWidgetIds) {
deleteTitlePref(context, appWidgetId)
}
}
// 當第一個Widget被放出來時觸發
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
// 當最後一個Widget被移除時觸發
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val widgetText = loadTitlePref(context, appWidgetId)
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.realtime_case_dash_board_app_widget)
views.setTextViewText(R.id.appwidget_text, widgetText)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
```
### Step.3 配置文件:在res/xml下創建Widget的配置XML文件。
1. 設定檔範例如下,詳細細節可以到[官網](https://developer.android.com/develop/ui/views/appwidgets?hl=zh-tw)查,部分欄位說明
- configure 可選:有要讓每次小工具放到桌面時,跑出設定頁的話,要把Activity的位置放上去,沒有就不用設定
- description:小工具的描述,但有些版本或手機不會跑出來
- resizeMode:是否可以水平/垂直拉,會被最小值擋住。
- updatePeriodMillis:每隔多久會觸發widget的onUpdate,最低限制是3600000毫秒(1小時),越短頻率更新越耗效能。
2. 有版本差異的欄位說明
- 最小/預設大小設定
- minWidth、minHeight:單位是dp,不知道怎麼算可以看官網,或是先用右鍵生成
- targetCellWidth、targetCellHeight:直接用格子數代表,Android12以上要設定
- 可以看[官網的格數dp對照表](https://developer.android.com/develop/ui/views/appwidgets/layouts?hl=zh-tw#anatomy_determining_size)
- 小工具預覽圖
- **previewImage** => 小工具的預覽圖案
- previewLayout => 這個會直接跑xml檔的預覽,Android 12以上才能用;沒設定就是顯示previewImage
- 小工具是否可在螢幕鎖屏使用
- widgetCategory:小工具可以在哪邊用 home_screen (主畫面) | keyguard (鎖定屏幕 - Android 5以後無法用)
- initialKeyguardLayout 可選:有放到螢幕鎖定的小工具才要,但Android 5 以後此權限已被拔除,所以基本上不會用到
3. 完整widget_info 範例
```
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="設定的Activity位置,要包含包名 ex: com.example.SettingActivity"
android:description="@string/app_widget_description"
android:initialKeyguardLayout="@layout/realtime_case_dash_board_app_widget"
android:initialLayout="@layout/realtime_case_dash_board_app_widget"
android:minWidth="250dp"
android:minHeight="110dp"
android:previewImage="@drawable/example_appwidget_preview"
android:previewLayout="@layout/realtime_case_dash_board_app_widget"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen|keyguard" />
```
### Step.4 更新Manifest:在AndroidManifest.xml中添加`<receiver>`標籤以聲明您的Widget提供者。
```xml=
<receiver
android:name="Step2.創建的Wiget檔案位置"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/Step3.創建的xml" />
</receiver>
<!-- 如果有用到 Widget ListView 要在定義這個Service -->
<service android:name="ListView實現的RemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false">
<intent-filter>
<action android:name="android.widget.RemoteViewsService" />
</intent-filter>
</service>
<!-- 如果有小工具的設定頁面的話 -->
<activity
android:name="小工具設定頁面的Activity"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
```
## 一些Widget需要知道的Tips
1. 官方自己的自動更新時間,限制最少要間隔30分鐘,常見作法會直接用內建的AlarmManager 做計時,而AlarmManager的最小間隔限制是1分鐘。
- 自動更新:30min
- AlarmManager: 1min
- 其他方式:看各自方式的限制
2. 要注意如果刷新的越頻繁,就越會吃資源&電量,代價就是要嘛使用者去調整APP權限,加入白名單中,不然就是很有機會被系統強制中斷,然後小工具就會當住,要移除重放才會正常。
3. 會很常用到`PendingIntent`,記得flags要注意,有些情況是要設置成PendingIntent.FLAG_MUTABLE,例如用到ListViews的時候。
4. 目前無支援ViewBinding
5. 使用前記得詳讀[官方文件](https://developer.android.com/develop/ui/views/appwidgets?hl=zh-tw)
6. 可以直接看官方Sample:
- [ListView_Sample - 單純畫面](https://android.googlesource.com/platform/development/+/refs/heads/main/samples/StackWidget/src/com/example/android/stackwidget?autodive=0%2F)
- [WeatherListWidget_Sample - 假資料模擬API更新](https://android.googlesource.com/platform/development/+/refs/heads/main/samples/WeatherListWidget/)
## 即時看板工具實現
先說結論,目前先Pending了~ 因為自動更新後很常遇到整個Widgets掛掉,因此就沒做即時更新了,目前嘗試以一小時更新一次+手動觸發,觀察是否還會掛掉。
### 即時看板用到的技術
1. 基本的Widget更新畫面&接收資料方式
2. 自動更新 - AlarmManager (雖然後面沒使用,但一樣可以說XD)
3. Widget打Api
4. ListView 的使用 - 畫面顯示&點擊事件
5. 先上Code,這裡只有Update & 直接說範例,要看完整請往上直接看官方Sample即可:
```kotlin=
override fun onUpdate(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetIds: IntArray?
) {
//檢查必要的東西是否都在,不在就不做事
if(context == null || appWidgetManager == null){
debugLog("onUpdate context is null $context / $appWidgetManager")
return
}
context ?: return
appWidgetManager ?: return
//更新每個Widget
appWidgetIds?.forEach { widgetId ->
//過濾掉無效的APPWIDGET_ID
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
return
}
//開始更新處理
coroutineScope.launch {
try {
//part3.打Api
val errorMsg = updateCaseListData()
withContext(Dispatchers.Main){
//part1.更新畫面資訊
updateAppWidget(context, appWidgetManager, widgetId, errorMsg)
//part4.更新ListView
appWidgetManager.notifyListViewUpdateData(widgetId)
}
} catch (e: Exception) {
debugLog(e)
}
}
}
//part2.開始啟動自動更新的鬧鐘
context.startAlarmManager(appWidgetIds)
}
}
```
### Part1.基本的Widget更新畫面&接收資料方式
1. 更新畫面,都是先創出RemoteViews
2. 再透過AppWidgetManger更新,更新有分成不同種類,EX:
- 全部刷新(最耗資源,全部都重建) `updateAppWidget(appWidgetId, remoteViews)`
- 部分刷新(只更新有變化的地方) `partiallyUpdateAppWidget(appWidgetId, remoteViews)`
- 更新ListView(跟呼叫RecycleView notifyDataChanged一樣) `notifyAppWidgetViewDataChanged`
```kotlin=
private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
errorMsg: String = ""
) {
//創出RemoteViews
val remoteViews = RemoteViews(context.packageName, R.layout.widget_realtime_case_dash_board)
//操作RemoteViews更新要更新的地方
remoteViews.setHeaderInfo()
remoteViews.setUpdateClicker(context, appWidgetId)
remoteViews.setListViewData(context, appWidgetId)
remoteViews.setListViewClicker(context, appWidgetId)
// 更新到Widget上
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
```
#### 基本畫面顯示
1. 要直接用有開出來的方法,做設定 EX: `setTextViewText(viewId, text)` 其實就是 `findViewById<TextView>(viewId).text = text`
```kotlin=
private fun RemoteViews.setHeaderInfo() {
val searchHint = when {
areaHistory.isNotEmpty() || categoryHistory.isNotEmpty() -> "正在顯示:篩選後案件"
else -> "正在顯示:全部案件"
}
setTextViewText(R.id.tvWidgetRealtimeCaseDashboardSubTitle, searchHint)
//取得現在時間 EX: 12:00
val currentTime = Calendar.getInstance(Locale.TAIWAN).time
//date to YYYY/MM/DD HH:mm like 2024/02/27 12:00
val dateString = android.text.format.DateFormat.format("yyyy/MM/dd", currentTime).toString()
//date to HH:mm
val timeString = android.text.format.DateFormat.format("HH:mm", currentTime).toString()
setTextViewText(
R.id.tvWidgetRealtimeCaseDashboardReloadTime,
"$dateString $timeString 🔄️"
)
}
```
#### 基本畫面設定點擊事件
1. 要先創建PendingIntent,然後設定當該View被點擊時,就會觸發該Intent,也就是他是依靠Intent驅動的
2. 把PendingIntent設定到View,就像setOnClicker一樣,只是他是先new好,也只有intent能用,剩下判斷要寫到onReceive收到該action或妳要的extras內容後再自己寫看要怎麼辦
3. 簡單來說:定義Intent -> 放入View點擊觸發 -> 點擊後行為 onReceive定義
```kotlin=
private fun RemoteViews.setUpdateClicker(context: Context, appWidgetId: Int) {
//設定更新列表資料的點擊效果,點時間或空資料
val clickViewIntent =
Intent(context, RealtimeCaseDashBoardWidgetProvider::class.java).apply {
//action & Extras 內容都可以自己定義
action = CLICK_ACTION
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
putExtra(UPDATE_LIST, true)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
clickViewIntent,
//Android12以上一定要加PendingIntent.FLAG_IMMUTABLE or .FLAG_MUTABLE,不然會報錯,差別就是該Intent還能不能調整
// PendingIntent.FLAG_UPDATE_CURRENT 代表這個Intent有收到要立刻處理,所以兩個都會加
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
//把PendingIntent設定到View
setOnClickPendingIntent(
R.id.tvWidgetRealtimeCaseDashboardReloadTime,
pendingIntent
)
setOnClickPendingIntent(
R.id.tvWidgetRealtimeCaseDashboardEmpty,
pendingIntent
)
}
```
#### 點擊事件後續
```kotlin=
override fun onReceive(context: Context?, intent: Intent?) {
//...
// 假設有收到UPDATE_LIST就代表 更新整個Widget
val updateList = intent?.getBooleanExtra(UPDATE_LIST, false) ?: false
val isClickAction = intent?.action.orEmpty() == CLICK_ACTION
if (updateList) {
//做事 _ Like原本要寫在onClickerListener裡的動作
updateCaseData(context, appWidgetId, appWidgetIds, isClickAction)
}
}
}
```
### Part2. 自動更新 - AlarmManager
1. 做一個更新的PendingIntent,時間到觸發即可。
2. 這邊觸發是用`PendingIntent.getBroadcast`,因此還是會在onReceive收到
3. AppWidgetManager.ACTION_APPWIDGET_UPDATE,是Widget內建的Action會觸發onUpdate
```kotlin=
private fun Context.startAlarmManager(appWidgetIds: IntArray? = intArrayOf()) {
testInfoLog("startAlarmManager ${appWidgetIds.toString()}")
val alarmManager =
getSystemService(Context.ALARM_SERVICE) as? AlarmManager ?: return
val intent = Intent(this, RealtimeCaseDashBoardWidgetProvider::class.java).apply {
//這邊可自行定義,AppWidgetManager.ACTION_APPWIDGET_UPDATE,是Widget內建的會觸發onUpdate
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
putExtra(UPDATE_LIST, true)
}
val pendingIntent =
PendingIntent.getBroadcast(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
//設定每60分鐘更新一次 => 最低限制的間隔時間為1分鐘,目前實測1分鐘滿容易導致他就不更新了
val updateTime = 1000 * 60 * 60 * 1L
alarmManager.setRepeating(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime(),
updateTime,
pendingIntent
)
}
```
### Part3. Widget打Api
其實就跟正常打API一樣,只是資料如果要傳送給ListView,要透過本地傳遞,EX: SharedPerence、ContentProvider、Room等等
```kotlin=
/**
* 打完資料後會儲存到SP裡面,在呼叫onDataSetChanged時WidgetViewsFactory會拿這個資料再轉成換面顯示的Data
*/
private suspend fun updateCaseListData(): String {
//創建API參數...
//打API
caseRepository.getCaseList(reqProj).fold(
onSuccess = { response ->
val dataList = response.dataList
val newData = dataList.mapNotNull {
//轉成ListView的資料格式
WidgetViewsFactory.WidgetItem(
caseId = it.caseId,
caseTitle = it.caseTitle,
casePostTime = it.updateTime
)
}
var errorMsg = response.message
if(newData.isNotEmpty()){
val jsonStr = gson.toJson(newData)
//存到本地資料
caseListStr = jsonStr
}else{
//存到本地資料
caseListStr = ""
errorMsg = response.message.ifEmpty { "沒有符合條件的案件,請至APP更改篩選條件" }
}
return errorMsg
},
onFailure = {
debugLog(it)
return "讀取案件列表失敗,請嘗試重新安裝小工具"
}
)
}
```
### Part4. ListView 的使用 - 畫面顯示&點擊事件
1. 要使用ListView,需要繼承`RemoteViewsService`&實現`RemoteViewsFactory`才能使用
2. 他的概念是透過`RemoteViewsService`,去監聽一些事件,再透過這些事件的觸發做對應的事情,有點像provider
3. 但他的主要職責只是在於渲染ListView畫面
4. AppWidgetProvider:
- 把RemoteViewsService跟ListView的Adapter綁定
- 綁定資料空時的畫面Layout
- 設定每個item被點擊時的PendingIntent
5. RemoteViewsFactory:
- 設定每個item要顯示的畫面
- 製作FillInIntent,給予該item的Data
- 更新DataList
6. 幾個跟一般用RecycleView不同的點:
- DataList更新不是外面直接傳入,而是要透過不同管道拿取。EX:直接打API或本地拿取,建議本地,因為如果還有錯誤處理外面沒辦法判讀,職責也容易模糊。
- 點擊事件的更新:
- Provider的setPendingIntentTemplate意思是當使用者點擊時,都會傳遞這個PendingIntent出來,像是一個範本的概念。
- RemoteViewsFactory的setOnClickFillInIntent,則是只要填充到Intent的內容
- 結合起來也就是當item被點擊時,會創出Template給的PendingIntent,然後把FillInIntent的Extras放入,再回傳出去
- 因此該PendingIntent會被改變,切記flags一定要添加`PendingIntent.FLAG_MUTABLE`,否則會無法正確把FillInIntent的資料放入
#### RemoteViewsService
1. 很Easy,其實就是把RemoteViewsFactory的實體給他而已
```kotlin=
class WidgetService: RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return WidgetViewsFactory(this.applicationContext, intent)
}
}
```
2. 因為是Service,要到Manifest定義
```xml=
<!-- Widget ListView -->
<service
android:name="RemoteViewsService Class"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS">
<intent-filter>
<action android:name="android.widget.RemoteViewsService" />
</intent-filter>
</service>
```
#### RemoteViewsFactory 實現
1. 在設定點擊是用FillInIntent,他的概念是會把原本放到ListView的PendingIntent,把FillInItntent填充進去,因此每個item的Data資料要放,就是從這邊放入
```kotlin=
class WidgetViewsFactory(
private val context: Context,
intent: Intent
) : RemoteViewsFactory, KoinComponent {
data class WidgetItem(
@SerializedName("case_id")
val caseId: String,
@SerializedName("case_title")
val caseTitle: String,
@SerializedName("case_post_time")
val casePostTime: String
)
private val appWidgetId: Int = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
private val coroutineScope = createCoroutineScope(Dispatchers.IO)
/**
* 更新會在[RealtimeCaseDashBoardWidgetProvider] 打完API更新,這是本地資料SP(依自己的實現即可)
*/
private var caseListStr by Preference()
private val gson by inject<Gson>()
private val dataList = mutableListOf<WidgetItem>()
override fun onCreate() {
//可以做些初始化動作
}
override fun onDataSetChanged() {
//不切coroutinr也沒差,就是跟新dataList資料而已
//更新後會自動觸發onGetViewAt的Fuction
coroutineScope.launch {
val newData = getCaseList()
withContext(Dispatchers.Main) {
dataList.clear()
dataList.addAll(newData.toList())
}
}
}
//從本地拿取資料
private fun getCaseList(): List<WidgetItem> {
return try {
val type = object : TypeToken<List<WidgetItem>>() {}.type
gson.fromJson<List<WidgetItem>>(caseListStr, type)
} catch (e: Exception) {
debugLog("Widget getCaseList error: $e")
emptyList()
}
}
override fun onDestroy() {
testInfoLog("Widget onDestroy")
coroutineScope.cancel()
dataList.clear()
}
override fun getCount(): Int {
return dataList.size
}
//重點,設定View要怎麼顯示
override fun getViewAt(position: Int): RemoteViews {
//設定要怎麼顯示View
val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_realtime_case)
val data = dataList.getOrNull(position) ?: return remoteViews
remoteViews.setTextViewText(R.id.tvItemWidgetRealtimeCaseTitle, data.caseTitle)
remoteViews.setTextViewText(R.id.tvItemWidgetRealtimeCaseTime, data.casePostTime)
//設定每個View的點擊事件,用FillInIntent
val fillInIntent = Intent().apply {
Bundle().also { extras ->
extras.putString(RealtimeCaseDashBoardWidgetProvider.CASE_ID, data.caseId)
putExtras(extras)
}
}
remoteViews.setOnClickFillInIntent(R.id.llItemWidgetRealtimeCase, fillInIntent)
return remoteViews
}
/**
* 回傳null 會用預設的載入中顯示
*/
override fun getLoadingView(): RemoteViews? {
return null
}
override fun getViewTypeCount(): Int {
return 1
}
//返回該位置的Item的ID,盡量唯一值,會跟下面的ID選項相同
override fun getItemId(position: Int): Long {
return position.toLong()
}
//返回的ID是否為唯一值且穩定,true的話會提高效率
override fun hasStableIds(): Boolean {
return true
}
}
```
#### Provider設定
```kotlin=
/** 設定ListView的畫面顯示 */
private fun RemoteViews.setListViewData(context: Context, appWidgetId: Int) {
val listViewIntent = Intent(context, WidgetService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
//將 RemoteViewsService 跟 Adapter綁定
setRemoteAdapter(R.id.listViewWidgetRealtimeCaseDashboard, listViewIntent)
//設定空畫面,需在同個FrameLayout中,當資料空時會自動切換
setEmptyView(
R.id.listViewWidgetRealtimeCaseDashboard,
R.id.tvWidgetRealtimeCaseDashboardEmpty
)
}
/** 設定listView點擊事件 */
private fun RemoteViews.setListViewClicker(context: Context, appWidgetId: Int) {
val listItemClickViewIntent: PendingIntent =
Intent(context, RealtimeCaseDashBoardWidgetProvider::class.java).run {
action = REALTIME_CASE_ITEM_CLICK_ACTION
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
//請注意一定要設定 PendingIntent.FLAG_MUTABLE,因為FillInIntent會改變Intent的內容!
PendingIntent.getBroadcast(
context,
0,
this,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
}
//設定每個項目的PendingIntent
setPendingIntentTemplate(
R.id.listViewWidgetRealtimeCaseDashboard,
listItemClickViewIntent
)
}
```
##### 空畫面xml檔
1. 直接把空畫面跟ListView重疊即可,不需要設定visiable,當空值時他會自動切換
```xml=
<FrameLayout
android:id="@+id/frameLayoutWidgetRealtimeCaseDashboard"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listViewWidgetRealtimeCaseDashboard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:clipToPadding="false"
android:paddingBottom="15dp" />
<TextView
android:id="@+id/tvWidgetRealtimeCaseDashboardEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/neutral_400"
android:gravity="center"
android:text="@string/realtimeCaseDashBoardWidgetEmpty"
android:textSize="20sp"/>
</FrameLayout>
```
### 小結
1. 目前擱置中,尚存Bug,例如建多個工具後有問題等等... 尚有滿多地方需再繼續研究。
2. 主要剩餘突破點為,當被系統殺死後,有什麼方法有辦法喚回,恢復正常運作
3. 如果沒辦法就是看更新頻率低+手動觸發,如何讓使用者體驗是良好的,而不會覺得很麻煩。
## 之後可繼續研究
1. 可以設定APPWIDGET_CONFIGURE,讓使用者在使用時直接設定一些資訊,例如可以自訂顏色、文字,或是條件等等。
- 會是另外開一個Activity讓他做事,感覺彈性跟可以做的事情滿多的。
2. 可能會朝向較固定的資料開發,EX:顯示特定QRCode,先嘗試跟產品結合,上線,在看有沒有遇到什麼坑。