Skip to main content


Classes e orientação a objetos


A Orientação a Objetos é uma forma de organizar o código. Em vez de só usar variáveis e funções soltas, você agrupa dados e ações dentro de algo chamado classe. A classe é um molde, graças a esse esse molde (classe) nós podemos criar objetos.


Cada vez que você usa uma classe para criar algo, o código gera um novo objeto com base naquele molde, ou seja, uma nova instância. Essa instância é independente das outras: ela segue a estrutura definida pela classe, mas armazena seus próprios dados.


Uma classe define a estrutura e o comportamento de um tipo de objeto. Isso inclui atributos (variáveis associadas a cada instância) e métodos (funções que operam sobre esses atributos).


Exemplos de objetos podem ser:

Usuário
Produto
Conta bancária
Pedido
Servidor
Pagamento

Cada objeto pode ter:

ParteSignificado
AtributosDados do objeto
MétodosAções do objeto
ConstrutorForma de criar o objeto


O que é uma classe


Uma classe é um modelo para criar objetos.

class Usuario {
String nome;
int idade;

Usuario(this.nome, this.idade);
}

A linha abaixo cria uma classe chamada Usuario.

class Usuario {

A linha abaixo representa os atributos da classe.

String nome;
int idade;

Já a linha abaixo representa o construtor, ele define como criar um objeto Usuario.

Usuario(this.nome, this.idade);


O que é um objeto


O Objeto é uma instância de uma classe, já a classe é o modelo. O objeto é o item criado a partir desse modelo.

class Usuario {
String nome;
int idade;

Usuario(this.nome, this.idade);
}

void main() {
Usuario usuario = Usuario('Fulano', 27);

print(usuario.nome);
print(usuario.idade);
}

this

O this é usado para acessar atributos de uma classe, não importa em qual parte da classe esteja sendo executada. Ele é parecido com o self do Python, mas em Dart não precisa ser declarado como parâmetro nos métodos.


A linha abaixo cria um objeto da classe Usuario.

Usuario usuario = Usuario('Fulano', 27);

Se você já conhece linguagens como Python, pode estranhar um pouco a forma de criar objetos em Dart. Em Python, normalmente escreveríamos algo assim:

usuario = Usuario('Fulano', 27)

Já em Dart, podemos escrever:

Usuario usuario = Usuario('Fulano', 27);

A primeira palavra Usuario indica o tipo da variável. Isso significa que a variável chamada usuario só deve guardar objetos da classe Usuario.


A segunda parte, chama o construtor da classe Usuario, criando um novo objeto com os valores 'Fulano' e 27.

Usuario('Fulano', 27)

Separando a linha:

Usuario usuario = Usuario('Fulano', 27);

Temos:

  • Usuario → tipo da variável
  • usuario → nome da variável
  • Usuario('Fulano', 27) → criação do objeto

Em Dart, também é possível usar var para deixar o compilador descobrir o tipo automaticamente:

var usuario = Usuario('Fulano', 27);

Nesse caso, o Dart entende que usuario é do tipo Usuario, porque o valor atribuído foi criado a partir da classe Usuario.


Isso deve ser assim porque a variável é do tipo da classe, isso ocorre porque a classe define o formato daquele objeto. Se a classe é Usuario, o objeto criado a partir dela é do tipo Usuario.


Isso permite ao Dart saber quais atributos e métodos existem e impedir valores incompatíveis.



Classe x objeto


Vamos começar criando apenas uma classe:

class Produto {
String nome;
double preco;

Produto(this.nome, this.preco);
}

Ela ainda não é um produto real no programa. Agora precisamos criar um objeto:

void main() {
Produto mouse = Produto('Mouse', 50.00);
Produto teclado = Produto('Teclado', 150.00);

print(mouse.nome);
print(teclado.nome);
}

Como podemos ver, a classe define a estrutura. Já o objeto guarda os valores reais.



Atributos


Atributos são variáveis que pertencem a uma classe.

class Servidor {
String hostname;
String ip;
bool ativo;

Servidor(this.hostname, this.ip, this.ativo);
}

Aqui os atributos são:

String hostname;
String ip;
bool ativo;

Criando o objeto:

void main() {
Servidor servidor = Servidor('web01', '192.168.0.10', true);

print(servidor.hostname);
print(servidor.ip);
print(servidor.ativo);
}


Ordem dos Atributos


A classe não usa a ordem dos atributos declarados para saber como preencher os valores. Para isso, ela usa a ordem dos parâmetros definidos no construtor.

class Servidor {
String hostname;
String ip;
bool ativo;

Servidor(this.hostname, this.ip, this.ativo);
}

Neste código, os atributos da classe são:

String hostname;
String ip;
bool ativo;

A ordem usada para preencher esses atributos é definida pelo construtor:

