Detalhes sobre o pacote Provider for Flutter

Olá Habr!

Nossos planos de médio prazo incluem o lançamento do livro Flutter. Em relação à linguagem do Dart como tópico, ainda assumimos uma posição mais cautelosa, portanto, tentaremos avaliar sua relevância de acordo com os resultados deste artigo. Ele se concentrará no pacote Provider e, portanto, no gerenciamento de estado no Flutter.

O Provider é um pacote de gerenciamento de estado escrito por Remy Rusle e adotado pelo Google e pela comunidade Flutter. Mas o que é gerenciamento de estado? Para iniciantes, o que é uma condição? Deixe-me lembrá-lo de que o estado são apenas dados para representar a interface do usuário no seu aplicativo. O gerenciamento de estado é uma abordagem para criar esses dados, acessando, manipulando e descartando-os. Para entender melhor o pacote do provedor, descrevemos brevemente o histórico do gerenciamento de estado em Flutter.

1. StatefulWidget


StatelessWidget é um componente simples da interface do usuário que é exibido apenas quando há dados. Não StatelessWidgethá "memória"; é criado e destruído conforme necessário. O Flutter também possui um StatefulWidget , no qual há uma memória, graças a ele um satélite de longa duração - o objeto State . Essa classe possui um método setState(), quando chamado, é lançado um widget que reconstrói o estado e o exibe em um novo formulário. Essa é a forma mais simples de gerenciamento de estado do Flutter, fornecida imediatamente. Aqui está um exemplo com um botão que sempre exibe a hora em que foi pressionado pela última vez:

class _MyWidgetState extends State<MyWidget> {
  DateTime _time = DateTime.now();  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text(_time.toString()),
      onPressed: () {
        setState(() => _time = DateTime.now());
      },
    );
  }
}

Então, qual é o problema dessa abordagem? Suponha que seu aplicativo tenha algum estado global armazenado na raiz StatefulWidget. Ele contém dados destinados ao uso em várias partes da interface do usuário. Esses dados são compartilhados e passados ​​para cada widget filho na forma de parâmetros. Quaisquer eventos nos quais se planeja alterar esses dados serão exibidos na forma de retornos de chamada. Assim, através de todos os widgets intermediários, muitos parâmetros e retornos de chamada são transmitidos, o que pode levar a confusão em breve. Pior, quaisquer atualizações na raiz mencionada anteriormente levarão a uma reconstrução de toda a árvore de widgets, o que é ineficiente.

2. InheritedWidget


InheritedWidget é um widget especial cujos descendentes podem acessá-lo sem um link direto. Apenas ao voltar para InheritedWidget, um widget consumidor pode se registrar para uma reconstrução automática, o que ocorrerá ao reconstruir um widget ancestral. Essa técnica permite organizar com mais eficiência a atualização da interface do usuário. Em vez de reconstruir grandes partes do aplicativo em resposta a uma pequena mudança de estado, você pode selecionar seletivamente apenas os widgets específicos que precisam ser reconstruídos. Você já trabalhou InheritedWidgetsempre que usou MediaQuery.of(context)ou Theme.of(context). É verdade que é menos provável que você tenha implementado seu próprio InheritedWidget com preservação de estado. O fato é que implementá-los corretamente não é fácil.

3. ScopedModel


ScopedModel é um pacote criado em 2017 por Brian Egan, que facilita o uso InheritedWidgetpara armazenar o estado do aplicativo. Primeiro, você precisa criar um objeto de estado que herda de Model e depois chamá- notifyListeners()lo quando suas propriedades mudarem. A situação é remanescente da implementação da interface PropertyChangeListener em Java.

class MyModel extends Model {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Para fornecer nosso objeto de estado, envolvemos esse objeto em um widget ScopedModelna raiz do nosso aplicativo:

ScopedModel<MyModel>(
  model: MyModel(),
  child: MyApp(...)
)

Agora, qualquer MyModelwidget descendente poderá acessar usando o widget ScopedModelDescendant . A instância do modelo é passada para o parâmetro builder:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<MyModel>(
      builder: (context, child, model) => Text(model.foo),
    );
  }
}

