Faça um aplicativo Tesla App em Flutter com com design

Tempo de leitura: 9 minutes

Neste tutorial, vou mostrar a você como construir um ótimo aplicativo similar ao da Tesla App usando o Flutter e alguns componentes, é 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

Primeiro você precisará criar um novo diretório chamado ‘Assets’ e adicionar as pastas do diretório ‘Assets’ do nosso github, link abaixo não se esqueça de adicionar o diretório dentro do arquivo pubspec.yaml apenas copie e cole este código dentro das dependências

dependencies:
  flutter:
    sdk: flutter
  flutter_svg: ^2.0.10+1
  provider: ^6.1.2

Abaixo uma amostra da pasta de Assets.

Agora vamos começar a construir o aplicativo

Pastas a serem usadas, no projeto.

Código fonte:

A pasta constants contem apenas uma pasta com parâmetros básicos do projeto.

Lib->Src->constants

constants.dart

import 'package:flutter/material.dart';

const Color primaryColor = Color(0xFF53F9FF);
const Color redColor = Color(0xFFFF5368);

const double defaultPadding = 16.0;
const Duration defaultDuration = Duration(milliseconds: 300);

 

A pasta models contem apenas uma pasta com modelos de dados simulados para o projeto.

Lib->Src->models

typepsi.dart

class TyrePsi {
  final double psi;
  final int temp;
  final bool isLowPressure;

  TyrePsi({required this.psi, required this.temp, required this.isLowPressure});
}

final List<TyrePsi> demoPsiList = [
  TyrePsi(psi: 23.6, temp: 56, isLowPressure: true),
  TyrePsi(psi: 35.0, temp: 41, isLowPressure: false),
  TyrePsi(psi: 34.6, temp: 41, isLowPressure: false),
  TyrePsi(psi: 34.8, temp: 42, isLowPressure: false),
];

A pasta screens contem uma pasta de componentes para a tela home_screen, e também seu controlador do projeto.

Lib->Src->screens->components

battery_status.dart

import 'package:flutter/material.dart';

import '../../constants/constants.dart';

class BatteryStatus extends StatelessWidget {
  const BatteryStatus({
    Key? key,
    required this.constrains,
  }) : super(key: key);

  final BoxConstraints constrains;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: constrains.maxHeight,
      width: constrains.maxWidth,
      child: Column(
        children: [
          Text(
            "220 mi",
            style: Theme.of(context)
                .textTheme
                .displaySmall!
                .copyWith(color: Colors.white),
          ),
          Text(
            "62%",
            style: TextStyle(fontSize: 24),
          ),
          Spacer(),
          Text(
            "CHARGING",
            style: const TextStyle(fontSize: 20),
          ),
          Text(
            "18 min remaining",
            style: const TextStyle(fontSize: 20),
          ),
          SizedBox(height: constrains.maxHeight * 0.14),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: defaultPadding),
            child: DefaultTextStyle(
              style: TextStyle(
                fontSize: 18,
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text("22 mi/hr"),
                  Text("232 v"),
                ],
              ),
            ),
          ),
          const SizedBox(height: defaultPadding),
        ],
      ),
    );
  }
}

 

bottom_navigation.dart

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

import '../../constants/constants.dart';

class TeslaAppBottomNavigation extends StatelessWidget {
  TeslaAppBottomNavigation({
    Key? key,
    required this.onTap,
    required this.selectedTab,
  }) : super(key: key);


  final ValueChanged<int> onTap;
  final int selectedTab;

  static List<String> _tabsIcons = [
    "assets/icons/Lock.svg",
    "assets/icons/Charge.svg",
    "assets/icons/Temp.svg",
    "assets/icons/Tyre.svg"
  ];

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      onTap: onTap,
      currentIndex: selectedTab,
      type: BottomNavigationBarType.fixed,
      backgroundColor: Colors.black,
      items: List.generate(
        _tabsIcons.length,
        (index) => BottomNavigationBarItem(
          icon: SvgPicture.asset(
            _tabsIcons[index],
            colorFilter: ColorFilter.mode(index == selectedTab ? primaryColor : Colors.white54, BlendMode.srcIn),
          ),
          label: "",
        ),
      ),
    );
  }
}

