frontend/mobile

[android / Kotlin] flutter 에서 home screen widget 만들기

김포레스트 2023. 10. 25. 10:05

우리는 앱개발을 위해 flutter를 설치했다.

android와 ios를 한꺼번에 커버하기 위해서가 가장 큰 이유였다.

 

그런데, home screen widget은 flutter에서 dart로 만들 수 없고, 패키지를 설치한 다음에

안드로이드와 ios각각 개발을 해줘야 한다고 한다.

 

이런 네이티브 개발을 피하고 싶었던건데!!!!!

 

필요하면 해야지 뭐.

 

일단은 안드로이드용 홈스크린 위젯을 만들어보기로 한다.

이미 플러터에서 위젯이라는 개념을 사용하고 있기 때문에 flutter widget등으로 검색하면

우리가 원하는 휴대폰 위젯 기능이 안나온다. 

 

그래서 홈스크린 위젯이라는 키워드로 검색 해줘야 함.

 

어쨌든. 

 

우리는 어플에서 카운터 기능을 만들어 줄 것이고,

그 카운터 기능을 홈스크린 위젯에 연동시켜줄 것이다.

 

 

제일 먼저 패키지를 설치해 줘야 한다.

pubspec.yaml 파일을 열고 다음을 추가 해 준 뒤, Pub get 버튼 누르기.

dependencies:
  home_widget: ^0.3.0

 

패키지관련 공식 내용은 이곳에서 확인 할 수 있다

 

https://pub.dev/packages/home_widget

 

home_widget | Flutter Package

A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS.

pub.dev

 

패키지 설치 후

단계는 다음과 같다. 

1. 홈스크린위젯 ui 만들기(res/layout/widget_layout.xml)
2. 홈스크린위젯 구성 정의하기(res/xml/widget_info.xml)
3. 홈스크린위젯 기능 만들기(java/com/example/flutter_sample/WidgetProvider.kt)
4. 홈스크린위젯 수신기 추가(AndroidManifest.xml)
5. 필요에 따라 main.dart 수정

 

 

1. 홈스크린위젯 ui 만들기(res/layout/widget_layout.xml)

1.1 android\app\src\main\res 폴더에 layout 폴더를 새로 추가한다.

1.2 layout 폴더에 widget_layout.xml 파일을 생성한다.

<FrameLayout
    android:id="@+id/widget_root"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="180dp"
    android:layout_height="110dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#1f303d">

        <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" />

        <Button
            android:id="@+id/bt_update"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:text="Update Counter"
            android:textColor="@android:color/holo_blue_dark"
            android:textSize="12sp" />
    </RelativeLayout>
</FrameLayout>

이 파일은 홈스크린 위젯의 모양을 만드는 것이라고 생각하면 된다.

html 태그 안에 css 코드를 인라인으로 잔뜩 넣어준 모양과 비슷하다.

그래서 html과 css파일의 분리를 철저하게 실행했던 나로서는 약간.. 머랄까....흐음......

밥에 김치국물 잔뜩 묻어있는 느낌이랄까......

 

추후에 레이아웃 관련 내용도 포스팅 할 예정.

 

 

 

2. 홈스크린위젯 구성 정의하기(res/xml/widget_info.xml)

 

 2.1 android\app\src\main\res 폴더에 xml 폴더를 새로 추가한다.

 2.2 xml 폴더에 widget_info.xml 파일을 새로 추가한다.

<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_layout"
    android:minWidth="280dp"
    android:minHeight="210dp"
    android:minResizeWidth="280dp"
    android:minResizeHeight="210dp"
    android:widgetCategory="home_screen" />

홈스크린 위젯의 기본적인 내용을 구성하는 파일이다.

레이아웃 파일과 연동되어있다는 것을 알리고,

사이즈 등등의 기본적인 부분들을 정의한다.

 

 

 

3. 홈스크린위젯 기능 만들기(java/com/example/flutter_sample/WidgetProvider.kt)

3.1 java/com/example/flutter_sample 폴더 내에 MainActivity.java 혹은 MainActivity.kts 파일이 있을텐데, 그 파일이 있는 지 확인 하고 WidgetProvider.kt 파일을 새로 추가한다.

package com.example.flutter_sample2;  //프로젝트마다 바뀌는 부분

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;
import com.example.flutter_sample2.R;  //프로젝트마다 바뀌는 부분
import com.example.flutter_sample2.MainActivity;  //프로젝트마다 바뀌는 부분

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 counter = widgetData.getInt("_counter", 0)

                var counterText = "Your counter value is: $counter"

                if (counter == 0) {
                    counterText = "You have not pressed the counter button"
                }

                setTextViewText(R.id.tv_counter, counterText)

                // 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)
        }
    }
}

이 파일에서는 홈스크린 위젯에서 사용할 기능, 즉 함수를 정의한다.

어떤 내용을 출력할 것인지 변수 설정도 하고,

어떤 버튼을 누르면 어떤 기능이 실행되는지도 정의해준다. 

 

var counterText = "Your counter value is: $counter"

counterText라는 변수를 설정한다.

 

setTextViewText(R.id.tv_counter, counterText)

그리고, layout에서 어떤 id를 지닌 textview에 연결을 해줄것인지 설정한다.

위 내용은 tv_counter라는 id를 가진 textview에 counterText변수를 넣어서 보여줄 예정인 것이다.

 

