Como adicionar um widget de tela inicial ao app do Flutter

1. Introdução

O que são widgets?

Para desenvolvedores do Flutter, a definição comum de widget se refere a componentes de interface criados usando o framework Flutter. No contexto deste codelab, um widget se refere a uma versão mini de um app que fornece uma visão das informações do app sem abrir o app. No Android, os widgets ficam na tela inicial. No iOS, eles podem ser adicionados à tela inicial, à tela de bloqueio ou à visualização "Hoje".

f0027e8a7d0237e0.png b991e79ea72c8b65.png

Qual a complexidade máxima de um widget?

A maioria dos widgets da tela inicial é simples. Eles podem conter texto básico, gráficos simples ou, no Android, controles básicos. O Android e o iOS limitam os componentes e recursos da interface que você pode usar.

819b9fffd700e571.png 92d62ccfd17d770d.png

Criar a interface para widgets

Devido a essas limitações da interface, não é possível desenhar diretamente a interface de um widget da tela inicial usando o framework Flutter. Em vez disso, você pode adicionar widgets criados com frameworks de plataforma, como o Jetpack Compose ou o SwiftUI, ao seu app Flutter. Este codelab discute exemplos de compartilhamento de recursos entre o app e os widgets para evitar a reescrita de interfaces complexas.

O que você vai criar

Neste codelab, você vai criar widgets da tela inicial no Android e no iOS para um app simples do Flutter usando o pacote home_widget, que permite aos usuários ler artigos. Seus widgets vão:

  • Mostrar dados do seu app Flutter.
  • Mostrar texto usando recursos de fonte compartilhados do app Flutter.
  • Mostra uma imagem de um widget do Flutter renderizado.

a36b7ba379151101.png

Esse app Flutter inclui duas telas (ou rotas):

  • A primeira mostra uma lista de artigos de notícias com títulos e descrições.
  • O segundo mostra o artigo completo com um gráfico criado usando CustomPaint.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

O que você aprenderá

  • Como criar widgets da tela inicial no iOS e no Android.
  • Como usar o pacote home_widget para compartilhar dados entre o widget da tela inicial e o app Flutter.
  • Como reduzir a quantidade de código que você precisa reescrever.
  • Como atualizar o widget da tela inicial no app Flutter.

2. Configurar o ambiente de desenvolvimento

Para as duas plataformas, você precisa do SDK do Flutter e de uma IDE. Você pode usar o ambiente de desenvolvimento integrado de sua preferência para trabalhar com o Flutter. Pode ser o Visual Studio Code com as extensões do Dart Code e do Flutter ou o Android Studio ou IntelliJ com os plug-ins do Flutter e do Dart instalados.

Para criar o widget da tela de início do iOS:

  • É possível executar este codelab em um dispositivo iOS físico ou no simulador iOS.
  • É necessário configurar um sistema macOS com o ambiente de desenvolvimento integrado Xcode. Isso instala o compilador necessário para criar a versão iOS do seu app.

Para criar o widget da tela inicial do Android:

  • Você pode executar este codelab em um dispositivo Android físico ou no emulador do Android.
  • Configure seu sistema de desenvolvimento com o Android Studio. Isso instala o compilador necessário para criar a versão Android do seu app.

Acessar o código inicial

Faça o download da versão inicial do seu projeto no GitHub

Na linha de comando, clone o repositório do GitHub em um diretório flutter-codelabs:

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

Depois de clonar o repositório, você encontra o código deste codelab no diretório flutter-codelabs/homescreen_codelab. Esse diretório contém o código do projeto concluído para cada etapa do codelab.

Abra o app inicial

Abra o diretório flutter-codelabs/homescreen_codelab/step_03 no ambiente de desenvolvimento integrado de sua preferência.

Instalar pacotes

Todos os pacotes necessários foram adicionados ao arquivo pubspec.yaml do projeto. Para recuperar as dependências do projeto, execute o seguinte comando:

$ flutter pub get

3. Adicionar um widget básico à tela inicial

Primeiro, adicione o widget da tela inicial usando as ferramentas nativas da plataforma.

Criar um widget básico da tela inicial do iOS

Adicionar uma extensão de app ao seu app Flutter iOS é semelhante a adicionar uma extensão de app a um app SwiftUI ou UIKit:

  1. Execute open ios/Runner.xcworkspace em uma janela de terminal no diretório do projeto do Flutter. Como alternativa, clique com o botão direito do mouse na pasta ios do VSCode e selecione Open in Xcode. Isso abre o espaço de trabalho padrão do Xcode no seu projeto do Flutter.
  2. Selecione Arquivo → Novo → Destino no menu. Isso adiciona uma nova meta ao projeto.
  3. Uma lista de modelos vai aparecer. Selecione Extensão de widget.
  4. Digite "NewsWidgets" na caixa Nome do produto para esse widget. Desmarque as caixas de seleção Incluir atividade em tempo real e Incluir intenção de configuração.

