# 淺談 Android Widget - 2 _ 實作篇QRCode顯示篇
## 前言
繼上次[淺談 Android Widget - 1](/6w8LtFEDSMGKRrU6Ew3qKg) 後,這次來實作一個更新頻率較低的畫面,顯示一個QRCode畫面。
### 需求規格總覽
1. 使用者未登入時,有登入按鈕可直接進入App登入頁。
2. 使用者有登入時,顯示QRCode、使用者姓名。
3. 皆有重新整理按鈕,已刷新狀態。
4. 不同登入狀態時,畫面文案不同。
5. 每日會自動刷新、App登入狀態改變時也會刷新。
### 本次會使用到的方式
1. 從App內呼叫widget更新
2. widget手動更新
3. widget按鈕 -> 開啟到App 指定頁
4. 在Widget使用Glide
5. 在Widget 顯示/隱藏 View
### 畫面預覽
未登入時:

登入後:

## 各使用方式說明
初始的細節就不贅述了,可以看上一篇文章,僅就這次有新用到的部分分享
### 1. 從App內呼叫widget更新
在需要的地方,透過送AppWidgetManager 本身有的broadcase即可,他也可以呼叫enabled、deleted等等的方法
更改對應的intent.action即可
- clazz: 放的是有實作的AppWidgetProvider,每個widget都會有一個
- 以此為例,代表送一個widget update的資料到broadcase,會觸發widget的`onReceive` => `onUpdate`
- 但他有延遲,最慢一分鐘才會更新&有機率丟失,所以Widget建議還是要有可以手動觸發刷新的機制
```kotlin=
fun Context.notifyWidgetUpdate(clazz: Class<out AppWidgetProvider>) {
val intent = Intent(this, clazz)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
sendBroadcast(intent)
}
```
### 2. widget手動更新
此處所做的是當使用者點擊重新整理後,重新檢查登入狀態及刷新畫面,思路是:
1. 定義一個自己辨識的Tag(String)
2. 設定view點擊事件,有觸發的話送包含這個tag的 broadcase事件
3. 在onReceive時,偵測這個事件,有的話就做希望的行為
=> 因此可以選擇要全部更新,還是只動某部分的view
```kotlin=
companion object {
/**
* 1. 定義一個自己辨識的Tag(String)
**/
const val UPDATE_QRCODE = "UPDATE_QRCODE"
}
fun update(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int){
val remoteViews = RemoteViews(context.packageName, R.layout.widget_invite_evaluate_qrcode)
remoteViews.setUpdateClicker(context, appWidgetId)
}
/**
* 2. 設定view點擊事件,有觸發的話送包含這個tag的 broadcase事件
**/
private fun RemoteViews.setUpdateClicker(context: Context, appWidgetId: Int) {
//設定點擊的觸發intent
val clickViewIntent =
Intent(context, {你的WidgetProvider}::class.java).apply {
action = Constants.MessageNotificationKeys.CLICK_ACTION
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
//放入自定義的辨別Tag
putExtra(UPDATE_QRCODE, true)
}
//包裝成pendingIntent 因為widgetClick只吃他,注意因為是送Broadcast,所以用 `getBroadcast`
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
clickViewIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
//設置點擊事件 點擊的viewId + PendingIntent)
setOnClickPendingIntent(
R.id.tvWidgetInviteEvaluateQrcodeReload,
pendingIntent
)
}
/**
* 在onReceive時,偵測這個事件,有的話就做希望的行為
**/
override fun onReceive(context: Context?, intent: Intent?) {
//取得此次更新的WidgetId,可能是Array也可能是單一個,以單一個為主
val (appWidgetIds, appWidgetId) = getAppWidgetIds(intent)
// 看這次的intent有沒有包含自定義的標籤
val updateQrcode = intent?.getBooleanExtra(UPDATE_QRCODE, false) ?: false
if(updateQrcode){
//有的話,更新你想更新的地方,當然也可以全部刷新
updateQrcode(context, appWidgetIds)
}
super.onReceive(context, intent)
}
```
### 3. widget按鈕 -> 開啟到App 指定頁
這次是利用專案的Deeplink轉導開啟,但實際上要直接開到指定Activity也是可以的,只是如果你是有一個主要Activity一定要有開啟的話,會被跳過~
```kotlin=
//使用範例
fun update(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int){
val remoteViews = RemoteViews(context.packageName, R.layout.widget_invite_evaluate_qrcode)
remoteViews.setLoginClicker(context, appWidgetId)
}
private fun RemoteViews.setLoginClicker(context: Context) {
//這邊是利用Deeplink轉導,當然也是可以直接開到指定頁面
val intent = MainActivity.createIntent(context).apply {
data = Uri.parse("{你的Deeplink連結}")
}
//注意這裡因為是要開activity,所以是用getActivity
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
//設置點擊事件 點擊的viewId + PendingIntent)
setOnClickPendingIntent(
R.id.tvWidgetInviteEvaluateLogin,
pendingIntent
)
}
```
### 4. 在Widget使用Glide
因為無法直接用Glide.into(ImageView),因此只能先將圖片下載成bitmap後,在透過setImageViewBitmap的方法放入,步驟=
1. 先下載/取得Bitmap
2. 處理取得成功&失敗情況,並設置ImageView
3. 重新渲染widget畫面 (因為Glide去載bitmap會切執行緒,因此好了以後要手動呼叫渲染)
```kotlin=
//使用範例
fun update(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int){
val remoteViews = RemoteViews(context.packageName, R.layout.widget_invite_evaluate_qrcode)
remoteViews.setQRCodeImageView(context, appWidgetManager, appWidgetId)
}
private fun RemoteViews.setQRCodeImageView(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
//看你如何取得你的圖片下載網址
val webUrl = getWebUrl()
Glide.with(context)
.asBitmap()
.load(webUrl)
.into(object : CustomTarget<Bitmap>() {
//成功時用setImageViewBitmap
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
setImageViewBitmap(R.id.ivWidgetInviteEvaluateQrcode, resource)
//設定後記得要重新刷新
appWidgetManager.updateAppWidget(appWidgetId, this@setQRCodeImageView)
}
//失敗時直接用setImageViewResource放預設圖片
override fun onLoadCleared(placeholder: Drawable?) {
setImageViewResource(
R.id.ivWidgetInviteEvaluateQrcode,
R.drawable.img_broken_image
)
//設定後記得要重新刷新
appWidgetManager.updateAppWidget(appWidgetId, this@setQRCodeImageView)
}
})
}
```
### 5. 在Widget 顯示/隱藏 View
用remoteViews的`setViewVisibility(viewId, View.VISIBLE/INVISIBLE/GONE)` 方法即可。
### 6. 防連點刷新
因為widget只要很頻繁刷新,滿容易壞的,如果有連點有些操作也會異常,因此防連點還是需要的,目前的作法是在`onReceive`時阻擋他的事件。
**防雷點是,lastClickTime(紀錄點擊時間的參數)記得一定要放在object中**,因為provider他隨時可能會處在被消滅狀態,或是拿出不同實體,因此有類似的參數你會每次都拿到新的
```kotlin=
companion object {
private const val CLICK_INTERVAL = 1000L // 防止連點的間隔時間
//注意紀錄上次點擊時間的變數要放在companion object中
private var lastClickTime = 0L
}
override fun onReceive(context: Context?, intent: Intent?) {
val currentTime = System.currentTimeMillis()
//這裡是明確找有符合的action 才做防止連點,避免點擊跟系統或自己呼叫的更新重疊撞到,因此只針對明確要防的處理
val isDoubleClickAction = listOf(
Constants.MessageNotificationKeys.CLICK_ACTION
).contains(intent?.action.orEmpty())
//在緩衝時間,或是不需要管他的action時,才會往後走
if (currentTime - lastClickTime >= CLICK_INTERVAL || !isDoubleClickAction) {
//更新點擊時間
lastClickTime = currentTime
//處理要處理的項目,ex: 更新QRcode只讓他一秒內更新一次
when {
updateQrcode -> updateQrcode(context, appWidgetIds)
}
}
}
```
## 小結
1. 這次的實做比起上次即時看板來的簡易多,所需要實現的類別也較少,故障率也較低,算是不錯的實驗。
2. 更新頻率仍是widget的一大致命傷,除了手動更新時會快一些,不然在App開啟中手動呼叫的變化,會看你前一分鐘有沒有刷新到,以及你關掉App的速度,有可能會沒送成功。
=> 不過解法也還好,就是在規劃上記得準備好手動刷新的接口就行。
3. 總體來說對比上次失敗的情況,目前的是可正常運用在專案中了。
## 之後可持續研究部分
1. 可以在看有沒有什麼適合做widget的,像是儀錶板之類的,讓使用者可以在外部手動刷新看有沒有訊息
2. 或是可以看看能不能結合推播刷新widget等相關機制
3. 可以再嘗試看看有打api 或 ListView等其他元件或情境的操作