Flutter 앱에 홈 화면 위젯 추가

1. 소개

위젯이란 무엇인가요?

Flutter 개발자의 경우 widget의 일반적인 정의는 Flutter 프레임워크를 사용하여 만든 UI 구성요소를 의미합니다. 이 Codelab의 컨텍스트에서 위젯은 앱을 열지 않고도 앱 정보를 볼 수 있는 미니 버전의 앱을 의미합니다. Android에서는 위젯이 홈 화면에 표시됩니다. iOS에서는 홈 화면, 잠금 화면 또는 오늘 뷰에 추가할 수 있습니다.

f0027e8a7d0237e0.png b991e79ea72c8b65.png

위젯은 얼마나 복잡할 수 있나요?

홈 화면 위젯은 대부분 단순합니다. 기본 텍스트, 간단한 그래픽 또는 Android의 경우 기본 컨트롤을 포함할 수 있습니다. Android와 iOS 모두 개발자가 사용할 수 있는 UI 구성요소 및 기능을 제한합니다.

819b9fffd700e571.png 92d62ccfd17d770d.png

위젯 UI 만들기

이러한 UI 제한으로 인해 Flutter 프레임워크를 사용하여 홈 화면 위젯의 UI를 직접 그릴 수 없습니다. 대신 Jetpack Compose 또는 SwiftUI와 같은 플랫폼 프레임워크로 만든 위젯을 Flutter 앱에 추가할 수 있습니다. 이 Codelab에서는 복잡한 UI를 다시 작성하지 않도록 앱과 위젯 간에 리소스를 공유하는 예를 설명합니다.

빌드할 항목

이 Codelab에서는 사용자가 기사를 읽을 수 있는 home_widget 패키지를 사용하여 간단한 Flutter 앱을 위해 Android와 iOS 모두에서 홈 화면 위젯을 빌드합니다. 위젯이 다음과 같이 작동합니다.

  • Flutter 앱의 데이터를 표시합니다.
  • Flutter 앱에서 공유한 글꼴 애셋을 사용하여 텍스트를 표시합니다.
  • 렌더링된 Flutter 위젯의 이미지를 표시합니다.

a36b7ba379151101.png

이 Flutter 앱에는 두 화면 (또는 경로)이 포함되어 있습니다.

  • 첫 번째에는 헤드라인과 설명이 포함된 뉴스 기사 목록이 표시됩니다.
  • 두 번째 표에는 CustomPaint를 사용하여 만든 차트와 함께 전체 문서가 표시됩니다.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

학습할 내용

  • iOS 및 Android에서 홈 화면 위젯을 만드는 방법
  • home_widget 패키지를 사용하여 홈 화면 위젯과 Flutter 앱 간에 데이터를 공유하는 방법
  • 재작성해야 하는 코드의 양을 줄이는 방법
  • Flutter 앱에서 홈 화면 위젯을 업데이트하는 방법

2. 개발 환경 설정

두 플랫폼 모두 Flutter SDKIDE가 필요합니다. Flutter 작업에 원하는 IDE를 사용할 수 있습니다. Dart 코드 및 Flutter 확장 프로그램이 포함된 Visual Studio Code, Flutter 및 Dart 플러그인이 설치된 Android 스튜디오 또는 IntelliJ일 수 있습니다.

iOS 홈 화면 위젯을 만들려면 다음 단계를 따르세요.

  • 이 Codelab은 실제 iOS 기기 또는 iOS 시뮬레이터에서 실행할 수 있습니다.
  • Xcode IDE로 macOS 시스템을 구성해야 합니다. 이렇게 하면 앱의 iOS 버전을 빌드하는 데 필요한 컴파일러가 설치됩니다.

Android 홈 화면 위젯을 만들려면 다음 단계를 따르세요.

  • 이 Codelab은 실제 Android 기기 또는 Android Emulator에서 실행할 수 있습니다.
  • Android 스튜디오를 사용하여 개발 시스템을 구성해야 합니다. 이렇게 하면 앱의 Android 버전을 빌드하는 데 필요한 컴파일러가 설치됩니다.

시작 코드 가져오기

GitHub에서 프로젝트의 초기 버전 다운로드

명령줄에서 GitHub 저장소를 flutter-codelabs 디렉터리로 클론합니다.

$ git clone https://github.com/flutter/codelabs.git flutter-codelabs

저장소를 클론한 후 flutter-codelabs/homescreen_codelab 디렉터리에서 이 Codelab의 코드를 찾을 수 있습니다. 이 디렉터리에는 Codelab의 각 단계에서 완성된 프로젝트 코드가 포함되어 있습니다.

시작 앱 열기

flutter-codelabs/homescreen_codelab/step_03 디렉터리를 원하는 IDE로 엽니다.

패키지 설치

모든 필수 패키지가 프로젝트의 pubspec.yaml 파일에 추가되었습니다. 프로젝트 종속 항목을 검색하려면 다음 명령어를 실행합니다.

