Como criar um aplicativo de cronômetro com o Flutter (Riverpod, MVC)

Tempo de leitura: 6 minutes

Olá, pessoal!

Você já pensou que deseja trabalhar com uma função periodicamente? Por exemplo, se você quiser criar um aplicativo de cronômetro, deverá contar o tempo. Na verdade, isso não é tão complicado se você usar apenas um widget com estado. Entretanto, acho que você deseja manter o estado do timer globalmente e usá-lo em outros widgets. Nesse momento, você deve usar o Provider, o Riverpod, etc. Portanto, hoje quero compartilhar como criar um aplicativo de timer usando o Riverpod.

Esse aplicativo tem apenas uma função de timer, mas você pode personalizá-lo adicionando uma função à função de timer. Para fazer isso, você precisa entender como funciona a função de cronômetro. Portanto, explicarei esse ponto em detalhes. Vamos nos aprofundar no assunto!

 

Preparação

Em primeiro lugar, você deve criar um aplicativo Flutter e obter pacotes para esse aplicativo. Execute o comando abaixo e crie um novo aplicativo Flutter.

flutter create timer_app

Em seguida, obtenha o package abaixo com o comando

  • flutter_riverpod
  • freezed_annotation
  • freezed
  • build_runner
flutter pub get <PACKAGE_NAME>

Por fim, vamos criar os diretórios e arquivos necessários. No diretório “lib”, localize-os da seguinte forma.

lib/
├── src/
├── --- enum
│       └── state.dart
├── main.dart
├── --- model
│       ├── timer_model.dart
│       └── timer_model.freezed.dart
├── --- pages
│       └── home.dart
└── --- --- view
             └── timer_view_model.dart

Agora você está preparado para a codificação!

Codificação

A partir de agora, vamos para a seção de codificação.

Primeiro, vamos criar um enum em “/src/enum/state.dart”.

enum TimerState { start, stop, reset }

 

Em seguida, vamos criar um modelo para armazenar dados. Edite o arquivo “src/model/timer_model.dart” desta forma.

Há vários erros como esse, mas você deve ignorá-los. Depois de executar os comandos, todos eles desaparecerão.

import 'package:freezed_annotation/freezed_annotation.dart';

import '../enum/state.dart';

part "timer_model.freezed.dart";

@freezed
class TimerModel with _$TimerModel {
  const TimerModel._();
  const factory TimerModel({
    // properties
    @Default(0) int totalSecond,
    @Default(TimerState.reset) TimerState timerState,
  }) = _TimerModel;

  // Methods
  TimerModel setTotalSecond(int totalSecond) =>
      copyWith(totalSecond: totalSecond);
  TimerModel setTimerState(TimerState timerState) =>
      copyWith(timerState: timerState);
}

Nesse código, você definiu propriedades e métodos que podem ser usados globalmente.

Cada propriedade é:

  • totalSecond: mantém o tempo que um usuário define.
  • timerState: mantém o estado do cronômetro. O tipo é enum e tem três estados: iniciar, parar e reiniciar.

Cada método é:

  • setTotalSecond: define o tempo total restante no momento
  • setTimerState: define o estado atual do temporizador

O código abaixo significa que você gera um novo arquivo executando um comando. Isso torna as propriedades estáticas. Assim, você pode encapsular as propriedades e torná-las seguras. Você pode alterar o valor usando apenas o método “copyWith“.

part "timer_model.freezed.dart";

Agora, execute o comando para gerar um arquivo de modelo.

dart run build_runner build -d

Se esse comando for executado com êxito, você verá um novo arquivo dentro do diretório do modelo.

Você terminou de criar o modelo, portanto, vamos prosseguir com a criação de um modelo de visualização. O modelo de visualização tem métodos que interagem com a visualização e o modelo. Edite o arquivo “timer_view_model.dart” desta forma.

[“timer_view_model.dart”]
import 'dart:async';

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

import '../../enum/state.dart';
import '../../model/timer_model.dart';

final timerViewModelProvider =
    StateNotifierProvider.autoDispose<TimerViewModel, TimerModel>(
        (ref) => TimerViewModel());

class TimerViewModel extends StateNotifier<TimerModel> {
  TimerViewModel() : super(const TimerModel());

  late Timer _timer;

  int getSecond() {
    return ((state.totalSecond % 3600).round() % 60);
  }

  int getMinute() {
    return (((state.totalSecond % 3600).round() / 60).floor());
  }

  int getHour() {
    return (state.totalSecond / 3600).round();
  }

  void startTimer() {
    _timer = Timer.periodic(
      const Duration(seconds: 1),
      (Timer timer) {
        if (state.timerState == TimerState.start && state.totalSecond > 0) {
          state = state.setTotalSecond(state.totalSecond - 1);
        }
        checkIfFinished();
      },
    );
  }

  void checkIfFinished() {
    if (state.totalSecond == 0) {
      state.setTimerState(TimerState.reset);
      if (kDebugMode) {
        print("The time has come");
      }
      _timer.cancel();
    }
  }

  void stopTimer() {
    _timer.cancel();
  }

  void setTotalSecond(int hour, int minute, int second) {
    int totalSecond = hour * 3600 + minute * 60 + second;
    state = state.setTotalSecond(totalSecond);
  }

  void setTimerState(TimerState timerState) {
    state = state.setTimerState(timerState);
  }
}

