Dodawanie widżetu ekranu głównego do aplikacji Flutter

1. Wprowadzenie

Co to są widżety?

W przypadku programistów Flutter powszechna definicja widżetu odnosi się do komponentów interfejsu utworzonych przy użyciu platformy Flutter. W kontekście tego ćwiczenia w Codelabs widżet to miniaturowa wersja aplikacji, która udostępnia informacje o aplikacji bez jej otwierania. Na Androidzie widżety znajdują się na ekranie głównym. W iOS można je dodawać na ekranie głównym, ekranie blokady lub w widoku Dzisiaj.

f0027e8a7d0237e0.png b991e79ea72c8b65.png

Jak złożony może być widżet?

Większość widżetów na ekranie głównym jest prosta. Mogą zawierać podstawowy tekst, prostą grafikę, a w Androidzie – podstawowe elementy sterujące. Zarówno Android, jak i iOS, ograniczają liczbę komponentów i funkcji interfejsu, z których możesz korzystać.

819b9fffd700e571.png 92d62ccfd17d770d.png

Tworzenie interfejsu widżetów

Z powodu tych ograniczeń interfejsu Flutter nie umożliwia bezpośredniego rysowania interfejsu widżetu na ekranie głównym. Zamiast tego możesz dodać do swojej aplikacji Flutter widżety utworzone za pomocą platform takich jak Jetpack Compose czy SwiftUI. W tym ćwiczeniu w Codelabs znajdziesz przykłady udostępniania zasobów między aplikacją a widżetami, aby uniknąć przepisywania złożonego interfejsu.

Co utworzysz

W ramach tego ćwiczenia w programie utworzysz widżety ekranu głównego na Androida i iOS w prostej aplikacji Flutter, korzystając z pakietu home_Widget, który umożliwi użytkownikom czytanie artykułów. Twoje widżety będą:

  • Pokazuj dane z aplikacji Flutter.
  • Wyświetlaj tekst, korzystając z zasobów czcionek udostępnionych z aplikacji Flutter.
  • Wyświetla obraz wyrenderowanego widżetu Flutter.

a36b7ba379151101.png

Ta aplikacja Flutter ma 2 ekrany (czyli trasy):

  • Pierwsza zawiera listę artykułów informacyjnych z nagłówkami i opisami.
  • Drugi zawiera pełny artykuł z wykresem utworzonym za pomocą funkcji CustomPaint.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

Czego się nauczysz

  • Jak tworzyć widżety ekranu głównego na urządzeniach z iOS i Androidem
  • Jak używać pakietu home_Widget do udostępniania danych między widżetem ekranu głównego a aplikacją Flutter.
  • Jak zmniejszyć ilość kodu potrzebną do przepisywania.
  • Jak zaktualizować widżet ekranu głównego w aplikacji Flutter.

2. Konfigurowanie środowiska programistycznego

W przypadku obu platform potrzebujesz pakietu SDK Flutter i IDE. Do pracy z Flutter możesz używać preferowanego IDE. Może to być Visual Studio Code z rozszerzeniami Dart Code i Flutter albo Android Studio lub IntelliJ z zainstalowanymi wtyczkami Flutter i Dart.

Aby utworzyć widżet ekranu głównego iOS:

  • Możesz uruchomić te ćwiczenia w Codelabs na fizycznym urządzeniu z iOS lub w symulatorze iOS.
  • Musisz skonfigurować system macOS przy użyciu Xcode IDE. Spowoduje to zainstalowanie kompilatora niezbędnego do utworzenia wersji aplikacji na iOS.

Aby utworzyć widżet ekranu głównego na urządzeniu z Androidem:

  • Możesz uruchomić to ćwiczenia w Codelabs na fizycznym urządzeniu z Androidem lub w emulatorze Androida.
  • Musisz skonfigurować system programistyczny pod kątem Android Studio. Spowoduje to zainstalowanie kompilatora niezbędnego do utworzenia wersji aplikacji na Androida.

Pobierz kod startowy