아까 생성한 widget_layout.xml파일을 보면 

이렇게 id값을 tv_counter로 설정해놓은 것을 알 수 있다. 

이렇게 layout.xml 파일과 provider.kt파일이 유기적으로 연결이 되어있어야 한다. 

 

 

4. 홈스크린위젯 수신기 추가(AndroidManifest.xml)

android\app\src\main 폴더 안에는 AndroidManifest.xml 파일이 이미 존재하는데,

이 파일에 홈스크린 위젯의 데이터와 함수 등등을 수신해올 수 있는 '수신기'코드를 추가해야 한다.

 

<!-- 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>

<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"/>

이 부분을 추가하면 된다.

 

전체 파일 내용은 다음과같다. 

(하지만 가지고 있는 프로젝트 내용에 따라 조금씩 다를 수 있으니 기왕이면 아래의 내용을 복붙하지말고 그냥 자기가 가진 코드에 위의 코드를 추가하도록하자) 

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="flutter_sample2"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!-- 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>

        <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"/>

        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

 

 

여기까지는 홈스크린위젯 과 관련된 패키지와 파일을 추가 한 내용이다.

이제 main.dart에 가서 카운터 함수를 만들고, 백그라운드에서도 카운터 함수를 실행 가능하도록 해줘야 하며,

레이아웃에도 넣어주어야 한다. 

 

5. 필요에 따라 main.dart 수정

 

5.1 상단에 패키지 import 하기 

import 'package:home_widget/home_widget.dart';

잊지 말자. 우리는 플러터 안에서 코딩하고 있다. 

패키지를 설치해줬다면 임포트 하는 것은 당연한 일이다.

 

5.2 void main 함수 안에 추가

  WidgetsFlutterBinding.ensureInitialized();
  await HomeWidget.registerBackgroundCallback(backgroundCallback);

비동기 함수로 실행해야 하기 때문에 main 함수는 future 안에 넣어줘야 한다. 

 

5.3 백그라운드에서 실행되는 함수 설정

Future<void> backgroundCallback(Uri? uri) async {
  if (uri?.host == 'updatecounter') {
    int _counter = 0;
    await HomeWidget.getWidgetData<int>('_counter', defaultValue: 0)
        .then((int? value) {
      _counter = value ?? 0;
      _counter++;
    });
    await HomeWidget.saveWidgetData<int>('_counter', _counter);
    await HomeWidget.updateWidget(
        name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
  }
}

5.4 앱실행시 구동되는 함수 설정

int _counter = 0;

  @override
  void initState() {
    super.initState();
    HomeWidget.widgetClicked.listen((Uri? uri) => loadData());
    loadData();
  }

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

  Future<void> updateAppWidget() async {
    await HomeWidget.saveWidgetData<int>('_counter', _counter);
    await HomeWidget.updateWidget(
        name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
  }

  void _incrementCounter() {
    setState(() => _counter++);
    updateAppWidget();
  }

 

5.5 레이아웃 설정

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

 

 

main.dart 전체 코드는 아래와 같다.

import 'package:flutter/material.dart';
import 'package:home_widget/home_widget.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await HomeWidget.registerBackgroundCallback(backgroundCallback);
  runApp(MyApp());
}

Future<void> backgroundCallback(Uri? uri) async {
  if (uri?.host == 'updatecounter') {
    int _counter = 0;
    await HomeWidget.getWidgetData<int>('_counter', defaultValue: 0)
        .then((int? value) {
      _counter = value ?? 0;
      _counter++;
    });
    await HomeWidget.saveWidgetData<int>('_counter', _counter);
    await HomeWidget.updateWidget(
        name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    HomeWidget.widgetClicked.listen((Uri? uri) => loadData());
    loadData();
  }

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

  Future<void> updateAppWidget() async {
    await HomeWidget.saveWidgetData<int>('_counter', _counter);
    await HomeWidget.updateWidget(
        name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
  }

  void _incrementCounter() {
    setState(() => _counter++);
    updateAppWidget();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

 

 

 

 

해결해야 할점

1. 백그라운드 실행 함수와 앱실행시 실행되는 함수에 중복되는 코드가 있다. 
2. 홈스크린 위젯을 여러개 삽입하고 싶다면?!

 

 

 

참고링크

https://codelabs.developers.google.com/flutter-home-screen-widgets#2

 

Adding a Home Screen widget to your Flutter App  |  Google Codelabs

In this codelab, you’ll create a Home Screen widget for your iOS or Android Flutter app. You’ll start with a basic Flutter news app. You’ll then use native frameworks to create the UI for the widgets themselves. Finally, you’ll learn how to share r

codelabs.developers.google.com

https://medium.com/@ashishgarg1998/how-to-create-home-screen-app-widgets-in-flutter-ce3458f3638e

 

How to create Home Screen App Widgets in Flutter!

Hey! Welcome to my first article on Medium.

medium.com

https://cafe.naver.com/flutterjames/680

 

Home Screen Widget - 홈 화면 위젯

https://youtu.be/pw3uW6RGV00 #시작 전 프로젝트에서 다루게 될 네이티브(WEB, Android, IOS, Desktop 등) 학습은 늘 병행해주세요...

cafe.naver.com

https://www.youtube.com/watch?v=pw3uW6RGV00