Tremulação sob o capô

Olá a todos! Meu nome é Mikhail Zotiev, trabalho como desenvolvedor Flutter na Surf. Eu, como provavelmente a maioria dos outros desenvolvedores que trabalham com o Flutter, gosto principalmente de como é fácil criar aplicativos bonitos e convenientes com sua ajuda. Leva muito pouco tempo para entrar no desenvolvimento do Flutter. Recentemente, trabalhei no desenvolvimento de jogos e agora mudei completamente para o desenvolvimento móvel multiplataforma no Flutter.

Qual é a simplicidade? Com uma dúzia de widgets básicos, você pode criar interfaces de usuário bastante decentes. E com o tempo, quando a bagagem usada é bastante decente, é improvável que alguma tarefa o deixe parado: seja um design incomum ou uma animação sofisticada. E o mais interessante - provavelmente você pode usá-lo sem nem pensar na pergunta: "Como funciona?"

Como Flutter tem código aberto, decidi descobrir o que está por trás (no lado Dart da Força) e compartilhá-lo com você.



Ferramenta


Todos nós ouvimos a frase da equipe de desenvolvimento de estruturas mais de uma vez: "Tudo no Flutter é widgets" . Vamos ver se é realmente assim. Para fazer isso, passamos à classe Widget (a seguir - o widget) e começamos a nos familiarizar gradualmente com o conteúdo.

A primeira coisa que leremos na documentação da classe:
Descreve a configuração para um [elemento].

Acontece que o próprio widget é apenas uma descrição de algum elemento (daqui em diante - o elemento).
Widgets são a hierarquia de classe central na estrutura Flutter. Um widget é uma descrição imutável de parte de uma interface do usuário. Os widgets podem ser inflados em elementos, que gerenciam a árvore de renderização subjacente.
Para resumir, a frase "Tudo no Flutter é um widget" é o nível mínimo de entendimento de como tudo está organizado para usar o Flutter. O widget é a classe central na hierarquia Flutter. Ao mesmo tempo, existem muitos mecanismos adicionais ao seu redor que ajudam a estrutura a lidar com sua tarefa.

Então, aprendemos mais alguns fatos:

  • widget - uma descrição imutável de uma parte da interface do usuário;
  • o widget está associado a uma visão avançada chamada elemento;
  • um elemento controla alguma entidade da árvore de renderização.

Você deve ter notado uma coisa estranha. A interface do usuário e a imutabilidade se encaixam muito mal, diria até que esses são conceitos completamente incompatíveis. Mas voltaremos a isso quando surgir uma imagem mais completa do dispositivo do mundo Flutter, mas, por enquanto, continuaremos a nos familiarizar com a documentação do widget.
Os próprios widgets não têm estado mutável (todos os seus campos devem ser finais).
Se você deseja associar um estado mutável a um widget, considere usar um [StatefulWidget], que cria um objeto [State] (via [StatefulWidget.createState]) sempre que for inflado em um elemento e incorporado à árvore.
Este parágrafo complementa um pouco o primeiro parágrafo: se precisarmos de uma configuração mutável, usamos a entidade State especial (doravante denominada state), que descreve o estado atual desse widget. No entanto, o estado não está associado ao widget, mas à sua representação elementar.
Um determinado widget pode ser incluído na árvore zero ou mais vezes. Em particular, um determinado widget pode ser colocado na árvore várias vezes. Cada vez que um widget é colocado na árvore, ele é inflado em um [Elemento], o que significa que um widget que é incorporado à árvore várias vezes será inflado várias vezes.
O mesmo widget pode ser incluído na árvore de widgets várias vezes ou nem sequer ser incluído. Mas toda vez que um widget é incluído na árvore de widgets, um elemento é mapeado para ele.

Então, nesta fase, os widgets estão quase prontos, vamos resumir:

  • widget - a classe central da hierarquia;
  • widget é alguma configuração;
  • widget - uma descrição imutável de uma parte da interface do usuário;
  • o widget está associado a um elemento que controla a renderização de alguma maneira;
  • o estado alterado do widget pode ser descrito por alguma entidade, mas não está conectado ao widget, mas ao elemento que representa esse widget.

