Faça um aplicativo Travel App em Flutter com Animation

Tempo de leitura: 9 minutes

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)