door_lock.dart

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

class DoorLock extends StatelessWidget {
  const DoorLock({
    Key? key,
    required this.isDoorLock,
    required this.press,
  }) : super(key: key);

  final bool isDoorLock;
  final VoidCallback press;

  Widget doorLock() {
    return isDoorLock
        ? SvgPicture.asset(
            "assets/icons/door_lock.svg",
            key: ValueKey("lock"),
          )
        : SvgPicture.asset(
            "assets/icons/door_unlock.svg",
            key: ValueKey("unlock"),
          );
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: press,
      child: AnimatedSwitcher(
        switchInCurve: Curves.easeInOutBack,
        duration: Duration(milliseconds: 300),
        transitionBuilder: (child, animation) => ScaleTransition(
          scale: animation,
          child: child,
        ),
        child: doorLock(),
      ),
    );
  }
}

 

temp_counter.dart

import 'package:flutter/material.dart';

class TempCounter extends StatelessWidget {
  const TempCounter({
    Key? key,
    required this.temperature,
    required this.pressIncrement,
    required this.pressDecrement,
  }) : super(key: key);

  final int temperature;
  final VoidCallback pressIncrement, pressDecrement;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        IconButton(
          padding: EdgeInsets.zero,
          onPressed: pressIncrement,
          icon: Icon(
            Icons.arrow_drop_up,
            size: 48,
          ),
        ),
        Text(
          temperature.toString() + "\u2103",
          style: TextStyle(fontSize: 86),
        ),
        IconButton(
          padding: EdgeInsets.zero,
          onPressed: pressDecrement,
          icon: Icon(
            Icons.arrow_drop_down,
            size: 48,
          ),
        ),
      ],
    );
  }
}

temp_details.dart

import 'package:flutter/material.dart';
import 'package:tesla/src/screens/home_controller.dart';

import '../../constants/constants.dart';
import 'temp_counter.dart';
import 'temperature_btn.dart';

class TempDetails extends StatelessWidget {
  TempDetails({
    Key? key,
    required this.constrains,
    required this.press,
    required this.isCoolTabSelected,
  }) : super(key: key);

  final BoxConstraints constrains;
  final VoidCallback press;
  final bool isCoolTabSelected;

  final HomeController _controller = HomeController();

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: constrains.maxHeight,
      width: constrains.maxWidth,
      child: Padding(
        padding: const EdgeInsets.all(defaultPadding),
        child: AnimatedBuilder(
            animation: _controller,
            builder: (context, snapshot) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Spacer(),
                  SizedBox(
                    height: 110,
                    child: Row(
                      // mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        TemperatureBtn(
                          svgSrc: "assets/icons/coolShape.svg",
                          text: "Cool",
                          press: press,
                          isActive: isCoolTabSelected,
                        ),
                        const SizedBox(width: defaultPadding * 1.5),
                        TemperatureBtn(
                          svgSrc: "assets/icons/heatShape.svg",
                          text: "Heat",
                          press: press,
                          isActive: !isCoolTabSelected,
                          activeColor: redColor,
                        ),
                      ],
                    ),
                  ),
                  Spacer(flex: 2),
                  TempCounter(
                    temperature: _controller.coolTem,
                    pressDecrement: () => _controller.coolTemHandler(false),
                    pressIncrement: () => _controller.coolTemHandler(true),
                  ),
                  Spacer(flex: 2),
                  Text("CURRENT TEMPERATURE"),
                  const SizedBox(height: defaultPadding),
                  Row(
                    children: [
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            "Inside".toUpperCase(),
                          ),
                          Text(
                            "20" + "\u2103",
                            style: Theme.of(context).textTheme.headlineSmall,
                          )
                        ],
                      ),
                      const SizedBox(width: defaultPadding),
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            "Outside".toUpperCase(),
                            style: TextStyle(color: Colors.white60),
                          ),
                          Text(
                            "35" + "\u2103",
                            style: Theme.of(context)
                                .textTheme
                                .headlineSmall!
                                .copyWith(color: Colors.white60),
                          )
                        ],
                      ),
                    ],
                  ),
                ],
              );
            }),
      ),
    );
  }
}

 

