Ótimas extensões pra Objetos em Dart & Flutter

Em Dart, existem várias características interessantes, e vou explicar extensões e genéricos para que as sugestões no final deste artigo sejam melhor compreendidas. Se você já está familiarizado com eles, pode pular para a última parte.


Extensões:

Em Dart, há uma característica maravilhosa chamada extensão. Essa funcionalidade permite adicionar métodos e parâmetros a instâncias de uma classe sem a necessidade de estender, implementar ou misturar usando with. Aqui está um exemplo de uma extensão em int para adicionar um getter que responde se o número inteiro é divisível por 3.

extension EvenInteger on int {
  bool get isDivisibleBy3 => this % 3 == 0;
}

Com essa extensão, você pode fazer algo como o seguinte:

void function(int parameter) {
  // Algum código.
  if (parameter.isDivisibleBy3) {
    print('$parameter é divisível por 3');
  }
  // Algum código.
}

A propósito, os nomes das extensões não são necessários, mas se você quiser adicioná-los a um arquivo separado e importá-los, eles devem ter um nome e não devem começar com um _ (sublinhado), pois isso os tornará visíveis apenas para sua biblioteca.


Genericos:

Em Dart, também existe outra característica chamada genéricos, representada pelos sinais <>, e eles existem para criar um tipo mais específico. A maioria das pessoas que usa Flutter já os viu com Columns e Rows tendo uma propriedade children que recebe um List<Widget>, mas você também pode criar uma lista de inteiros, por exemplo:

List<int> lista1 = [1, 2, 3];

Eles também podem ser usados para representar um tipo mais abstrato, então podemos criar uma lista de números como:

List<num> lista2 = [1, 0.2, 3];

Qualquer número pode ser adicionado a esta lista2, mas apenas inteiros na primeira (lista1).


Extensões e usos:

Agora que todos já estão familiarizados com ambos os conceitos que vamos usar, vamos direto ao ponto!

Isso não é nulo?

Você já teve a experiência de ter um retorno profundamente aninhado de uma variável/método, que você precisava salvar em uma variável para testar se o retorno não é nulo, para passá-lo para uma nova chamada de função/método. E se esse resultado fosse nulo, não fazer nada?

final result1 = obj1.propety.method(variable);
if (result != null) return function(result1);
return null;

Principalmente no Flutter, vejo isso de vez em quando, e como são todos widgets, tenho que usar o operador ternário ou de alguma forma criar uma variável em algum lugar para fazer este teste.

Solução

Você pode criar uma extensão em todos os Objects que resolva seu problema!

extension ObjectsExtensions<T extends Object> on T {
  R apply<R>(R Function(T self) f) => f(this);
}

Deixe-me explicar. Quando você cria uma classe/mixin/extensão, você pode adicionar os mesmos <> para se referir a um tipo desconhecido com um nome para esse tipo, neste caso, T. Isso permite que você crie uma variável com esse tipo dentro de sua classe, mesmo que ainda não conheça o tipo exato.

Você pode ser mais específico com esse tipo se disser que ele deve estender algum tipo específico, neste caso, eu disse que T deve estender Object. Isso implica que qualquer objeto não nulo em seu programa que possa ver esta extensão, possui este método.

Por que eu criei isso com genéricos e também, por que eu não simplesmente usei Object nos tipos de retorno e parâmetro?

Bem, isso tem a ver com o método que estamos criando. Isso faz com que quando chamamos o método apply, a função interna nos dê um objeto do tipo T como parâmetro, e não um objeto do tipo Object. Isso mantém nosso código coerente com o tipo e temos certeza qual é o tipo da variável dada mesmo dentro de nossa função interna.

Com isso, você pode facilmente resolver o problema acima com apenas esta linha:

return obj1.propety.method(variable)?.apply((self) => function(self));

Mesmo problema, teste diferente

Você já teve o mesmo problema descrito acima, mas com outro teste? Como se um número é par, ou qualquer outro teste?

final result1 = obj1.propety.method(variable);
if (result?.isEven ?? false) return function(result1);
if (result?.isOdd ?? false) return function(result1 * 2);
return null;

Solução

extension ObjectsExtensions<T extends Object> on T {
  T? when(bool Function(T self) predicate, {T? Function(T self)? otherwise}) {
    if (predicate(this)) return this;
    return otherwise?.call(this);
  }
}

O que isso nos permite fazer é o seguinte:

return obj1.propety.method(variable)?.when(
  (self) => self.isEven, 
  otherwise: (self) => self * 2,
)?.apply((self) => function(self));

Conclusão e créditos

Essas extensões me ajudaram a programar muito, várias vezes e realmente espero que também possam ajudá-lo! Também existem mais duas extensões, não tão úteis, que eu criei com base nesta resposta https://stackoverflow.com/a/69114947/9576870, que realmente resolvem os testes de tipo usando "métodos" em vez de operadores:

extension NullableObjectsExtensions<T> on T {
  bool isSubtypeOf<S>() => <T>[] is List<S>;
  bool isSupertypeOf<S>() => <S>[] is List<T>;
}

Creditos

Apply: https://github.com/dart-lang/language/issues/361#issuecomment-1347411411 When: https://github.com/dart-lang/language/issues/2440