$ flutter pub get

3. 기본 홈 화면 위젯 추가

먼저 네이티브 플랫폼 도구를 사용하여 홈 화면 위젯을 추가합니다.

기본 iOS 홈 화면 위젯 만들기

Flutter iOS 앱에 앱 확장 프로그램을 추가하는 것은 SwiftUI 또는 UIKit 앱에 앱 확장 프로그램을 추가하는 것과 유사합니다.

  1. Flutter 프로젝트 디렉터리의 터미널 창에서 open ios/Runner.xcworkspace를 실행합니다. 또는 VSCode에서 ios 폴더를 마우스 오른쪽 버튼으로 클릭하고 Open in Xcode를 선택합니다. 그러면 Flutter 프로젝트에서 기본 Xcode 작업공간이 열립니다.
  2. 메뉴에서 파일 → 새로 만들기 → 대상을 선택합니다. 그러면 프로젝트에 새 대상이 추가됩니다.
  3. 템플릿 목록이 표시됩니다. 위젯 확장 프로그램을 선택합니다.
  4. 'NewsWidgets'를 입력합니다. 이 위젯의 제품 이름 상자에 입력합니다. Include Live ActivityInclude Configuration Intent 체크박스를 모두 선택 해제합니다.

샘플 코드 검사

새 대상을 추가하면 Xcode가 선택한 템플릿을 기반으로 샘플 코드를 생성합니다. 생성된 코드 및 WidgetKit에 대한 자세한 내용은 Apple의 앱 확장 프로그램 문서 를 통해 개인정보처리방침을 정의할 수 있습니다.

샘플 위젯 디버그 및 테스트

  1. 먼저 Flutter 앱의 구성을 업데이트합니다. Flutter 앱에 새 패키지를 추가하고 Xcode에서 프로젝트의 대상을 실행하려는 경우 이 작업을 실행해야 합니다. 앱 구성을 업데이트하려면 Flutter 앱 디렉터리에서 다음 명령어를 실행합니다.
$ flutter build ios --config-only
  1. Runner(실행기)를 클릭하여 대상 목록을 표시합니다. 방금 만든 위젯 대상인 NewsWidgets를 선택하고 실행을 클릭합니다. iOS 위젯 코드 변경 시 Xcode에서 위젯 대상을 실행합니다.

bbb519df1782881d.png

  1. 시뮬레이터나 기기 화면에 기본 홈 화면 위젯이 표시됩니다. 앱이 표시되지 않는 경우 화면에 추가할 수 있습니다. 홈 화면을 길게 클릭한 다음 왼쪽 상단의 +를 클릭합니다.

18eff1cae152014d.png

  1. 앱 이름을 검색합니다. 이 Codelab에서는 '홈 화면 위젯'을 검색합니다.

a0c00df87615493e.png

  1. 홈 화면 위젯을 추가하면 시간을 제공하는 간단한 텍스트가 표시되어야 합니다.

기본 Android 위젯 만들기

  1. Android에 홈 화면 위젯을 추가하려면 Android 스튜디오에서 프로젝트의 빌드 파일을 엽니다. 이 파일은 android/build.gradle에서 찾을 수 있습니다. 또는 VSCode에서 android 폴더를 마우스 오른쪽 버튼으로 클릭하고 Open in Android Studio를 선택합니다.
  2. 프로젝트가 빌드되면 왼쪽 상단에서 앱 디렉터리를 찾습니다. 이 디렉터리에 새 홈 화면 위젯을 추가합니다. 디렉토리를 마우스 오른쪽 버튼으로 클릭하고 새로 만들기 -> 위젯 -> App Widget에서 사용할 수 있습니다.

f19d8b7f95ab884e.png

  1. Android 스튜디오에 새 양식이 표시됩니다. 클래스 이름, 배치, 크기, 출발어 등 홈 화면 위젯에 관한 기본 정보를 추가합니다.

이 Codelab에서는 다음 값을 설정합니다.

  • Class Name 상자를 NewsWidget에 추가할 수 있습니다.
  • 최소 너비 (셀) 드롭다운을 3으로 설정
  • 최소 높이 (셀) 드롭다운을 3으로 설정

샘플 코드 검사

양식을 제출하면 Android 스튜디오는 여러 파일을 만들고 업데이트합니다. 이 Codelab과 관련된 변경사항은 아래 표에 나와 있습니다.

작업

대상 파일

변경

업데이트

AndroidManifest.xml

NewsWidget을 등록하는 새 수신기를 추가합니다.

만들기

res/layout/news_widget.xml

홈 화면 위젯 UI를 정의합니다.

만들기

res/xml/news_widget_info.xml

홈 화면 위젯 구성을 정의합니다. 이 파일에서 위젯의 크기나 이름을 조정할 수 있습니다.

만들기

java/com/example/homescreen_widgets/NewsWidget.kt

홈 화면 위젯에 기능을 추가하는 Kotlin 코드를 포함합니다.

