Skip to content

[Android JetPack] WokerManager와 Widget 도입

KwakEuiJin edited this page Jun 1, 2023 · 5 revisions

Android Kotlin 명식이 프로젝트 개발에서 Worker Manager를 활용한 Widget에서 API 통신 경험

소개: Android 앱 개발에서 백그라운드 작업을 효율적으로 처리하고자 할 때, Worker Manager는 이 글에서는 Android Kotlin 앱 개발 과정에서 Widget을 사용하여 API 통신을 수행하는 경험과 Worker Manager의 활용에 대해 소개하려고 합니다.

명식이 프로젝트 Widget 도입기

1. 명식이 프로젝트에서 Widget의 필요성:

앱 개발 과정에서 사용자에게 더 편리한 기능을 제공하기 위해 Widget은 매우 유용한 컴포넌트입니다. 특히 학식 메뉴를 사용자에게 알려주는 것이 주요 기능인 명식이 프로젝트에서 Widget은 홈 화면에서 학식 메뉴에 대해 쉽고 편리한 접근이 가능하다는 이점이 생깁니다. 이러한 Widget을 사용하여 앱과 상호작용하고, 실시간 학식 정보를 표시하며, 사용자 경험을 개선할 수 있기에 Widget의 도입을 결정하였습니다.

2. Worker Manager의 활용:

명식이 프로젝트에서는 Widget을 도입하게 된다면 보여져야 하는 학식 데이터는 API 통신을 통해 받아올 수 있으며 하루 주기로 업데이트 되어야 한다는 기술적인 문제에 대면하였습니다. 이러한 기술적인 문제를 해결을 위해 백그라운드 API 통신 + Schedule 을 가능케 하는 Worker Manager의 도입에 대해 고민하게 되었습니다. Worker Manager는 Android Jetpack 아키텍처 컴포넌트 중 하나로, 백그라운드에서 비동기 작업을 실행할 수 있는 기능을 제공합니다. 또한 Worker Manager를 사용하면 작업을 예약하고 관리할 수 있으며, 앱이 활성화되어 있지 않을 때에도 작업을 수행을 가능케 합니다. 그렇기에 Worker Manager를 도입하여 백그라운드 작업을 효율적으로 처리하고 앱 성능을 개선하였습니다.

3. widget 사용시 주의 사항"

명식이 위젯 탑재 버전 이후 상용 QA중에 발견한 문제점입니다.

  1. 배터리 절전모드를 사용중인 디바이스에서는 Widget의 onUpdate onRecive 함수의 콜백이 무시되는 경우가 있습니다.
  2. 특히 크리티컬한 문제로는 백그라운드 API통신 기능이 제대로 동작하지 않는 이슈가 있습니다.

해결방안: 명식이 앱 진입시 절전모드 예외처링 동의 Permission 다이얼로그를 통해 절전모드 예외처리를 하게 되면 정상적인 위젯 동작이 가능해집니다.

구현 과정:

1. Widget Provider 추가:

먼저, AppWidgetProvider를 상속받은 클래스를 만들고, 필요한 메서드를 오버라이딩하여 Widget의 동작을 정의합니다. 아래는 명식이 프로젝트의 AppWidgetProvider를 구성하기 위한 XML 파일 및 클래스입니다.

widget_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/item_widget_menu"
    android:minWidth="250dp"
    android:minHeight="180dp"
    android:resizeMode="vertical"
    android:widgetCategory="home_screen" />

MenuWidget.kt

class MenuWidget : AppWidgetProvider() {

    override fun onUpdate(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray?) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
      
    }
}

Manifest

    <receiver
        android:name=".presentation.widget.MenuWidget"
        android:exported="true">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/widget_menu" />
    </receiver>

2. Widget UI 개발:

Widget Layout을 구성하기 위한 xml 파일을 구성합니다. 이때의 주의사항이 있습니다.

  • 제한된 레이아웃 컨테이너: FrameLayout, LinearLayout, RelativeLayout과 같은 몇 가지 기본적인 레이아웃 컨테이너로 제한되어 있습니다. (constraint 는 사용 불가능)
  • 제한된 위젯 컴포넌트: Widget 레이아웃에서는 다양한 위젯 컴포넌트를 사용할 수 있지만, 모든 컴포넌트가 사용 가능한 것은 아닙니다. 사용 가능한 위젯 컴포넌트에는 TextView, ImageView, ProgressBar, Button 등이 있습니다. 그러나 사용자 인터랙션을 위한 EditText와 같은 일부 컴포넌트는 사용할 수 없습니다.
  • 제한된 스타일과 속성: Widget 레이아웃에서는 사용할 수 있는 스타일과 속성도 제한됩니다. 일부 스타일이나 속성은 Widget 레이아웃에서 제대로 동작하지 않을 수 있습니다. 예를 들어, 일부 뷰의 android:layout_weight 속성이나 android:visibility 속성은 제한적으로 동작할 수 있습니다.

3. Widget 백그라운드 API 통신을 위한 Worker Manager 개발:

명식이 프로젝트에서는 현재 Repository 패턴과 Hilt를 통해 Android Architecture를 구현하고자 하고 있습니다. 그렇기에 API통신을 위한 비즈니스 로직을 Worker Manager에서 호출하기 위해서는 FoodRepository의 의존성을 Hilt를 통해 참조 가능하도록 구성하여야 합니다. 하지만 Worker Manager에서 Hilt를 사용하기 위해서는 추가적인 과정이 필요했습니다.
참고 링크

위 링크와 같이 라이브러리를 추가하고 Provider의 명시적인 선언과 Application 단에서의 HiltWorkerFactory의 Setting을 통해서 의존성 주입시 발생하는 에러를 방지하였습니다.

위젯이 메뉴를 보여주기 위해서 API통신을 시작할 때 WorkerManager의 예약은 총 2가지 유형의 형태로 실행되도록 하였습니다.

  1. OneTimeWorkRequestBuilder를 통해 즉시 API통신 후 메뉴를 보여주도록 했습니다.
  2. PeriodicWorkRequestBuilder를 통해 1시간마다 API 통신 이후 Menu RemoteView에 접근하여 데이터를 업로드 하도록 처리하였습니다.
private fun startWidgetUpdateWorker(context: Context) {
    val updateOnceRequest = OneTimeWorkRequestBuilder<UpdateWidgetWorker>()
        .setConstraints(constraints)
        .build()

    val updateRequest = PeriodicWorkRequestBuilder<UpdateWidgetWorker>(1, TimeUnit.HOURS)
        .setConstraints(constraints)
        .addTag(TAG)
        .build()

    WorkManager.getInstance(context).apply {
        enqueue(updateOnceRequest)
        enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, updateRequest)
    }
}

계속 ...