Inspecionar o exemplo de código

Quando você adiciona um novo destino, o Xcode gera um exemplo de código com base no modelo selecionado. Para mais informações sobre o código gerado e o WidgetKit, consulte a documentação de extensão de app da Apple .

Depurar e testar o widget de exemplo

  1. Primeiro, atualize a configuração do app Flutter. Isso é necessário quando você adiciona novos pacotes ao app Flutter e planeja executar um destino no projeto do Xcode. Para atualizar a configuração do app, execute o seguinte comando no diretório do app Flutter:
$ flutter build ios --config-only
  1. Clique em Runner para abrir uma lista de destinos. Selecione o destino do widget que você acabou de criar, NewsWidgets, e clique em Executar. Execute o destino do widget no Xcode quando você mudar o código do widget iOS.

bbb519df1782881d.png

  1. A tela do simulador ou do dispositivo precisa mostrar um widget básico da tela inicial. Se ele não aparecer, adicione-o à tela. Clique e pressione a tela inicial e clique no + no canto superior esquerdo.

18eff1cae152014d.png

  1. Pesquise o nome do app. Para este codelab, pesquise "Widgets da tela inicial".

a0c00df87615493e.png

  1. Depois de adicionar o widget à tela inicial, ele vai mostrar um texto simples com a hora.

Como criar um widget básico do Android

  1. Para adicionar um widget da tela inicial no Android, abra o arquivo de build do projeto no Android Studio. Ele está localizado em android/build.gradle. Se preferir, clique com o botão direito do mouse na pasta android no VSCode e selecione Open in Android Studio.
  2. Depois que o projeto for criado, localize o diretório do app no canto superior esquerdo. Adicione o novo widget da tela inicial a esse diretório. Clique com o botão direito do mouse no diretório e selecione New -> Widget -> App Widget.

f19d8b7f95ab884e.png

  1. O Android Studio mostra um novo formulário. Adicione informações básicas sobre o widget da tela inicial, incluindo nome da classe, posicionamento, tamanho e idioma de origem.

Para este codelab, defina os seguintes valores:

  • Caixa Nome da classe para NewsWidget
  • Largura mínima (células) para 3
  • No menu suspenso Altura mínima (células), selecione 3.

Inspecionar o exemplo de código

Quando você envia o formulário, o Android Studio cria e atualiza vários arquivos. As mudanças relevantes para este codelab estão listadas na tabela abaixo.

Ação

Arquivo de destino

Mudar

Atualizar

AndroidManifest.xml

Adiciona um novo receptor que registra o NewsWidget.

Criar

res/layout/news_widget.xml

Define a interface do widget da tela inicial.

Criar

res/xml/news_widget_info.xml

Define a configuração do widget da tela inicial. Você pode ajustar as dimensões ou o nome do widget nesse arquivo.

Criar

java/com/example/homescreen_widgets/NewsWidget.kt

Contém o código Kotlin para adicionar funcionalidade ao widget da tela inicial.

Você pode encontrar mais detalhes sobre esses arquivos ao longo deste codelab.

Depurar e testar o widget de exemplo

Agora, execute o aplicativo e confira o widget da tela inicial. Depois de criar o app, navegue até a tela de seleção de aplicativos do seu dispositivo Android e toque e mantenha pressionado o ícone desse projeto do Flutter. Selecione Widgets no menu pop-up.

dff7c9f9f85ef1c7.png

O dispositivo Android ou emulador mostra o widget padrão da tela inicial para Android.

4. Enviar dados do app Flutter para o widget da tela inicial

É possível personalizar o widget básico da tela inicial que você criou. Atualize o widget da tela inicial para mostrar uma manchete e um resumo de uma notícia. A captura de tela a seguir mostra um exemplo do widget da tela inicial exibindo um título e um resumo.

acb90343a3e51b6d.png

Para transmitir dados entre o app e o widget da tela inicial, você precisa escrever código Dart e nativo. Esta seção divide esse processo em três partes:

  1. Escrever código Dart no seu app Flutter que pode ser usado no Android e no iOS
  2. Como adicionar funcionalidade nativa do iOS
  3. Como adicionar funcionalidade nativa do Android