Pobierz początkową wersję projektu z GitHuba

Przy użyciu wiersza poleceń skopiuj repozytorium GitHub do katalogu flutter-codelabs:

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

Po sklonowaniu repozytorium znajdziesz kod ćwiczenia z programowania w katalogu flutter-codelabs/homescreen_codelab. Ten katalog zawiera kod ukończonego projektu dla każdego kroku ćwiczenia w Codelabs.

Otwórz aplikację startową

Otwórz katalog flutter-codelabs/homescreen_codelab/step_03 w preferowanym IDE.

Instalowanie pakietów

Wszystkie wymagane pakiety zostały dodane do pliku pubspec.yaml projektu. Aby pobrać zależności projektu, uruchom to polecenie:

$ flutter pub get

3. Dodawanie podstawowego widżetu do ekranu głównego

Najpierw dodaj widżet do ekranu głównego za pomocą narzędzi platformy natywnej.

Tworzenie podstawowego widżetu na ekranie głównym iOS

Dodawanie rozszerzenia do aplikacji Flutter na iOS jest podobne do dodawania rozszerzenia do aplikacji SwiftUI lub UIKit:

  1. Uruchom open ios/Runner.xcworkspace w oknie terminala z katalogu projektu Flutter. Możesz też kliknąć prawym przyciskiem myszy folder ios w VSCode i wybrać Otwórz w Xcode. Otworzy się domyślny obszar roboczy Xcode w projekcie Flutter.
  2. W menu wybierz Plik → Nowy → Cel. Spowoduje to dodanie nowego miejsca docelowego do projektu.
  3. Pojawi się lista szablonów. Wybierz Rozszerzenie widżetu.
  4. Wpisz „NewsWidgets” w polu Product Name (Nazwa produktu) dla tego widżetu. Odznacz pola wyboru Uwzględnij aktywność na żywo i Uwzględnij intencję konfiguracji.

Sprawdzanie przykładowego kodu

Gdy dodasz nowe środowisko docelowe, Xcode wygeneruje przykładowy kod na podstawie wybranego przez Ciebie szablonu. Więcej informacji o wygenerowanym kodzie i narzędziu WidgetKit znajdziesz w dokumentacji rozszerzeń aplikacji Apple

Debugowanie i testowanie przykładowego widżetu

  1. Najpierw zaktualizuj konfigurację aplikacji Flutter. Musisz to zrobić podczas dodawania nowych pakietów w aplikacji Flutter i planujesz uruchomienie miejsca docelowego w projekcie z Xcode. Aby zaktualizować konfigurację aplikacji, uruchom to polecenie w katalogu aplikacji Flutter:
$ flutter build ios --config-only
  1. Kliknij Uruchom, aby wyświetlić listę celów. Wybierz nowo utworzony cel widżetu – NewsWidgets i kliknij Uruchom. Po zmianie kodu widżetu iOS uruchom miejsce docelowe widżetu z Xcode.

bbb519df1782881d.png

  1. Na ekranie symulatora lub urządzenia powinien być widoczny podstawowy widżet ekranu głównego. Jeśli go nie widzisz, możesz go dodać do ekranu. Kliknij i przytrzymaj ekran główny, a następnie kliknij + w lewym górnym rogu.

18eff1cae152014d.png

  1. Wyszukaj nazwę aplikacji. Na potrzeby tego ćwiczenia w programie wyszukaj „Widżety na ekranie głównym”

a0c00df87615493e.png

  1. Po dodaniu widżetu do ekranu głównego powinien się wyświetlić zwykły tekst z godziną.

Tworzenie podstawowego widżetu na Androida

  1. Aby dodać widżet na ekranie głównym w Androidzie, otwórz plik kompilacji projektu w Android Studio. Znajdziesz go pod adresem android/build.gradle. Możesz też kliknąć prawym przyciskiem myszy folder android w VSCode i wybrać Otwórz w Android Studio.
  2. Po skompilowaniu projektu znajdź katalog aplikacji w lewym górnym rogu. Dodaj do tego katalogu nowy widżet ekranu głównego. Kliknij katalog prawym przyciskiem myszy i wybierz opcję New (Nowy) -> Widżet -> Widżet aplikacji.