temperature_btn.dart

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

import '../../constants/constants.dart';

class TemperatureBtn extends StatelessWidget {
  const TemperatureBtn({
    Key? key,
    required this.svgSrc,
    required this.text,
    required this.press,
    this.isActive = false,
    this.activeColor = primaryColor,
  }) : super(key: key);

  final String svgSrc, text;
  final VoidCallback press;
  final bool isActive;
  final Color activeColor;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: press,
      child: Column(
        children: [
          AnimatedContainer(
            curve: Curves.easeInOutBack,
            duration: Duration(milliseconds: 200),
            height: isActive ? 76 : 50,
            width: isActive ? 76 : 50,
            child: SvgPicture.asset(
              svgSrc,
              colorFilter: ColorFilter.mode(isActive ? activeColor : Colors.white38, BlendMode.srcIn),
              fit: BoxFit.cover,
            ),
          ),
          const SizedBox(height: defaultPadding / 2),
          AnimatedDefaultTextStyle(
            duration: Duration(milliseconds: 200),
            style: TextStyle(
                color: isActive ? activeColor : Colors.white38,
                fontSize: 16,
                fontWeight: isActive ? FontWeight.bold : FontWeight.normal),
            child: Text(
              text.toUpperCase(),
            ),
          ),
        ],
      ),
    );
  }
}

tyres.dart

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

List<Positioned> tyres(BoxConstraints constrains) {
  return [
    Positioned(
      top: constrains.maxHeight * 0.2,
      left: constrains.maxWidth * 0.22,
      child: SvgPicture.asset("assets/icons/FL_Tyre.svg"),
    ),
    Positioned(
      top: constrains.maxHeight * 0.2,
      right: constrains.maxWidth * 0.22,
      child: SvgPicture.asset("assets/icons/FL_Tyre.svg"),
    ),
    Positioned(
      top: constrains.maxHeight * 0.63,
      right: constrains.maxWidth * 0.22,
      child: SvgPicture.asset("assets/icons/FL_Tyre.svg"),
    ),
    Positioned(
      top: constrains.maxHeight * 0.63,
      left: constrains.maxWidth * 0.22,
      child: SvgPicture.asset("assets/icons/FL_Tyre.svg"),
    ),
  ];
}

 

Lib->Src->screens

home_controller.dart

import 'package:flutter/material.dart';

class HomeController extends ChangeNotifier {
  int selectedTab = 0;

  onBottomNavigationTabChage(int index) {
    selectedTab = index;
    notifyListeners();
  }

  bool leftDoorLock = true;
  bool rightDoorLock = true;
  bool trunkLock = true;
  bool bonnetLock = true;

  void chnageLeftDoorLock() {
    leftDoorLock = !leftDoorLock;
    notifyListeners();
  }

  void chnageRightDoorLock() {
    rightDoorLock = !rightDoorLock;
    notifyListeners();
  }

  void chnagetrunkLock() {
    trunkLock = !trunkLock;
    notifyListeners();
  }

  void chnageBonnetLock() {
    bonnetLock = !bonnetLock;
    notifyListeners();
  }

  bool isCoolSelected = true;

  void updateTempTab() {
    isCoolSelected = !isCoolSelected;
    notifyListeners();
  }

  int coolTem = 20;
  void coolTemHandler(isIncremt) {
    isIncremt ? coolTem++ : coolTem--;
    notifyListeners();
  }

  bool isShowTyre = false;

  void showTyreHadaler(int index) {
    if (selectedTab == 3 && index != 3)
      isShowTyre = false;
    else if (selectedTab != 3 && index == 3)
      Future.delayed(Duration(milliseconds: 400), () {
        isShowTyre = true;
      });
  }
}

home_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tesla/src/constants/constants.dart';
import 'package:tesla/src/screens/home_controller.dart';
import 'package:tesla/src/models/TyrePsi.dart';

import 'components/battery_status.dart';
import 'components/bottom_navigation.dart';
import 'components/door_lock.dart';
import 'components/temp_details.dart';
import 'components/tyres.dart';

class HomeScreen extends StatefulWidget {
  
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
  late AnimationController _batteryAnimationController;
  late Animation<double> _animationBattery;
  late Animation<double> _animationBatteryStatus;