Elemento


Pelo que aprendemos, a pergunta implora: "Quais são esses elementos que governam tudo?" Faça o mesmo - abra a documentação para a classe Element.
Uma instanciação de um [Widget] em um local específico na árvore.
Um elemento é uma representação de um widget em um local específico em uma árvore.
Os widgets descrevem como configurar uma subárvore, mas o mesmo widget pode ser usado para configurar várias subárvores simultaneamente, porque os widgets são imutáveis. Um [Elemento] representa o uso de um widget para configurar um local específico na árvore. Com o tempo, o widget associado a um determinado elemento pode mudar, por exemplo, se o widget pai for reconstruído e criar um novo widget para esse local.
O widget descreve a configuração de alguma parte da interface do usuário, mas como já sabemos, o mesmo widget pode ser usado em diferentes locais da árvore. Cada um desses locais será representado por um elemento correspondente. Porém, com o tempo, o widget associado ao item pode ser alterado. Isso significa que os elementos são mais tenazes e continuam a ser usados, apenas atualizando suas conexões.

Esta é uma decisão bastante racional. Como já definimos acima, os widgets são uma configuração imutável que simplesmente descreve uma parte específica da interface, o que significa que eles devem ser muito leves. E os elementos na área cujo controle é muito mais pesado, mas eles não são recriados desnecessariamente.

Para entender como isso é feito, considere o ciclo de vida de um elemento:

  • Widget.createElement , .
  • mount . .
  • .
  • , (, ), . runtimeType key, . , , .
  • , , , , ( deactivate).
  • , . , , (unmount), .
  • Quando você inclui novamente elementos na árvore, por exemplo, se o elemento ou seus ancestrais tiverem uma chave global, ele será removido da lista de elementos inativos, o método de ativação será chamado e o objeto renderizado associado a esse elemento será incorporado novamente na árvore de renderização. Isso significa que o item deve aparecer na tela novamente.

Na declaração de classe, vemos que o elemento implementa a interface BuildContext. Um BuildContext é algo que controla a posição de um widget em uma árvore de widgets, como segue em sua documentação. Corresponde quase exatamente à descrição do item. Essa interface é usada para evitar a manipulação direta do elemento, mas ao mesmo tempo fornece acesso aos métodos de contexto necessários. Por exemplo, findRenderObject, que permite encontrar o objeto da árvore de renderização correspondente a esse elemento.

Renderderbject


Resta lidar com o último link dessa tríade - RenderObject . Como o nome indica, este é um objeto da árvore de visualização. Ele tem um objeto pai, bem como um campo de dados que o objeto pai usa para armazenar informações específicas sobre esse objeto em si, por exemplo, sua posição. Este objeto é responsável pela implementação dos protocolos básicos de renderização e layout.

RenderObject não limita o modelo de uso de objetos filhos: pode haver nenhum, um ou muitos. Além disso, o sistema de posicionamento não se limita a: o sistema cartesiano, coordenadas polares, tudo isso e muito mais está disponível para uso. Não há restrições quanto ao uso de protocolos de localização: ajustando a largura ou altura, limitando o tamanho, especificando o tamanho e a localização do pai ou, se necessário, usando os dados do objeto pai.

Imagem do mundo da vibração


Vamos tentar criar uma imagem geral de como tudo funciona juntos.

Já observamos acima, o widget é uma descrição imutável, mas a interface do usuário não é de todo estática. Essa discrepância é removida dividindo-se em três níveis de objetos e a divisão de zonas de responsabilidade.

  • , .
  • , .
  • , — , .

imagem

Vejamos como essas árvores se parecem com um exemplo simples:

imagem

Nesse caso, temos alguns StatelessWidget agrupados em um widget Padding e contendo texto dentro.

Vamos nos colocar no lugar do Flutter - nos foi dada essa árvore de widgets.

Flutter: “Ei, Padding, preciso do seu elemento”
Padding: “Claro, segure SingleChildRenderObjectElement”

