프로그래밍/flutter

[flutter] Flutter에서 홈 화면 앱 위젯 2개 만드는 방법 (안드로이드)

소행성왕자 2023. 10. 25. 15:52

flutter 에서 홈 화면 앱 위젯 2개 만드는 방법을 소개합니다.

순서는 아래와 같이 추가해주면 됩니다.

  1. AndroidManifest.xml  2번째 위젯 추가
  2. res/xml/widget_info_large.xml 추가
  3. res/layout/widget_layout_large.xml 추가
  4. WidgetProviderLarge.kt 추가

안드로이드 위젯 전체 흐름도

 

프로젝트 구조

 

.AndroidManifest.xml  2번째 위젯 추가

   <!-- 1번 위젯 Your Background receiver and service goes here -->
   <receiver android:name="AppWidgetProvider" 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_info" />
   </receiver>
   <!-- //1번 위젯 -->

   <receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver" android:exported="true">
       <intent-filter>
           <action android:name="es.antonborri.home_widget.action.BACKGROUND" />
       </intent-filter>
   </receiver>
   <service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
       android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true"/>

   <!-- 2번 위젯 -->
   <receiver android:name="AppWidgetProviderLarge" 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_info_large" />
   </receiver>
   <!-- //2번 위젯 -->

Android 홈 화면 위젯 (home widget)을 정의하는 데 사용되는 AndroidManifest.xml 파일의 일부입니다. 

위 코드는 두 개의 위젯 (1번 위젯 및 2번 위젯)을 정의하고 각 위젯의 설정 및 동작을 정의합니다.

중요하게 봐야할 코드는 <receiver android:name="AppWidgetProvider"  부분과 android:resource="@xml/widget_info" 입니다.

.res/xml/widget_info.xml (1번위젯)
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_layout"
    android:minWidth="240dp"
    android:minHeight="110dp"
    android:minResizeWidth="240dp"
    android:minResizeHeight="110dp"
    android:widgetCategory="home_screen" />


.res/xml/widget_info_large.xml (2번위젯)
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_layout_large"
    android:minWidth="280dp"
    android:minHeight="310dp"
    android:minResizeWidth="280dp"
    android:minResizeHeight="310dp"
    android:widgetCategory="home_screen" />

Android 홈 화면 위젯 (home widget)을 정의하는 데 사용되는 XML 요소인 <appwidget-provider>를 정의합니다.

xmlns:android: XML 파일 내에서 Android 네임스페이스를 사용하기 위한 선언입니다.

android:initialLayout: 위젯의 초기 레이아웃 리소스를 지정합니다. 

이 리소스는 홈 화면에 위젯이 추가될 때 초기에 표시되는 레이아웃을 정의합니다. 

@layout/widget_layout 와 @layout/widget_layout_large는 홈 위젯의 초기 레이아웃 리소스 파일을 가리킵니다.


android:minWidth 및 android:minHeight: 위젯의 최소 너비와 높이를 정의합니다. 

홈 화면에서 위젯의 크기를 조절할 때 최소 크기가 이러한 값보다 작아지지 않습니다.

android:minResizeWidth 및 android:minResizeHeight: 위젯의 크기를 변경할 때 최소 크기를 정의합니다. 

사용자가 위젯 크기를 조절할 때 최소 크기가 이러한 값보다 작아지지 않습니다.


android:widgetCategory: 위젯이 어떤 카테고리에 속하는지를 정의합니다. 

이 속성은 홈 화면에서 위젯을 그룹화하거나 필터링하는 데 사용됩니다. 

"home_screen"은 홈 화면 위젯을 의미합니다.

.res/layout/widget_layout.xml (1번위젯)
<LinearLayout
    android:id="@+id/widget_root"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="240dp"
    android:layout_height="110dp"
    android:background="#1f303d"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_prdcd_sell"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="10dp"
        android:text="--"
        android:textColor="@android:color/white"
        android:textSize="14sp" />
    <TextView
        android:id="@+id/tv_prdcd_buy"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="10dp"
        android:text="--"
        android:textColor="@android:color/white"
        android:textSize="14sp" />

    <Button
        android:id="@+id/bt_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="환율 업데이트"
        android:textColor="@android:color/holo_blue_dark"
        android:textSize="12sp" />
</LinearLayout>




.res/layout/widget_layout_large.xml (2번위젯) 
<LinearLayout
    android:id="@+id/widget_root_large"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="280dp"
    android:layout_height="310dp"
    android:background="#1f303d"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_counter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:gravity="center_horizontal"
        android:padding="12dp"
        android:text="--"
        android:textColor="@android:color/white"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/tv_prdcd_sell"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="12dp"
        android:text="--"
        android:textColor="@android:color/white"
        android:textSize="16sp" />
    <TextView
        android:id="@+id/tv_prdcd_buy"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="12dp"
        android:text="--"
        android:textColor="@android:color/white"
        android:textSize="16sp" />

    <Button
        android:id="@+id/bt_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="환율 업데이트 라지"
        android:textColor="@android:color/holo_blue_dark"
        android:textSize="16sp" />