  late AnimationController _tempAnimationController;
  late Animation<double> _animationCarShift;
  late Animation<double> _animationShowTempInfo;
  late Animation<double> _animationCoolGlow;

  late AnimationController _tyreAnimationController;

  late Animation<double> _animationTyre1Psi;
  late Animation<double> _animationTyre2Psi;
  late Animation<double> _animationTyre3Psi;
  late Animation<double> _animationTyre4Psi;

  void setupBatteryAnimation() {
    _batteryAnimationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 600));
    _animationBattery = CurvedAnimation(
      parent: _batteryAnimationController,
      curve: Interval(0.0, 0.5),
    );

    _animationBatteryStatus = CurvedAnimation(
      parent: _batteryAnimationController,
      curve: Interval(0.6, 1),
    );
  }

  void setupTemAnimation() {
    _tempAnimationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 1500),
    );

    _animationCarShift = CurvedAnimation(
      parent: _tempAnimationController,
      curve: Interval(0.2, 0.4),
    );
    _animationShowTempInfo = CurvedAnimation(
      parent: _tempAnimationController,
      curve: Interval(0.45, 0.65),
    );
    _animationCoolGlow = CurvedAnimation(
      parent: _tempAnimationController,
      curve: Interval(0.7, 1),
    );
  }

  void setupTyreAnimation() {
    _tyreAnimationController = AnimationController(
        vsync: this, duration: Duration(milliseconds: 1200));

    _animationTyre1Psi = CurvedAnimation(
      parent: _tyreAnimationController,
      curve: Interval(0.34, 0.5, curve: Curves.easeOutQuad),
    );
    _animationTyre2Psi = CurvedAnimation(
      parent: _tyreAnimationController,
      curve: Interval(0.5, 0.66, curve: Curves.easeOutQuad),
    );
    _animationTyre3Psi = CurvedAnimation(
      parent: _tyreAnimationController,
      curve: Interval(0.66, 0.82, curve: Curves.easeOutQuad),
    );
    _animationTyre4Psi = CurvedAnimation(
      parent: _tyreAnimationController,
      curve: Interval(0.82, 1, curve: Curves.easeOutQuad),
    );
  }

  final _controller = HomeController();

  @override
  void initState() {
    setupBatteryAnimation();
    setupTemAnimation();
    setupTyreAnimation();
    super.initState();
  }

  @override
  void dispose() {
    _batteryAnimationController.dispose();
    _tempAnimationController.dispose();
    _tyreAnimationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Size _size = MediaQuery.of(context).size;
    // int _currentTabIndex = _controller.selectedTab;
    return AnimatedBuilder(
      animation: Listenable.merge(
          [_controller, _batteryAnimationController, _tempAnimationController]),
      builder: (context, _) {
        return Scaffold(
          bottomNavigationBar: TeslaAppBottomNavigation(
            onTap: (index) {
              if (index == 1)
                _batteryAnimationController.forward();
              else if (_controller.selectedTab == 1 && index != 1)
                _batteryAnimationController.reverse(from: 0.7);
              if (index == 2)
                _tempAnimationController.forward();
              else if (_controller.selectedTab == 2 && index != 2)
                _tempAnimationController.reverse(from: 0.4);

              if (index == 3)
                _tyreAnimationController.forward();
              else if (_controller.selectedTab == 3 && index != 3) {
                _tyreAnimationController.reverse();
                // _animationShowTyre.status;
              }

              // if (index == 3) _tempAnimationController.forward();
              _controller.showTyreHadaler(index);
              _controller.onBottomNavigationTabChage(index);
            },
            selectedTab: _controller.selectedTab,
          ),
          body: SafeArea(
            child: SizedBox(
              // height: double.infinity,
              child: LayoutBuilder(
                builder: (context, constrains) {
                  return Stack(
                    alignment: Alignment.center,
                    // fit: StackFit.expand,
                    children: [
                      SizedBox(
                          height: constrains.maxHeight,
                          width: constrains.maxWidth),
                      Positioned(
                        height: constrains.maxHeight,
                        width: constrains.maxWidth,
                        left:
                            constrains.maxWidth / 2 * _animationCarShift.value,
                        child: Padding(
                          padding: EdgeInsets.symmetric(
                              vertical: constrains.maxHeight * 0.1),
                          child: SvgPicture.asset(
                            "assets/icons/Car.svg",
                            width: double.infinity,
                            // height: constrains.maxHeight * 0.8,
                          ),
                        ),
                      ),
                      AnimatedPositioned(
                        duration: defaultDuration,
                        right: _controller.selectedTab == 0
                            ? constrains.maxWidth * 0.05
                            : constrains.maxWidth / 2, //20
                        child: AnimatedOpacity(
                          duration: defaultDuration,
                          opacity: _controller.selectedTab == 0 ? 1 : 0,
                          child: DoorLock(
                            isDoorLock: _controller.rightDoorLock,
                            press: _controller.chnageRightDoorLock,
                          ),
                        ),
                      ),
                      AnimatedPositioned(
                        duration: defaultDuration,
                        left: _controller.selectedTab == 0
                            ? constrains.maxWidth * 0.05
                            : constrains.maxWidth / 2,
                        child: AnimatedOpacity(
                          duration: defaultDuration,
                          opacity: _controller.selectedTab == 0 ? 1 : 0,
                          child: DoorLock(
                            isDoorLock: _controller.leftDoorLock,
                            press: _controller.chnageLeftDoorLock,
                          ),
                        ),
                      ),
                      AnimatedPositioned(
                        duration: defaultDuration,
                        top: _controller.selectedTab == 0
                            ? constrains.maxHeight * 0.13
                            : constrains.maxHeight * 0.4, //20
                        child: AnimatedOpacity(
                          duration: defaultDuration,
                          opacity: _controller.selectedTab == 0 ? 1 : 0,
                          child: DoorLock(
                            isDoorLock: _controller.bonnetLock,
                            press: _controller.chnageBonnetLock,
                          ),
                        ),
                      ),
                      AnimatedPositioned(
                        duration: defaultDuration,
                        bottom: _controller.selectedTab == 0
                            ? constrains.maxHeight * 0.17
                            : constrains.maxHeight * 0.4,
                        child: AnimatedOpacity(
                          duration: defaultDuration,
                          opacity: _controller.selectedTab == 0 ? 1 : 0,
                          child: DoorLock(
                            isDoorLock: _controller.trunkLock,
                            press: _controller.chnagetrunkLock,
                          ),
                        ),
                      ),

                      // Battery
                      Positioned(
                        child: Opacity(
                          // duration: defaultDuration,
                          opacity: _animationBattery.value,
                          child: SvgPicture.asset(
                            "assets/icons/Battery.svg",
                            width: constrains.maxWidth * 0.45,
                          ),
                        ),
                      ),
                      Positioned(
                        top: 50 * (1 - _animationBatteryStatus.value),
                        child: Opacity(
                          opacity: _animationBatteryStatus.value,
                          // opacity: 1,
                          child: BatteryStatus(constrains: constrains),
                        ),
                      ),

                      // Temp
                      Positioned(
                        top: 40 * (1 - _animationShowTempInfo.value),
                        child: Opacity(
                          opacity: _animationShowTempInfo.value,
                          child: TempDetails(
                            constrains: constrains,
                            press: _controller.updateTempTab,
                            isCoolTabSelected: _controller.isCoolSelected,
                          ),
                        ),
                      ),
                      Positioned(
                        right: -180 * (1 - _animationCoolGlow.value),
                        child: AnimatedSwitcher(
                          duration: defaultDuration,
                          child: _controller.isCoolSelected
                              ? Image.asset(
                                  "assets/images/Cool_glow_2.png",
                                  key: UniqueKey(),
                                  width: 200,
                                )
                              : Image.asset(
                                  "assets/images/Hot_glow_4.png",
                                  key: UniqueKey(),
                                  width: 200,
                                ),
                        ),
                      ),
                      if (_controller.isShowTyre) ...tyres(constrains),
                      GridView(
                        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 2,
                          mainAxisSpacing: defaultPadding,
                          crossAxisSpacing: defaultPadding,
                          childAspectRatio:
                              constrains.maxWidth / constrains.maxHeight,
                        ),
                        children: [
                          ScaleTransition(
                            scale: _animationTyre1Psi,
                            child: TyrePsiCard(tyrePsi: demoPsiList[0]),
                          ),
                          ScaleTransition(
                            scale: _animationTyre2Psi,
                            child: TyrePsiCard(tyrePsi: demoPsiList[1]),
                          ),
                          ScaleTransition(
                            scale: _animationTyre3Psi,
                            child: TyrePsiCard(
                              tyrePsi: demoPsiList[2],
                              isBottomTwoTyre: true,
                            ),
                          ),
                          ScaleTransition(
                            scale: _animationTyre4Psi,
                            child: TyrePsiCard(
                              tyrePsi: demoPsiList[3],
                              isBottomTwoTyre: true,
                            ),
                          ),
                        ],
                      ),
                    ],
                  );
                },
              ),
            ),
          ),
        );
      },
    );
  }
}