imagem

Flutter: “Elemento, aqui é o seu lugar,
acalme-se SingleChildRenderObjectElement: “Gente, está tudo bem, mas eu preciso do RenderObject”
Flutter: “Padding, like desenhar você? ”
Preenchimento: “Hold it, RenderPadding”
SingleChildRenderObjectElement: “Ótimo, comece a trabalhar”

imagem

Flutter:"Então, quem é o próximo?" StatelessWidget, agora você deixa o elemento »
StatelessWidget: « Aqui StatelessElement »
Flutter: « StatelessElement, você estará sujeito a SingleChildRenderObjectElement, aqui está o lugar, embarcando »
StatelessElement: « OK »

imagem

Flutter: « o RichText, elemento presente, por favor »
o RichText fornece MultiChildRenderObjectElement
Flutter: “MultiChildRenderObjectElement, aqui está, comece”
MultiChildRenderObjectElement: “Preciso de uma renderização para trabalhar”
Flutter: “RichText, precisamos de um objeto de renderização”
RichText: “Aqui está um RenderParagraph”
Flutter:“RenderParagraph você receberá instruções RenderPadding e controlará MultiChildRenderObjectElement”
MultiChildRenderObjectElement: “Agora está tudo bem, estou pronto”

imagem

Certamente você fará uma pergunta legítima: “Onde está o objeto de renderização para StatelessWidget, por que não está lá, decidimos acima que os elementos vinculam configurações com display? " Vamos prestar atenção na implementação básica do método mount, discutida nesta seção da descrição do ciclo de vida.

void mount(Element parent, dynamic newSlot) {
    assert(_debugLifecycleState == _ElementLifecycle.initial);
    assert(widget != null);
    assert(_parent == null);
    assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
    assert(slot == null);
    assert(depth == null);
    assert(!_active);
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null)
        _owner = parent.owner;
    if (widget.key is GlobalKey) {
        final GlobalKey key = widget.key;
        key._register(this);
    }
    _updateInheritance();
    assert(() {
        _debugLifecycleState = _ElementLifecycle.active;
        return true;
    }());
}

Não veremos nela a criação de um objeto de renderização. Mas o elemento implementa um BuildContext, que possui um método de pesquisa de objeto de visualização findRenderObject, que nos levará ao seguinte getter:

RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
        assert(result == null); 
        if (element is RenderObjectElement)
            result = element.renderObject;
        else
            element.visitChildren(visit);
    }
    visit(this);
    return result;
}

No caso básico, um elemento pode não criar um objeto de renderização; somente RenderObjectElement e seus descendentes são necessários para fazer isso; no entanto, nesse caso, um elemento em algum nível de aninhamento deve ter um elemento filho que possui um objeto de renderização.

Parece porque todas essas dificuldades. Até três árvores, diferentes áreas de responsabilidade, etc. A resposta é bastante simples - é aqui que o desempenho do Flutter é construído. Os widgets são configurações imutáveis; portanto, são frequentemente recriados, mas ao mesmo tempo são bastante leves, o que não afeta o desempenho. Mas Flutter está tentando reutilizar elementos pesados ​​o máximo possível.

Considere um exemplo.

Texto no meio da tela. O código neste caso será mais ou menos assim:

body: Center(
    child: Text(“Hello world!”)
),

Nesse caso, a árvore de widgets ficará assim:

imagem

Depois que o Flutter cria as três árvores, obtemos a seguinte imagem:

imagem

O que acontece se mudarmos o texto que vamos exibir?

imagem

Agora temos uma nova árvore de widgets. Acima, falamos sobre o máximo possível de reutilização de elementos. Dê uma olhada no método da classe Widget, sob o nome falante canUpdate .

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
}

Verificamos o tipo do widget anterior e o novo, bem como suas chaves. Se eles são iguais, não há necessidade de alterar o item.

Portanto, antes da atualização, o primeiro elemento é o Center, após a atualização, também o Center. Ambos não têm chaves, uma completa coincidência. Podemos atualizar o link do item para um novo widget.

imagem