  Servidor(this.hostname, this.ip, this.ativo);

Isso significa que, ao criar um objeto, os valores devem ser passados nessa mesma ordem:

void main() {
Servidor servidor = Servidor('web01', '192.168.0.10', true);
}

Então a ordem esperada é:

  • 1º valor → hostname
  • 2º valor → ip
  • 3º valor → ativo

Portanto, quem define a ordem dos valores recebidos é o construtor, não a ordem em que os atributos foram declarados na classe.


Para evitar confusão com ordem, você pode usar parâmetros nomeados:

class Servidor {
String hostname;
String ip;
bool ativo;

Servidor({
required this.hostname,
required this.ip,
required this.ativo,
});
}

void main() {
Servidor servidor = Servidor(
hostname: 'web01',
ip: '192.168.0.10',
ativo: true,
);

print(servidor.hostname);
print(servidor.ip);
print(servidor.ativo);
}


Usando this


O this representa o próprio objeto.

class Usuario {
String nome;

Usuario(this.nome);
}


Declaração de atributos em Python vs Dart


Em Python, a criação de atributos é mais flexível, já que um atributo pode ser criado diretamente dentro de um método usando self, mesmo que ele não tenha sido declarado antes no corpo da classe, exemplo:

class Usuario:
def carregar(self):
self.nome = 'Fulano'

Nesse exemplo, o atributo nome passa a existir no objeto quando o método carregar é executado. Python permite esse comportamento porque é uma linguagem mais dinâmica.


Já em Dart, o comportamento é diferente. Os atributos de um objeto normalmente precisam ser declarados no corpo da classe antes de serem usados, exemplo:

class Usuario {
String nome;

Usuario(this.nome);
}

Nesse caso, a linha abaixo declara que todo objeto da classe Usuario terá um atributo chamado nome:

String nome;

Depois disso, o construtor recebe um valor e inicializa esse atributo:

Usuario(this.nome);

Diferente do Python, Dart não permite criar um novo atributo de instância livremente dentro de qualquer método sem declará-lo antes na classe. Por exemplo, isso não funciona em Dart:

class Usuario {
void carregar() {
nome = 'Fulano'; // erro: nome não foi declarado
}
}

Para funcionar, o atributo precisa existir na classe:

class Usuario {
late String nome;

void carregar() {
nome = 'Fulano';
}
}

Nesse caso, usamos late para informar que o atributo será inicializado depois. A principal diferença é que Python permite criar atributos de forma dinâmica durante a execução, enquanto Dart exige uma estrutura mais definida. A classe declara quais atributos o objeto terá, e esses atributos são inicializados no construtor, com valor padrão, com late ou como nullable.



Alterando atributos


Depois de criar um objeto, você pode alterar seus atributos, se eles não forem final.

class Usuario {
String nome;
int idade;

Usuario(this.nome, this.idade);
}

void main() {
Usuario usuario = Usuario('Fulano', 27);

usuario.idade = 28;

print(usuario.idade);
}


Atributos final


Devemos usar final quando o atributo não deve mudar depois que o objeto foi criado.

class Usuario {
final String cpf;
String nome;

Usuario(this.cpf, this.nome);
}

void main() {
Usuario usuario = Usuario('123.456.789-00', 'Fulano');

usuario.nome = 'Fulano Silva';

print(usuario.cpf);
print(usuario.nome);
}


Métodos


O métodos são funções, mas que pertencem a uma classe.

class Usuario {
String nome;
bool ativo;

Usuario(this.nome, this.ativo);

void exibirStatus() {
if (ativo) {
print('$nome está ativo');
} else {
print('$nome está inativo');
}
}
}

void main() {
Usuario usuario = Usuario('Ana', true);

usuario.exibirStatus();
}

No exemplo acima, o void exibirStatus() { cria um método dentro da classe Usuario. Já a linha usuario.exibirStatus(); chama o método usando o objeto usuario.


Um método é parecido com função, mas pertence a uma classe.



Método usando atributos internos


Um método pode usar os atributos do próprio objeto.

class Produto {
String nome;
double preco;
int quantidade;

// Construtor:
Produto(this.nome, this.preco, this.quantidade);

// Método:
double calcularTotal() {
return preco * quantidade;
}
}

void main() {
Produto produto = Produto('Monitor', 900.00, 2);

print(produto.calcularTotal());
}

O método usa os atributos preco e quantidade do próprio objeto. Observe que dentro do método, não é obrigatório o uso do this ao usar atributos da classe. Em Python, o self é obrigatório em métodos de instância, já o Dart não torna isso obrigatório.


Ele não é obrigatório quando não existe conflito de nomes. Se tiver conflito, por exemplo, uma variável dentro de um método com o mesmo nome de um atributo do objeto, ai precisamos usar o this.



Construtores


O construtor é a parte da classe executada quando um novo objeto é criado. Ele é responsável por inicializar o objeto, recebendo os valores necessários e preenchendo os atributos definidos na classe.


Além disso, o construtor também pode executar validações iniciais e chamar métodos internos da própria classe para preparar o objeto antes de ele ser usado.

class Produto {
String nome;
double preco;

Produto(this.nome, this.preco) {
validarPreco();
}

void validarPreco() {
if (preco < 0) {
throw Exception('Preço não pode ser negativo');
}
}
}

Nesse exemplo, o construtor recebe nome e preco, inicializa os atributos e chama o método validarPreco() para garantir que o objeto não seja criado com um valor inválido.


O construtor Produto(this.nome, this.preco) { recebe dois valores e coloca diretamente nos atributos da classe. Essa é a forma curta de escrever e a mais usual, mas podemos escrever de forma que fique mais fácil de entender, como:

class Conta {
String titular;
double saldo;

Conta(String titularInformado, double saldoInformado)
: titular = titularInformado,
saldo = saldoInformado;
}

Outra forma seria com uso de late:

class Conta {
late String titular;
late double saldo;

Conta(String titular, double saldo) {
this.titular = titular;
this.saldo = saldo;
}
}

Mas isso não é o jeito mais recomendado, evite usar late, principalmente quando estiver começando com Dart.



Construtor com validação


Podemos validar dados ao criar o objeto.

class Produto {
String nome;
double preco;

Produto(this.nome, this.preco) {
if (preco < 0) {
throw Exception('Preço não pode ser negativo');
}
}
}

void main() {
Produto produto = Produto('Mouse', 50.00);

print(produto.nome);
print(produto.preco);
}

Isso impede a criação de um produto com preço inválido.



Construtor nomeado


Em Dart, podemos criar construtores nomeados. Eles servem para criar objetos de formas diferentes.

class Usuario {
String nome;
bool ativo;

Usuario(this.nome, this.ativo);

Usuario.ativo(this.nome) : ativo = true;
Usuario.inativo(this.nome) : ativo = false;
}

void main() {
Usuario ana = Usuario.ativo('Ana');
Usuario Fulano = Usuario.inativo('Fulano');

print(ana.ativo);
print(Fulano.ativo);
}

Construtores nomeados deixam a criação do objeto mais clara.



Composição de classes


A composição acontece quando uma classe usa outra classe internamente. Em vez de uma classe tentar guardar tudo sozinha, ela pode ter atributos que são objetos de outras classes.


Exemplo simples:

class Endereco {
String cidade;
String estado;

Endereco(this.cidade, this.estado);
}

class Usuario {
String nome;

// Declarando um atributo chamado endereco, dizendo que ele será do tipo Endereco:
Endereco endereco;

Usuario(this.nome, this.endereco);
}

Nesse exemplo, a classe Usuario possui um atributo chamado endereco. Esse atributo não é String, int ou bool. Ele é do tipo Endereco, ou seja, um usuário tem um endereço, mas esse endereço é um atributo, que por sua vez é um objeto de outra classe.


Isso é chamado de composição.



Por que usar composição?


Sem composição, a classe Usuario poderia ficar assim:

class Usuario {
String nome;
String cidade;
String estado;

Usuario(this.nome, this.cidade, this.estado);
}

Funciona, mas mistura informações diferentes dentro da mesma classe, e com Composição, ele deixa o código mais organizado.

class Endereco {
String cidade;
String estado;

Endereco(this.cidade, this.estado);
}

class Usuario {
String nome;
Endereco endereco;

Usuario(this.nome, this.endereco);
}

Agora cada classe tem uma responsabilidade mais clara.

  • Usuario → dados do usuário
  • Endereco → dados do endereço


@override


O @override é uma anotação usada para indicar que um método está sendo reescrito a partir de uma classe pai. Isso aparece bastante no Flutter porque muitos widgets herdam de classes prontas, como StatelessWidget, StatefulWidget e State. Essas classes já definem alguns métodos que a aplicação precisa implementar, como build() e createState().


Um exemplo comum em Flutter é este:

@override
Widget build(BuildContext context) {
return const Text('Controle de servidores');
}

O método build() não foi criado do zero. Ele já é esperado pela classe pai. Quando usamos @override, estamos avisando ao Dart que esse método está substituindo ou implementando um método que veio da classe pai.


Antes de pensar no Flutter, fica mais fácil entender com um exemplo simples de herança.

class Servico {
void executar() {
print('Executando serviço genérico');
}
}

Essa classe representa um serviço genérico. Ela tem um método chamado executar(). Agora podemos criar uma classe mais específica:

class BackupService extends Servico {
@override
void executar() {
print('Executando backup');
}
}

A classe BackupService herda de Servico, mas reescreve o método executar(). A classe pai tem este método:

void executar() {
print('Executando serviço genérico');
}

A classe filha troca esse comportamento por outro:

@override
void executar() {
print('Executando backup');
}

Isso é sobrescrita de método. A classe filha continua sendo um tipo de Servico, mas agora tem uma forma própria de executar a ação.



Por que usar @override


O @override ajuda a verificar se o método realmente existe na classe pai. Por exemplo:

class BackupService extends Servico {
@override
void executarr() {
print('Executando backup');
}
}

Nesse caso, o método foi escrito errado: executarr() em vez de executar(). Como existe @override, o Dart consegue avisar que não existe nenhum método chamado executarr() na classe pai para ser sobrescrito. Isso evita um erro comum, que é achar que um método foi reescrito, quando na verdade foi criado outro método com nome errado.


Sem @override, o Dart poderia entender que executarr() é apenas um novo método da classe, e o erro ficaria mais difícil de perceber.



@override no Flutter


No Flutter, o @override aparece muito por causa da forma como os widgets funcionam.

class AppServidores extends StatelessWidget {
const AppServidores({super.key});

@override
Widget build(BuildContext context) {
return const Text('Controle de servidores');
}
}

A classe AppServidores herda de StatelessWidget.

class AppServidores extends StatelessWidget

Isso significa que AppServidores é um widget sem estado e precisa implementar o método build(). O método build() é responsável por montar a interface daquele widget.

@override
Widget build(BuildContext context) {
return const Text('Controle de servidores');
}

O @override indica que esse build() está vindo da estrutura esperada pelo StatelessWidget.