Inheritance com Mixin no Flutter

Tempo de leitura: 4 minutes

O que é Inheritance? Vamos supor que haja uma funcionalidade que seja usada em todas as telas do aplicativo ou na maioria das telas do aplicativo. O que você prefere: implementar essa funcionalidade escrevendo o código uma vez ou criar um método com os parâmetros necessários e usar esse método em todas as telas chamando-o de simples. É claro que preferimos a reutilização do código, o que é chamado de herança.

Agora, se houver uma classe que exija mais de uma propriedade de classe, não será possível estender essa classe para mais de uma classe. Portanto, para evitar essa ambiguidade de herança, precisamos de mixins. Os mixins são realmente ótimos no Dart, pois nos proporcionam flexibilidade para a reutilização do código.

Os mixins são classes normais cujo método pode ser usado em outra classe sem estender a classe. No Dart, podemos fazer isso usando a palavra-chave with.

Neste blog, aprenderemos como criar um mixin em um flutter que registra o tempo gasto em uma determinada tela. Essa funcionalidade é muito importante se estivermos usando a análise em nosso aplicativo para rastrear o comportamento do usuário.

Por que precisamos de um mixin para fazer isso? Digamos que haja 50 telas em um aplicativo e queremos implementar um timer em cada tela, então precisamos escrever o código para o timer em cada tela. Se fizermos isso, não será uma boa prática, pois estaremos usando o mesmo código em cada tela repetidamente. Portanto, podemos criar um mixin para isso.

 

Crie um mixin chamado TimerMixin .

mixin TimerMixin {}

Criaremos dois métodos startTimer e disposeTimer que iniciarão o cronômetro e o interromperão.

startTimer() {
 Timer.periodic(const Duration(seconds: 1), (Timer timer) {
    timerData.clear();
    timerData.add(timer.tick.toInt());
  });
  return timerData;
}

Para continuar aumentando o valor da duração a cada segundo, podemos usar a propriedade Timer periodic para executar uma função recorrente. Ela recebe uma propriedade de duração e um retorno de chamada que recebe o Timer . Dentro da callback, podemos adicionar o tique-taque do timer em uma lista.

timerData.clear(); por que precisamos disso?

Vamos supor que o usuário mantenha a tela aberta por um longo período de tempo; nesse caso, o tamanho da lista aumentará muito, portanto, para manter a complexidade do espaço, estamos removendo todos os elementos da lista e armazenando o próximo elemento nela.

void disposeTimer() {
  hoursStr = ((timerData[0] / (60 * 60)) % 60).floor().toString().padLeft(2, "0");
  minutesStr = ((timerData[0] / 60) % 60).floor().toString().padLeft(2, "0");
  secondsStr = (timerData[0] % 60).floor().toString().padLeft(2, "0");
  print("$hoursStr:$minutesStr:$secondsStr");
}

disposeTimer o método será executado quando a tela o descartar. Esse método pegará o número inteiro armazenado em timerData e o analisará para uma hora, um minuto e um segundo.

mixin.dart

import 'dart:async';
import 'package:flutter/foundation.dart';

mixin TimerMixin {
  String hoursStr = '00';
  String minutesStr = '00';
  String secondsStr = '00';
  List<int> timerData = [];

  startTimer() {
    Timer.periodic(const Duration(seconds: 1), (Timer timer) {
      timerData.clear();
      timerData.add(timer.tick.toInt());
    });
    return timerData;
  }

  void disposeTimer() {
    hoursStr =
        ((timerData[0] / (60 * 60)) % 60).floor().toString().padLeft(2, "0");

    minutesStr = ((timerData[0] / 60) % 60).floor().toString().padLeft(2, "0");
    secondsStr = (timerData[0] % 60).floor().toString().padLeft(2, "0");

    if (kDebugMode) {
      print("$hoursStr:$minutesStr:$secondsStr");
    }
  }
}

Agora, crie uma classe completa de estado. Para usar mixin nela, usaremos a palavra-chave with.

