Artigo original: How to Use the Provider Pattern in Flutter

Neste artigo, vamos conferir o padrão provider no Flutter. Alguns outros padrões, como a Arquitetura BLoC, usam o padrão provider internamente. Ele, no entanto, é muito fácil de aprender e possui muito menos código repetitivo.

Neste artigo, vamos usar o aplicativo padrão de Contador fornecido pelo Flutter e refatorá-lo para que use o padrão provider.

Se quiser saber o que a equipe do Flutter tem a dizer sobre o padrão provider, confira essa palestra de 2019 na sede do Google (vídeo em inglês).

Se quiser aprender mais sobre a arquitetura BLoC, confira aqui (texto em inglês).

Começando

Crie um projeto do Flutter e coloque nele o nome que quiser.

Primeiro, precisamos remover todos os comentários. Assim, teremos um código mais limpo para trabalhar:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Agora, adicionamos a dependência para o padrão provider no arquivo pubspec.yaml. Atualmente, a versão mais recente é a 4.1.2.

O arquivo pubspec.yaml ficará assim:

name: provider_pattern_explained
description: A new Flutter project.

publish_to: 'none' 

version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.2

  cupertino_icons: ^0.1.3

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

O aplicativo padrão é basicamente um widget com monitoramento de estado, que é criado novamente toda vez que você clica em FloatingActionButton (que, por sua vez, chama setState()).

Agora, vamos convertê-lo em um widget sem monitoramento de estado.

Criando o Provider

Vamos criar nosso provider. Ele será a fonte da verdade do nosso aplicativo. É onde armazenaremos nosso state (estado) que, neste caso, é a contagem atual.

Crie uma classe chamada Counter e adicione a variável count:

import 'package:flutter/material.dart';

class Counter {
  var _count = 0;
}

Para convertê-la em uma classe provider, estenda ChangeNotifier, do pacote material.dart. Isso nos fornece o método notifyListeners() e notificará todas as funções ouvintes quando mudarmos um valor.

Depois, adicione um método para incrementar o contador:

import 'package:flutter/material.dart';

class Counter extends ChangeNotifier {
  var _count = 0;
  void incrementCounter() {
    _count += 1;
  }
}

No fim desse método, chamaremos notifyListeners(). Isso acionará uma mudança em todo o aplicativo para qualquer widget que esteja ouvindo.

Esta é a beleza do padrão provider no Flutter – você não precisa se importar com despachar manualmente para streams.

Por fim, crie uma função getter para retornar o valor do contador. Vamos usá-la para mostrar o valor mais atual:

import 'package:flutter/material.dart';

class Counter extends ChangeNotifier {
  var _count = 0;
  int get getCounter {
    return _count;
  }

  void incrementCounter() {
    _count += 1;
    notifyListeners();
  }
}

Ouvindo cliques no botão

Agora que temos o provider configurado, podemos continuar e usá-lo em nosso widget principal.

Primeiramente, vamos converter MyHomePage para um widget sem monitoramento de estado. Vamos ter que remover a chamada para a função setState(), pois ela está disponível apenas em um StatefulWidget (widget com monitoramento de estado):

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  int _counter = 0;
  final String title;
  MyHomePage({this.title});
  void _incrementCounter() {}
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Com isso feito, podemos usar o padrão provider no Flutter para indicar e obter o valor do contador. A cada clique no botão, precisamos incrementar o valor do contador em 1.

Então, no método _incrementCounter (que é chamado quando o botão é pressionado), adicione esta linha:

Provider.of<Counter>(context, listen: false).incrementCounter();

O que acontece aqui? Você pediu para o Flutter subir na árvore de widget e encontrar o primeiro lugar onde Counter é fornecido (mostrarei como fazer isso na próxima seção). É isso que Provider.of() faz.

Os genéricos (valores dentro dos <>) dizem ao Flutter qual tipo de provider procurar. Então, o Flutter sobe na árvore do widget até encontrar o valor fornecido. Se o valor não estiver fornecido em nenhum lugar, então uma exceção é criada.

Finalmente, quando você já tem o provider, pode chamar qualquer método a partir dele. Aqui, chamamos nosso método de incrementCounter.

Também precisamos de um contexto (em inglês, context). Então, aceitamos context como argumento e modificamos o método onPressed para que também passe context:

