안드로이드에서 AppWidgetProvider를 이용해 홈 위젯 추가하기

Sehee Jeong·2021년 5월 17일
4
post-thumbnail

홈 위젯을 사용하기 위해서 필요한 클래스와 xml 정의에 대해 간략하게 알아보고, 코틀린에서는 어떻게 사용하면 좋을지 예제와 함께 정리해보았다.

✅ 개념 정리해보기

AndroidManifest

        <receiver android:name=".presentation.widget.AppWidget">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/covid_appwidget"/>
        </receiver>
  • intent filter에는 AppWidgetManager에 정의되어있는 BroadcastReceiver를 명시해준다.
  1. ACTION_APPWIDGET_ENABLED
    앱 위젯의 첫번째 인스턴스가 설치될 때 전달된다.
  2. ACTION_APP_WIDGET_DISABLED
    앱 위젯의 마지막 인스턴스가 제거될 때 전달된다.
  3. ACTION_APPWIDGET_UPDATE
    앱 위젯이 갱신될 때마다 전달된다.
  4. ACTION_APPWIDGET_OPTIONS_CHANGED
    앱 위젯의 크기나 옵션이 바뀌었을 때 전달된다. 새로운 옵션의 정보는 Bundle로 전달된다.
  5. ACTION_APPWIDGET_DELETED
    앱 위젯의 일부가 제거되었을 때 전달된다. 삭제되는 인스턴스의 ID 배열이 전달된다.
  6. ACTION_APPWIDGET_CONFIGURE
    앱 위젯이 등록될 때 전달된다.
  • meta data 에는 appwidget.provider 를 명시하여, 앱 위젯의 속성이라는 것을 밝히며 xml을 지정한다.



위젯의 특성을 정의하는 메타파일 -AppWidgetProviderInfo

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minHeight="72dp"
    android:minResizeWidth="72dp"
    android:minWidth="146dp"        
    android:updatePeriodMillis="86400000"
    android:initialLayout="@layout/layout_covid_appwidget"
    android:previewImage="@drawable/abc_vector_test"
    android:configure="com.jshme.covidwidget.presentation.MainActivity"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>
  1. minHeight, minWidth : 위젯을 설정할 수 있는 최소 너비, 높이
  2. minResizeWidth, minResizeHeight : 홈에 설정한 위젯을 사용자가 크기를 변경하려고 했을 때, 최소한으로 줄일 수 있는 너비와 높이
  3. updatePeriodMillis : 홈 위젯의 업데이트 주기. 최소 30분에 한번씩 업데이트 설정이 가능하지만, 1시간에 1번을 권장한다.
  4. previewImage : 홈 위젯을 설정할 때 보이는 프리뷰 이미지
  5. configure : 사용자가 홈 위젯을 추가하고나서 띄울 액티비티
  6. resizeMode : 위젯의 크기를 조절할 수 있는 규칙
  7. widgetCategory : 앱 위젯을 홈 화면(home_screen), 잠금 화면(keyguard) 또는 둘 다에 표시할 수 있는지 여부

👉 width, height 크기는 보통 cell 단위로 잡는 것을 권장한다. cell이란 홈스크린을 일정한 비율로 나눈 영역이며 cell 크기는 계산은 (셀 개수 * 74) - 2 (단위 : dp) 로 잡는다. (1cell: 72dp, 2cell: 146dp ...)



앱 위젯 레이아웃을 위한 RemoteView

우리는 모든 View와 ViewGroup으로 화면을 구성하지만, 앱 위젯은 사용할 수 있는 View와 ViewGroup이 제한적이다. 앱 위젯은 RemoteView 객체를 중심으로 구현되는데, 이 RemoveView 라는 녀석은 한정된 클래스 만을 지원해주기 때문이다. (@RemoteView Annotation이 붙은 뷰만 지원해준다.)

RemoteView 가 지원하는 View List
FrameLayout
LinearLayout
RelativeLayout
GridLayout
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView

