Esta é a segunda parte da minha série sobre Flutter Architecture:
Os fluxos são o principal componente do RxVMS ; seu entendimento é absolutamente necessário para trabalhar com esta biblioteca; portanto, falaremos sobre eles com mais detalhes neste post.
Descobriu-se que incluir Rx neste post seria muito longo, então eu o dividi em duas partes.
Deixe fluir
Eu li muitos comentários que eles dizem que fluxos, e especialmente Rx, são muito complicados para entender e, como resultado, usar.
Gostaria que você soubesse que não me considero um guru de Rx. Aproveitar todo o seu poder não é fácil, e admito que continuo estudando. Mas deixe-me corrigir um erro desde o início: você não precisa ser um assistente de Rx para começar a obter muitos benefícios com o uso de threads e desta tecnologia . Farei todos os esforços para explicar os fluxos para você da maneira mais acessível.
O que são tópicos?
Na minha opinião, a melhor analogia com as roscas é a correia transportadora. Você pode colocar algo em uma extremidade e esse "algo" será automaticamente transferido para a outra. Diferentemente do pipeline físico, os threads manipulam objetos de dados, transferindo-os automaticamente desde o início - mas onde? Como no pipeline real, se não houver nada para capturar os dados do outro lado, eles simplesmente "cairão" e desaparecerão (isso, é claro, não é verdade no Dart Streams, mas é melhor lidar com fluxos como se fosse esse o caso) .

Para evitar a perda de dados, você pode definir uma "interceptação" na saída do fluxo. Dessa forma, você pode capturar dados e executar as manipulações necessárias sempre que os objetos de dados chegarem ao final do fluxo.