import 'package:flutter/material.dart';
import 'maxin.dart';

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});
  
  @override
  // ignore: library_private_types_in_public_api
  _DemoPageState createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> with TimerMixin {
  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text("Hello"),
      ),
    );
  }
}

Agora podemos usar todos os métodos TimerMixin dentro da DemoPage. Dentro dos métodos initState e dispose, podemos usá-los.

@override
  void initState() {
    startTimer();
    super.initState();
  }

  @override
  void dispose() {
    disposeTimer();
    super.dispose();
  }

 

main.dart

import 'package:flutter/material.dart';

import 'app/splash_screen.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.teal,
      ),
      debugShowCheckedModeBanner: false,
      home: const SplashScreen(),
    );
  }
}

splash_screen.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'maxin_demo_page.dart';

class SplashScreen extends StatefulWidget {
  const SplashScreen({super.key});

  @override
  VideoState createState() => VideoState();
}

class VideoState extends State<SplashScreen>
    with SingleTickerProviderStateMixin {
  var _visible = true;

  late AnimationController animationController;
  late Animation<double> animation;

  startTime() async {
    // ignore: no_leading_underscores_for_local_identifiers
    var _duration = const Duration(seconds: 2);
    return Timer(_duration, navigationPage);
  }

  void navigationPage() {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (context) => const MyDemoPage()),
    );
  }

  @override
  void initState() {
    super.initState();

    animationController =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));
    animation =
        CurvedAnimation(parent: animationController, curve: Curves.easeOut);

    animation.addListener(() => setState(() {}));
    animationController.forward();

    setState(() {
      _visible = !_visible;
    });
    startTime();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Image.asset(
                'assets/devs.jpg',
                width: animation.value * 250,
                height: animation.value * 250,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

maxin.dart

import 'dart:async';

import 'package:flutter/foundation.dart';

mixin TimerMixin {
  String hoursStr = '00';
  String minutesStr = '00';
  String secondsStr = '00';
  List<int> timerData = [];

  startTimer() {
    Timer.periodic(const Duration(seconds: 1), (Timer timer) {
      timerData.clear();
      timerData.add(timer.tick.toInt());
    });
    return timerData;
  }

  void disposeTimer() {
    hoursStr =
        ((timerData[0] / (60 * 60)) % 60).floor().toString().padLeft(2, "0");

    minutesStr = ((timerData[0] / 60) % 60).floor().toString().padLeft(2, "0");
    secondsStr = (timerData[0] % 60).floor().toString().padLeft(2, "0");

    if (kDebugMode) {
      print("$hoursStr:$minutesStr:$secondsStr");
    }
  }
}

myapp_demo_page.dart

import 'package:demo_dismissible/app/maxin_demo_page.dart';
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.redAccent,
            side: const BorderSide(width: 3, color: Colors.white),
            elevation: 3,
            shape:
                RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
            padding: const EdgeInsets.all(20),
          ),
          child: const Text(
            "Navigate",
            style: TextStyle(
              color: Colors.white,
            ),
          ),
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => const MaxinDemoPage(),
              ),
            );
          },
        ),
      ),
    );
  }
}

 

maxin_demo_page.dart

import 'package:demo_dismissible/app/maxin_demo_page.dart';
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.redAccent,
            side: const BorderSide(width: 3, color: Colors.white),
            elevation: 3,
            shape:
                RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
            padding: const EdgeInsets.all(20),
          ),
          child: const Text(
            "Navigate",
            style: TextStyle(
              color: Colors.white,
            ),
          ),
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => const MaxinDemoPage(),
              ),
            );
          },
        ),
      ),
    );
  }
}

 

Fluxo do aplicativo

A página inicial conterá um botão simples no centro; ao pressioná-lo, o usuário será direcionado para a nova página e, ao voltar para a página inicial a partir da nova página, a duração gasta pelo usuário na nova página será exibida no console de execução.

Código completo no github (link)