이 Codelab 전반에서 이러한 파일에 관한 자세한 내용을 확인할 수 있습니다.

샘플 위젯 디버그 및 테스트

이제 애플리케이션을 실행하고 홈 화면 위젯을 확인합니다. 앱을 빌드한 후 Android 기기의 애플리케이션 선택 화면으로 이동하여 이 Flutter 프로젝트의 아이콘을 길게 누릅니다. 팝업 메뉴에서 위젯을 선택합니다.

dff7c9f9f85ef1c7.png

Android 기기나 에뮬레이터에 Android용 기본 홈 화면 위젯이 표시됩니다.

4. Flutter 앱에서 홈 화면 위젯으로 데이터 전송

내가 만든 기본 홈 화면 위젯을 맞춤설정할 수 있습니다. 홈 화면 위젯을 업데이트하여 뉴스 기사의 헤드라인과 요약을 표시합니다. 다음 스크린샷은 헤드라인과 요약을 표시하는 홈 화면 위젯의 예를 보여줍니다.

acb90343a3e51b6d.png

앱과 홈 화면 위젯 간에 데이터를 전달하려면 Dart 네이티브 코드를 작성해야 합니다. 이 섹션에서는 이 프로세스를 세 부분으로 나뉩니다.

  1. Android와 iOS에서 모두 사용할 수 있는 Flutter 앱에서 Dart 코드 작성
  2. 네이티브 iOS 기능 추가
  3. 네이티브 Android 기능 추가

iOS 앱 그룹 사용

iOS 상위 앱과 위젯 확장 프로그램 간에 데이터를 공유하려면 두 타겟 모두 동일한 앱 그룹에 속해야 합니다. 앱 그룹에 대한 자세한 내용은 Apple의 앱 그룹 문서를 참고하세요.

번들 식별자를 업데이트합니다.

Xcode에서 대상 설정으로 이동합니다. Signing & 기능 탭에서 팀 및 번들 식별자가 설정되어 있는지 확인합니다.

Xcode에서 앱 그룹을 실행기 타겟과 NewsWidgetExtension 타겟 모두에 추가합니다.

+ 기능 ->을 선택합니다. 앱 그룹을 클릭하고 새 앱 그룹을 추가합니다. 실행기 (상위 앱) 대상과 위젯 대상 모두에 대해 위 작업을 반복합니다.

135e1a8c4652dac.png

Dart 코드 추가

iOS 및 Android 앱은 몇 가지 방법으로 Flutter 앱과 데이터를 공유할 수 있습니다.이러한 앱과 통신하려면 기기의 로컬 key/value 스토어를 활용합니다. iOS는 이 스토어를 UserDefaults이라고 하고 Android에서는 이 스토어를 SharedPreferences이라고 합니다. home_widget 패키지는 이러한 API를 래핑하여 두 플랫폼에 데이터를 저장하는 작업을 간소화하고 홈 화면 위젯이 업데이트된 데이터를 가져올 수 있도록 합니다.

707ae86f6650ac55.png

광고 제목 및 설명 데이터는 news_data.dart 파일에서 가져옵니다. 이 파일에는 모의 데이터와 NewsArticle 데이터 클래스가 포함되어 있습니다.

lib/news_data.dart

class NewsArticle {
  final String title;
  final String description;
  final String? articleText;

  NewsArticle({
    required this.title,
    required this.description,
    this.articleText = loremIpsum,
  });
}

광고 제목 및 설명 값 업데이트

Flutter 앱에서 홈 화면 위젯을 업데이트하는 기능을 추가하려면 lib/home_screen.dart 파일로 이동합니다. 파일의 내용을 다음 코드로 바꿉니다. 그런 다음 <YOUR APP GROUP>를 앱 그룹의 식별자로 바꿉니다.

lib/home_screen.dart

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

import 'article_screen.dart';
import 'news_data.dart';

// TODO: Replace with your App Group ID
const String appGroupId = '<YOUR APP GROUP>';              // Add from here
const String iOSWidgetName = 'NewsWidgets';
const String androidWidgetName = 'NewsWidget';             // To here.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

void updateHeadline(NewsArticle newHeadline) {             // Add from here
  // Save the headline data to the widget
  HomeWidget.saveWidgetData<String>('headline_title', newHeadline.title);
  HomeWidget.saveWidgetData<String>(
      'headline_description', newHeadline.description);
  HomeWidget.updateWidget(
    iOSName: iOSWidgetName,
    androidName: androidWidgetName,
  );
}                                                          // To here.

class _MyHomePageState extends State<MyHomePage> {