Lembre-se:
- Se a armadilha não estiver definida, os dados simplesmente desaparecerão para sempre e não haverá como recuperá-la (novamente, não exatamente com o Dart Streams, mas é melhor você fingir que está)
- Depois de enviar dados para o fluxo, você não precisa pausar o programa e esperar até ele chegar ao fim, tudo isso acontece em segundo plano.
- A armadilha pode receber dados a qualquer momento, não é necessário imediatamente após o envio (mas não se preocupe, os fluxos são realmente muito rápidos). Imagine que você não sabe com que rapidez ou por quanto tempo a correia transportadora se move. Isso significa que colocar algo no fluxo é completamente separado da reação ao elemento na outra extremidade. Sua armadilha funcionará e pegará o item quando ele chegar lá. (Alguns de vocês já devem perceber que isso se encaixa bem na maneira reativa que o Flutter atualiza seus widgets)
- Você pode definir uma armadilha muito antes do início do trabalho e o primeiro item aparecer
- O fluxo funciona de acordo com o princípio FIFO. Os dados sempre vêm na ordem em que são colocados no fluxo.
O que é Rx?
Rx, abreviação de Extensões Reativas, são fluxos de esteróides. Esse é um conceito muito semelhante ao Streams, que foi inventado para a estrutura .Net pela equipe da Microsoft. Como o .Net já possuía o tipo Stream, que é usado para E / S de arquivos, eles chamavam os fluxos Rx de Observables e criaram muitas funções para manipular os dados que passavam por eles. O Dart possui Streams embutidos em sua especificação de idioma que já oferecem a maioria dessas funcionalidades, mas não todas. É por isso que o pacote RxDart foi desenvolvido; É baseado no Dart Streams, mas estende sua funcionalidade. Vou cobrir Rx e RxDart na próxima parte desta série.
Alguns termos
Dart Streams e Rx usam alguma terminologia que pode parecer assustadora, então aqui está a tradução. Primeiro vem o termo Dart, depois Rx.
- Stream / Observável . Este é o "pipeline" descrito anteriormente. O fluxo pode ser convertido em Observável e, sempre que for esperado, você pode atribuir um Observável. Portanto, não se confunda se eu misturar esses termos no processo de explicar
- listen / subscrever - defina a interceptação do ouvinte
- Controlador de Stream / Assunto . O lado "esquerdo" da correia transportadora onde você coloca os dados no fluxo. Eles diferem ligeiramente em suas propriedades e características, mas servem ao mesmo propósito.
- Emitindo um item / dados . O momento em que os dados aparecem na saída do "pipeline"
Criação de stream
Se você pretende continuar estudando o tópico, clone este projeto com exemplos. Vou usar o sistema de teste Dart / Flutter.
Para criar um fluxo, você cria um StreamController
var controller = new StreamController<String>(); controller.add("Item1");
O tipo de modelo (neste caso, String) passado ao criar o StreamController determina o tipo de objetos que podemos enviar para o fluxo. Pode ser qualquer tipo! Você pode criar um StreamController<List<MyObject>>()
se desejar e o fluxo transferirá a planilha inteira em vez de um único objeto.
Configuração de interceptação
Se você executou o teste especificado, não conseguiu ver nada, porque nada pegou nossa linha na saída do fluxo. Agora defina a armadilha:
var controller = new StreamController<String>(); controller.stream.listen((item) => print(item));
Agora a armadilha é configurada usando o método .listen()
. O registro se parece com controller.stream.listen
, mas se você rolar para trás, como algum tipo de álbum dos anos 60, o verdadeiro significado do que está escrito aparecerá: "ouça o fluxo desse controlador"
Você precisa passar uma certa função para o método .listen()
para manipular de alguma forma os dados recebidos. A função deve aceitar um parâmetro do tipo especificado ao criar o StreamController, neste caso, String.
Se você executar o código acima, verá
Item1 Item2 Item3
Na minha opinião, o maior problema para os novatos no Streams é que você pode determinar a reação do elemento emitido muito antes do primeiro elemento ser colocado no fluxo, acionando uma chamada para essa reação.
Terminar a escuta
O código acima perdeu a parte pequena, mas importante. listen()
retorna um StreamSubscription
- um objeto de assinatura de fluxo. Uma chamada para o método .cancel()
encerra a assinatura, liberando recursos e impedindo que a função de escuta seja chamada depois que se torna desnecessária.
var controller = new StreamController<String>(); StreamSubscription subscription = controller.stream.listen((item) => print(item));
Detalhes do ouvinte
A função listen()
pode ser uma função lambda ou simples.
void myPrint(String message) { print(message); } StreamSubscription subscription = controller.stream.listen((item) => print(item));
Nota importante: a maioria dos fluxos Dart permite apenas uma assinatura única, ou seja, eles não podem ser reinscritos após a conclusão da assinatura - isso gerará uma exceção. Essa é a diferença de outras implementações de Rx.
A assinatura completa de listen()
é assim:
StreamSubscription<T> listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError});
Isso significa que você pode fazer mais do que apenas passar um manipulador para os dados enviados. Você também pode ter um manipulador para erros e outro para fechar o fluxo no lado do controlador ( onDone
). As exceções onError()
de dentro do Stream onError()
se você fornecer, caso contrário , elas serão simplesmente engolidas e você nunca saberá que algo deu errado.
Exemplo de segmento de vibração
Para facilitar o entendimento dos capítulos seguintes, criei um ramo de repositório separado.
Por favor, clone-a
Como primeiro exemplo, peguei o conhecido aplicativo de contador que você obtém ao criar um novo projeto Flutter e o reorganizei um pouco. Eu adicionei uma classe de modelo para armazenar o estado do aplicativo, que é basicamente um valor de contador:
class Model { int _counter = 0; StreamController _streamController = new StreamController<int>(); Stream<int> get counterUpdates => _streamController.stream; void incrementCounter() { _counter++; _streamController.add(_counter); } }
aqui você pode ver um modelo muito típico: em vez de publicar todo o StreamController, simplesmente publicamos sua propriedade Stream.
Para disponibilizar o modelo para a interface do usuário, eu o tornei um campo estático no objeto App, porque não queria inserir InheritedWidget ou ServiceLocator. Para um exemplo simples, isso vai acabar com ele, mas eu não faria isso nesta aplicação!
Adicione ao main.dart
:
class _MyHomePageState extends State<MyHomePage> { int _counter = 0; StreamSubscription streamSubscription; @override void initState() { streamSubscription = MyApp.model.counterUpdates.listen((newVal) => setState(() { _counter = newVal; })); super.initState(); }
initState()
bom lugar para definir o ouvinte e, como bons cidadãos de Darts, sempre liberamos a assinatura em dispose()
, certo?
Na árvore de widgets, precisamos apenas adaptar o manipulador onPressed do botão FAB (botão com uma ação flutuante).
floatingActionButton: new FloatingActionButton( onPressed: MyApp.model.incrementCounter, tooltip: 'Increment', child: new Icon(Icons.add), ),
Dessa maneira, criamos uma separação limpa entre View e Model usando o Stream.
Aplicar StreamBuilder
Fonte
Em vez de usar initState()
e setState()
para nossas necessidades, o Flutter vem com um conveniente widget StreamBuilder
. Como você deve ter adivinhado, são necessárias uma função Stream e um método construtor chamado sempre que o Stream retorna um novo valor. E agora não precisamos de inicialização e liberação explícitas:
body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Text( 'You have pushed the button this many times:', ), StreamBuilder<int>( initialData: 0, stream: MyApp.model.counterUpdates, builder: (context, snappShot) { String valueAsString = 'NoData'; if (snappShot != null && snappShot.hasData) { valueAsString = snappShot.data.toString(); } return Text( valueAsString, style: Theme.of(context).textTheme.display1, ); }), ], ), ),
Estamos quase terminando, prometo. Aqui estão três coisas que você deve saber:
- A grande vantagem de usar o StreamBuilder sobre a primeira solução é que chamar
setState()
em listen()
sempre reorganiza a página inteira, enquanto o StreamBuilder chama apenas seu builder
- A variável
snapShot
contém os dados mais recentes recebidos do Stream. Sempre verifique se ele contém dados válidos antes de usá-lo. Com base nos princípios de inicialização durante, o StreamBuilder não pode obter um valor durante o primeiro quadro. Para contornar isso, passamos o valor para initialData
, que é usado para o primeiro assembly, ou seja, para o primeiro quadro da tela. Se não passarmos dados initialData
, nosso construtor será chamado pela primeira vez com dados inválidos. Uma alternativa ao uso initialData
é retornar um widget de espaço reservado se snapShot
inválido, que é exibido até obtermos dados válidos, por exemplo:
No próximo post, veremos como converter dados em nossos fluxos e fazê-lo em tempo real. Muito obrigado a Scott Stoll por ler as provas e comentários importantes.