Usar grupos de apps iOS

Para compartilhar dados entre um app iOS principal e uma extensão de widget, ambos os destinos precisam pertencer ao mesmo grupo de apps. Para saber mais sobre grupos de apps, consulte a documentação da Apple sobre grupos de apps.

Atualize o identificador do pacote:

No Xcode, acesse as configurações do destino. Na guia Assinatura e recursos, verifique se a equipe e o identificador do pacote estão definidos.

Adicione o grupo de apps aos destinos Runner e NewsWidgetExtension no Xcode:

Selecione + Capacidade -> Grupos de apps e adicione um novo grupo de apps. Repita para o destino do Runner (app principal) e do widget.

135e1a8c4652dac.png

Adicionar o código Dart

Os apps iOS e Android podem compartilhar dados com um app Flutter de algumas maneiras diferentes.Para se comunicar com esses apps, use o armazenamento local key/value do dispositivo. O iOS chama esse armazenamento de UserDefaults, e o Android chama de SharedPreferences. O pacote home_widget envolve essas APIs para simplificar o salvamento de dados em qualquer plataforma e permite que os widgets da tela inicial extraiam dados atualizados.

707ae86f6650ac55.png

Os dados de título e descrição vêm do arquivo news_data.dart. Esse arquivo contém dados simulados e uma classe de dados 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,
  });
}

Atualizar os valores de título e descrição

Para adicionar a funcionalidade de atualizar o widget da tela inicial no app Flutter, navegue até o arquivo lib/home_screen.dart. Substitua o conteúdo do arquivo pelo código a seguir. Em seguida, substitua <YOUR APP GROUP> pelo identificador do grupo de apps.

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

A função updateHeadline salva os pares de chave/valor no armazenamento local do dispositivo. A chave headline_title contém o valor de newHeadline.title. A chave headline_description contém o valor do newHeadline.description. A função também notifica a plataforma nativa de que novos dados para os widgets da tela inicial podem ser recuperados e renderizados.

Modificar o floatingActionButton

Chame a função updateHeadline quando o floatingActionButton for pressionado, conforme mostrado:

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

Com essa mudança, quando um usuário pressiona o botão Atualizar título na página de um artigo, os detalhes do widget da tela inicial são atualizados.

Atualizar o código do iOS para mostrar os dados do artigo

Para atualizar o widget da tela inicial no iOS, use o Xcode.

Abra o arquivo NewsWidgets.swift no Xcode:

Configure o TimelineEntry.

Substitua a struct SimpleEntry pelo seguinte código:

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
}

Essa struct NewsArticleEntry define os dados recebidos para serem transmitidos ao widget da tela inicial quando atualizados. O tipo TimelineEntry exige um parâmetro de data.Para saber mais sobre o protocolo TimelineEntry, consulte a documentação TimelineEntry da Apple.

Edite o View que mostra o conteúdo

Modifique o widget da tela inicial para mostrar o título e a descrição do artigo em vez da data. Para mostrar texto no SwiftUI, use a visualização Text. Para empilhar visualizações umas sobre as outras no SwiftUI, use a visualização VStack.

Substitua a visualização NewsWidgetEntryView gerada pelo seguinte código:

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

Edite o provedor para informar ao widget da tela inicial quando e como atualizar

Substitua o Provider atual pelo código a seguir. Em seguida, substitua o identificador do grupo de apps por <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)
              }
    }
}

O Provider no código anterior está em conformidade com um TimelineProvider. O Provider tem três métodos diferentes:

  1. O método placeholder gera uma entrada de marcador de posição quando o usuário visualiza o widget da tela inicial pela primeira vez.

45a0f64240c12efe.png

  1. O método getSnapshot lê os dados dos padrões do usuário e gera a entrada para o horário atual.
  2. O método getTimeline retorna entradas da linha do tempo. Isso é útil quando você tem pontos previsíveis para atualizar seu conteúdo. Este codelab usa a função getSnapshot para acessar o estado atual. O método .atEnd informa ao widget da tela inicial para atualizar os dados após o horário atual.

Comente o NewsWidgets_Previews

O uso de prévias está fora do escopo deste codelab. Para mais detalhes sobre como visualizar widgets da tela de início do SwiftUI, consulte a documentação da Apple sobre depuração de widgets.

Salve todos os arquivos e execute novamente o destino do app e do widget.

Execute os destinos novamente para validar se o app e o widget da tela inicial funcionam.

  1. Selecione o esquema do app no Xcode para executar o destino do app.
  2. Selecione o esquema de extensão no Xcode para executar o destino da extensão.
  3. Acesse a página de um artigo no app.
  4. Clique no botão para atualizar o título. O widget da tela inicial também vai atualizar o título.