</LinearLayout>

위 코드는 Android 위젯의 레이아웃을 정의하는 XML 레이아웃 파일입니다.

이 레이아웃은 위젯이 홈 화면에 어떻게 표시될지를 결정합니다.

<LinearLayout>: 위젯의 루트 컨테이너입니다. 

android:id 속성을 사용하여 이 레이아웃을 식별할 수 있습니다.

위 코드는 android:id="@+id/widget_root"  와 android:id="@+id/widget_root_large" 로 식별합니다.

위 식별된 코드는 WidgetProvider.kt WidgetProviderLarge.kt 에서 식별합니다. 

android:layout_width 및 android:layout_height: 이 레이아웃의 너비와 높이를 정의합니다.

android:background: 레이아웃의 배경 색상 또는 배경 이미지를 정의합니다. 

여기서는 #1f303d로 지정하여 어두운 파란색 배경을 나타냅니다.

android:orientation: vertical로 설정하여 하위 뷰들이 세로 방향으로 배치되도록 합니다.

아래에 있는 하위 뷰들은 이 LinearLayout에 포함됩니다:

<TextView>: 텍스트를 표시하는 위젯입니다. 

여기에서는 tv_prdcd_sell 및 tv_prdcd_buy로 두 개의 텍스트 뷰가 정의됩니다.

android:id: 각 텍스트 뷰를 식별하는 고유한 ID를 부여합니다.

android:layout_width 및 android:layout_height: 텍스트 뷰의 너비와 높이를 정의합니다.

android:gravity: 텍스트의 수평 정렬 방향을 정의합니다. center_horizontal로 설정하여 텍스트가 가운데 정렬됩니다.

android:padding: 텍스트 주위의 여백을 정의합니다.

android:text: 초기 텍스트 내용을 정의합니다.

android:textColor: 텍스트의 글꼴 색상을 정의합니다.

android:textSize: 텍스트의 글꼴 크기를 정의합니다.

<Button>: 버튼을 표시하는 위젯입니다. 여기에서는 "환율 업데이트"라는 버튼이 정의됩니다.

android:id: 버튼을 식별하는 고유한 ID를 부여합니다.

android:layout_width 및 android:layout_height: 버튼의 너비와 높이를 정의합니다.

android:text: 버튼에 표시되는 텍스트를 정의합니다.

android:textColor: 버튼 텍스트의 글꼴 색상을 정의합니다.

android:textSize: 버튼 텍스트의 글꼴 크기를 정의합니다.

이 레이아웃은 홈 화면 위젯의 모양과 디자인을 결정하며, 여기에서 정의된 뷰들은 위젯이 화면에 어떻게 표시될지를 결정합니다.

.WidgetProvider.kt (1번위젯)
package com.coforward.webview_flutter_onlyweb_kt

import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
import es.antonborri.home_widget.HomeWidgetLaunchIntent
import es.antonborri.home_widget.HomeWidgetProvider

class AppWidgetProvider : HomeWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
        appWidgetIds.forEach { widgetId ->
            val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {

                // Open App on Widget Click
                val pendingIntent = HomeWidgetLaunchIntent.getActivity(context,
                    MainActivity::class.java)
                setOnClickPendingIntent(R.id.widget_root, pendingIntent)

                val prdcdSellString = widgetData.getString("_prdcdSell", "0")
                val prdcdBuyString = widgetData.getString("_prdcdBuy", "0")

                val counterTextSell = "USD/KRW 팔때 $prdcdSellString"
                val counterTextBuy = "USD/KRW 살때 $prdcdBuyString"

                setTextViewText(R.id.tv_prdcd_sell, counterTextSell)
                setTextViewText(R.id.tv_prdcd_buy, counterTextBuy)

                // Pending intent to update counter on button click
                val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context,
                    Uri.parse("myAppWidget://updatecounter"))
                setOnClickPendingIntent(R.id.bt_update, backgroundIntent)
            }
            appWidgetManager.updateAppWidget(widgetId, views)
        }
    }
}


.WidgetProviderLarge.kt (2번위젯)
package com.coforward.webview_flutter_onlyweb_kt

import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
import es.antonborri.home_widget.HomeWidgetLaunchIntent
import es.antonborri.home_widget.HomeWidgetProvider