f19d8b7f95ab884e.png

  1. Android Studio wyświetli nowy formularz. Dodaj podstawowe informacje o widżecie ekranu głównego, w tym nazwę zajęć, miejsce docelowe, rozmiar i język źródłowy.

W tym ćwiczeniu z programowania ustaw te wartości:

  • pole Nazwa klasy w widżecie NewsWidget,
  • Menu Minimalna szerokość (komórka) do 3
  • Menu Minimalna wysokość (komórki) do 3

Sprawdzanie przykładowego kodu

Gdy prześlesz formularz, Android Studio utworzy i zaktualizuje kilka plików. Zmiany, które pojawią się w tym ćwiczeniach z programowania, znajdziesz w tabeli poniżej

Działanie

Plik docelowy

Zmień

Aktualizuj

AndroidManifest.xml

Dodaje nowego odbiorcę, który rejestruje widżet NewsWidget.

Utwórz

res/layout/news_widget.xml

Definiuje interfejs widżetu ekranu głównego.

Utwórz

res/xml/news_widget_info.xml

Określa konfigurację widżetu ekranu głównego. W tym pliku możesz dostosować wymiary lub nazwę widżetu.

Utwórz

java/com/example/homescreen_widgets/NewsWidget.kt

Zawiera kod Kotlin, który dodaje funkcje do widżetu na ekranie głównym.

Więcej informacji o tych plikach znajdziesz w tym ćwiczeniu z programowania.

Debugowanie i testowanie przykładowego widżetu

Teraz uruchom aplikację, aby zobaczyć widżet ekranu głównego. Po skompilowaniu aplikacji przejdź do ekranu wyboru aplikacji na urządzeniu z Androidem i przytrzymaj ikonę tego projektu Flutter. Wybierz Widżety z menu.

dff7c9f9f85ef1c7.png

Urządzenie z Androidem lub emulator wyświetla domyślny widżet ekranu głównego dla Androida.

4. Wysyłaj dane z aplikacji Flutter do widżetu na ekranie głównym

Możesz dostosować utworzony przez siebie podstawowy widżet ekranu głównego. Zaktualizuj widżet ekranu głównego tak, aby wyświetlał nagłówek i podsumowanie artykułu z wiadomościami. Poniższy zrzut ekranu przedstawia przykładowy widżet ekranu głównego z nagłówkiem i podsumowaniem.

acb90343a3e51b6d.png

Aby przekazywać dane między aplikacją a widżetem ekranu głównego, musisz napisać kod Dart oraz kod natywny. W tej sekcji proces dzieli się na 3 części:

  1. Pisanie kodu DART w aplikacji Flutter, którego można używać zarówno na Androida, jak i iOS
  2. Dodawanie natywnych funkcji systemu iOS
  3. Dodawanie natywnych funkcji Androida

Korzystanie z grup aplikacji na iOS

Aby można było udostępniać dane między aplikacją nadrzędną na iOS a rozszerzeniem widżetu, oba elementy docelowe muszą należeć do tej samej grupy aplikacji. Więcej informacji o grupach aplikacji znajdziesz w dokumentacji Apple dotyczącej grup aplikacji.

Zaktualizuj identyfikator pakietu:

W Xcode otwórz ustawienia celu. W sekcji Capabilities (Możliwości), sprawdź, czy skonfigurowano identyfikator zespołu i pakietu.

Dodaj grupę aplikacji zarówno do środowiska docelowego Runner, jak i do celu NewsWidgetExtension w Xcode:

Wybierz + Uprawnienia -> grupy aplikacji i dodaj nową. Powtórz te czynności w przypadku środowiska docelowego Runner (aplikacji nadrzędnej) oraz docelowego widżetu.

135e1a8c4652dac.png

Dodawanie kodu Dart