Atualizar o código do Android

Adicione o XML do widget da tela inicial.

No Android Studio, atualize os arquivos gerados na etapa anterior.Abra o arquivo res/layout/news_widget.xml. Ele define a estrutura e o layout do widget da tela inicial. Selecione Código no canto superior direito e substitua o conteúdo desse arquivo pelo seguinte código:

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>

Esse XML define duas visualizações de texto, uma para o título e outra para a descrição do artigo. Essas visualizações de texto também definem o estilo. Você vai voltar a esse arquivo ao longo deste codelab.

Atualizar a funcionalidade do NewsWidget

Abra o arquivo de código-fonte Kotlin NewsWidget.kt. Esse arquivo contém uma classe gerada chamada NewsWidget que estende a classe AppWidgetProvider.

A classe NewsWidget contém três métodos da superclasse. Você vai modificar o método onUpdate. O Android chama esse método para widgets em intervalos fixos.

Substitua o conteúdo do arquivo NewsWidget.kt pelo seguinte código:

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

Agora, quando onUpdate é chamado, o Android recebe os valores mais recentes do armazenamento local usando o método the widgetData.getString() e chama setTextViewText para mudar o texto mostrado no widget da tela inicial.

Testar as atualizações

Teste o app para garantir que os widgets da tela inicial sejam atualizados com novos dados. Para atualizar os dados, use a opção Atualizar tela inicial FloatingActionButton nas páginas dos artigos. O widget da tela inicial será atualizado com o título do artigo.

5ce1c9914b43ad79.png

5. Usar fontes personalizadas do app Flutter no widget da tela inicial do iOS

Até agora, você configurou o widget da tela inicial para ler os dados fornecidos pelo app Flutter. O app do Flutter inclui uma fonte personalizada que você pode usar no widget da tela inicial. Você pode usar a fonte personalizada no widget da tela de início do iOS. Não é possível usar fontes personalizadas em widgets da tela inicial no Android.

Atualizar o código do iOS

O Flutter armazena os recursos no mainBundle de aplicativos iOS. Você pode acessar os recursos nesse pacote no código do widget da tela inicial.

Na struct NewsWidgetsEntryView no arquivo NewsWidgets.swift, faça as seguintes mudanças:

Crie uma função auxiliar para receber o caminho do diretório de recursos do 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
       }
   ...
}

Registre a fonte usando o URL do arquivo de fonte personalizada.

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

Atualize a visualização de texto do título para usar sua fonte personalizada.

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

Quando você executa o widget da tela inicial, ele usa a fonte personalizada para o título, conforme mostrado na imagem a seguir:

93f8b9d767aacfb2.png

6. Renderizar widgets do Flutter como uma imagem

Nesta seção, você vai mostrar um gráfico do seu app Flutter como um widget da tela inicial.

Esse widget oferece um desafio maior do que o texto mostrado na tela inicial. É muito mais fácil mostrar o gráfico do Flutter como uma imagem do que tentar recriá-lo usando componentes de UI nativos.

Codifique o widget da tela inicial para renderizar o gráfico do Flutter como um arquivo PNG. O widget da tela inicial pode mostrar essa imagem.

Escrever o código Dart

No lado do Dart, adicione o método renderFlutterWidget do pacote home_widget. Esse método usa um widget, um nome de arquivo e uma chave. Ele retorna uma imagem do widget do Flutter e a salva em um contêiner compartilhado. Forneça o nome da imagem no seu código e verifique se o widget da tela inicial pode acessar o contêiner. O key salva o caminho completo do arquivo como uma string no armazenamento local do dispositivo. Isso permite que o widget da tela inicial encontre o arquivo se o nome mudar no código Dart.

Neste codelab, a classe LineChart no arquivo lib/article_screen.dart representa o gráfico. O método de build dele retorna um CustomPainter que pinta esse gráfico na tela.

Para implementar esse recurso, abra o arquivo lib/article_screen.dart. Importe o pacote home_widget. Em seguida, substitua o código na classe _ArticleScreenState pelo seguinte:

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

Este exemplo faz três mudanças na classe _ArticleScreenState.

Cria uma GlobalKey

O GlobalKey recebe o contexto do widget específico, que é necessário para saber o tamanho dele .

lib/article_screen.dart

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

Adiciona imagePath

A propriedade imagePath armazena o local da imagem em que o widget do Flutter é renderizado.

lib/article_screen.dart

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