  @override                                                // Add from here
  void initState() {
    super.initState();

    HomeWidget.setAppGroupId(appGroupId);

    // Mock read in some data and update the headline
    final newHeadline = getNewsStories()[0];
    updateHeadline(newHeadline);
  }                                                        // To here.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: const Text('Top Stories'),
            centerTitle: false,
            titleTextStyle: const TextStyle(
                fontSize: 30,
                fontWeight: FontWeight.bold,
                color: Colors.black)),
        body: ListView.separated(
          separatorBuilder: (context, idx) {
            return const Divider();
          },
          itemCount: getNewsStories().length,
          itemBuilder: (context, idx) {
            final article = getNewsStories()[idx];
            return ListTile(
              key: Key('$idx ${article.hashCode}'),
              title: Text(article.title!),
              subtitle: Text(article.description!),
              onTap: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) {
                      return ArticleScreen(article: article);
                    },
                  ),
                );
              },
            );
          },
        ));
  }
}

updateHeadline 함수는 키-값 쌍을 기기의 로컬 저장소에 저장합니다. headline_title 키에는 newHeadline.title의 값이 포함됩니다. headline_description 키에는 newHeadline.description의 값이 포함됩니다. 또한 이 함수는 홈 화면 위젯의 새 데이터를 검색하고 렌더링할 수 있음을 네이티브 플랫폼에 알립니다.

floatingActionButton 수정

다음과 같이 floatingActionButton를 누르면 updateHeadline 함수를 호출합니다.

lib/article_screen.dart

// New: import the updateHeadline function
import 'home_screen.dart';

...

floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
            content: Text('Updating home screen widget...'),
          ));
          // New: call updateHeadline
          updateHeadline(widget.article);
        },
        label: const Text('Update Homescreen'),
      ),
...

이번 변경으로 사용자가 기사 페이지에서 Update Headline 버튼을 누르면 홈 화면 위젯 세부정보가 업데이트됩니다.

기사 데이터를 표시하도록 iOS 코드 업데이트

iOS용 홈 화면 위젯을 업데이트하려면 Xcode를 사용합니다.

Xcode에서 NewsWidgets.swift 파일을 엽니다.

TimelineEntry를 구성합니다.

SimpleEntry 구조체를 다음 코드로 바꿉니다.

ios/NewsWidgets/NewsWidgets.swift

// The date and any data you want to pass into your app must conform to TimelineEntry
struct NewsArticleEntry: TimelineEntry {
    let date: Date
    let title: String
    let description:String
}

NewsArticleEntry 구조체는 업데이트 시 홈 화면 위젯에 전달할 수신 데이터를 정의합니다. TimelineEntry 유형에는 날짜 매개변수가 필요합니다.TimelineEntry 프로토콜에 대한 자세한 내용은 Apple의 TimelineEntry 문서를 확인하세요.

콘텐츠를 표시하는 View 수정

날짜 대신 뉴스 기사 헤드라인과 설명이 표시되도록 홈 화면 위젯을 수정하세요. SwiftUI에 텍스트를 표시하려면 Text 뷰를 사용합니다. SwiftUI에서 뷰를 서로 쌓으려면 VStack 뷰를 사용합니다.

생성된 NewsWidgetEntryView 뷰를 다음 코드로 바꿉니다.

ios/NewsWidgets/NewsWidgets.swift

//View that holds the contents of the widget
struct NewsWidgetsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
      VStack {
        Text(entry.title)
        Text(entry.description)
      }
    }
}

홈 화면 위젯에 업데이트 시기와 방법을 알리도록 제공자 수정

기존 Provider를 다음 코드로 바꿉니다. 그런 다음 <YOUR APP GROUP>을 앱 그룹 식별자로 대체합니다.

ios/NewsWidgets/NewsWidgets.swift

struct Provider: TimelineProvider {

// Placeholder is used as a placeholder when the widget is first displayed
    func placeholder(in context: Context) -> NewsArticleEntry {
//      Add some placeholder title and description, and get the current date
      NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description")
    }

// Snapshot entry represents the current time and state
    func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
      let entry: NewsArticleEntry
      if context.isPreview{
        entry = placeholder(in: context)
      }
      else{
        //      Get the data from the user defaults to display
        let userDefaults = UserDefaults(suiteName: <YOUR APP GROUP>)
        let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
        let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
        entry = NewsArticleEntry(date: Date(), title: title, description: description)
      }
        completion(entry)
    }

//    getTimeline is called for the current and optionally future times to update the widget
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
//      This just uses the snapshot function you defined earlier
      getSnapshot(in: context) { (entry) in
// atEnd policy tells widgetkit to request a new entry after the date has passed
        let timeline = Timeline(entries: [entry], policy: .atEnd)
                  completion(timeline)
              }
    }
}

이전 코드의 ProviderTimelineProvider를 준수합니다. Provider에는 다음과 같은 세 가지 메서드가 있습니다.

  1. placeholder 메서드는 사용자가 처음 홈 화면 위젯을 미리 볼 때 자리표시자 항목을 생성합니다.

45a0f64240c12efe.png

  1. getSnapshot 메서드는 사용자 기본값에서 데이터를 읽고 현재 시간의 항목을 생성합니다.
  2. getTimeline 메서드는 타임라인 항목을 반환합니다. 이는 콘텐츠를 업데이트할 시기가 예측되었을 때 도움이 됩니다. 이 Codelab에서는 getSnapshot 함수를 사용하여 현재 상태를 가져옵니다. .atEnd 메서드는 현재 시간이 지나면 데이터를 새로고침하도록 홈 화면 위젯에 지시합니다.