Zarówno aplikacje na iOS, jak i na Androida, mogą udostępniać dane aplikacji Flutter na kilka różnych sposobów.Aby komunikować się z tymi aplikacjami, użyj lokalnego sklepu key/value na urządzeniu. iOS nazywa ten sklep UserDefaults, a na Androidzie – SharedPreferences. Pakiet home_Widget zawiera te interfejsy API, aby uprościć zapisywanie danych na każdej z tych platform i umożliwić widżetom ekranu głównego pobieranie zaktualizowanych danych.

707ae86f6650ac55.png

Nagłówek i tekst reklamy pochodzą z pliku news_data.dart. Ten plik zawiera przykładowe dane i klasę danych 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,
  });
}

Aktualizowanie wartości nagłówka i tekstu reklamy

Aby dodać funkcję aktualizowania widżetu ekranu głównego z aplikacji Flutter, przejdź do pliku lib/home_screen.dart. Zastąp zawartość pliku poniższym kodem. Następnie zastąp <YOUR APP GROUP> identyfikatorem grupy aplikacji.

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

Funkcja updateHeadline zapisuje pary klucz-wartość w pamięci lokalnej urządzenia. Klucz headline_title zawiera wartość newHeadline.title. Klucz headline_description zawiera wartość newHeadline.description. Funkcja powiadamia też natywną platformę, że można pobrać i renderować nowe dane dla widżetów na ekranie głównym.

Modyfikowanie obiektu FloatActionButton

Wywołaj funkcję updateHeadline po naciśnięciu klawisza floatingActionButton w następujący sposób:

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

Dzięki tej zmianie, gdy użytkownik naciśnie przycisk Zaktualizuj nagłówek na stronie z artykułem, dane widżetu na ekranie głównym zostaną zaktualizowane.

Zaktualizuj kod iOS, aby wyświetlić dane artykułu

Aby zaktualizować widżet ekranu głównego na iOS, użyj Xcode.

Otwórz plik NewsWidgets.swift w Xcode:

Skonfiguruj TimelineEntry.

Zastąp strukturę SimpleEntry tym kodem:

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
}

Ta struktura NewsArticleEntry definiuje przychodzące dane, które są przekazywane do widżetu na ekranie głównym po aktualizacji. Typ TimelineEntry wymaga parametru daty.Aby dowiedzieć się więcej o protokole TimelineEntry, zapoznaj się z dokumentacją firmy Apple BillingEntry.

Edytuj pole View wyświetlające treść

Zmodyfikuj widżet ekranu głównego tak, aby zamiast daty wyświetlał się nagłówek i opis artykułu z wiadomościami. Aby wyświetlić tekst w interfejsie SwiftUI, użyj widoku Text. Aby nakładać widoki na siebie w SwiftUI, użyj widoku VStack.

Zastąp wygenerowany widok NewsWidgetEntryView tym kodem:

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

Edytuj dostawcę, aby informować widżet na ekranie głównym o czasie i sposobie aktualizacji

Zastąp obecny kod Provider tym kodem. Następnie zastąp identyfikator grupy aplikacji ciągiem <TWOJA GRUPA APLIKACJI>:

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

Element Provider w poprzednim kodzie jest zgodny z TimelineProvider. Provider udostępnia 3 różne metody:

  1. Metoda placeholder generuje wpis zastępczy, gdy użytkownik po raz pierwszy wyświetli podgląd widżetu na ekranie głównym.

45a0f64240c12efe.png

  1. Metoda getSnapshot odczytuje dane z wartości domyślnych użytkownika i generuje wpis z bieżącą godziną.
  2. Metoda getTimeline zwraca wpisy osi czasu. Jest to przydatne, gdy w odpowiednim momencie masz zaplanowaną aktualizację. To ćwiczenie w Codelabs wykorzystuje funkcję getSnapshot do pobrania bieżącego stanu. Metoda .atEnd informuje widżet ekranu głównego, że ma odświeżyć dane po upływie bieżącego czasu.