Adiciona a chave ao widget para renderizar

O _globalKey contém o widget do Flutter renderizado na imagem. Nesse caso, o widget do Flutter é o Center, que contém o LineChart.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   Center(
      // New: Add this key
 key: _globalKey,
 child: const LineChart(),
   ),
   ...
}
  1. Salva o widget como uma imagem

O método renderFlutterWidget é chamado quando o usuário clica no floatingActionButton. O método salva o arquivo PNG resultante como "screenshot" no diretório do contêiner compartilhado. O método também salva o caminho completo da imagem como a chave do nome do arquivo no armazenamento do dispositivo.

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

Atualizar o código do iOS

Para iOS, atualize o código para receber o caminho do arquivo do armazenamento e mostre o arquivo como uma imagem usando o SwiftUI.

Abra o arquivo NewsWidgets.swift para fazer as seguintes mudanças:

Adicione filename e displaySize à struct NewsArticleEntry

A propriedade filename contém a string que representa o caminho para o arquivo de imagem. A propriedade displaySize contém o tamanho do widget da tela inicial no dispositivo do usuário. O tamanho do widget da tela inicial vem do context.

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

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

Atualize a função placeholder

Inclua um marcador de posição filename e 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)
    }

Extraia o nome do arquivo de userDefaults em getSnapshot.

Isso define a variável filename como o valor filename no armazenamento userDefaults quando o widget da tela inicial é atualizado.

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

Crie um ChartImage que mostre a imagem de um caminho

A visualização ChartImage cria uma imagem com base no conteúdo do arquivo gerado no lado do Dart. Aqui, você define o tamanho como 50% do frame.

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

Use o ChartImage no corpo de NewsWidgetsEntryView

Adicione a visualização ChartImage ao corpo da NewsWidgetsEntryView para mostrar o ChartImage no widget da tela inicial.

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
}

Testar as mudanças

Para testar as mudanças, execute novamente o destino do app Flutter (Runner) e o destino da extensão no Xcode. Para ver a imagem, navegue até uma das páginas de artigos no app e pressione o botão para atualizar o widget da tela inicial.

33bdfe2cce908c48.png

Atualizar o código do Android

O código Android funciona da mesma forma que o código iOS.

  1. Abra o arquivo android/app/res/layout/news_widget.xml. Ele contém os elementos da interface do widget da tela inicial. Substitua o conteúdo dele pelo seguinte código:

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>

Esse novo código adiciona uma imagem ao widget da tela inicial, que (por enquanto) mostra um ícone de estrela genérico. Substitua esse ícone de estrela pela imagem que você salvou no código Dart.

  1. Abra o arquivo NewsWidget.kt. Substitua o conteúdo dele pelo seguinte código:

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

Esse código Dart salva uma captura de tela no armazenamento local com a chave filename. Ele também recebe o caminho completo da imagem e cria um objeto File com base nele. Se a imagem existir, o código Dart vai substituir a imagem no widget da tela inicial pela nova.

  1. Recarregue o app e navegue até a tela de um artigo. Pressione Atualizar tela inicial. O widget da tela inicial mostra o gráfico.

7. Próximas etapas

Parabéns!

Parabéns, você criou widgets da tela inicial para seus apps do Flutter em iOS e Android.

Como vincular conteúdo no seu app Flutter

Dependendo de onde o usuário clicar, talvez você queira direcioná-lo a uma página específica do app. Por exemplo, no app de notícias deste codelab, talvez você queira que o usuário veja o artigo da manchete exibida.

Esse recurso está fora do escopo deste codelab. Você pode encontrar exemplos de como usar um fluxo fornecido pelo pacote home_widget para identificar inicializações de apps em widgets da tela inicial e enviar mensagens do widget da tela inicial pelo URL. Para saber mais, consulte a documentação sobre links diretos em docs.flutter.dev.

Atualizar o widget em segundo plano

Neste codelab, você acionou uma atualização do widget da tela inicial usando um botão. Embora isso seja razoável para testes, no código de produção, talvez você queira que o app atualize o widget da tela inicial em segundo plano. Você pode usar o plug-in workmanager para criar tarefas em segundo plano e atualizar os recursos necessários para o widget da tela inicial. Para saber mais, confira a seção Atualização em segundo plano no pacote home_widget.

No iOS, o widget da tela inicial também pode fazer uma solicitação de rede para atualizar a interface. Para controlar as condições ou a frequência dessa solicitação, use a linha do tempo. Para saber mais sobre como usar a linha do tempo, consulte a documentação da Apple "Manter um widget atualizado" .

Leitura adicional