class AppWidgetProviderLarge : HomeWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
        appWidgetIds.forEach { widgetId ->
            val views = RemoteViews(context.packageName, R.layout.widget_layout_large).apply {

                // Open App on Widget Click
                val pendingIntent = HomeWidgetLaunchIntent.getActivity(context,
                    MainActivity::class.java)
                setOnClickPendingIntent(R.id.widget_root_large, pendingIntent)

                val counter = widgetData.getInt("_counter", 0)
                val prdcdSellString = widgetData.getString("_prdcdSell", "0")
                val prdcdBuyString = widgetData.getString("_prdcdBuy", "0")

                var counterText = "현재 카운터 : $counter"
                val counterTextSell = "USD/KRW 팔때 $prdcdSellString"
                val counterTextBuy = "USD/KRW 살때 $prdcdBuyString"

                setTextViewText(R.id.tv_counter, counterText)
                setTextViewText(R.id.tv_prdcd_sell, counterTextSell)
                setTextViewText(R.id.tv_prdcd_buy, counterTextBuy)

                // Pending intent to update counter on button click
                val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context,
                    Uri.parse("myAppWidget://updatecounterlarge"))
                setOnClickPendingIntent(R.id.bt_update, backgroundIntent)

            }
            appWidgetManager.updateAppWidget(widgetId, views)
        }
    }
}

위 코드는 안드로이드 홈 화면에서 동작하는 위젯을 만들기 위한 코드입니다.

AppWidgetProvider 클래스: HomeWidgetProvider 클래스를 상속하여 위젯을 정의합니다.

1번 위젯 class AppWidgetProvider : HomeWidgetProvider()

2번 위젯 class AppWidgetProviderLarge : HomeWidgetProvider()

onUpdate 메소드: 위젯이 업데이트될 때 실행되는 메소드로, 홈 화면에서 위젯이 업데이트될 때 호출됩니다. 

이 메소드에서 위젯의 내용을 업데이트합니다.

appWidgetIds: appWidgetManager에 의해 제공된 위젯 ID 목록입니다. 

위젯 ID 목록을 반복하며 각 위젯에 대해 동일한 동작을 수행하게 됩니다.

RemoteViews: 홈 화면에서 위젯을 표시하기 위한 뷰 그룹을 생성합니다.

setOnClickPendingIntent: HomeWidgetLaunchIntent 클래스를 사용하여 홈 화면에서 위젯을 클릭했을 때 실행할 액티비티를 지정합니다. 

R.id.widget_root는 위젯의 루트 뷰입니다. res/xml/widget_info.xml 과 연결 되어 있습니다.

1번 위젯 setOnClickPendingIntent(R.id.widget_root, pendingIntent)

2번 위젯 setOnClickPendingIntent(R.id.widget_rootLarge, pendingIntent)

widgetData: SharedPreferences를 사용하여 데이터를 가져옵니다. 

widgetData.getString("_prdcdSell", "0")는 "_prdcdSell" 키에 해당하는 값을 가져옵니다. 

만약 값이 없을 경우 기본값으로 "0"을 사용합니다.

setTextViewText: RemoteViews를 사용하여 위젯의 텍스트 뷰의 내용을 설정합니다. 

위젯의 "tv_prdcd_sell" 및 "tv_prdcd_buy" 텍스트 뷰에 환율 정보를 설정합니다.

HomeWidgetBackgroundIntent: 위젯을 클릭할 때 실행할 백그라운드 인텐트를 설정합니다. 

이것을 사용하여 위젯 버튼 클릭에 대한 백그라운드 작업을 처리할 수 있습니다.

appWidgetManager.updateAppWidget(widgetId, views): 위젯의 내용을 업데이트하고 홈 화면에 반영합니다.

즉, 위 코드는 홈 화면에서 동작하는 위젯을 생성하고, 위젯을 클릭하면 액티비티가 실행되며, 위젯에 환율 정보를 표시하도록 설정하는 기능을 수행합니다.

 

아래코드는 업데이트 버튼 클릭할때 dart 와 연동하는 코드 입니다.

1번 위젯에서 업데이트 버튼 클릭

Uri.parse("myAppWidget://updatecounter"))

2번 위젯에서 업데이트 버튼 클릭

Uri.parse("myAppWidget://updatecounterlarge"))

main.dart

