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 IU criados usando o framework do Flutter. No contexto deste codelab, um widget se refere a uma miniversão de um app que oferece uma visualização das informações sem abrir o app. No Android, os widgets ficam na tela inicial. No iOS, elas podem ser adicionadas à tela inicial, à tela de bloqueio ou à visualização "Hoje".

f0027e8a7d0237e0.png b991e79ea72c8b65.png

Qual a complexidade 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. Tanto o Android quanto o iOS limitam os componentes e recursos da interface que podem ser usados.

819b9fffd700e571.png 92d62ccfd17d770d.png

Criar a interface para widgets

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

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:

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

a36b7ba379151101.png

Este app do Flutter inclui duas telas (ou rotas):

  • A primeira mostra uma lista de matérias com títulos e descrições.
  • A segunda 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 ambas as plataformas, você precisa do SDK do Flutter e de um ambiente de desenvolvimento integrado. Você pode usar seu ambiente de desenvolvimento integrado preferido para trabalhar com o Flutter. Pode ser o Visual Studio Code com as extensões do Flutter e do Dart Code (link em inglês), ou o Android Studio ou o IntelliJ com os plug-ins do Flutter e do Dart (links em inglês) instalados.

Para criar o widget da tela inicial do iOS:

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

Para criar o widget da tela inicial do Android:

  • É possível executar este codelab em um dispositivo Android físico ou no Android Emulator.
  • Você precisa configurar seu sistema de desenvolvimento com o Android Studio. Isso instala o compilador necessário para criar a versão para 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 do Flutter-codelabs:

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

Depois de clonar o repositório, você pode encontrar 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.

Abrir 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 da plataforma nativa.

Como criar um widget básico da tela inicial do iOS

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

  1. Execute open ios/Runner.xcworkspace em uma janela do 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 Abrir no Xcode. Isso abre o espaço de trabalho padrão do Xcode no seu projeto do Flutter.
  2. Selecione File → Novo → Destino no menu. Isso adiciona um novo destino ao projeto.
  3. Uma lista de modelos será exibida. Selecione Extensão de widget.
  4. Digite "NewsWidgets" na caixa Nome do produto desse widget. Desmarque as caixas de seleção Include Live Activity e Include Configuration Intent.

Inspecionar o exemplo de código

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

Depurar e testar seu widget de amostra

  1. Primeiro, atualize a configuração do app do Flutter. Você precisa fazer isso ao adicionar novos pacotes no app Flutter e planejar executar um destino no projeto do Xcode. Para atualizar a configuração do app, execute o seguinte comando no diretório de apps do Flutter:
$ flutter build ios --config-only
  1. Clique em Executor para abrir uma lista de destinos. Selecione o destino do widget recém-criado, NewsWidgets, e clique em Executar. Execute o destino do widget do Xcode ao alterar o código do widget do iOS.

bbb519df1782881d.png

  1. O simulador ou a tela do dispositivo precisa exibir um widget básico da tela inicial. Caso não o veja, adicione-o à tela. Clique e mantenha pressionada a tela inicial e clique em + no canto superior esquerdo.

18eff1cae152014d.png

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

a0c00df87615493e.png

  1. Depois que você adicionar o widget da tela inicial, ele vai mostrar um texto simples com a hora certa.

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. Esse arquivo pode ser encontrado em android/build.gradle. Como alternativa, clique com o botão direito do mouse na pasta android do VSCode e selecione Abrir no 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 este diretório. Clique com o botão direito do mouse no diretório e selecione New -> Widget -> widget de app.

f19d8b7f95ab884e.png

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

Para este codelab, defina os seguintes valores:

  • caixa Nome da classe para NewsWidget
  • Menu suspenso Largura mínima (células) como 3
  • Menu suspenso Altura mínima (células) como 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 seu widget neste 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 seu widget de amostra

Agora, execute o aplicativo e veja 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 ou emulador Android mostra o widget padrão da tela inicial para Android.

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

Você pode personalizar o widget básico da tela inicial que você criou. Atualize o widget da tela inicial para exibir uma manchete e um resumo de uma notícia. A captura de tela abaixo mostra um exemplo do widget da tela inicial mostrando um título e um resumo.

acb90343a3e51b6d.png

Para transmitir dados entre o app e o widget da tela inicial, é necessário programar em Dart e código nativo. Esta seção divide esse processo em três partes:

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

Usar os grupos de apps para iOS

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

Atualize o identificador do pacote:

No Xcode, acesse as configurações de destino. Na seção Como assinar e Capabilities, verifique se o identificador do pacote e da equipe 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 Runner (app pai) e o destino do widget.

135e1a8c4652dac.png

Adicionar o código Dart

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

707ae86f6650ac55.png