NewsWidgets_Previews 주석 처리

미리보기 사용은 이 Codelab에서 다루지 않습니다. SwiftUI 홈 화면 위젯 미리보기에 관한 자세한 내용은 Apple의 위젯 디버깅 문서를 참고하세요.

모든 파일을 저장하고 앱 및 위젯 타겟을 다시 실행합니다.

타겟을 다시 실행하여 앱과 홈 화면 위젯이 작동하는지 확인합니다.

  1. Xcode에서 앱 스키마를 선택하여 앱 타겟을 실행합니다.
  2. Xcode에서 확장 프로그램 스키마를 선택하여 확장 프로그램 타겟을 실행합니다.
  3. 앱에서 기사 페이지로 이동합니다.
  4. 버튼을 클릭하여 광고 제목을 업데이트합니다. 홈 화면 위젯도 헤드라인을 업데이트합니다.

Android 코드 업데이트

홈 화면 위젯 XML 추가

Android 스튜디오에서 이전 단계에서 생성된 파일을 업데이트합니다.res/layout/news_widget.xml 파일을 엽니다. 홈 화면 위젯의 구조와 레이아웃을 정의합니다. 오른쪽 상단의 Code(코드)를 선택하고 이 파일의 콘텐츠를 다음 코드로 교체합니다.

android/app/res/layout/news_widget.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/widget_container"
   style="@style/Widget.Android.AppWidget.Container"
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:background="@android:color/white"
   android:theme="@style/Theme.Android.AppWidgetContainer">
   
   <TextView
       android:id="@+id/headline_title"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="20sp"
       android:textStyle="bold" />

   <TextView
       android:id="@+id/headline_description"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_below="@+id/headline_title"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:layout_marginTop="4dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="16sp" />

</RelativeLayout>

이 XML은 두 개의 텍스트 뷰를 정의합니다. 하나는 기사 제목용이고 다른 하나는 기사 설명용입니다. 이러한 텍스트 뷰는 스타일도 정의합니다. 이 Codelab 전반에 걸쳐 이 파일로 돌아옵니다.

NewsWidget 기능 업데이트

NewsWidget.kt Kotlin 소스 코드 파일을 엽니다. 이 파일에는 AppWidgetProvider 클래스를 확장하는 NewsWidget라는 생성된 클래스가 포함되어 있습니다.

NewsWidget 클래스에는 슈퍼클래스의 메서드 세 개가 포함되어 있습니다. onUpdate 메서드를 수정합니다. Android는 고정된 간격으로 위젯에 이 메서드를 호출합니다.

NewsWidget.kt 파일의 콘텐츠를 다음 코드로 바꿉니다.

android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt

// Import will depend on App ID.
package com.mydomain.homescreen_widgets

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews

// New import.
import es.antonborri.home_widget.HomeWidgetPlugin


/**
 * Implementation of App Widget functionality.
 */
class NewsWidget : AppWidgetProvider() {
    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray,
    ) {
        for (appWidgetId in appWidgetIds) {
            // Get reference to SharedPreferences
            val widgetData = HomeWidgetPlugin.getData(context)
            val views = RemoteViews(context.packageName, R.layout.news_widget).apply {

                val title = widgetData.getString("headline_title", null)
                setTextViewText(R.id.headline_title, title ?: "No title set")

                val description = widgetData.getString("headline_description", null)
                setTextViewText(R.id.headline_description, description ?: "No description set")
            }

            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

이제 onUpdate가 호출되면 Android는 the widgetData.getString() 메서드를 사용하여 로컬 저장소에서 최신 값을 가져온 다음 setTextViewText를 호출하여 홈 화면 위젯에 표시되는 텍스트를 변경합니다.

업데이트 테스트

앱을 테스트하여 홈 화면 위젯이 새 데이터로 업데이트되는지 확인합니다. 데이터를 업데이트하려면 기사 페이지의 Update Home Screen FloatingActionButton을 사용합니다. 홈 화면 위젯이 기사 제목으로 업데이트됩니다.

5ce1c9914b43ad79.png

5. iOS 홈 화면 위젯에서 Flutter 앱 맞춤 글꼴 사용

지금까지 Flutter 앱이 제공하는 데이터를 읽도록 홈 화면 위젯을 구성했습니다. Flutter 앱에는 홈 화면 위젯에 사용할 수 있는 맞춤 글꼴이 포함되어 있습니다. iOS 홈 화면 위젯에서 맞춤 글꼴을 사용할 수 있습니다. Android에서는 홈 화면 위젯에서 맞춤 글꼴을 사용할 수 없습니다.

iOS 코드 업데이트

Flutter는 iOS 애플리케이션의 mainBundle에 애셋을 저장합니다. 홈 화면 위젯 코드에서 이 번들의 애셋에 액세스할 수 있습니다.

NewsWidgets.swift 파일의 NewsWidgetsEntryView 구조체에서 다음과 같이 변경합니다.

Flutter 애셋 디렉터리 경로를 가져오는 도우미 함수를 만듭니다.

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...

   // New: Add the helper function.
   var bundle: URL {
           let bundle = Bundle.main
           if bundle.bundleURL.pathExtension == "appex" {
               // Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
               var url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
               url.append(component: "Frameworks/App.framework/flutter_assets")
               return url
           }
           return bundle.bundleURL
       }
   ...
}

URL을 사용하여 맞춤 글꼴 파일에 글꼴을 등록합니다.

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...

   // New: Register the font.
   init(entry: Provider.Entry){
     self.entry = entry
     CTFontManagerRegisterFontsForURL(bundle.appending(path: "/fonts/Chewy-Regular.ttf") as CFURL, CTFontManagerScope.process, nil)
   }
   ...
}