Future<void> main() async {
  // 앱 초기화 로직 및 설정
  await initialization(null);

  // Dynatrace 초기화
  Dynatrace().startWithoutWidget();

  // 홈위젯 사용
  WidgetsFlutterBinding.ensureInitialized();
  await HomeWidget.registerBackgroundCallback(backgroundCallback);

  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

....
....

Future<void> backgroundCallback(Uri? uri) async {
  print(">>>>>backgroundCallback");
  print(uri?.host);


  // 1번 위젯
  if (uri?.host == 'updatecounter') {

    // 스타루트 페이지 가지고오 C110816
    String html = await fetchPrdCd();
    // fetch page 를 분석
    Map<String, dynamic> parsedPrdcdJson = fetchSplit(html);

    var prdcdSell = parsedPrdcdJson['USDKRW']['sell'];
    var prdcdBuy = parsedPrdcdJson['USDKRW']['buy'];

    print("fetch 숫자 Sell: $prdcdSell");
    print("fetch 숫자 Buy : $prdcdBuy");

    int _counter = 0;
    await HomeWidget.getWidgetData<int>('_counter', defaultValue: 0)
        .then((int? value) {
      _counter = value ?? 0;
      _counter++;
    });

    await HomeWidget.saveWidgetData<String>('_prdcdSell', prdcdSell);
    await HomeWidget.saveWidgetData<String>('_prdcdBuy', prdcdBuy);

    await HomeWidget.saveWidgetData<int>('_counter', _counter);
    await HomeWidget.updateWidget(name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
  }
  else if (uri?.host == 'updatecounterlarge') {

    // 스타루트 페이지 가지고오 C110816
    String html = await fetchPrdCd();
    // fetch page 를 분석
    Map<String, dynamic> parsedPrdcdJson = fetchSplit(html);

    var prdcdSell = parsedPrdcdJson['USDKRW']['sell'];
    var prdcdBuy = parsedPrdcdJson['USDKRW']['buy'];

    print("fetch 숫자 Sell: $prdcdSell");
    print("fetch 숫자 Buy : $prdcdBuy");

    int _counter = 0;
    await HomeWidget.getWidgetData<int>('_counter', defaultValue: 0)
        .then((int? value) {
      _counter = value ?? 0;
      _counter++;
    });

    await HomeWidget.saveWidgetData<String>('_prdcdSell', prdcdSell);
    await HomeWidget.saveWidgetData<String>('_prdcdBuy', prdcdBuy);

    await HomeWidget.saveWidgetData<int>('_counter', _counter);
    await HomeWidget.updateWidget(name: 'AppWidgetProviderLarge', iOSName: 'AppWidgetProviderLarge');

  }
}



/**
 * 환율 조회 page fetch
 */
Future<String> fetchPrdCd() async {
  final response = await http.get(Uri.parse('http://abcd.com/11'));
  if(response.statusCode == 200) {
    return  response.body;
  } else {
    throw Exception("Failed to fetchPrdCd");
  }
}

/**
 * 환율 조회 page fetch html split
 */
Map<String, dynamic> fetchSplit(String html) {
  List<String> result = html.split('---kb_fx_auth_info---');
  Map<String, dynamic> parsedPrdcdJson = jsonDecode(result[1].trim());
  return parsedPrdcdJson;
}

이 코드는 백그라운드 작업을 처리하는 backgroundCallback 함수입니다. 

이 함수는 uri 매개변수를 통해 백그라운드 작업의 종류를 확인하고 해당 작업을 수행합니다.

if (uri?.host == 'updatecounter'): 이 부분은 URI의 호스트(host)가 'updatecounter'인 경우를 처리합니다. 

즉, 1번 위젯의 업데이트를 수행하는 부분입니다.

fetchPrdCd(): 환율 정보를 가져오기 위한 함수입니다. 

HTTP GET 요청을 통해 환율 정보가 있는 웹 페이지를 가져옵니다.

fetchSplit(html): 가져온 웹 페이지의 내용을 분석하여 환율 정보를 추출하는 함수입니다. 

이 함수는 JSON 형식의 데이터를 파싱하여 환율 정보를 얻습니다.

HomeWidget.getWidgetData<int>('_counter', defaultValue: 0): '_counter' 키에 저장된 정수 값을 가져오는 코드입니다. 

만약 해당 값이 없으면 기본값으로 0을 사용합니다.

HomeWidget.saveWidgetData<String>('_prdcdSell', prdcdSell): '_prdcdSell' 키에 prdcdSell 값을 문자열 형태로 저장합니다. 

이렇게 저장된 데이터는 위젯에서 사용됩니다.

HomeWidget.updateWidget(name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider'): 업데이트된 데이터를 1번 위젯에 적용하도록 하는 코드입니다. 

'name' 및 'iOSName'은 위젯의 이름을 지정하는 부분입니다.

위 코드는 1번 위젯과 2번 위젯 각각의 업데이트를 처리하며, 각각의 환율 정보를 가져와 위젯 데이터에 저장하고, 해당 데이터를 홈 화면에 업데이트하는 역할을 수행합니다.

 

 

참고

https://stackoverflow.com/questions/2570004/how-to-add-multiple-widgets-in-the-same-app

 

How to add multiple widgets in the same app?

I've just finished my Android widget. Now I need to have different sizes of this widget for the user to choose from. For example, I need a medium, small and large size widget, so when the user in...

stackoverflow.com