위 클래스를 제외하고는 다른 뷰는 만들 수 없다. 하지만 홈화면에 Checkbox와 같이 RemoteView를 지원하지 않는 뷰를 홈 위젯으로 만들고 싶다면, RemoteView 가 지원하는 뷰를 이용해 커스텀하는 것은 어떨까 👀 고된 길이 될 수 있겠지만 꽤 흥미로울 것 같다.

RemoteView 는 View가 아닌 최상위 객체인 object를 상속받고있는 클래스인데, 자세한 설명은 아래와 같다.

A class that describes a view hierarchy that can be displayed in another process. The hierarchy is inflated from a layout resource file, and this class provides some basic operations for modifying the content of the inflated hierarchy.

(다른 프로세스에서... View의 Hierarchy 를 만들 수 있는 클래스?🤔 hmmmm....what... what..)

AppWidget 은 실제 어플리케이션과 다른 프로세스에서 동작하게 되어 직접 홈 화면에 위젯을 바로 그릴 수 없다. 이 때 자신이 원하는 형태의 화면을 그릴 수 있도록 RemoteView 라는 뷰의 설계도를 들고 AppWidgetHost에게 부탁하는 과정이 필요한데, 이를 위처럼 설명했다고 보면 된다.

하지만, 우리는 이미 appwidget-provider tag 를 이용해 android:initialLayout="@layout/layout_covid_appwidget" 를 설정해주었는데, 왜 "뷰의 설계도"라는 것이 필요한 것일까?

initialLayout 속성은 앱 위젯을 맨 처음에 만들게 될 때 그려지게 된다. 하지만 그려지고난 후, 화면을 새롭게 업데이트를 해야하는 상황, 혹은 특정 뷰에 이벤트를 넣어야하는 상황이 필요할 때 RemoteView 를 사용하게 된다. 앱 위젯은 하나의 BroadcastReceiver에 불과하기 때문에, onReceive() 가 종료되는 순간에 소멸되어 오랫동안 뷰를 간직할 수 없다는 한계를 가지고 있다.

그래서, 앱 위젯이 자신이 필요한 뷰를 직접 만드는 것보다 뷰에 필요한 설계도(RemoteView) 만 만들고, 실제 화면 갱신과 업데이트는 AppWidgetHost 에게 위임하게 된다. 이러한 과정은 앱 위젯이 화면을 갱신할 때마다 반복하게 된다. 즉, 매번 새롭게 RemoteView를 만들고 통째로 넘겨주는 방식이다. 하지만 설계도만 넘겨줄 뿐이라 새로운 View가 매번 생성되는 것은 아니다.



AppWidgetProvider

AppWidgetProvider 는 BroadcastReceiver 클래스를 상속받은 형태이며, BroadcastReceiver가 발생되었을 때 관련된 콜백메소드를 호출하게된다.
1. onUpdate() : AppWidgetProviderInfo 에 정의된 업데이트 주기에 맞추어 호출되거나, 앱 위젯이 처음 홈 스크린에 추가될 때 호출된다.
2. onDeleted() : 홈 위젯이 제거될 때 호출된다.
3. onEnabled() : 홈 위젯이 처음 생성될 때 호출된다. 예를들어, 유저가 앱 위젯 2개를 차례로 추가하게 된다면, 맨 처음 추가시에만 호출될 것이다.
4. onDeisable() : 존재하는 마지막 홈 위젯이 제거될 때 호출된다.



✅ 코드로 적용해보기

홈 위젯 설정 시 위젯설정 화면 으로 이동하고, 설정한 값들이 홈 화면의 위젯으로 보이도록 만들 예정이다.

홈 화면위젯설정 화면 (AddWidgetActivity)



1. AndroidManifest.xml

    <application
                 ....
                 
<activity android:name=".presentation.ui.add.AddWidgetActivity">
            <intent-filter>
              // 이 속성이 있어야 앱 위젯이 추가될 때 "AddWidgetActivity" 화면으로 이동한다.
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
            </intent-filter>
        </activity>

        <receiver android:name=".presentation.widget.AppWidget">

            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/covid_appwidget" />
        </receiver>
</application>