class TyrePsiCard extends StatelessWidget {
  const TyrePsiCard({
    Key? key,
    required this.tyrePsi,
    this.isBottomTwoTyre = false,
  }) : super(key: key);

  final TyrePsi tyrePsi;
  final bool isBottomTwoTyre;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(defaultPadding),
      decoration: BoxDecoration(
        color:
            tyrePsi.isLowPressure ? redColor.withOpacity(0.1) : Colors.white10,
        borderRadius: BorderRadius.all(Radius.circular(6)),
        border: Border.all(
            color: tyrePsi.isLowPressure ? redColor : primaryColor, width: 2),
      ),
      child: isBottomTwoTyre
          ? Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (tyrePsi.isLowPressure) LowPressureText(),
                Spacer(),
                Text(
                  "${tyrePsi.temp}\u2103",
                  style: TextStyle(fontSize: 16),
                ),
                const SizedBox(height: defaultPadding),
                PsiText(psi: tyrePsi.psi),
              ],
            )
          : Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                PsiText(psi: tyrePsi.psi),
                const SizedBox(height: defaultPadding),
                Text(
                  "${tyrePsi.temp}\u2103",
                  style: TextStyle(fontSize: 16),
                ),
                Spacer(),
                if (tyrePsi.isLowPressure) LowPressureText(),
              ],
            ),
    );
  }
}