Qualquer widget descendente também poderá atualizar o modelo, o que provocará automaticamente uma reconstrução de qualquer ScopedModelDescendants(desde que nosso modelo chame corretamente notifyListeners()):

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = ScopedModel.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

ScopedModelganhou popularidade no Flutter como uma ferramenta para gerenciamento de estado, mas seu uso é limitado ao fornecimento de objetos que herdam a classe Modele usam esse padrão de notificação de alterações.

4. BLoC


Na conferência do Google I / O '18 , o padrão Business Logic Component (BLoC) foi introduzido , que serve como mais uma ferramenta para extrair o estado dos widgets. As classes BLoC são componentes de longa duração que não são da interface do usuário que preservam o estado e o expõem como fluxos e receptores. Levando a lógica de estado e de negócios além da interface do usuário, você pode implementar o widget como simples StatelessWidgete usar o StreamBuilder para reconstrução automática. Como resultado, o widget "fica burro" e fica mais fácil testar.

Exemplo de classe BLoC:

class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    //  
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
 ,   BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    //  
 });
}

O problema com o padrão BLoC é que não é óbvio como criar e destruir objetos BLoC. Como a instância foi criada myBlocno exemplo acima? Como ligamos dispose()para nos livrar dele? Os fluxos requerem uso StreamController, que deve ser closedassim que se tornar desnecessário - isso é feito para evitar vazamentos de memória. (Não existe tal coisa como um destruidor de classe no dardo; apenas uma classe Stateno StatefulWidgettem um método dispose()). Além disso, não está claro como compartilhar esse BLoC entre vários widgets. Geralmente, é difícil para os desenvolvedores dominar o BLoC. Existem vários pacotes que tentam simplificar isso.

5. Fornecedor


ProviderÉ um pacote escrito em 2018 por Remy Rusle, semelhante a ScopedModel, mas cujas funções não se limitam a, fornecendo uma subclasse de Model. Esse também é um wrapper que é concluído InheritedWidget, mas o provedor pode fornecer qualquer objeto de estado, incluindo BLoC, fluxos, futuros e outros. Como o provedor é tão simples e flexível, o Google anunciou na conferência Google I / O '19 que no futuro Providerserá o pacote preferido para gerenciar o estado. Claro, outros pacotes também são permitidos, mas se você tiver alguma dúvida, o Google recomenda parar Provider.

Providerconstruído "com widgets, para widgets".Providerpermite colocar qualquer objeto com um estado na árvore de widgets e abrir acesso a ele para qualquer outro widget (filho). Ele também Providerajuda a gerenciar a vida útil dos objetos de estado, inicializando-os com dados e executando uma limpeza após a remoção da árvore de widgets. Portanto, Provideré adequado mesmo para a implementação de componentes BLoC ou pode servir de base para outras soluções de gerenciamento de estado! Ou simplesmente usado para implementar dependências - um termo sofisticado que significa transferir dados para widgets de uma maneira que permita que você afrouxe a conexão e melhore a testabilidade do código. Finalmente,Providervem com um conjunto de aulas especializadas, graças às quais é ainda mais conveniente de usar. A seguir, examinaremos mais de perto cada uma dessas classes.

  • Fornecedor básico
  • ChangeNotifierProvider
  • StreamProvider
  • Futureprovider
  • ValueListenableProvider
  • MultiProvider
  • Proxyprovider

Instalação


Para usá-lo Provider, primeiro adicione uma dependência ao nosso arquivo pubspec.yaml:

provider: ^3.0.0

Depois importamos o pacote Provideronde for necessário:

import 'package:provider/provider.dart';

Provedor de base

Crie a base Provider na raiz do nosso aplicativo; isso conterá uma instância do nosso modelo:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

O parâmetro buildercria uma instância MyModel. Se você deseja passar uma instância existente para ela, use o construtor aqui Provider.value.

Em seguida, você pode consumir esta instância do modelo em qualquer lugar MyApp, usando o widget Consumer:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyModel>(
      builder: (context, value, child) => Text(value.foo),
    );
  }
}

No exemplo acima, a classe MyWidgetobtém uma instância MyModelusando o widget Consumidor . Este elemento nos dá buildercontendo nosso objeto no parâmetro value.