2. AppWidgetHelper Class 생성

  • AppWidget 관련 코드를 한 곳에서 관리하기 위해 Builder Pattern을 사용해 Helper class를 생성한다.
package com.jshme.covidwidget.presentation.ui.helper

import android.app.Activity
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.widget.RemoteViews
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import com.jshme.covidwidget.R

class AppWidgetHelper(
    private val context: Context
) {
    private var title: String? = null
    private var description: String? = null
    private var pendingIntent: (() -> PendingIntent)? = null

    @DrawableRes
    private var iconRes: Int? = null
    private var widgetId: Int = 0

    private lateinit var appWidgetManager: AppWidgetManager

    fun onInitialize(title: String? = null, description: String? = null): AppWidgetHelper = apply {
        appWidgetManager = AppWidgetManager.getInstance(context)
        this.title = title
        this.description = description
    }

    fun setWidgetId(id: Int?): AppWidgetHelper = apply {
        this.widgetId = id ?: AppWidgetManager.INVALID_APPWIDGET_ID
    }

    fun setTitleText(title: String?): AppWidgetHelper = apply {
        this.title = title
    }

    fun setIcon(@DrawableRes iconRes: Int?): AppWidgetHelper = apply {
        this.iconRes = iconRes
    }

    fun setDescriptionText(description: String?): AppWidgetHelper = apply {
        this.description = description
    }

    fun setOnClickListener(pendingIntent: () -> PendingIntent): AppWidgetHelper = apply {
        this.pendingIntent = pendingIntent
    }

    fun build() {
    // layout_covid_appwidget.xml 이 홈 위젯에 추가
        RemoteViews(context.packageName, R.layout.layout_covid_appwidget).apply {
            // 사용자가 입력한 텍스트와 이미지가 위젯에 보여지도록 설정
            setTextViewText(R.id.title, title ?: "")
            setTextViewText(R.id.description, description ?: "")
            iconRes?.let { icon -> setImageViewResource(R.id.icon, icon) }
            
            // layout에서 rootLayout을 클릭하면, 정의해주었던 PendingIntent가 작동
            setOnClickPendingIntent(R.id.root, pendingIntent?.invoke())
        }.also { views ->
            // 위젯 업데이트
            appWidgetManager.updateAppWidget(widgetId, views)
        }

        val resultValue = Intent().apply {
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
        }

        (context as AppCompatActivity).apply {
            setResult(Activity.RESULT_OK, resultValue)
            finish()
        }
    }
}



3. "저장" 버튼을 클릭할 때, "AppWidget"을 생성하도록 설정하기

class AddWidgetActivity : AppCompatActivity(), OnSelectIconDismissListener {
    ...
    
    private var appWidgetId: Int? = null
    @DrawableRes private var iconResId: Int? = null

    private val appWidgetHelper: AppWidgetHelper by lazy {
        AppWidgetHelper(this)
    }
    
    ...

    private fun initData() {
    // appWidgetId 를 가져온다.
        appWidgetId = intent.extras?.getInt(
            AppWidgetManager.EXTRA_APPWIDGET_ID,
            AppWidgetManager.INVALID_APPWIDGET_ID
        ) ?: AppWidgetManager.INVALID_APPWIDGET_ID

        dataBinding.saveButton.setOnClickListener {
            createAppWidget()
        }
    }

    // helper class 를 이용해 app widget 을 생성한다.
    private fun createAppWidget() {
        appWidgetHelper.onInitialize()
            .setWidgetId(appWidgetId)
            .setTitleText(viewModel.titleText.value)
            .setDescriptionText(viewModel.descriptionText.value)
            .setIcon(iconResId)
            .setOnClickListener {
                Intent(this, MainActivity::class.java)
                    .let { intent ->
                        PendingIntent.getActivity(this, 0, intent, 0)
                    }
            }
            .build()
    }

예제코드:

https://github.com/jsh-me/android-home-widget-sample

profile
android developer @bucketplace

1개의 댓글

comment-user-thumbnail
2022년 3월 28일

PrivateConst클래스가 빠져있어, 빌드가 안되네요. commit 가능하실까요?

답글 달기