Mas, além do tipo e da chave, o widget é uma descrição e configuração, e os valores dos parâmetros necessários para a exibição podem mudar. É por isso que o elemento, após atualizar o link para o widget, deve iniciar atualizações no objeto de renderização. No caso do Center, nada mudou e continuamos a comparar ainda mais.

Mais uma vez, o tipo e a chave nos dizem que não faz sentido recriar o elemento. O texto é descendente de StatelessWidget e não possui um objeto de exibição direta.

imagem

Vá para RichText. O widget também não mudou seu tipo; não há discrepâncias nas chaves. O item atualiza sua associação com o novo widget.

imagem

A conexão é atualizada, resta apenas atualizar as propriedades. Como resultado, RenderParagraph exibirá o novo valor de texto.

imagem

E assim que chegar a próxima etapa do desenho, veremos o resultado que esperamos.

Graças a esse tipo de trabalho, o Flutter atinge um desempenho tão alto.

O exemplo acima descreve o caso em que a estrutura do widget em si não foi alterada. Mas o que acontece se a estrutura mudar? O Flutter, é claro, continuará tentando maximizar o uso de objetos existentes, como entendemos a partir da descrição do ciclo de vida, mas novos elementos serão criados para todos os novos widgets, e os antigos e mais desnecessários serão excluídos no final do quadro.

Vejamos alguns exemplos. E para garantir o que foi dito acima, usamos a ferramenta Android Studio - Flutter Inspector.

@override
Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: _isFirst ? first() : second(),
        ),
        floatingActionButton: FloatingActionButton(
            child: Text("Switch"),
            onPressed: () {
                setState(() {
                    _isFirst = !_isFirst;
                });
            },
        ),
    );
}

Widget first() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "test",
            style: TextStyle(fontSize: 25),
        ),
        SizedBox(
            width: 5,
        ),
        Icon(
            Icons.error,
        ),
    ],
);

Widget second() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "one more test",
            style: TextStyle(fontSize: 25),
        ),
        Padding(
            padding: EdgeInsets.only(left: 5),
        ),
        Icon(
            Icons.error,
        ),
    ],
);

Nesse caso, clicando no botão, um dos widgets será alterado. Vamos ver o que o inspetor nos mostra.

imagem

imagem

Como podemos ver, Flutter recriou a renderização apenas para Padding, o restante foi reutilizado.

Considere mais uma opção na qual a estrutura muda de maneira mais global - alteramos os níveis de aninhamento.

Widget second() => Container(child: first(),);

imagem

imagem

Apesar do fato de a árvore não ter mudado visualmente, os elementos e objetos da árvore de renderização foram recriados. Isso aconteceu porque o Flutter se compara por nível (nesse caso, não importa que a maior parte da árvore não tenha sido alterada). A peneiração dessa parte ocorreu no momento da comparação entre Container e Row. No entanto, pode-se sair dessa situação. Isso nos ajudará a GlobalKey. Adicione uma chave para Row.

var _key = GlobalKey(debugLabel: "testLabel");

Widget first() => Row(
    key: _key,
    …
);

imagem

imagem

Assim que dissemos a Flutter que a peça poderia ser reutilizada, ele aproveitou a oportunidade.

Conclusão


Nós nos familiarizamos um pouco mais com a magia do Flutter e agora sabemos que ela não é apenas em widgets.

O Flutter é um mecanismo bem coordenado e bem planejado, com sua própria hierarquia, áreas de responsabilidade, com as quais você pode criar aplicativos não apenas bonitos, mas também produtivos. Obviamente, examinamos apenas uma parte pequena, embora bastante importante de seu dispositivo, e continuaremos analisando vários aspectos do funcionamento interno da estrutura em artigos futuros.

Espero que as informações neste artigo sejam úteis para entender como o Flutter funciona internamente e ajuda a encontrar soluções elegantes e produtivas durante o desenvolvimento.

Obrigado pela atenção!

Recursos


Flutter
"Como Flutter renderiza Widgets", de Andrew Fitz Gibbon, Matt Sullivan

All Articles