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 StatelessWidget
há "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 InheritedWidget
sempre 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 InheritedWidget
para 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 ScopedModel
na raiz do nosso aplicativo:ScopedModel<MyModel>(
model: MyModel(),
child: MyApp(...)
)
Agora, qualquer MyModel
widget 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';
},
);
}
}
ScopedModel
ganhou popularidade no Flutter como uma ferramenta para gerenciamento de estado, mas seu uso é limitado ao fornecimento de objetos que herdam a classe Model
e 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 StatelessWidget
e 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 myBloc
no exemplo acima? Como ligamos dispose()
para nos livrar dele? Os fluxos requerem uso StreamController
, que deve ser closed
assim 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 State
no StatefulWidget
tem 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 Provider
será 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
.Provider
construído "com widgets, para widgets".Provider
permite colocar qualquer objeto com um estado na árvore de widgets e abrir acesso a ele para qualquer outro widget (filho). Ele também Provider
ajuda 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,Provider
vem 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 Provider
onde for necessário:import 'package:provider/provider.dart';
Provedor de baseCrie a base Provide
r na raiz do nosso aplicativo; isso conterá uma instância do nosso modelo:Provider<MyModel>(
builder: (context) => MyModel(),
child: MyApp(...),
)
O parâmetro builder
cria 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 MyWidget
obtém uma instância MyModel
usando o widget Consumidor . Este elemento nos dá builder
contendo 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 MyWidget
que 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, Provider
não será possível "ver" que atualizamos corretamente a propriedade foo
e solicitamos que o widget seja MyWidget
atualizado em resposta.ChangeNotifierProviderMas há esperança! Você pode fazer nossa classe MyModel
implementar 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 foo
por getter
e 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 OtherWidget
atualizações da propriedade foo
na instância MyModel
, elas MyWidget
serão atualizadas automaticamente para refletir essa alteração. Legal certo?A propósito. Você provavelmente notou um manipulador de botão OtherWidget
com 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 OtherWidget
assim que o modelo for alterado MyModel
. Talvez não precisemos disso. No final, ele OtherWidget
simplesmente 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 Provider
exatamente assim.StreamProviderÀ primeira vista, não está claro por que é necessário StreamProvider
. No final, você pode usar o habitual StreamBuilder
se precisar consumir um fluxo no Flutter. Por exemplo, aqui ouvimos o fluxo onAuthStateChanged
fornecido 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 StreamProvider
na 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.FutureProviderSemelhante ao exemplo acima, FutureProvider
é uma alternativa ao padrão FutureBuilder
ao 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 Consumer
que no exemplo StreamProvider
acima.ValueListenableProviderValueListenable é 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 ValueNotifier
usa 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 Consumer
aninhada :ValueListenableProvider
counter
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 counter
de 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 counter
de 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++;
},
);
}
}
MultiProviderSe 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(...)
)
)
)
MultiProvider
Permite 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(...),
)
ProxyProviderProxyProvider
é 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 ProxyProvider
e 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) {
},
);
Conclusão
Quando usado, InheritedWidget
Provider
permite 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, Provider
tornou-se um pacote que vale a pena tentar sem demora!