Skomentuj: NewsWidgets_Previews

W tym ćwiczeniu z programowania korzystanie z podglądów nie jest możliwe. Więcej informacji o wyświetlaniu podglądu widżetów na ekranie głównym SwiftUI znajdziesz w dokumentacji Apple dotyczącej widżetów debugowania.

Zapisz wszystkie pliki i ponownie uruchom aplikację i cel widżetu.

Ponownie uruchom elementy docelowe, aby sprawdzić, czy aplikacja i widżet na ekranie głównym działają.

  1. Wybierz schemat aplikacji w Xcode, aby uruchomić miejsce docelowe aplikacji.
  2. Wybierz schemat rozszerzenia w Xcode, aby uruchomić cel rozszerzenia.
  3. Otwórz stronę artykułu w aplikacji.
  4. Kliknij ten przycisk, aby zaktualizować nagłówek. Widżet ekranu głównego powinien też zaktualizować nagłówek.

Aktualizowanie kodu Androida

Dodaj kod XML widżetu ekranu głównego.

W Android Studio zaktualizuj pliki wygenerowane w poprzednim kroku.Otwórz plik res/layout/news_widget.xml. Definiuje strukturę i układ widżetu ekranu głównego. W prawym górnym rogu kliknij Kod i zastąp zawartość tego pliku tym kodem:

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>

Ten plik XML określa dwa widoki tekstu – jeden dla nagłówka artykułu, a drugi dla opisu artykułu. Te widoki tekstu określają również styl. Wrócisz do tego pliku w trakcie tego ćwiczenia z programowania.

Aktualizowanie funkcji NewsWidget

Otwórz plik z kodem źródłowym Kotlin NewsWidget.kt. Ten plik zawiera wygenerowaną klasę o nazwie NewsWidget, która rozszerza klasę AppWidgetProvider.

Klasa NewsWidget zawiera 3 metody ze swojej klasy nadrzędnej. Zmodyfikujesz metodę onUpdate. Android wywołuje tę metodę w przypadku widżetów w stałych odstępach czasu.

Zastąp zawartość pliku NewsWidget.kt tym kodem:

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

Teraz po wywołaniu funkcji onUpdate Android pobiera najnowsze wartości z pamięci lokalnej za pomocą metody the widgetData.getString(), a następnie wywołuje setTextViewText, aby zmienić tekst wyświetlany w widżecie na ekranie głównym.

Testowanie aktualizacji

Przetestuj aplikację, aby upewnić się, że widżety na ekranie głównym są aktualizowane na podstawie nowych danych. Aby zaktualizować dane, kliknij Zaktualizuj ekran główny FloatingActionButton na stronach artykułów. W widżecie ekranu głównego powinien pojawić się tytuł artykułu.

5ce1c9914b43ad79.png

5. Używanie niestandardowych czcionek aplikacji Flutter w widżecie ekranu głównego iOS

Jak dotąd widżet ekranu głównego został skonfigurowany tak, aby odczytywał dane dostarczane przez aplikację Flutter. Aplikacja Flutter zawiera niestandardową czcionkę, której możesz użyć w widżecie ekranu głównego. Możesz użyć niestandardowej czcionki w widżecie na ekranie głównym iOS. Używanie niestandardowych czcionek w widżetach na ekranie głównym nie jest dostępne na urządzeniach z Androidem.

Zaktualizuj kod iOS

Flutter przechowuje swoje zasoby w mainBundle aplikacji na iOS. Dostęp do zasobów w tym pakiecie możesz uzyskać za pomocą kodu widżetu na ekranie głównym.

W strukturze NewsWidgetsEntryView w pliku NewsWidgets.swift wprowadź te zmiany

Utwórz funkcję pomocniczą, aby uzyskać ścieżkę do katalogu zasobów 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
       }
   ...
}

Zarejestruj czcionkę, podając adres URL pliku czcionek niestandardowych.

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

Zaktualizuj widok tekstu nagłówka, aby używać czcionki niestandardowej.

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

