Como criar um aplicativo de cronômetro com o Flutter (Riverpod, MVC)
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!