Cada método é

  • getSecond, getMinute, getHour: obtém cada tempo calculado a partir do tempo total.
  • startTimer: inicia o cronômetro. O método Timer.periodic() implementa a função após a passagem de 1 segundo. Portanto, você deve colocar uma função que deseja implementar periodicamente. Tenha cuidado para não colocar uma função que deva ser executada rapidamente, como a função stop the timer.
  • checkIfFinished: verifica se o cronômetro foi concluído. Em caso afirmativo, o programa imprime “the time has come” (chegou a hora) e interrompe o cronômetro. Esse processo é muito importante porque, se essa função parar o cronômetro (descartar o cronômetro), outro cronômetro será iniciado no próximo ciclo e o cronômetro passará 2 ou 3 vezes mais rápido do que o normal.
  • stopTimer: descarta o cronômetro.
  • setTotalSecond: define o tempo total restante no momento.
  • setTimerState: define o estado atual do cronômetro.

Agora vamos criar a interface do usuário e ver se o aplicativo está funcionando corretamente. Edite “home.dart” e “main.dart” da seguinte forma.

[“home.dart”]
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../enum/state.dart';
import 'view/timer_view_model.dart';

class Home2 extends ConsumerStatefulWidget {
  const Home2({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _Home2State();
}

class _Home2State extends ConsumerState<Home2> {
  final List<int> smList = List.generate(60, (index) => index);
  final List<int> hList = List.generate(24, (index) => index);
  int second = 0;
  int minute = 0;
  int hour = 0;

  @override
  Widget build(BuildContext context) {
    final state = ref.watch(timerViewModelProvider);
    final stateNotifier = ref.watch(timerViewModelProvider.notifier);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.amber[200],
        title: const Center(
          child: Text(
            "Timer app",
            style: TextStyle(
              color: Colors.black87,
            ),
          ),
        ),
      ),
      body: Container(
        color: Colors.amber[100],
        child: Center(
          child: Container(
            width: 350,
            height: 150,
            decoration: BoxDecoration(
              border: Border.all(
                color: Colors.blue[200]!,
                width: 7,
              ), //Border.all
              borderRadius: BorderRadius.circular(15),
              boxShadow: [
                BoxShadow(
                  color: Colors.yellow.shade400,
                  offset: const Offset(
                    3.0,
                    4.0,
                  ), //Offset
                  blurRadius: 5.0,
                  spreadRadius: 2.0,
                ), //BoxShadow
                const BoxShadow(
                  color: Colors.black54,
                  offset: Offset(0.0, 0.0),
                  blurRadius: 0.0,
                  spreadRadius: 0.0,
                ), //BoxShadow
              ],
            ),
            child: Container(
              height: MediaQuery.of(context).size.height,
              width: double.infinity,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                color: Colors.amber[100],
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  const Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text('Horas'),
                      SizedBox(width: 66),
                      Text('Minutos'),
                      SizedBox(width: 66),
                      Text('Segundos'),
                    ],
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      DropdownButton(
                        value: stateNotifier.getHour(),
                        items: hList.map((int item) {
                          return DropdownMenuItem(
                            value: item,
                            child: Text(item.toString()),
                          );
                        }).toList(),
                        onChanged: (int? index) {
                          stateNotifier.setTotalSecond(
                              index!,
                              stateNotifier.getMinute(),
                              stateNotifier.getSecond());
                        },
                      ),
                      Container(
                        alignment: Alignment.center,
                        width: 70,
                        child: const Text(":"),
                      ),
                      DropdownButton(
                        value: stateNotifier.getMinute(),
                        items: smList.map((int item) {
                          return DropdownMenuItem(
                            value: item,
                            child: Text(item.toString()),
                          );
                        }).toList(),
                        onChanged: (int? index) {
                          stateNotifier.setTotalSecond(stateNotifier.getHour(),
                              index!, stateNotifier.getSecond());
                        },
                      ),
                      Container(
                        alignment: Alignment.center,
                        width: 70,
                        child: const Text(":"),
                      ),
                      DropdownButton(
                        value: stateNotifier.getSecond(),
                        items: smList.map((int item) {
                          return DropdownMenuItem(
                            value: item,
                            child: Text(item.toString()),
                          );
                        }).toList(),
                        onChanged: (int? index) {
                          stateNotifier.setTotalSecond(stateNotifier.getHour(),
                              stateNotifier.getMinute(), index!);
                        },
                      ),
                    ],
                  ),
                  const SizedBox(
                    width: 70,
                  ),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.amber,
                      side: const BorderSide(width: 2, color: Colors.white60),
                      elevation: 2,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                      shadowColor: Colors.red,
                      padding: const EdgeInsets.fromLTRB(45, 10, 45, 10),
                    ),
                    onPressed: () {
                      if (state.timerState == TimerState.start) {
                        stateNotifier.setTimerState(TimerState.stop);
                        stateNotifier.stopTimer();
                      } else {
                        stateNotifier.setTimerState(TimerState.start);
                        stateNotifier.startTimer();
                      }
                    },
                    child: state.timerState == TimerState.start
                        ? const Text(
                            "Para",
                            style: TextStyle(color: Colors.white70),
                          )
                        : const Text(
                            "Inicio",
                            style: TextStyle(color: Colors.white),
                          ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
[“main.dart”]
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:timer_app/src/pages/home.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Timer app',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const Home2(),
    );
  }
}

Em home.dart, você define as variáveis “state” e “stateNotifier”. Usando essas variáveis, é possível acessar os dados e os métodos que você criou. Quando você executa métodos no stateNotifier, a interface do usuário é renderizada novamente.

Agora, vamos iniciar seu aplicativo em um emulador ou dispositivo. Ele deve ter a seguinte aparência.

Quando você define um cronômetro e pressiona o botão Iniciar, ele inicia. Quando você pressiona o botão de parada, ele para.

Código Completo no Meu GitHub (Git)

 

Agora você já aprendeu a implementar funções periodicamente. Espero que este artigo ajude seu desenvolvimento!

Se você gostou deste artigo, aperte o botão “Curtir”.

Tchau!