Agora, o que devemos fazer se quisermos atualizar os dados em nosso modelo? Digamos que temos outro widget em que, quando um botão é clicado, a propriedade deve ser atualizada foo:

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

Observe a sintaxe específica usada para acessar a instância MyModel. Funcionalmente, isso é equivalente a acessar o widget Consumer. O widget Consumeré útil nos casos em que o código não pode obter facilmente o link BuildContext.

O que você acha que acontecerá com o widget original MyWidgetque criamos anteriormente? Um novo significado será exibido nele bar? Infelizmente não . Não é possível ouvir alterações em objetos antigos antigos do Dart (pelo menos sem reflexão, o que não é fornecido no Flutter). Portanto, Providernão será possível "ver" que atualizamos corretamente a propriedade fooe solicitamos que o widget seja MyWidgetatualizado em resposta.

ChangeNotifierProvider

Mas há esperança! Você pode fazer nossa classe MyModelimplementar uma impureza ChangeNotifier. Demorará um pouco para alterar a implementação do nosso modelo e chamar um método especial notifyListeners()sempre que uma de nossas propriedades for alterada. Funciona aproximadamente da mesma maneira ScopedModel, mas, nesse caso, é bom que você não precise herdar de uma classe de modelo específica. É o suficiente para perceber a mistura ChangeNotifier. Aqui está o que parece:

class MyModel with ChangeNotifier {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Como você pode ver, substituímos nossa propriedade foopor gettere setter, apoiada pela variável privada _foo. Dessa forma, podemos "interceptar" quaisquer alterações feitas na propriedade foo e informar nossos ouvintes que nosso objeto mudou.

Agora, de fora Provider, podemos mudar nossa implementação para que ela use uma classe diferente chamada ChangeNotifierProvider:

ChangeNotifierProvider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

Como isso! Agora, quando nossas OtherWidgetatualizações da propriedade foona instância MyModel, elas MyWidgetserão atualizadas automaticamente para refletir essa alteração. Legal certo?

A propósito. Você provavelmente notou um manipulador de botão OtherWidgetcom o qual usamos a seguinte sintaxe:

final model = Provider.of<MyModel>(context);

Por padrão, essa sintaxe fará com que a instância seja reconstruída automaticamente OtherWidgetassim que o modelo for alterado MyModel. Talvez não precisemos disso. No final, ele OtherWidgetsimplesmente contém um botão que não muda quando o valor muda MyModel. Para evitar a reconstrução, você pode usar a seguinte sintaxe para acessar nosso modelo sem se registrar na reconstrução:

final model = Provider.of<MyModel>(context, listen: false);

Esse é outro encanto fornecido no pacote Providerexatamente assim.

StreamProvider

À primeira vista, não está claro por que é necessário StreamProvider. No final, você pode usar o habitual StreamBuilderse precisar consumir um fluxo no Flutter. Por exemplo, aqui ouvimos o fluxo onAuthStateChangedfornecido por FirebaseAuth:

@override
Widget build(BuildContext context {
  return StreamBuilder(
   stream: FirebaseAuth.instance.onAuthStateChanged, 
   builder: (BuildContext context, AsyncSnapshot snapshot){ 
     ...
   });
}

Para fazer o mesmo com a ajuda Provider, poderíamos fornecer nosso fluxo StreamProviderna raiz do nosso aplicativo:

StreamProvider<FirebaseUser>.value(
  stream: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(...),
}

Em seguida, consuma o widget filho, como geralmente é feito com Provider:

@override
Widget build(BuildContext context) {
  return Consumer<FirebaseUser>(
    builder: (context, value, child) => Text(value.displayName),
  );
}

Nosso código de widget não apenas se tornou muito mais limpo, como também abstrai o fato de que os dados vieram do fluxo. Se algum dia decidirmos alterar a implementação base, por exemplo, para FutureProvider, então nenhuma alteração no código do widget será necessária. Como você verá, isso se aplica a todos os outros provedores mostrados abaixo.

FutureProvider

Semelhante ao exemplo acima, FutureProvideré uma alternativa ao padrão FutureBuilderao trabalhar com widgets. Aqui está um exemplo:

FutureProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.currentUser(),
  child: MyApp(...),
);

Para consumir esse valor no widget filho, usamos a mesma implementação Consumerque no exemplo StreamProvideracima.

ValueListenableProvider

ValueListenable é uma interface Dart implementada pela classe ValueNotifier que pega um valor e notifica os ouvintes quando muda para outro valor. É possível, por exemplo, agrupar um contador inteiro em uma classe de modelo simples:

class MyModel {
  final ValueNotifier<int> counter = ValueNotifier(0);  
}

Ao trabalhar com tipos complexos, ele ValueNotifierusa o operador do ==objeto armazenado nele para determinar se o valor foi alterado.
Vamos criar o mais simples Provider, que conterá o nosso modelo principal, e será seguido por uma propriedade de escuta Consumeraninhada :ValueListenableProvidercounter

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: Consumer<MyModel>(builder: (context, value, child) {
    return ValueListenableProvider<int>.value(
      value: value.counter,
      child: MyApp(...)
    }
  }
}

Observe que esse provedor aninhado é do tipo int. Pode haver outros. Se você tiver vários provedores do mesmo tipo registrados, o Provedor retornará o “mais próximo” (ancestral mais próximo).

Veja como ouvir uma propriedade counterde qualquer widget filho:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<int>(
      builder: (context, value, child) {
        return Text(value.toString());
      },
    );
  }
}