Os dados do título e da descrição são provenientes 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 do título e da descrição

Para adicionar a funcionalidade de atualizar o widget da tela inicial no seu 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 seu 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 dos widgets da tela inicial podem ser recuperados e renderizados.

Modifique o flutuanteActionButton

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 exibir os dados do artigo

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

Abra o arquivo NewsWidgets.swift no Xcode:

Configure o TimelineEntry.

Substitua o 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
}

Esse struct NewsArticleEntry define os dados de entrada que serão transmitidos ao widget da tela inicial quando atualizado. O tipo TimelineEntry requer um parâmetro de data.Para saber mais sobre o protocolo TimelineEntry, consulte a documentação da LineEntry da Apple (link em inglês).

Edite a View que mostra o conteúdo.

Modifique o widget da tela inicial para exibir o título e a descrição da matéria em vez da data. Para exibir texto no SwiftUI, use a visualização Text. Para empilhar visualizações 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 a atualização deve ser feita

Substitua o Provider existente 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 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 horários previsíveis para atualizar seu conteúdo. Este codelab usa a função getSnapshot para receber o estado atual. O método .atEnd instrui o widget da tela inicial a atualizar os dados após o período atual.

Comente em NewsWidgets_Previews

O uso de visualizações está fora do escopo deste codelab. Para saber mais sobre a visualização de widgets da tela inicial 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 da extensão no Xcode para executar o destino da extensão.
  3. Acesse a página de uma matéria no app.
  4. Clique no botão para atualizar o título. O widget da tela inicial também deve 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. Ela define a estrutura e o layout do seu widget na tela inicial. Selecione Code no canto superior direito e substitua o conteúdo do 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 a manchete do artigo e outra para a descrição do artigo. Essas visualizações de texto também definem o estilo. Você vai voltar a ele 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 com o nome 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, em seguida, 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. Como usar fontes personalizadas do app Flutter no widget da tela inicial do iOS

Até agora, você configurou o widget da tela inicial para ler dados fornecidos pelo app Flutter. O app Flutter inclui uma fonte personalizada que você pode usar no widget da tela inicial. Você pode usar a fonte personalizada no widget da tela inicial do iOS. O uso de fontes personalizadas nos widgets da tela inicial não está disponível no Android.

Atualizar o código do iOS

O Flutter armazena os recursos no mainBundle de aplicativos iOS. É possível acessar os recursos desse pacote no código do widget da tela inicial.

Na estrutura NewsWidgetsEntryView do arquivo NewsWidgets.swift, faça as seguintes alterações

Crie uma função auxiliar para acessar o caminho até o 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 para seu 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 uma 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 agora usa a fonte personalizada para o título, conforme exibido na seguinte imagem:

93f8b9d767aacfb2.png

6. Renderizar widgets do Flutter como uma imagem

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

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

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

Programar o código Dart

No Dart, adicione o método renderFlutterWidget do pacote home_widget. Esse método recebe um widget, um nome de arquivo e uma chave. Ela 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 compilação retorna um CustomPainter que pinta o 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 ver o tamanho desse widget .

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 renderização

O _globalKey contém o widget do Flutter que é renderizado para a imagem. Nesse caso, o widget do Flutter é o centro 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 "captura de tela" ao diretório de contêiner compartilhado. O método também salva o caminho completo da imagem como a chave de nome de 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

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

Abra o arquivo NewsWidgets.swift para fazer as seguintes alterações:

Adicione filename e displaySize ao struct de 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)
    }

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

Criar 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 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 de NewsWidgetsEntryView para exibir 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 do 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 do Android funciona da mesma forma que o do iOS.

  1. Abra o arquivo android/app/res/layout/news_widget.xml. Ele contém os elementos de interface do widget da tela inicial. Substitua o conteúdo por este 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) exibe 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 por este 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 a partir dele. Se a imagem existir, o código Dart substituirá a imagem no widget da tela inicial pela nova.

  1. Atualize o app e navegue até uma tela de 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 de tela inicial para seus apps do Flutter para iOS e Android.

Como criar links para conteúdo no seu app do Flutter

É possível direcionar o usuário a uma página específica no app, dependendo de onde ele clicar. Por exemplo, no app de notícias deste codelab, você pode querer que o usuário veja a matéria da manchete exibida.

Esse recurso está fora do escopo deste codelab. É possível encontrar exemplos de uso de um stream fornecido pelo pacote home_widget para identificar inicializações de apps nos widgets da tela inicial e enviar mensagens desse widget por meio do URL. Para saber mais, consulte a documentação sobre links diretos em docs.flutter.dev.

Como atualizar seu 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 seu 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, você também pode fazer com que o widget da tela inicial faça 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 Como manter um widget atualizado" na documentação do Google Cloud.

Leitura adicional