void _incrementCounter(BuildContext context) {
  Provider.of<Counter>(context, listen: false).incrementCounter();
}

Observação: indicamos listen como falso, pois não precisamos escutar nenhum valor aqui. Estamos apenas despachando uma ação que será executada.

Fornecendo o Provider

O padrão provider no Flutter procurará o último valor fornecido. O diagrama abaixo ajudará você a entender melhor.

Provider-Pattern-Flow

Neste diagrama, o objeto A VERDE estará disponível para o resto dos elementos abaixo dele, que são B, C, D, E, e F.

Agora, suponha que queremos adicionar funcionalidade ao aplicativo e criamos outro provider, Z. Z precisa ser usado por E e F.

Onde é o melhor lugar para adicioná-lo?

Podemos adicioná-lo na raiz abaixo de A. Ficaria assim:

Provider-Pattern-Flow-2
Tradução da frase: "Funciona, mas não é muito eficiente :("

Esse método, porém, não é muito eficiente.

O Flutter passará por todos os widgets abaixo e, então, finalmente, subirá até a raiz. Se você possui árvores de widget muito longas – algo que você provavelmente terá em um aplicativo em produção – não é uma boa ideia colocar tudo na raiz.

Ao invés disso, podemos olhar para o denominador comum de E e F, que é C. Se colocamos Z abaixo de E e F, dará certo.

Provider-Pattern-Flow-3
Tradução da frase: "Funciona, mas será que podemos melhorar ainda mais?"

Se quisermos adicionar outro objeto X, requerido por E e F? Faremos a mesma coisa. Note, porém, como a árvore continua crescendo.

Provider-Pattern-Flow-4
Tradução das frases: "E assim por diante. Uma árvore muito longa"

Há uma maneira melhor de gerenciar isso. Que tal se providenciarmos todos os objetos em um nível?

Provider-Pattern-Flow-5

Isso é perfeito. Será a maneira de implementarmos nosso padrão provider no Flutter. Usaremos algo chamado MultiProvider, que nos deixa declarar diversos providers em um nível.

Deixaremos MultiProvider cercar o widget MaterialApp:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider.value(
          value: Counter(),
        ),
      ],
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: MyHomePage(title: "AndroidVille Provider Pattern"),
      ),
    );
  }
}

Com isso, fornecemos o provider para nossa árvore de widget e podemos usá-lo em qualquer lugar abaixo deste nível da árvore.

Há apenas mais uma coisa a fazer: precisamos atualizar o valor que é mostrado.

Atualizando o texto

Para atualizar o texto, obtenha o provider na função do widget MyHomePage. Vamos usar o getter que criamos para obter o valor mais recente.

Então, simplesmente adicione esse valor ao widget de texto abaixo.

Terminamos! Nosso arquivo main.dart ficará assim no final:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_pattern_explained/counter.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider.value(
          value: Counter(),
        ),
      ],
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: MyHomePage(title: "AndroidVille Provider Pattern"),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage({this.title});
  void _incrementCounter(BuildContext context) {
    Provider.of<Counter>(context, listen: false).incrementCounter();
  }

  @override
  Widget build(BuildContext context) {
    var counter = Provider.of<Counter>(context).getCounter;
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _incrementCounter(context),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Observação: não definimos listen:false nesse caso, pois queremos ficar sabendo de qualquer atualização no valor do contador.

Aqui está o código fonte no GitHub se você quiser ver:  https://github.com/Ayusch/Flutter-Provider-Pattern.

Peço que informe se tiver qualquer problema.

Boas-vindas à AndroidVille :)

A AndroidVille é uma comunidade de desenvolvedores para dispositivos móveis onde compartilhamos conhecimento relacionado com o desenvolvimento para Android, desenvolvimento em Flutter, tutoriais sobre React Native, Java, Kotlin e muito mais.

Clique neste link para juntar-se ao SLACK do AndroidVille. É totalmente gratuito!

Se você gostou deste artigo, fique à vontade para compartilhá-lo no Facebook ou LinkedIn. Você pode seguir o autor no LinkedIn, no Twitter, no Quora e no Medium, onde ele responde perguntas relacionadas ao desenvolvimento para dispositivos móveis, Android e Flutter.