맞춤 글꼴을 사용하도록 제목 텍스트 뷰를 업데이트합니다.

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...


   var body: some View {
    VStack {
      // Update the following line.
      Text(entry.title).font(Font.custom("Chewy", size: 13))
      Text(entry.description)
    }
   }
   ...
}

이제 홈 화면 위젯을 실행하면 다음 이미지와 같이 헤드라인의 맞춤 글꼴이 사용됩니다.

93f8b9d767aacfb2.png

6. Flutter 위젯을 이미지로 렌더링

이 섹션에서는 Flutter 앱의 그래프를 홈 화면 위젯으로 표시합니다.

이 위젯은 홈 화면에 표시한 텍스트보다 더 큰 도전과제를 제공합니다. Flutter 차트는 네이티브 UI 구성요소를 사용하여 다시 만드는 것보다 이미지로 표시하는 것이 훨씬 쉽습니다.

Flutter 차트를 PNG 파일로 렌더링하도록 홈 화면 위젯을 코딩합니다. 홈 화면 위젯에 해당 이미지가 표시될 수 있습니다.

Dart 코드 작성

Dart 측에서 home_widget 패키지의 renderFlutterWidget 메서드를 추가합니다. 이 메서드는 위젯, 파일 이름 및 키를 사용합니다. Flutter 위젯의 이미지를 반환하여 공유 컨테이너에 저장합니다. 코드에 이미지 이름을 제공하고 홈 화면 위젯이 컨테이너에 액세스할 수 있는지 확인합니다. key는 전체 파일 경로를 기기의 로컬 저장소에 문자열로 저장합니다. 이렇게 하면 Dart 코드에서 이름이 변경되면 홈 화면 위젯이 파일을 찾을 수 있습니다.

이 Codelab에서는 lib/article_screen.dart 파일의 LineChart 클래스가 차트를 나타냅니다. 빌드 메서드는 이 차트를 화면에 그리는 CustomPainter를 반환합니다.

이 기능을 구현하려면 lib/article_screen.dart 파일을 엽니다. home_widget 패키지를 가져옵니다. 다음으로 _ArticleScreenState 클래스의 코드를 다음 코드로 바꿉니다.

lib/article_screen.dart

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

import 'home_screen.dart';
import 'news_data.dart';

...

class _ArticleScreenState extends State<ArticleScreen> {
  // New: add this GlobalKey
  final _globalKey = GlobalKey();
  String? imagePath;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.article.title!),
      ),
      // New: add this FloatingActionButton
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () async {
          if (_globalKey.currentContext != null) {
            var path = await HomeWidget.renderFlutterWidget(
              const LineChart(),
              fileName: 'screenshot',
              key: 'filename',
              logicalSize: _globalKey.currentContext!.size,
              pixelRatio:
                  MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
            );
            setState(() {
              imagePath = path as String?;
            });
          }
          updateHeadline(widget.article);
        },
        label: const Text('Update Homescreen'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16.0),
        children: [
          Text(
            widget.article.description!,
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 20.0),
          Text(widget.article.articleText!),
          const SizedBox(height: 20.0),
          Center(
            // New: Add this key
            key: _globalKey,
            child: const LineChart(),
          ),
          const SizedBox(height: 20.0),
          Text(widget.article.articleText!),
        ],
      ),
    );
  }
}

이 예에서는 _ArticleScreenState 클래스에 세 가지 변경사항을 적용합니다.

GlobalKey 만들기

GlobalKey는 특정 위젯의 컨텍스트를 가져오며 이는 해당 위젯의 크기를 가져오는 데 필요합니다 .

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   // New: add this GlobalKey
   final _globalKey = GlobalKey();
   ...
}

imagePath 추가

imagePath 속성은 Flutter 위젯이 렌더링되는 이미지의 위치를 저장합니다.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   // New: add this imagePath
   String? imagePath;
   ...
}

렌더링할 키를 위젯에 추가

