Flutter 앱에 홈 화면 위젯 추가

1. 소개

위젯이란 무엇인가요?

Flutter 개발자에게 위젯의 일반적인 정의는 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에서는 사용자가 기사를 읽을 수 있는 간단한 Flutter 앱용으로 Android와 iOS에서 모두 home_widget 패키지를 사용하여 홈 화면 위젯을 빌드합니다. 위젯은 다음을 수행합니다.

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

a36b7ba379151101.png

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

  • 첫 번째는 광고 제목과 설명이 포함된 뉴스 기사 목록을 표시합니다.
  • 두 번째는 CustomPaint를 사용하여 생성된 차트가 포함된 전체 기사를 표시합니다.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

학습할 내용

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

2. 개발 환경 설정

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

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

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

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

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

시작 코드 가져오기

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

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

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

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

시작 앱 열기

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

패키지 설치

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

$ flutter pub get

3. 기본 홈 화면 위젯 추가

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

기본 iOS 홈 화면 위젯 만들기

Flutter iOS 앱에 앱 확장을 추가하는 것은 SwiftUI 또는 UIKit 앱에 앱 확장을 추가하는 것과 비슷합니다.

  1. Flutter 프로젝트 디렉터리의 터미널 창에서 open ios/Runner.xcworkspace를 실행합니다. 또는 VSCode에서 ios 폴더를 마우스 오른쪽 버튼으로 클릭하고 Xcode에서 열기를 선택합니다. 그러면 Flutter 프로젝트에서 기본 Xcode 작업공간이 열립니다.
  2. 메뉴에서 File → New → Target을 선택합니다. 이렇게 하면 프로젝트에 새 타겟이 추가됩니다.
  3. 템플릿 목록이 표시됩니다. 위젯 확장 프로그램을 선택합니다.
  4. 이 위젯의 제품 이름 상자에 'NewsWidgets'를 입력합니다. 실시간 활동 포함 및 구성 인텐트 포함 체크박스를 모두 선택 해제합니다.

샘플 코드 검사

새 타겟을 추가하면 Xcode에서 선택한 템플릿을 기반으로 샘플 코드를 생성합니다. 생성된 코드와 WidgetKit에 관한 자세한 내용은 Apple의 앱 확장 프로그램 문서 를 참고하세요.

샘플 위젯 디버깅 및 테스트

  1. 먼저 Flutter 앱의 구성을 업데이트합니다. Flutter 앱에 새 패키지를 추가하고 Xcode에서 프로젝트의 타겟을 실행하려는 경우 이 작업을 실행해야 합니다. 앱의 구성을 업데이트하려면 Flutter 앱 디렉터리에서 다음 명령어를 실행합니다.
$ flutter build ios --config-only
  1. 러너를 클릭하여 타겟 목록을 표시합니다. 방금 만든 위젯 타겟(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. 프로젝트가 빌드된 후 왼쪽 상단에서 앱 디렉터리를 찾습니다. 이 디렉터리에 새 홈 화면 위젯을 추가합니다. 디렉터리를 마우스 오른쪽 버튼으로 클릭하고 New -> Widget -> App Widget을 선택합니다.

f19d8b7f95ab884e.png

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

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

  • 클래스 이름 상자를 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에서 타겟의 설정으로 이동합니다. 서명 및 기능 탭에서 팀과 번들 식별자가 설정되어 있는지 확인합니다.

Xcode에서 Runner 타겟과 NewsWidgetExtension 타겟 모두에 App Group을 추가합니다.

+ 기능 -> 앱 그룹을 선택하고 새 앱 그룹을 추가합니다. 러너 (상위 앱) 타겟과 위젯 타겟 모두에 대해 반복합니다.

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'),
      ),
...

이번 변경으로 사용자가 도움말 페이지에서 광고 제목 업데이트 버튼을 누르면 홈 화면 위젯 세부정보가 업데이트됩니다.

기사 데이터를 표시하도록 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)
              }
    }
}

이전 코드의 Provider는 TimelineProvider를 준수합니다. 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 파일을 엽니다. 홈 화면 위젯의 구조와 레이아웃을 정의합니다. 오른쪽 상단에서 코드를 선택하고 해당 파일의 콘텐츠를 다음 코드로 바꿉니다.

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 클래스에는 슈퍼클래스의 메서드 3개가 포함되어 있습니다. 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를 호출하여 홈 화면 위젯에 표시된 텍스트를 변경합니다.

업데이트 테스트

앱을 테스트하여 홈 화면 위젯이 새 데이터로 업데이트되는지 확인합니다. 데이터를 업데이트하려면 도움말 페이지에서 홈 화면 업데이트 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 앱의 그래프를 홈 화면 위젯으로 표시합니다.

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

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를 포함하는 Center입니다.

lib/article_screen.dart

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

사용자가 floatingActionButton을 클릭하면 renderFlutterWidget 메서드가 호출됩니다. 이 메서드는 결과 PNG 파일을 공유 컨테이너 디렉터리에 'screenshot'으로 저장합니다. 이 메서드는 이미지의 전체 경로를 기기 저장소에 파일 이름 키로 저장합니다.

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 구조체에 filename 및 displaySize 추가

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

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

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

placeholder 함수 업데이트

자리표시자 filename 및 displaySize를 포함합니다.

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

getSnapshot에서 userDefaults의 파일 이름 가져오기

이렇게 하면 홈 화면 위젯이 업데이트될 때 userDefaults 저장소의 filename 값이 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의 '위젯을 최신 상태로 유지' 문서 를 참고하세요.

추가 자료