class PsiText extends StatelessWidget {
  const PsiText({
    Key? key,
    required this.psi,
  }) : super(key: key);

  final double psi;

  @override
  Widget build(BuildContext context) {
    return Text.rich(
      TextSpan(
        text: psi.toString(),
        style: Theme.of(context).textTheme.headlineMedium!.copyWith(
              color: Colors.white,
              fontWeight: FontWeight.w600,
            ),
        children: [
          TextSpan(
            text: "psi",
            style: TextStyle(fontSize: 24),
          ),
        ],
      ),
    );
  }
}

class LowPressureText extends StatelessWidget {
  const LowPressureText({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          "Low".toUpperCase(),
          style: Theme.of(context)
              .textTheme
              .displaySmall!
              .copyWith(color: Colors.white, fontWeight: FontWeight.bold),
        ),
        Text(
          "PRESSURE",
          style: TextStyle(
            fontSize: 20,
          ),
        ),
      ],
    );
  }
}

A pasta src contem as pastas já demonstradas acima (constants, models e screens) e o arquivo principal main do projeto.

Lib->Src

main.dart

import 'dart:async';
import 'dart:developer';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tesla/src/screens/home_controller.dart';
import 'package:tesla/src/screens/home_screen.dart';

void main() {
  ErrorWidget.builder = (FlutterErrorDetails errorDatails) => CustomError(errorDatails: errorDatails);

  runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    runApp(MyApp());
  }, (error, stack) {
    log('Erro não tratado',error: error, stackTrace: stack);
    throw error;
  });
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: Colors.black,
      ),
      home: ChangeNotifierProvider(
        create: (context) => HomeController(),
        child: HomeScreen(),
      ),
    );
  }
}

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),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Abaixo imagens do projeto

Vídeo demonstrando o projeto

 

 

Segue o código fonte completo no meu Github (link)