_globalKey에는 이미지에 렌더링되는 Flutter 위젯이 포함되어 있습니다. 이 경우 Flutter 위젯은 LineChart가 포함된 센터입니다.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   Center(
      // New: Add this key
 key: _globalKey,
 child: const LineChart(),
   ),
   ...
}
  1. 위젯을 이미지로 저장

renderFlutterWidget 메서드는 사용자가 floatingActionButton를 클릭할 때 호출됩니다. 이 메서드는 결과로 생성된 PNG 파일을 '스크린샷'으로 저장합니다. 공유 컨테이너 디렉터리로 복사됩니다 또한 이 메서드는 이미지의 전체 경로를 기기 저장소에 파일 이름 키로 저장합니다.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   floatingActionButton: FloatingActionButton.extended(
 onPressed: () async {
   if (_globalKey.currentContext != null) {
     var path = await HomeWidget.renderFlutterWidget(
       LineChart(),
       fileName: 'screenshot',
       key: 'filename',
       logicalSize: _globalKey.currentContext!.size,
       pixelRatio:
         MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
     );
     setState(() {
        imagePath = path as String?;
     });
    }
  updateHeadline(widget.article);
  },
   ...
}

iOS 코드 업데이트

iOS의 경우 코드를 업데이트하여 저장소에서 파일 경로를 가져오고 SwiftUI를 사용하여 파일을 이미지로 표시합니다.

NewsWidgets.swift 파일을 열어 다음과 같이 변경합니다.

NewsArticleEntry 구조체filenamedisplaySize 추가

filename 속성에는 이미지 파일의 경로를 나타내는 문자열이 포함됩니다. displaySize 속성은 사용자 기기의 홈 화면 위젯 크기를 보유합니다. 홈 화면 위젯의 크기는 context에서 가져옵니다.

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

   // New: add the filename and displaySize.
   let filename: String
   let displaySize: CGSize
}

placeholder 함수 업데이트

자리표시자 filenamedisplaySize를 포함합니다.

ios/NewsWidgets/NewsWidgets.swift

func placeholder(in context: Context) -> NewsArticleEntry {
      NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description", filename: "No screenshot available",  displaySize: context.displaySize)
    }

getSnapshotuserDefaults에서 파일 이름을 가져옵니다.

이렇게 하면 홈 화면 위젯이 업데이트될 때 filename 변수가 userDefaults 저장소의 filename 값으로 설정됩니다.

ios/NewsWidgets/NewsWidgets.swift

func getSnapshot(
   ...

   let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
   let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
   // New: get fileName from key/value store
   let filename = userDefaults?.string(forKey: "filename") ?? "No screenshot available"
   ...
)

경로의 이미지를 표시하는 ChartImage를 만듭니다.

ChartImage 뷰는 Dart 측에서 생성된 파일의 콘텐츠로 이미지를 생성합니다. 여기에서는 크기를 프레임의 50% 로 설정합니다.

ios/NewsWidgets/NewsWidgets.swift

struct NewsWidgetsEntryView : View {
   ...

   // New: create the ChartImage view
   var ChartImage: some View {
        if let uiImage = UIImage(contentsOfFile: entry.filename) {
            let image = Image(uiImage: uiImage)
                .resizable()
                .frame(width: entry.displaySize.height*0.5, height: entry.displaySize.height*0.5, alignment: .center)
            return AnyView(image)
        }
        print("The image file could not be loaded")
        return AnyView(EmptyView())
    }
   ...
}

NewsWidgetsEntryView의 본문에 ChartImage 사용

NewsWidgetsEntryView의 본문에 ChartImage 뷰를 추가하여 홈 화면 위젯에 ChartImage를 표시합니다.

ios/NewsWidgets/NewsWidgets.swift

VStack {
   Text(entry.title).font(Font.custom("Chewy", size: 13))
   Text(entry.description).font(.system(size: 12)).padding(10)
   // New: add the ChartImage to the NewsWidgetEntryView
   ChartImage
}

변경사항 테스트

변경사항을 테스트하려면 Xcode에서 Flutter 앱 (Runner) 타겟과 확장 프로그램 타겟을 모두 다시 실행합니다. 이미지를 보려면 앱에서 기사 페이지 중 하나로 이동한 다음 버튼을 눌러 홈 화면 위젯을 업데이트하세요.

33bdfe2cce908c48.png

Android 코드 업데이트

Android 코드는 iOS 코드와 동일하게 작동합니다.

  1. android/app/res/layout/news_widget.xml 파일을 엽니다. 여기에는 홈 화면 위젯의 UI 요소가 포함되어 있습니다. 파일 내용을 다음 코드로 바꿉니다.