Gdy uruchomisz widżet ekranu głównego, używa on teraz czcionki niestandardowej w nagłówku pokazanym na tym obrazie:

93f8b9d767aacfb2.png

6. Renderowanie widżetów Flutter jako obrazów

W tej sekcji zobaczysz wykres z aplikacji Flutter jako widżet na ekranie głównym.

Ten widżet stanowi większe wyzwanie niż tekst wyświetlany na ekranie głównym. O wiele łatwiej jest wyświetlić wykres Flutter jako obraz, niż odtworzyć go za pomocą natywnych komponentów interfejsu.

Zakoduj widżet ekranu głównego, aby renderował wykres Flutter jako plik PNG. Widżet ekranu głównego może wyświetlać ten obraz.

Pisanie kodu Dart

Po stronie Dart dodaj metodę renderFlutterWidget z pakietu home_Widget. Ta metoda wymaga widżetu, nazwy pliku i klucza. Zwraca obraz widżetu Flutter i zapisuje go w udostępnionym kontenerze. Wpisz w kodzie nazwę obrazu i upewnij się, że widżet na ekranie głównym ma dostęp do kontenera. key zapisuje pełną ścieżkę pliku jako ciąg znaków w pamięci lokalnej urządzenia. Dzięki temu widżet ekranu głównego może znaleźć plik, jeśli nazwa zmieni się w kodzie Dart.

W tym ćwiczeniu w Codelabs klasa LineChart w pliku lib/article_screen.dart reprezentuje wykres. Jej metoda kompilacji zwraca obiekt CustomPainter, który umieszcza ten wykres na ekranie.

Aby zaimplementować tę funkcję, otwórz plik lib/article_screen.dart. Zaimportuj pakiet home_Widget. Następnie zastąp kod w klasie _ArticleScreenState tym kodem:

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!),
        ],
      ),
    );
  }
}

W tym przykładzie wprowadzono 3 zmiany w klasie _ArticleScreenState.

Tworzy klucz GlobalKey

GlobalKey pobiera kontekst konkretnego widżetu, który jest potrzebny do pobrania rozmiaru tego widżetu .

lib/article_screen.dart

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

Dodaje ścieżkę imagePath

Właściwość imagePath przechowuje lokalizację obrazu, w której jest renderowany widżet Flutter.

lib/article_screen.dart

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

Dodaje klucz do widżetu, który ma być renderowany

_globalKey zawiera widżet Flutter renderowany w obrazie. W tym przypadku widżet Flutter to centrum, które zawiera obiekt LineChart.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   Center(
      // New: Add this key
 key: _globalKey,
 child: const LineChart(),
   ),
   ...
}
  1. Zapisuje widżet jako obraz

Metoda renderFlutterWidget jest wywoływana, gdy użytkownik kliknie floatingActionButton. Metoda zapisuje wynikowy plik PNG jako „zrzut ekranu”. do katalogu udostępnionego kontenera. Ta metoda zapisuje też pełną ścieżkę do obrazu jako klucz nazwy pliku w pamięci urządzenia.

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);
  },
   ...
}

Aktualizowanie kodu iOS

W iOS zaktualizuj kod, aby uzyskać ścieżkę pliku z pamięci i wyświetl go jako obraz za pomocą SwiftUI.

Otwórz plik NewsWidgets.swift, aby wprowadzić te zmiany:

Dodaj filename i displaySize do struktury NewsArticleEntry

Właściwość filename zawiera ciąg znaków reprezentujący ścieżkę do pliku obrazu. Właściwość displaySize przechowuje rozmiar widżetu ekranu głównego na urządzeniu użytkownika. Rozmiar widżetu ekranu głównego pochodzi z context.

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

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

Zaktualizuj funkcję placeholder

Dodaj obiekty zastępcze filename i 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)
    }

Pobierz nazwę pliku z userDefaults w getSnapshot

Powoduje to ustawienie zmiennej filename na wartość filename w pamięci userDefaults podczas aktualizowania widżetu ekranu głównego.

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"
   ...
)

