Tipos de construtores em Dart: guia prático para iniciantes
18 min18 minutos de leitura
18 min18 minutos de leitura
Autor(a)
Jhoisnáyra Vitória Rodrigues de Almeida
Scuba Team Mobile (com foco em Flutter). Estudante de Ciência da Computação na UFPI, pesquisadora da área de Internet das Coisas e formada em Eletrônica pelo IFPI.
Inscreva-se em nossa Newsletter
Fique por dentro de conteúdos, insights e oportunidades do universo tech. Receba novidades e lançamentos direto no seu e-mail.
Se você já conhece a linguagem Dart, ou está iniciando seus estudos sobre classes em Dart, pode se deparar com os diversos tipos de construtores disponíveis.
O Dart possui construtores parecidos com outras linguagens, facilitando a assimilação, e outros um pouco mais avançados, como o factory Dart, que exigem um pouco mais de prática.
Entender o que são construtores, quais tipos existem e como usá-los pode te ajudar a fazer códigos ainda mais legíveis e otimizados, vamos mergulhar nessa leitura para aprender mais sobre o assunto?
O que são construtores em Dart e como funcionam nas classes Dart?
Dart é uma linguagem que segue o paradigma de Orientação a Objetos (OO), ou seja, utiliza o conceito de classe, que nada mais é do que a representação de objetos com características e comportamentos em comum.
Simplificando, a classe é um “molde” ou uma “forma” (no sentido de forma de bolo, por exemplo). Vamos ver um exemplo?
Imagine que queremos representar diversos funcionários de um escritório de vendas de papéis e materiais de escritório chamado Dunder Mifflin, que possui vários funcionários (recepcionista, vendedores, gerente, entre outros). Podemos criar a classe de um objeto chamado `Funcionario`, em Dart:
Esta classe (funcionário) possui quatro atributos ou características, que são `nome`, `idade`, `cargo` e `hobby`. Agora podemos criar uma instância (um objeto) dessa classe, e é aí que entram os construtores.
Assim, os construtores “constroem” objetos (instâncias) a partir de uma classe - vamos entender isso, na prática, a seguir.
Construtor em Dart e Syntax sugar: como inicializar classes
Caso você não implemente seu construtor em uma classe, o Dart gera um construtor padrão, e, ao criarmos uma instância dessa classe, ele é chamado. Porém, esse construtor não possui argumentos e normalmente não é isso que queremos.
Logo, vamos criar nosso construtor!
Ainda utilizando a classe `Funcionario`, do exemplo anterior, o construtor deve receber um parâmetro chamado `nome` (do tipo String, ou seja, uma cadeia de caracteres), outro chamado `idade` (do tipo `int`, um valor numérico inteiro), e por último `cargo` e `hobby` (ambos também do tipo String):
Repare que estamos usando o sinal `?` na frente da declaração dos tipos de nossas variáveis. Por quê? Isso acontece pois, nesse tipo de construtor, essas variáveis devem ser "nullables", isto é, podem receber valores nulos ou não-nulos. É importante ressaltar que desde o Dart 2.12, o null safety é obrigatório e tipos são não-anuláveis por padrão. Desde o Dart 3.0, o suporte a código não null-safe foi completamente removido.
Existe ainda uma forma melhor de escrever exatamente a mesma declaração, o chamado Syntax Sugar (Sintaxe doce, em livre tradução).
Mas… o que é isso, mesmo?
Syntax Sugar refere-se a formas de simplificar e facilitar a escrita e leitura do código, tornando-o mais elegante e claro. Dá uma olhada em como ficaria o nosso construtor com a Syntax Sugar:
Observe que, nesse caso, não usamos o sinal `?` na declaração dos nossos atributos, pois o objeto `Funcionario`, ao ser inicializado, terá necessariamente valores não nulos!
Tanto para o construtor comum quanto para o construtor que utiliza Syntax Sugar, podemos criar a instância do objeto Funcionario da seguinte forma:
Antes de vermos exemplos de cada um deles, precisamos entender o que significam os termos posicional, nomeado, obrigatório e opcional.
Imagine que estamos construindo a aplicação para registrar o cadastro de funcionários do escritório. Existem dois tipos de informações das pessoas.
Informações obrigatórias: nome, idade, cargo;
Informações opcionais: como hobby.
Pensando, primeiro, nas informações obrigatórias (como nome, idade e cargo), podemos relacioná-las com os parâmetros obrigatórios: são aqueles que obrigatoriamente precisamos passar através do construtor no momento da chamada, ou seja, não podemos deixá-los vazios.
Em contrapartida, temos informações opcionais que estão ligadas aos parâmetros opcionais - os quais são o oposto do obrigatório -, pois eles podem ou não ser passados através do construtor (como o hobby, por exemplo).
O nome diz tudo: são parâmetros opcionais na hora de passá-los no construtor. Tudo depende da escolha da pessoa programadora no momento da criação do objeto.
Agora, para entender melhor os parâmetros posicionais e nomeados:
Parâmetros posicionais são aqueles que precisam estar numa posição específica durante a criação do objeto com o construtor;
Os parâmetros nomeados não precisam de uma posição ou ordem específica, pois eles recebem e são identificados por nomes específicos.
Vamos ver alguns exemplos?
Parâmetros Opcionais Posicionais em Dart
Vimos que:
Parâmetros opcionais são aqueles que não precisamos necessariamente passá-los através do construtor (na hora de criar um objeto);
Parâmetros posicionais precisam estar em uma ordem específica para serem reconhecidos.
Abaixo, veja um exemplo no qual o parâmetro `hobby` é transformado em opcional posicional:
Nesse exemplo, podemos (ou não) passar o parâmetro `hobby` e, dessa maneira, o atributo precisa ser nullable, isto é, pode (ou não) ser nulo. Para deixá-lo nulo, escrevemos um sinal de `?` na definição do seu tipo dentro da classe:
Outra opção é atribuir um valor à variável dentro do próprio construtor, assim, caso não seja passado nenhum valor, a variável ainda assim não será nula. Por exemplo, se quisermos que, por padrão, quando não passamos o valor de `hobby` ela seja `Sem Hobby`, podemos fazer da seguinte maneira:
Em ambos os casos, tanto utilizando uma variável nullable na classe quanto utilizando um valor padrão no construtor, podemos criar objetos que podem ou não ter o atributo `hobby`, veja só:
> “E se eu criar um objeto do tipo Funcionario com os atributos em ordens diferentes?”
Nessa situação, caso você troque valores de variáveis do mesmo tipo no construtor, não haverá erros (é tranquilo trocar `nome` por `cargo` ou `hobby` - todos são strings), mas se forem tipos diferentes, haverá problema (como trocar `idade` por `nome` e/ou `cargo` e/ou `hobby` - ou seja, trocar um int por string), exemplo:
O código acima gera um erro de compilação com a seguinte mensagem: “O tipo de argumento 'String' não pode ser atribuído ao tipo de parâmetro 'int”, pois trocamos a ordem dos valores de `cargo` por `idade`.
Esses são cuidados importantes ao trabalhar com parâmetros posicionais.
Parâmetros Opcionais Nomeados em Dart
Quando usamos parâmetros opcionais nomeados, eles podem (ou não) ser passados, porém eles possuem nomes, ou seja, são identificados através de nomeações e não posições. Vamos tornar o atributo `hobby` opcional e nomeado:
Como você vê, usamos chaves - {} - para criar parâmetros opcionais e nomeados.
Do mesmo modo que no exemplo anterior, ao tornar essa variável opcional, ela pode ou não ser nula. Portanto, devemos adicionar o sinal `?` na frente do seu tipo ou podemos usar um valor padrão dentro do próprio construtor (para que ela seja preenchida e, assim, não seja nula):
Quando passamos os valores para o construtor, eles precisam estar completos (sem parâmetros a mais ou a menos) e na posição correta (nome, idade, cargo, hobby):
Esses são os parâmetros mais rígidos: a posição específica não pode mudar, porém o valor pode mudar (se for do mesmo tipo).
Parâmetros Obrigatórios Nomeados em Dart
Você pode querer também utilizar parâmetros obrigatórios que sejam nomeados, para que não precisem estar em um ordem específica e até mesmo para deixar mais legível a construção do seu objeto.
E, para torná-lo nomeado, pode utilizar chaves ({}).
Porém, ao utilizar apenas as chaves, os parâmetros tornam-se nomeados e opcionais. Para torná-los obrigatórios, usamos o modificador required antes do argumento, tornando sua passagem necessária no construtor. Veja o exemplo:
Tornamos os parâmetros `nome`, `idade`, `cargo` e `hobby` nomeados e obrigatórios e, dessa maneira, na criação de um objeto com esse tipo de construtor, faríamos da seguinte forma:
Para sintetizar os parâmetros dos construtores, deixo um infográfico com uma classe chamada `Bolo`, com `massa` e `recheio`; nessa classe, podemos deixar alguns atributos obrigatórios, outros opcionais e nomeados ou posicionais. Vamos ver:
O que é o construtor nomeado em Dart?
O Dart nos permite criar vários construtores dentro de uma mesma classe.
Como fazemos isso?
Bem, usamos os chamados construtores nomeados, que nada mais são do que construtores com nomes.
E qual a vantagem disso? Bom, os construtores nomeados apresentam duas vantagens:
Sobrecarga de construtores: Esse é o termo técnico para quando temos mais de um construtor dentro de uma classe. Às vezes, queremos que cada construtor tenha uma função específica; para diferenciá-los, colocamos um nome diferente para cada um. Isso permite mais liberdade ao escrever uma classe;
Legibilidade do código: construtores nomeados tornam o código mais legível e retornam uma instância exata de um objeto, assim como um construtor comum.
Imagine que, agora, em nosso cadastro de funcionários, existem casos em que o funcionário quer informar o hobby e casos em que ele não quer. Como solucionamos esse problema?
Poderíamos resolver isso utilizando um parâmetro opcional.
Outra solução seria usar mais um construtor! Assim, ficariam dois construtores:
O primeiro (não nomeado) retorna uma instância com o atributo `hobby`;
O segundo (nomeado como `semHobby`) retorna uma instância sem o atributo `hobby`.
Com a solução dos dois construtores nomeados, o código fica bem mais legível:
Agora temos dois construtores: um comum, que possui o atributo `hobby`, e outro nomeado, que não possui o atributo `hobby`. Podemos fazer uso dos dois sem problemas, veja:
Existem situações em que pode ser melhor criar objetos que não possam ser alterados, por fazer mais sentido e também em prol do desempenho da aplicação, visto que objetos que têm seus valores definidos e inalterados consomem menos memória que outros. Como poderíamos fazer isso em Dart?
Para criar uma instância da sua classe que não mude, podemos utilizar um construtor constante. Este construtor usa um modificador de imutabilidade chamado `const`, que determina que o objeto não pode inicializar sem um valor e também que, após receber um valor, ele não pode ser alterado.
Os atributos da classe usados nesse construtor também precisam do modificador de imutabilidade `final`, que, diferente de `const`, não exige um valor inicial na declaração, mas impede alterações após receber um valor. Vamos ver um exemplo:
Quando criamos uma instância dessa classe com esse tipo de construtor, usamos novamente o modificador `const` e não podemos alterar posteriormente qualquer atributo (ou recebemos um erro de compilação):
O que é Factory em Dart? Como funciona o construtor de fábrica na linguagem Dart
Para entendermos o que é o construtor de fábrica, é importante começarmos conhecendo o Método de Fábrica (em inglês, Factory Method), um Design Pattern, que nos auxilia na criação de objetos, aumentando a flexibilidade e reutilização de código.
O Método de Fábrica consiste em criar objetos sem expor a lógica de criação ao cliente; vejamos um exemplo:
Analisando o diagrama acima, descobrimos que é possível criar um código que tenha uma classe mãe chamada `Funcionario` e duas classes filhas chamadas `Gerente` e `Vendedor`, que devem ter as mesmas características da sua classe mãe.
O conceito usado para explicar esse cenário se chama herança, pois as classes filhas herdam os atributos e métodos da classe mãe. Vamos desenvolver nosso código:
// Classe mãe classFuncionario {
String nome;
int idade;
String hobby;
Funcionario(this.nome, this.idade, this.hobby);
}
// Classe filha classGerenteextendsFuncionario {
Gerente(super.nome, super.idade, super.hobby);
}
// Classe filha classVendedorextendsFuncionario {
Vendedor(super.nome, super.idade, super.hobby);
}
Dessa maneira, como você pode observar, não temos mais o atributo `cargo`, pois vamos utilizar uma lógica interna (dentro da classe `Funcionario`) para definir qual instância devemos retornar (`Gerente` ou `Vendedor`).
Para isso, precisamos então utilizar o nosso construtor de fábrica, usando a palavra chave `factory` na frente do construtor nomeado e retornando para cada caso uma instância diferente:
No código acima, temos um parâmetro opcional nomeado chamado `cargo` que recebemos a fim de usar uma estrutura de seleção condicional (`switch case`), na qual analisamos esse parâmetro e, para cada caso, retornamos algo diferente:
se recebermos “Gerente”, retornamos uma instância do tipo `Gerente`;
se recebermos “Vendedor(a)”, retornamos uma instância do tipo `Vendedor`; e
caso a gente não receba nada (ou chegue algo que não se encaixe nas opções anteriores), apenas retornamos uma instância de `Funcionario`.
Podemos testar nosso construtor criando uma instância para cada caso, dá uma olhada:
Legal, não é? Agora a nossa lógica de criação de objetos está bem mais organizada e você pode inclusive criar outros cargos (classes filhas) e adicionar à lógica.
O que é e como usar o construtor de redirecionamento em Dart?
Um construtor de redirecionamento pode ser usado em quatro casos para:
Redirecionar a um outro construtor;
Produzir um construtor comum;
Fazer um construtor com verificações;
Invocar uma classe mãe.
Vamos ver um exemplo de cada caso!
Para entender melhor os casos, vamos antes entender um conceito importante: a lista de inicializadores.
Entendendo a lista de inicializadores
O construtor redirecionador usa um conceito chamado “Lista de Inicializadores”. Assim, para entender esse conceito, vamos usar um exemplo muito usado no cotidiano da pessoa programadora de Dart que é a aplicação que lida com conexão à internet.
É comum que informações da internet venham em um formato conhecido chamado JSON que pode ser transformado em um formato de variável chamado `map`.
Caso não saiba o que é um Map, por enquanto você só precisa entender que Map é um tipo de dado bastante utilizado, como uma string ou int.
Nesse caso, o redirecionamento pode ser utilizado para receber um valor do tipo JSON na nossa classe e inicializar os atributos do objeto. Veja, abaixo, um exemplo em que temos um construtor nomeado chamado `fromJson` (em português, “de Json”) que recebe um dado `map` e constrói o objeto a partir dele:
Dessa forma, cada linha separada por vírgula inicializa uma propriedade do objeto.
Podemos usar a Lista de Inicializadores para inicializar cada propriedade uma a uma, mas também é possível inicializar todos de uma vez com a ajuda de um construtor que já existe, e é exatamente isso que é um Construtor Redirecionador!
Conhecendo o construtor “redirecionador”
Começando por um exemplo em que queiramos redirecionar um construtor para outro: imagine que temos dois construtores: um nomeado (`semHobby`), e outro comum. Assim, queremos que o construtor nomeado apenas redirecione para o comum e atribua à `hobby` o valor `null`.
Vale saber que essa técnica existe. No entanto, o Dart recomenda dar preferência à opção baseada em Syntax Sugar a seguir.
Usando o construtor de redirecionamento comum
Ele também pode ser aplicado como um construtor comum, sem alterações no funcionamento normal, embora a sintaxe simplificada (Syntax Sugar) seja mais usada, veja:
Usando o construtor de redirecionamento para fazer verificações
Utilizamos esse caso quando queremos fazer testes automatizados em um projeto. Ele faz verificações utilizando o assert, no qual podemos usar um construtor com redirecionamento que testa se um valor satisfaz alguma condição, por exemplo:
No exemplo acima, estamos verificando se o valor do parâmetro `idade` é maior ou igual a 18.
Lembre-se de que o objeto ainda pode ser criado mesmo que não atenda à condição, pois a verificação com assert é útil especialmente para testes durante o desenvolvimento em Dart.
Usando construtor de redirecionamento para invocar uma classe mãe
Por fim, você pode já saber que o redirecionamento é usado quando trabalhamos com herança, isto é, uma classe herda características e comportamentos de outra.
No exemplo utilizado no tópico sobre Construtor de Fábrica, não utilizamos o construtor de redirecionamento, mas poderíamos! Veja:
// Classe filha: Gerente
classGerenteextendsFuncionario{
Gerente(String nome, int idade, String hobby) : super(nome, idade, hobby);
}
No exemplo acima, estamos utilizando um construtor de redirecionamento na classe filha chamada `Gerente` que estende da mãe `Funcionario`.
Conclusão
Parabéns por ter concluído a leitura!
Agora você já deve ser capaz de compreender o que são e como utilizar os diversos construtores que apresentei: comum, syntax sugar, nomeado, constante, de fábrica e redirecionamento, além dos tipos de parâmetros que podemos usar neles.
Experimente testar os códigos, fazer alterações e criar suas próprias classes e construtores para praticar e consolidar o conhecimento.