android/app/res/layout/news_widget.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/widget_container"
   style="@style/Widget.Android.AppWidget.Container"
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:background="@android:color/white"
   android:theme="@style/Theme.Android.AppWidgetContainer">

   <TextView
       android:id="@+id/headline_title"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="20sp"
       android:textStyle="bold" />

   <TextView
       android:id="@+id/headline_description"
       style="@style/Widget.Android.AppWidget.InnerView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_below="@+id/headline_title"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:layout_marginTop="4dp"
       android:background="@android:color/white"
       android:text="Title"
       android:textSize="16sp" />
   
   <!--New: add this image view -->
   <ImageView
       android:id="@+id/widget_image"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:layout_below="@+id/headline_description"
       android:layout_alignBottom="@+id/headline_title"
       android:layout_alignParentStart="true"
       android:layout_alignParentLeft="true"
       android:layout_marginStart="8dp"
       android:layout_marginLeft="8dp"
       android:layout_marginTop="6dp"
       android:layout_marginBottom="-134dp"
       android:layout_weight="1"
       android:adjustViewBounds="true"
       android:background="@android:color/white"
       android:scaleType="fitCenter"
       android:src="@android:drawable/star_big_on"
       android:visibility="visible"
       tools:visibility="visible" />

</RelativeLayout>

이 새 코드는 현재 홈 화면 위젯에 이미지를 추가합니다. 위젯에는 현재 일반 별표 아이콘이 표시됩니다. 이 별표 아이콘을 Dart 코드에 저장한 이미지로 바꿉니다.

  1. NewsWidget.kt 파일을 엽니다. 콘텐츠를 다음 코드로 바꿉니다.

android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt

// Import will depend on App ID.
package com.mydomain.homescreen_widgets

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.RemoteViews
import java.io.File
import es.antonborri.home_widget.HomeWidgetPlugin


/**
 * Implementation of App Widget functionality.
 */
class NewsWidget : AppWidgetProvider() {
    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray,
    ) {
        for (appWidgetId in appWidgetIds) {
            val widgetData = HomeWidgetPlugin.getData(context)
            val views = RemoteViews(context.packageName, R.layout.news_widget).apply {

                val title = widgetData.getString("headline_title", null)
                setTextViewText(R.id.headline_title, title ?: "No title set")

                val description = widgetData.getString("headline_description", null)
                setTextViewText(R.id.headline_description, description ?: "No description set")

                // New: Add the section below
               // Get chart image and put it in the widget, if it exists
                val imageName = widgetData.getString("filename", null)
                val imageFile = File(imageName)
                val imageExists = imageFile.exists()
                if (imageExists) {
                    val myBitmap: Bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
                    setImageViewBitmap(R.id.widget_image, myBitmap)
                } else {
                    println("image not found!, looked @: ${imageName}")
                }
                // End new code
            }

            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

이 Dart 코드는 filename 키를 사용하여 로컬 저장소에 스크린샷을 저장합니다. 또한 이미지의 전체 경로를 가져오고 여기서 File 객체를 만듭니다. 이미지가 있으면 Dart 코드가 홈 화면 위젯의 이미지를 새 이미지로 바꿉니다.

  1. 앱을 새로고침하고 기사 화면으로 이동합니다. 홈 화면 업데이트를 누릅니다. 홈 화면 위젯에 차트가 표시됩니다.

7. 다음 단계

축하합니다.

축하합니다. Flutter iOS 및 Android 앱용 홈 화면 위젯을 성공적으로 만들었습니다.

Flutter 앱의 콘텐츠에 연결

사용자가 클릭하는 위치에 따라 사용자를 앱의 특정 페이지로 유도할 수 있습니다. 예를 들어 이 Codelab의 뉴스 앱에서는 표시된 헤드라인의 뉴스 기사를 사용자에게 표시할 수 있습니다.

이 기능은 이 Codelab에서 다루지 않습니다. home_widget 패키지가 제공하는 스트림을 사용하여 홈 화면 위젯에서 앱 실행을 식별하고 URL을 통해 홈 화면 위젯에서 메시지를 보내는 예를 확인할 수 있습니다. 자세한 내용은 docs.flutter.dev의 딥 링크 문서 를 참고하세요.

백그라운드에서 위젯 업데이트

이 Codelab에서는 버튼을 사용하여 홈 화면 위젯의 업데이트를 트리거했습니다. 이는 테스트에는 합리적이지만 프로덕션 코드에서는 앱이 백그라운드에서 홈 화면 위젯을 업데이트하도록 할 수 있습니다. Workmanager 플러그인을 사용하여 백그라운드 작업을 만들어 홈 화면 위젯에 필요한 리소스를 업데이트할 수 있습니다. 자세한 내용은 home_widget 패키지의 백그라운드 업데이트 섹션을 참고하세요.

iOS의 경우 홈 화면 위젯이 UI 업데이트를 위해 네트워크 요청을 하도록 할 수도 있습니다. 요청의 조건이나 빈도를 제어하려면 타임라인을 사용하세요. 타임라인 사용에 관한 자세한 내용은 Apple의 '위젯을 최신 상태로 유지하기'를 참고하세요. 문서를 참조하세요. 를 통해 개인정보처리방침을 정의할 수 있습니다.

추가 자료