Mas aqui está como atualizar uma propriedade counterde outro widget. Atenção: precisamos acessar a cópia original MyModel.

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.counter.value++;
      },
    );
  }
}

MultiProvider

Se você usa muitos widgetsProvider, na raiz do aplicativo você obtém uma estrutura feia de muitos anexos:

Provider<Foo>.value( 
  value: foo, 
  child: Provider<Bar>.value( 
    value: bar, 
    child: Provider<Baz>.value( 
      value: baz , 
      child: MyApp(...)
    ) 
  ) 
)

MultiProviderPermite declarar todos eles no mesmo nível. É apenas açúcar sintático: no nível intra-sistema, todos permanecem aninhados de qualquer maneira.

MultiProvider( 
  providers: [ 
    Provider<Foo>.value(value: foo), 
    Provider<Bar>.value(value: bar), 
    Provider<Baz>.value(value: baz), 
  ], 
  child: MyApp(...), 
)

ProxyProvider

ProxyProvider é uma classe interessante adicionada na terceira versão do pacoteProvider. Ele permite que você declare fornecedores que podem depender de outros fornecedores, até seis em um. Neste exemplo, a classe Bar é específica da instânciaFoo. Isso é útil ao compilar um conjunto raiz de serviços que dependem um do outro.

MultiProvider ( 
  providers: [ 
    Provider<Foo> ( 
      builder: (context) => Foo(),
    ), 
    ProxyProvider<Foo, Bar>(
      builder: (context, value, previous) => Bar(value),
    ), 
  ], 
  child: MyApp(...),
)

O primeiro argumento de tipo genérico é o tipo do qual o seu depende ProxyProvidere o segundo é o tipo que ele retorna.

Como ouvir muitos provedores ao mesmo tempo


E se quisermos que um único widget escute muitos provedores e reconstrua-os quando algum deles mudar? Você pode ouvir até 6 fornecedores ao mesmo tempo usando as opções do widget Consumer. Receberemos instâncias como parâmetros de método adicionais builder.

Consumer2<MyModel, int>(
  builder: (context, value, value2, child) {
    //value  MyModel
    //value2  int
  },
);

Conclusão


Quando usado, InheritedWidget Providerpermite gerenciar o estado como é habitual no Flutter. Ele permite que os widgets acessem objetos de estado e os ouçam de maneira que o mecanismo de notificação subjacente seja abstraído. É mais fácil gerenciar a vida útil dos objetos de estado criando pontos de ancoragem para criar esses objetos conforme necessário e se livrar deles quando necessário. Esse mecanismo pode ser usado para implementar facilmente dependências e até como base para opções de gerenciamento de estado mais avançadas. Com a bênção do Google e o crescente apoio da comunidade Flutter, Providertornou-se um pacote que vale a pena tentar sem demora!

All Articles