Utwórz obiekt ChartImage, który wyświetla obraz ze ścieżki

Widok ChartImage tworzy obraz na podstawie zawartości pliku wygenerowanego po stronie Dart. W tym miejscu ustawiasz rozmiar 50% klatki.

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

Użyj właściwości ChartImage w treści elementu NewsWidgetsEntryView

Dodaj widok ChartImage do treści elementu NewsWidgetsEntryView, aby wyświetlić obiekt ChartImage w widżecie ekranu głównego.

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
}

Testowanie zmian

Aby przetestować zmiany, uruchom ponownie miejsce docelowe aplikacji Flutter (Runner) i cel rozszerzenia z Xcode. Jeśli chcesz zobaczyć obraz, otwórz w aplikacji jedną ze stron artykułu i naciśnij przycisk, aby zaktualizować widżet na ekranie głównym.

33bdfe2cce908c48.png

Aktualizowanie kodu Androida

Kod na Androidzie działa tak samo jak kod na iOS.

  1. Otwórz plik android/app/res/layout/news_widget.xml. Zawiera elementy interfejsu widżetu ekranu głównego. Zastąp jego zawartość tym kodem:

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>

Ten nowy kod dodaje obraz do widżetu Ekran główny, który (na razie) wyświetla ogólną ikonę gwiazdki. Zastąp tę ikonę gwiazdki obrazem zapisanym w kodzie Dart.

  1. Otwórz plik NewsWidget.kt. Zastąp jego zawartość tym kodem:

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

Ten kod Dart zapisuje zrzut ekranu w pamięci lokalnej za pomocą klawisza filename. Pobiera również pełną ścieżkę obrazu i tworzy z niej obiekt File. Jeśli obraz istnieje, kod Dart zastępuje obraz w widżecie na ekranie głównym nowym obrazem.

  1. Załaduj ponownie aplikację i przejdź do ekranu z artykułem. Kliknij Zaktualizuj ekran główny. Widżet ekranu głównego wyświetla wykres.

7. Dalsze kroki

Gratulacje!

Gratulacje, udało Ci się utworzyć widżety ekranu głównego dla aplikacji Flutter na iOS i Androida.

Linki do treści w aplikacji Flutter

Możesz kierować użytkowników na określoną stronę w aplikacji w zależności od miejsca kliknięcia. Na przykład w aplikacji z wiadomościami objętymi tym ćwiczeniem z programowania możesz chcieć, aby użytkownik zobaczył artykuł z wiadomościami w wyświetlonym nagłówku.

Ta funkcja nie wykracza poza zakres tego ćwiczenia z programowania. Oto przykłady wykorzystania strumienia zawartego w pakiecie home_Widget do identyfikowania uruchamiania aplikacji z widżetów na ekranie głównym i wysyłania wiadomości z widżetu na ekranie głównym za pomocą adresu URL. Więcej informacji znajdziesz w dokumentacji precyzyjnych linków na stronie docs.flutter.dev.

Aktualizowanie widżetu w tle

W tym ćwiczeniu w programie udało Ci się aktywować aktualizację widżetu ekranu głównego za pomocą przycisku. Chociaż jest to uzasadnione z punktu widzenia testów, w kodzie produkcyjnym warto chcieć, by aplikacja aktualizowała widżet na ekranie głównym w tle. Za pomocą wtyczki Workmanager możesz tworzyć zadania w tle i aktualizować zasoby, których potrzebuje widżet na ekranie głównym. Więcej informacji znajdziesz w sekcji Aktualizacja w tle w pakiecie home_Widget.

W iOS widżet ekranu głównego może wysyłać żądania sieciowe do aktualizacji interfejsu użytkownika. Aby kontrolować warunki lub częstotliwość tego żądania, użyj osi czasu. Więcej informacji o korzystaniu z osi czasu znajdziesz w przewodniku Apple „Zapewnianie aktualizacji widżetu” dokumentacji.

Więcej informacji