Faça um aplicativo Travel App em Flutter com Animation
Neste tutorial, vou mostrar a você como construir um protótipo de aplicativo Trtavel App com Animation em Flutter. é um projeto muito legal que vai fazer você entender alguns conceitos avançados de programação. sem mais delongas, vamos começar a construir o projeto
Configure o projeto este e outras imagens no Github
Primeiro você precisará criar um novo diretório chamado ‘Assets’ e adicionar esta imagem dentro dele
Configure seu pubspect yaml
Não se esqueça de adicionar o diretório dentro do arquivo pubspec.yaml e adicionar o pacote Google Fonts também apenas copie e cole este código dentro das dependências, estamos usando a versão do fonts 6.2.1 ela pede o sdk do Dart no mínimo 3.3.0
enviroment: sdk: ">=3.3.0 <4.0.0" dependencies: flutter: sdk: flutter google_fonts: ^6.2.1
Agora vamos começar a construir o aplicativo
Pastas a serem usadas, no projeto.
e os Assets são (encontradas no Github no final do projeto)
Código fonte:
A pasta gen
contem um projeto em dart, gerado automaticamente pelo mesmo que gerou as imagens.
-> Pasta gen
assets.gen.dart
/// GENERATED CODE - DO NOT MODIFY BY HAND /// ***************************************************** /// FlutterGen /// ***************************************************** // coverage:ignore-file // ignore_for_file: type=lint // ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use import 'package:flutter/widgets.dart'; class $AssetsIconsGen { const $AssetsIconsGen(); /// File path: assets/icons/hamburger.svg String get hamburger => 'assets/icons/hamburger.svg'; /// List of all assets List<String> get values => [hamburger]; } class $AssetsImagesGen { const $AssetsImagesGen(); /// File path: assets/images/Ellipse 36.png AssetGenImage get ellipse36 => const AssetGenImage('assets/images/Ellipse 36.png'); /// File path: assets/images/Ellipse 37.png AssetGenImage get ellipse37 => const AssetGenImage('assets/images/Ellipse 37.png'); /// File path: assets/images/Ellipse 39.png AssetGenImage get ellipse39 => const AssetGenImage('assets/images/Ellipse 39.png'); /// File path: assets/images/Ellipse 53.png AssetGenImage get ellipse53 => const AssetGenImage('assets/images/Ellipse 53.png'); /// File path: assets/images/pexels-aleksey-kuprikov-3493777.jpg AssetGenImage get pexelsAlekseyKuprikov3493777 => const AssetGenImage('assets/images/pexels-aleksey-kuprikov-3493777.jpg'); /// File path: assets/images/pexels-lisa-fotios-1534560.jpg AssetGenImage get pexelsLisaFotios1534560 => const AssetGenImage('assets/images/pexels-lisa-fotios-1534560.jpg'); /// File path: assets/images/pexels-lisa-fotios-1559908.jpg AssetGenImage get pexelsLisaFotios1559908 => const AssetGenImage('assets/images/pexels-lisa-fotios-1559908.jpg'); /// File path: assets/images/pexels-michael-block-3225528.jpg AssetGenImage get pexelsMichaelBlock3225528 => const AssetGenImage('assets/images/pexels-michael-block-3225528.jpg'); /// File path: assets/images/pexels-trace-hudson-2724664.jpg AssetGenImage get pexelsTraceHudson2724664 => const AssetGenImage('assets/images/pexels-trace-hudson-2724664.jpg'); /// List of all assets List<AssetGenImage> get values => [ ellipse36, ellipse37, ellipse39, ellipse53, pexelsAlekseyKuprikov3493777, pexelsLisaFotios1534560, pexelsLisaFotios1559908, pexelsMichaelBlock3225528, pexelsTraceHudson2724664 ]; } class Assets { Assets._(); static const $AssetsIconsGen icons = $AssetsIconsGen(); static const $AssetsImagesGen images = $AssetsImagesGen(); } class AssetGenImage { const AssetGenImage(this._assetName); final String _assetName; Image image({ Key? key, AssetBundle? bundle, ImageFrameBuilder? frameBuilder, ImageErrorWidgetBuilder? errorBuilder, String? semanticLabel, bool excludeFromSemantics = false, double? scale, double? width, double? height, Color? color, Animation<double>? opacity, BlendMode? colorBlendMode, BoxFit? fit, AlignmentGeometry alignment = Alignment.center, ImageRepeat repeat = ImageRepeat.noRepeat, Rect? centerSlice, bool matchTextDirection = false, bool gaplessPlayback = false, bool isAntiAlias = false, String? package, FilterQuality filterQuality = FilterQuality.low, int? cacheWidth, int? cacheHeight, }) { return Image.asset( _assetName, key: key, bundle: bundle, frameBuilder: frameBuilder, errorBuilder: errorBuilder, semanticLabel: semanticLabel, excludeFromSemantics: excludeFromSemantics, scale: scale, width: width, height: height, color: color, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, alignment: alignment, repeat: repeat, centerSlice: centerSlice, matchTextDirection: matchTextDirection, gaplessPlayback: gaplessPlayback, isAntiAlias: isAntiAlias, package: package, filterQuality: filterQuality, cacheWidth: cacheWidth, cacheHeight: cacheHeight, ); } ImageProvider provider({ AssetBundle? bundle, String? package, }) { return AssetImage( _assetName, bundle: bundle, package: package, ); } String get path => _assetName; String get keyName => _assetName; }
Este não tente criar na mão faça o restante ou pegue do projeto no final.
-> Pasta models
trip_data.dart
typedef TripAdditionalInfo = ({String number, String title}); class TripData { final String title; final String imagePath; final List<TripAdditionalInfo> tripAdditionalInfos; const TripData({ required this.title, required this.imagePath, required this.tripAdditionalInfos, }); }
-> Pasta pages
home_page.dart
import 'package:flutter/material.dart'; import 'package:wander_animation/gen/assets.gen.dart'; import '../widgets/main_trip_card.dart'; import '../widgets/trip_small_cards.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, scrolledUnderElevation: 0, leading: IconButton(onPressed: () {}, icon: const Icon(Icons.menu)), actions: [ IconButton( icon: CircleAvatar( backgroundImage: AssetImage(Assets.images.ellipse36.path), ), onPressed: () {}, ) ], ), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 24), Container( margin: const EdgeInsets.only(left: 16), child: Text( 'Your trips', style: Theme.of(context) .textTheme .headlineLarge! .copyWith(fontWeight: FontWeight.bold), ), ), const SizedBox(height: 32), Text( 'Current trip'.toUpperCase(), style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const MainTripCard(), const SizedBox(height: 32), const TripSmallCard(), const SizedBox(height: 8), ], ), ), ), ); } }
trip_details_page.dart
import 'package:flutter/material.dart'; import 'package:wander_animation/gen/assets.gen.dart'; import 'package:wander_animation/widgets/trip_details_pageview_item.dart'; import '../widgets/details_page_background.dart'; import '../widgets/details_page_header.dart'; class TripDetailsPage extends StatefulWidget { const TripDetailsPage( {super.key, this.animation = const AlwaysStoppedAnimation(0)}); final Animation<double> animation; @override State<TripDetailsPage> createState() => _TripDetailsPageState(); } class _TripDetailsPageState extends State<TripDetailsPage> { ValueNotifier<double> offsetNotifier = ValueNotifier(0); final PageController _pageController = PageController(); @override void initState() { _pageController.addListener(_pageListener); super.initState(); } void _pageListener() { final screenSize = MediaQuery.of(context).size; final offsetValue = _pageController.offset / screenSize.height; offsetNotifier.value = offsetValue.clamp(0, 1); } @override void dispose() { _pageController ..removeListener(_pageListener) ..dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: offsetNotifier, builder: (context, offsetValue, _) { return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( elevation: 0.0, forceMaterialTransparency: true, leading: IconButton( onPressed: () { Navigator.of(context).pop(); }, style: IconButton.styleFrom( backgroundColor: Color.lerp(Colors.white, Colors.black26, 1 - offsetValue), ), icon: Icon( Icons.close, color: Color.lerp(Colors.white, Colors.black, offsetValue), ), ), actions: [ FadeTransition( opacity: AlwaysStoppedAnimation(offsetValue), child: IconButton( icon: CircleAvatar( backgroundImage: AssetImage(Assets.images.ellipse36.path), ), onPressed: () {}, ), ) ], backgroundColor: Colors.transparent, ), body: Stack( children: [ DetailsBackground(offset: offsetValue), DetailsPageHeader(offset: offsetValue), PageView( controller: _pageController, scrollDirection: Axis.vertical, children: [ const SizedBox.shrink(), TripDetailsPageViewItem(), ], ), Positioned( bottom: 60, right: -72.0 * (1 - offsetValue), child: FadeTransition( opacity: AlwaysStoppedAnimation(offsetValue), child: Container( height: 40, width: 72, decoration: BoxDecoration( color: Colors.lightBlueAccent[400], borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), bottomLeft: Radius.circular(20), ), ), child: const Icon(Icons.edit, color: Colors.white), ), ), ), ], ), ); }, ); } }
Pasta final e maior
-> Pasta widgets
details_page_background.dart
import 'package:flutter/material.dart'; import '../gen/assets.gen.dart'; class DetailsBackground extends StatelessWidget { const DetailsBackground({ super.key, required this.offset, }); final double offset; @override Widget build(BuildContext context) { return Positioned.fill( child: Hero( tag: 'bg', child: FadeTransition( opacity: AlwaysStoppedAnimation(1 - offset), child: Container( decoration: BoxDecoration( image: DecorationImage( fit: BoxFit.cover, image: AssetImage( Assets.images.pexelsTraceHudson2724664.path, ), ), ), ), ), ), ); } }
details_page_header.dart
import 'package:flutter/material.dart'; import 'user_chaip.dart'; class DetailsPageHeader extends StatelessWidget { const DetailsPageHeader({super.key, required this.offset}); final double offset; @override Widget build(BuildContext context) { return Align( alignment: FractionalOffset(0, 1 - offset), child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // const TitleSubtitle(), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Hero( tag: "date", child: Text( 'May 5-15', style: Theme.of(context).textTheme.titleSmall!.copyWith( color: Color.lerp(Colors.white, Colors.black, offset), ), ), ), const SizedBox(height: 16), FractionallySizedBox( widthFactor: 0.7, child: Hero( tag: "title", child: Text( 'Riding through the lands of the legends', style: Theme.of(context).textTheme.titleLarge!.copyWith( color: Color.lerp( Colors.white, Colors.black, offset), fontWeight: FontWeight.bold, ), ), ), ), ], ), const SizedBox(height: 16), const UserChips(), const SizedBox(height: 48), ], ), ), ), ); } }
Este depende do widget ‘user_chaip.dart’, no fim dos widgets.
hidden_header.dart
import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import '../gen/assets.gen.dart'; class HiddenHeader extends StatelessWidget { const HiddenHeader({ super.key, }); @override Widget build(BuildContext context) { return Opacity( opacity: 0, child: Column( children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'May 5-15', style: Theme.of(context).textTheme.titleSmall!.copyWith( color: Colors.white, ), ), const SizedBox(height: 16), FractionallySizedBox( widthFactor: 0.8, child: Text( 'Riding through the lands of the legends', style: Theme.of(context).textTheme.titleLarge!.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 16), Theme( data: ThemeData.dark() .copyWith(textTheme: GoogleFonts.montserratTextTheme()), child: Wrap( children: [ (name: 'Anne', imagePath: Assets.images.ellipse36.path), (name: 'Mike', imagePath: Assets.images.ellipse39.path), (name: 'Sophia', imagePath: Assets.images.ellipse37.path), ] .map( (e) => Container( margin: const EdgeInsets.only(right: 4), child: Material( color: Colors.transparent, child: Theme( data: ThemeData.dark().copyWith( textTheme: GoogleFonts.montserratTextTheme()), child: Chip( label: Padding( padding: const EdgeInsets.all(4.0), child: Text(e.name, style: const TextStyle( fontWeight: FontWeight.w600)), ), avatar: CircleAvatar( backgroundImage: AssetImage(e.imagePath), ), ), ), ), ), ) .toList(), ), ), ], ), ); } }
home_trip_card_lg.dart
import 'package:flutter/material.dart'; import '../gen/assets.gen.dart'; class HomeTripCardLg extends StatelessWidget { const HomeTripCardLg({ super.key, }); @override Widget build(BuildContext context) { return SizedBox( height: MediaQuery.of(context).size.height * 0.4, child: Stack( children: [ Hero( tag: "bg", child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( fit: BoxFit.cover, image: AssetImage( Assets.images.pexelsTraceHudson2724664.path, ), ), ), ), ), Align( alignment: Alignment.bottomLeft, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Hero( tag: "date", child: Text( 'May 5-15', style: Theme.of(context).textTheme.titleSmall!.copyWith( color: Colors.white, ), ), ), const SizedBox(height: 16), FractionallySizedBox( widthFactor: 0.8, child: Hero( tag: "title", child: Text( 'Riding through the lands of the legends', style: Theme.of(context) .textTheme .titleLarge! .copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ), ], ), const SizedBox(height: 48), ], ), ), ) ], ), ); } }
home_trip_card.dart
import 'package:flutter/material.dart'; class HomeTripCard extends StatelessWidget { const HomeTripCard({ super.key, required this.imagePath, required this.date, required this.title, }); final String imagePath; final String date; final String title; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), alignment: Alignment.bottomLeft, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( fit: BoxFit.cover, image: NetworkImage(imagePath), colorFilter: const ColorFilter.mode(Colors.black26, BlendMode.darken)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( date, style: Theme.of(context).textTheme.titleSmall!.copyWith( color: Colors.white, ), ), const SizedBox(height: 16), Text( title, style: Theme.of(context).textTheme.titleMedium!.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), ], ), ); } }
main_trip_card.dart
import 'package:flutter/material.dart'; import '../gen/assets.gen.dart'; import '../pages/trip_details_page.dart'; import 'home_trip_card_lg.dart'; import 'stacked_row.dart'; class MainTripCard extends StatelessWidget { const MainTripCard({super.key}); @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.none, children: [ GestureDetector( onTap: () => Navigator.push( context, PageRouteBuilder( transitionDuration: const Duration(milliseconds: 500), reverseTransitionDuration: const Duration(milliseconds: 500), pageBuilder: (_, animation, ___) { return TripDetailsPage(animation: animation); }, transitionsBuilder: (_, animation, __, child) => FadeTransition( opacity: animation, child: child, ), ), ), child: const HomeTripCardLg(), ), Positioned( bottom: -20, left: 20, child: StackedRow( items: [ Assets.images.ellipse36.path, Assets.images.ellipse39.path, Assets.images.ellipse37.path, ] .map( (e) => Hero( tag: e, child: CircleAvatar( backgroundImage: AssetImage(e), ), ), ) .toList(), size: 42, direction: TextDirection.rtl, xShift: 10, ), ) ], ); } }
Este widget ele precisa o pages ‘trip_details_page.dart’, e dos widgets ‘home_trip_card_lg.dart’ e ‘stacked_row.dart’* este mais abaixo.
message_row.dart
import 'package:flutter/material.dart'; class MessageRow extends StatelessWidget { const MessageRow({ super.key, required this.imagePath, required this.message, }); final String imagePath, message; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( backgroundImage: AssetImage(imagePath), ), const SizedBox(width: 16), Expanded( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black26, width: 2), ), padding: const EdgeInsets.all(16), // child: const Text('What a trip! Thanks for all the memories! Whats next?'), child: Text(message), ), ) ], ); } }
stacked_row.dart
import 'package:flutter/material.dart'; class StackedRow extends StatelessWidget { final List<Widget> items; final TextDirection direction; final double size; final double xShift; const StackedRow({ super.key, required this.items, this.direction = TextDirection.ltr, this.size = 100, this.xShift = 20, }); @override Widget build(BuildContext context) { final allItems = items .asMap() .map((index, item) { final left = size - xShift; final value = Container( width: size, height: size, margin: EdgeInsets.only(left: left * index), child: item, ); return MapEntry(index, value); }) .values .toList(); return Stack( children: direction == TextDirection.ltr ? allItems.reversed.toList() : allItems, ); } }
trip_data_card.dart
import 'package:flutter/material.dart'; import 'package:wander_animation/models/trip_data.dart'; class TripDataCard extends StatelessWidget { const TripDataCard({super.key, required this.tripData}); final TripData tripData; @override Widget build(BuildContext context) { return SizedBox.fromSize( size: const Size(180, 220), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( // color: Colors.blue, borderRadius: BorderRadius.circular(8), image: DecorationImage( image: NetworkImage( tripData.imagePath, ), fit: BoxFit.cover, colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.5), BlendMode.darken), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tripData.title.toUpperCase(), style: Theme.of(context).textTheme.titleSmall!.copyWith( color: Colors.white, ), ), const Spacer(), ...tripData.tripAdditionalInfos.map((e) { return Container( margin: const EdgeInsets.only(bottom: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( e.number, style: Theme.of(context).textTheme.headlineLarge!.copyWith( color: Colors.white, ), ), Text( e.title, style: Theme.of(context).textTheme.titleSmall!.copyWith( color: Colors.white70, ), ) ], ), ); }) ], ), ), ); } }
trip_details_pageview_item.dart
import 'package:flutter/material.dart'; import 'package:wander_animation/gen/assets.gen.dart'; import 'package:wander_animation/models/trip_data.dart'; import 'package:wander_animation/widgets/trip_data_card.dart'; import 'message_row.dart'; class TripDetailsPageViewItem extends StatelessWidget { TripDetailsPageViewItem({super.key}); final tripData = [ const TripData( title: 'Geo Summary', imagePath: 'https://images.pexels.com/photos/2678301/pexels-photo-2678301.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', tripAdditionalInfos: [(title: 'Over 11 days', number: '1,457 km')], ), const TripData( title: 'Media', imagePath: 'https://images.pexels.com/photos/3733269/pexels-photo-3733269.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', tripAdditionalInfos: [ (title: 'Photos', number: '257'), (title: 'Videos', number: '14'), ], ), ]; @override Widget build(BuildContext context) { return SafeArea( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 200), SizedBox( height: 250, child: ListView.separated( separatorBuilder: (context, index) => const SizedBox(width: 8), scrollDirection: Axis.horizontal, itemCount: tripData.length, itemBuilder: (context, index) => TripDataCard(tripData: tripData[index]), ), ), const SizedBox(height: 16), const Text('TRIP BOARD', style: TextStyle()), const SizedBox(height: 8), MessageRow( message: 'What a trip! Thanks for all the memories! Whats next?', imagePath: Assets.images.ellipse53.path), const SizedBox(height: 12), MessageRow( message: "Folk, that was fun. Next time with better car, not that piece of shit!\nHaha.", imagePath: Assets.images.ellipse37.path), ], ), ), ); } }
Este widget usa o ‘message_row.dart’, acima.
trip_small_cards.dart
import 'package:flutter/material.dart'; import 'home_trip_card.dart'; class TripSmallCard extends StatelessWidget { const TripSmallCard({super.key}); @override Widget build(BuildContext context) { return GridView( physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 8, crossAxisSpacing: 8, mainAxisExtent: 220, ), shrinkWrap: true, children: const [ HomeTripCard( imagePath: 'https://images.pexels.com/photos/2377432/pexels-photo-2377432.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', date: 'Mar 7-21', title: 'Road trips over Italian Riviera', ), HomeTripCard( imagePath: 'https://images.pexels.com/photos/1615807/pexels-photo-1615807.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', date: 'Jan 7-23', title: 'Weekend in Lisbon', ), HomeTripCard( imagePath: 'https://images.pexels.com/photos/1559908/pexels-photo-1559908.jpeg?auto=compress&cs=tinysrgb&w=600', date: 'Mar 7-21', title: 'Road trips over Italian Riviera', ), HomeTripCard( imagePath: 'https://images.pexels.com/photos/1550348/pexels-photo-1550348.jpeg?auto=compress&cs=tinysrgb&w=600', date: 'Mar 7-21', title: 'Road trips over Italian Riviera', ) ], ); } }
Este widget usa o ‘home_trip_card.dart’ acima.
user_chaip.dart
import 'package:flutter/material.dart'; import '../gen/assets.gen.dart'; class UserChips extends StatelessWidget { const UserChips({ super.key, }); @override Widget build(BuildContext context) { return Wrap( children: [ (name: 'Anne', imagePath: Assets.images.ellipse36.path), (name: 'Mike', imagePath: Assets.images.ellipse39.path), (name: 'Sophia', imagePath: Assets.images.ellipse37.path), ] .map( (e) => Container( margin: const EdgeInsets.only(right: 4), child: Hero( tag: e.imagePath, child: Material( type: MaterialType.transparency, child: Chip( backgroundColor: Colors.grey[800], shape: const StadiumBorder( side: BorderSide( // color: Color.lerp(Colors.red, Colors.black26, height) ?? Colors.transparent, ), ), side: const BorderSide(color: Colors.transparent), label: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Text( e.name, style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.white, ), ), ), avatar: CircleAvatar( backgroundImage: AssetImage(e.imagePath), ), ), ), ), ), ) .toList(), ); } }
E por ultimo no raiz das pastas, o principal ‘main.dart’
main.dart
import 'dart:async'; import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:wander_animation/pages/home_page.dart'; void main() { ErrorWidget.builder = (FlutterErrorDetails errorDatails) => CustomError(errorDatails: errorDatails); runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); runApp(const MyApp()); }, (error, stack) { log('Erro não tratado',error: error, stackTrace: stack); throw error; }); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'The Flutter Way Animation', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), textTheme: GoogleFonts.montserratTextTheme().apply( bodyColor: Colors.black, displayColor: Colors.black, ), ), home: const HomePage(), ); } } class CustomError extends StatelessWidget { final FlutterErrorDetails errorDatails; const CustomError({ super.key, required this.errorDatails, }); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Center( child: Container( decoration: BoxDecoration( color: Colors.redAccent.withOpacity(.3), borderRadius: BorderRadius.circular(12), ), width: 300, height: 300, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline_outlined, color: Colors.red, size: 80, ), const SizedBox( height: 10.0, ), const Text( 'Error Occurred!', style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), ), Text( kDebugMode ? 'Oops... something went wrong' : errorDatails.exception.toString(), textAlign: TextAlign.center, style: const TextStyle(fontSize: 16.0), ), ], ), ), ), ); } }
No main, tem 2 funções extras mais para evitar problemas, e ter uma tela bonita de erro, o CustomEror e o runZoneGuared se tiver duvidas entre em contato, no caso do runZoneGuared, tenho artigo sobre ele.
Segue o código fonte completo no meu Github (link)