Skip to content

Instantly share code, notes, and snippets.

@belachkar
Last active February 6, 2023 06:34
Show Gist options
  • Save belachkar/63d454be82ad4c0d5328e4e8ff7e7cec to your computer and use it in GitHub Desktop.
Save belachkar/63d454be82ad4c0d5328e4e8ff7e7cec to your computer and use it in GitHub Desktop.

Flutter tips

Table Of Content

Update Gradle

android\build.gradle:

dependencies {
  ext.kotlin_version = '1.7.0'

  dependencies {
    classpath 'com.android.tools.build:gradle:7.1.3'
  }
}

android\gradle\wrapper\gradle-wrapper.properties:

distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip

Firebase configuration

Google Sign In

To fix: PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 10: , null, null).

Android

  1. Open terminal inside your flutter project
  2. cd android
  3. ./gradlew signingReport or gradlew signingReport, To get SHA1 and SHA256 keys.
  4. Add those keys to your firebase project: Project settings -> general -> your Apps -> Android Apps -> Add fingerprint.
  5. Download and replace the google-services.json.
  6. flutter clean, may be not necessary.

Or, if you could run Google Sign In during debug but NOT in release, there is a high chance that you did not add your release key's SHA1 and SHA256 to firebase. To get the release key's SHAs, use keytool -list -v -keystore ~/key.jks -alias key.

You should end up with total of at least 6 SHA credentials: 2 from the debug key, 2 from Google Play linking, and 2 from the release key. Note that you do not need to redownload the google-services.json file after adding the release SHA credentials to firebase.

iOS

  1. Configure your Firebase project

  2. Select IOS

  3. Enter your Bundle ID

  4. Download credetials.

  5. Download and replace GoogleService-info.plist.

  6. Add this to your info.plist in ios\Runner\Info.plist:

    <key>CFBundleURLTypes</key>
    <array>
      <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
          <string><!--INSERT RESERVED_CLIENT_ID FROM GoogleService-Info.plist--></string>
        </array>
      </dict>
    </array>

Configurations - files

Enabling networking requests

MacOS

macos\Runner\DebugProfile.entitlements:

<dict>
 <key>com.apple.security.app-sandbox</key>
 <true/>
 <key>com.apple.security.cs.allow-jit</key>
 <true/>
 <key>com.apple.security.network.client</key>
 <true/>
 <key>com.apple.security.network.server</key>
 <true/>
</dict>

macos\Runner\Release.entitlements:

<dict>
 <key>com.apple.security.app-sandbox</key>
 <true/>
 <key>com.apple.security.network.client</key>
 <true/>
 <key>com.apple.security.network.server</key>
 <true/>
</dict>

Method extensions

String, Enum - Capitalization

extension StringExtension on String {
  String get capitalize => '${this[0].toUpperCase()}${substring(1)}';
  String get capitalizeFirstOfEach => split(' ').map((str) => str.capitalize).join(' ');
}

extension ComplexityText on Enum {
  String get capitalized => name.capitalize;
}

BuildContext - Theaming

extension TypographyUtils on BuildContext {
  ThemeData get theme => Theme.of(this);
  TextTheme get textTheme => theme.textTheme; // Modify this line
  ColorScheme get colors => theme.colorScheme;
  TextStyle? get displayLarge => textTheme.displayLarge?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get displayMedium => textTheme.displayMedium?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get displaySmall => textTheme.displaySmall?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get headlineLarge => textTheme.headlineLarge?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get headlineMedium => textTheme.headlineMedium?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get headlineSmall => textTheme.headlineSmall?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get titleLarge => textTheme.titleLarge?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get titleMedium => textTheme.titleMedium?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get titleSmall => textTheme.titleSmall?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get labelLarge => textTheme.labelLarge?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get labelMedium => textTheme.labelMedium?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get labelSmall => textTheme.labelSmall?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get bodyLarge => textTheme.bodyLarge?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get bodyMedium => textTheme.bodyMedium?.copyWith(
        color: colors.onSurface,
      );
  TextStyle? get bodySmall => textTheme.bodySmall?.copyWith(
        color: colors.onSurface,
      );
}

BoxConstraints - Layout

extension BreakpointUtils on BoxConstraints {
  bool get isTablet => maxWidth > 730;
  bool get isDesktop => maxWidth > 1200;
  bool get isMobile => !isTablet && !isDesktop;
}

String, Duration - Conversion

extension DurationString on String {
  /// Assumes a string (roughly) of the format '\d{1,2}:\d{2}'
  Duration toDuration() {
    final chunks = split(':');
    if (chunks.length == 1) {
      throw Exception('Invalid duration string: $this');
    } else if (chunks.length == 2) {
      return Duration(
        minutes: int.parse(chunks[0].trim()),
        seconds: int.parse(chunks[1].trim()),
      );
    } else if (chunks.length == 3) {
      return Duration(
        hours: int.parse(chunks[0].trim()),
        minutes: int.parse(chunks[1].trim()),
        seconds: int.parse(chunks[2].trim()),
      );
    } else {
      throw Exception('Invalid duration string: $this');
    }
  }
}

extension HumanizedDuration on Duration {
  String toHumanizedString() {
    final seconds = '${inSeconds % 60}'.padLeft(2, '0');
    String minutes = '${inMinutes % 60}';
    if (inHours > 0 || inMinutes == 0) {
      minutes = minutes.padLeft(2, '0');
    }
    String value = '$minutes:$seconds';
    if (inHours > 0) {
      value = '$inHours:$minutes:$seconds';
    }
    return value;
  }
}

Neumorphism - Widget

extension Neumorphism on Widget {
  addNeumorphism({
    double borderRadius = 10,
    double blurRadius = 10,
    double spreadRadius = 2,
    Offset offset = const Offset(5, 5),
    Color topShadowColor = Colors.white60,
    Color bottomShadowColor = const Color(0x26234395),
  }) {
    return Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
        boxShadow: [
          BoxShadow(
            offset: offset,
            blurRadius: blurRadius,
            spreadRadius: spreadRadius,
            color: bottomShadowColor,
          ),
          BoxShadow(
            offset: Offset(-offset.dx, -offset.dy),
            blurRadius: blurRadius,
            spreadRadius: spreadRadius,
            color: topShadowColor,
          ),
        ],
      ),
      child: this,
    );
  }
}

Compact Map

extension CompactMap<T> on Iterable<T?> {
  Iterable<E> compactMap<E>([E? Function(T?)? transform]) {
    return map(transform ?? (e) => e).where((e) => e != null).cast();
  }
}

Normalize on num

extension Normalize on num {
  num normalize(
    num min,
    num max, [
    num normalizedMin = 0.0,
    num normalizedMax = 1.0,
  ]) {
    // To avoid divided by zero exception
    final isEqual = min == max;
    if (isEqual) return normalizedMin;

    final range = max - min;
    final normalizedRange = normalizedMax - normalizedMin;

    return normalizedRange * ((this - min) / range) + normalizedMin;
  }
}

Hooks - flutter_hooks extension

useStream

Stream<String> getTime$(int seconds) {
  return Stream<String>.periodic(
    Duration(seconds: seconds),
    (_) => DateTime.now().toLocal().toIso8601String(),
  );
}

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final dateTime = useStream(getTime$(2));

    return Scaffold(
      appBar: AppBar(
        title: Text(dateTime.data ?? 'Home Page'),
      ),
      body: const Text('Home Page'),
    );
  }
}

useTextEditingController, useState, useEffect

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final controller = useTextEditingController();
    final text = useState<String>('');

    useEffect(() {
      controller.addListener(() {
        text.value = controller.text;
      });
      return null;
    }, [controller]);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
      body: Column(
        children: [
          TextField(controller: controller),
          Text('You typed ${text.value}'),
        ],
      ),
    );
  }
}

useFuture, useMemoized

const imgURL = 'https://bit.ly/3qYOtDm';

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final memorized = useMemoized(() {
      return NetworkAssetBundle(Uri.parse(imgURL))
          .load(imgURL)
          .then((data) => data.buffer.asUint8List())
          .then((data) => Image.memory(data, fit: BoxFit.cover));
    });

    final img = useFuture(memorized);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Hooks App'),
      ),
      body: Column(
        children: [
          if (img.hasData)
            Expanded(
              child: SizedBox(
                width: double.infinity,
                child: img.data!,
              ),
            ),
        ],
      ),
    );
  }
}

useListenable, useMemoized

class CountDown extends ValueNotifier<int> {
  late StreamSubscription sub;

  CountDown({required int from}) : super(from) {
    final str = Stream.periodic(const Duration(seconds: 1), (c) => from - c);
    sub = str.takeWhile((v) => v >= 0).listen((v) => value = v);
  }

  @override
  void dispose() {
    super.dispose();
    sub.cancel();
  }
}

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final countDown = useMemoized(() => CountDown(from: 6));
    final notifier = useListenable(countDown);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Hooks App'),
      ),
      body: Center(child: Text(notifier.value.toString())),
    );
  }
}

useAnimationController, useScrollController, useEffect

Changing an image size and opacity while scrolling.

const imgURL = 'https://bit.ly/3qYOtDm';
const imgHeight = 200.0;

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final opacityCtrl = useAnimationController(
      initialValue: 1.0,
    );

    final sizeCtrl = useAnimationController(
      initialValue: 1.0,
    );

    final scrollCtrl = useScrollController();

    useEffect(() {
      scrollCtrl.addListener(() {
        final scrollOffset = max(imgHeight - scrollCtrl.offset, 0.0);

        // Using normalize extension
        final normalized = scrollOffset.normalize(0.0, imgHeight).toDouble();

        opacityCtrl.value = normalized;
        sizeCtrl.value = normalized;
      });
    }, [scrollCtrl]);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Hooks App'),
      ),
      body: Column(
        children: [
          SizeTransition(
            sizeFactor: sizeCtrl,
            axis: Axis.vertical,
            axisAlignment: -1.0,
            child: Image.network(
              imgURL,
              opacity: opacityCtrl,
              height: imgHeight,
              width: double.infinity,
              fit: BoxFit.cover,
            ),
          ),
          Expanded(
            child: ListView.builder(
              controller: scrollCtrl,
              itemCount: 100,
              itemBuilder: ((context, i) {
                return ListTile(
                  title: Text('Person ${i + 1}'),
                );
              }),
            ),
          ),
        ],
      ),
    );
  }
}

useReducer - Store, State, Actions

Using hooks to control imgage rotation and opacity by dispatching actions.

import 'dart:math';

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

const imgURL = 'https://bit.ly/3qYOtDm';

enum Action {
  rotateLeft,
  rotateRight,
  increaseVisibility,
  reduceVisiility,
  none
}

@immutable
class State {
  final double rotationDeg;
  final double alpha;

  const State({required this.rotationDeg, required this.alpha});
  const State.zero() : this(rotationDeg: 0.0, alpha: 1.0);

  final _rotationStep = 20.0;
  final _alphaStep = 0.1;

  State rotateRight() => _rotate(_rotationStep);
  State rotateLeft() => _rotate(-1 * _rotationStep);
  State increaseOpacity() => _updateAlpha(_alphaStep);
  State reduceOpacity() => _updateAlpha(-1 * _alphaStep);

  State _rotate(double deg) {
    return State(rotationDeg: rotationDeg + deg, alpha: alpha);
  }

  State _updateAlpha(double v) {
    // To kep the value within [0.0 - 1.0].
    double newAlpha = min(alpha + v, 1.0);
    newAlpha = max(newAlpha, 0.0);

    return State(rotationDeg: rotationDeg, alpha: newAlpha);
  }
}

State reducer(State state, Action action) {
  switch (action) {
    case Action.rotateLeft:
      return state.rotateLeft();
    case Action.rotateRight:
      return state.rotateRight();
    case Action.increaseVisibility:
      return state.increaseOpacity();
    case Action.reduceVisiility:
      return state.reduceOpacity();
    case Action.none:
      return state;
  }
}

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    final store = useReducer<State, Action>(
      reducer,
      initialState: const State.zero(),
      initialAction: Action.none,
    );

    // Dispatch actions functions
    void rotateLeft() => store.dispatch(Action.rotateLeft);
    void rotateRight() => store.dispatch(Action.rotateRight);
    void increaseVisibility() => store.dispatch(Action.increaseVisibility);
    void reduceVisiility() => store.dispatch(Action.reduceVisiility);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Hooks App'),
      ),
      body: Column(
        children: [
          const SizedBox(height: 8.0),
          // Buttons - Rotation, Opacity
          Wrap(
            spacing: 16.0,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: [
              // Rotation buttons
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextButton(onPressed: rotateLeft, child: const Text('◀')),
                  Text('Rotate', style: textTheme.labelLarge),
                  TextButton(onPressed: rotateRight, child: const Text('▶')),
                ],
              ),
              // Opacity buttons
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextButton(
                      onPressed: increaseVisibility, child: const Text('▲')),
                  Text('Opacity', style: textTheme.labelLarge),
                  TextButton(
                      onPressed: reduceVisiility, child: const Text('▼')),
                ],
              ),
            ],
          ),
          const SizedBox(height: 8.0),
          // Image
          Expanded(
            child: Center(
              child: RotationTransition(
                turns: AlwaysStoppedAnimation(store.state.rotationDeg / 360),
                child: Opacity(
                  opacity: store.state.alpha,
                  child: const CircleAvatar(
                    radius: 150.0,
                    backgroundColor: Colors.black12,
                    child: CircleAvatar(
                      radius: 142.0,
                      backgroundImage: NetworkImage(imgURL),
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Stream controller - Rotating an image

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    late StreamController<double> controller;

    controller = StreamController<double>(
      onListen: () => controller.sink.add(0.0),
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('Hooks App'),
      ),
      body: Center(
        child: StreamBuilder<double>(
          stream: controller.stream,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return const Text('An error occured');
            }

            if (!snapshot.hasData) return const CircularProgressIndicator();

            // NO erros and data available
            final rotation = snapshot.data ?? 0.0;
            final rotationInDeg = rotation / 360.0;
            return RotationTransition(
              turns: AlwaysStoppedAnimation(rotationInDeg),
              child: GestureDetector(
                onTap: () => controller.sink.add(rotation + 10.0),
                child: const CircleAvatar(
                  radius: 140.0,
                  backgroundColor: Colors.black12,
                  child: CircleAvatar(
                    radius: 132.0,
                    backgroundImage: NetworkImage(imgURL),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

Theming

Custom text theme

/* STATIC method */
class CustomTextStyle {
  static TextStyle title(BuildContext context) {
    return Theme.of(context).textTheme.display4.copyWith(fontSize: 192.0);
  }
}

// Use
Text(
   'Hi',
   style: CustomTextStyle.title(context),
),

/* EXTENSION method */
extension CustomStyles on TextTheme {
 TextStyle get error => const TextStyle(decoration: TextDecoration.lineThrough, fontSize: 20.0, color: Colors.blue, fontWeight: FontWeight.bold);

// Use
Text("your text", style: Theme.of(context).textTheme.error)

Routing

Passing route data

fromRouteFile.dart:

final data = { 'location': location };
Navigator.pushReplacementNamed(context, homeViewRoute, arguments: data);

toRouteFile.dart:

Map<String, String?> data = {};
data = (ModalRoute.of(context)?.settings.arguments ?? <String, String?>{}) as Map<String, String?>;

Animations

Animated Container

import 'package:flutter/material.dart';

class Sandbox extends StatefulWidget {
  const Sandbox({super.key});

  @override
  State<Sandbox> createState() => _SandboxState();
}

class _SandboxState extends State<Sandbox> {
  var _margin = 20.0;
  var _opacity = 1.0;
  var _width = 180.0;
  var _color = Colors.blue;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sandbox')),
      body: AnimatedContainer(
        duration: const Duration(seconds: 1),
        margin: EdgeInsets.all(_margin),
        width: _width,
        color: _color,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => setState(() => _margin = 50),
              style: ElevatedButton.styleFrom(primary: Colors.orange),
              child: const Text('Animate MARGIN'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => setState(() => _color = Colors.purple),
              style: ElevatedButton.styleFrom(primary: Colors.orange),
              child: const Text('Animate COLOR'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => setState(() => _width = 300),
              style: ElevatedButton.styleFrom(primary: Colors.orange),
              child: const Text('Animate WIDTH'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => setState(() => _opacity = 0),
              style: ElevatedButton.styleFrom(primary: Colors.orange),
              child: const Text('Animate OPACITY'),
            ),
            // The opacity animation apply only to this widget
            AnimatedOpacity(
              opacity: _opacity,
              duration: const Duration(seconds: 1),
              child: const Text('Hide me!'),
            )
          ],
        ),
      ),
    );
  }
}

Tween animation builder

Ex: opacity & padding

import 'package:flutter/material.dart';
import '../common/durations.dart';

class ScreenTitle extends StatelessWidget {
  final String text;

  const ScreenTitle({super.key, required this.text});

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      duration: Durations.normal,
      tween: Tween(begin: 0.0, end: 1.0),
      builder: (context, _v, Widget? child) {
        return Opacity(
          opacity: _v,
          child: Padding(
            padding: EdgeInsets.only(top: _v * 20),
            child: child,
          ),
        );
      },
      child: Text(
        text,
        style: const TextStyle(
          fontSize: 36,
          color: Colors.white,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}

Hero Animation

Use the Hero widget to animate a widget from one screen to the next. By wraping the Image widget on both screens in a Hero widget with the same tag.

trip_tile.dart:

import 'package:flutter/material.dart';

import '../common/routes.dart';
import '../models/trip.dart';

class TripTile extends StatelessWidget {
  const TripTile({super.key, required this.trip});

  final Trip trip;

  @override
  Widget build(BuildContext context) {
    final navigator = Navigator.of(context);

    return ListTile(
      contentPadding: const EdgeInsets.all(25),
      // Image
      leading: ClipRRect(
        borderRadius: BorderRadius.circular(8.0),
        child: Hero(
          tag: 'img-${trip.img}',
          child: Image.asset(
            'assets/images/${trip.img}',
            height: 50.0,
          ),
        ),
      ),
      title: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          // Nights Nbr
          Text(
            '${trip.nights} nights',
            style: TextStyle(
              fontSize: 14,
              fontWeight: FontWeight.bold,
              color: Colors.blue[300],
            ),
          ),
          // Title
          Text(
            trip.title,
            style: TextStyle(fontSize: 20, color: Colors.grey[600]),
          ),
        ],
      ),
      // Price
      trailing: Text('\$${trip.price}'),
      onTap: () => navigator.pushNamed(RoutesName.details, arguments: trip),
    );
  }
}

details.dart:

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

import '../models/trip.dart';
import '../shared/heart.dart';

class Details extends StatelessWidget {
  const Details({super.key});

  @override
  Widget build(BuildContext context) {
    final Trip trip = ModalRoute.of(context)?.settings.arguments as Trip;

    return Scaffold(
      appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0),
      extendBodyBehindAppBar: true,
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            ClipRRect(
              child: Hero(
                tag: 'img-${trip.img}',
                child: Image.asset(
                  'assets/images/${trip.img}',
                  height: 360,
                  fit: BoxFit.cover,
                  alignment: Alignment.topCenter,
                ),
              ),
            ),
            const SizedBox(height: 30),
            ListTile(
              title: Text(
                trip.title,
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 20,
                  color: Colors.grey[800],
                ),
              ),
              subtitle: Text(
                '${trip.nights} night stay for only \$${trip.price}',
                style: const TextStyle(letterSpacing: 1),
              ),
              trailing: const Heart(),
            ),
            Padding(
              padding: const EdgeInsets.all(18),
              child: Text(
                Lorem.text(numParagraphs: 2, numSentences: 2),
                style: TextStyle(color: Colors.grey[600], height: 1.4),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Animation Controller

Set state

import 'package:flutter/material.dart';
import 'package:flutter_animations/common/durations.dart';

class Heart extends StatefulWidget {
  const Heart({super.key});

  @override
  State<Heart> createState() => _HeartState();
}

class _HeartState extends State<Heart> with SingleTickerProviderStateMixin {
  late AnimationController _animCtrlr;
  late Animation<Color?> _colorAnim;

  @override
  void initState() {
    super.initState();

    _animCtrlr = AnimationController(
      vsync: this,
      duration: Durations.slow,
    )
      ..forward()
      ..addListener(() => setState(() {}));

    _colorAnim = ColorTween(begin: Colors.grey[400], end: Colors.red)
        .animate(_animCtrlr);
  }

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(Icons.favorite, color: _colorAnim.value, size: 30),
      onPressed: () {},
    );
  }
}

AnimatedBuilder

import 'package:flutter/material.dart';

import '../common/durations.dart';

class Heart extends StatefulWidget {
  const Heart({super.key});

  @override
  State<Heart> createState() => _HeartState();
}

class _HeartState extends State<Heart> with SingleTickerProviderStateMixin {
  late AnimationController _animCtrlr;
  late Animation<Color?> _colorAnim;

  void _animate() {
    switch (_animCtrlr.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        // The condition is to ignore fast double clicks.
        if (_animCtrlr.value > 0.4) _animCtrlr.reverse();
        break;
      default:
        // The condition is to ignore fast double clicks.
        if (_animCtrlr.value < 0.6) _animCtrlr.forward();
    }
  }

  @override
  void initState() {
    super.initState();

    // Anime controller
    _animCtrlr = AnimationController(vsync: this, duration: Durations.normal);

    // Color controller
    final colorTween = ColorTween(begin: Colors.grey[400], end: Colors.red);
    _colorAnim = colorTween.animate(_animCtrlr);
  }

  @override
  void dispose() {
    super.dispose();
    _animCtrlr.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animCtrlr,
      builder: (context, _) {
        return IconButton(
          icon: Icon(Icons.favorite, color: _colorAnim.value, size: 30),
          onPressed: _animate,
        );
      },
    );
  }
}

Tween Sequence

import 'package:flutter/material.dart';

import '../common/durations.dart';

class Heart extends StatefulWidget {
  const Heart({super.key});

  @override
  State<Heart> createState() => _HeartState();
}

class _HeartState extends State<Heart> with SingleTickerProviderStateMixin {
  late AnimationController _animCtrlr;
  late Animation<Color?> _colorAnim;
  late Animation<double?> _sizeAnim;

  // The size values calculated manually
  // double get _calcAnimSize => (0.5 - (0.5 - _animCtrlr.value).abs()) * 20 + 30;

  void _animate() {
    switch (_animCtrlr.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        // The condition is to ignore fast double clicks.
        if (_animCtrlr.value > 0.5) _animCtrlr.reverse();
        break;
      default:
        // The condition is to ignore fast double clicks.
        if (_animCtrlr.value < 0.5) _animCtrlr.forward();
    }
  }

  @override
  void initState() {
    super.initState();

    // Animation controller
    _animCtrlr = AnimationController(vsync: this, duration: Durations.fast);

    // Color animation controller
    final colorTween = ColorTween(begin: Colors.grey[400], end: Colors.red);
    _colorAnim = colorTween.animate(_animCtrlr);

    // Size animation controller
    final sizeTweenSeq = TweenSequence([
      TweenSequenceItem(tween: Tween<double>(begin: 30, end: 50), weight: 50),
      TweenSequenceItem(tween: Tween<double>(begin: 50, end: 30), weight: 50),
    ]);
    _sizeAnim = sizeTweenSeq.animate(_animCtrlr);
  }

  @override
  void dispose() {
    super.dispose();
    _animCtrlr.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animCtrlr,
      builder: (context, _) {
        return IconButton(
          icon: Icon(
            Icons.favorite,
            color: _colorAnim.value,
            size: _sizeAnim.value,
            // size: _calcAnimSize,
          ),
          onPressed: _animate,
        );
      },
    );
  }
}

Animated List

Simple animation

import 'package:flutter/material.dart';
import 'package:flutter_animations/common/dbg.dart';

import '../data/trips_data.dart';
import '../models/trip.dart';
import 'trip_tile.dart';

class TripList extends StatefulWidget {
  const TripList({super.key});

  @override
  State<TripList> createState() => _TripListState();
}

class _TripListState extends State<TripList> {
  final _tripTiles = <TripTile>[];
  final _listKey = GlobalKey<AnimatedListState>();

  final _transOffset = Tween<Offset>(
    begin: const Offset(1, 0),
    end: const Offset(0, 0),
  );

  @override
  void initState() {
    super.initState();

    // Fire the `_addTrips` function after the build method is run.
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _addTrips());
  }

  void _addTrips() {
    // get data from db
    List<Trip> trips = tripsData;

    for (var trip in trips) {
      _tripTiles.add(TripTile(trip: trip));

      final addItemIndex = _tripTiles.length - 1;
      _listKey.currentState?.insertItem(addItemIndex);
    }
  }

  @override
  Widget build(context) {
    return AnimatedList(
      key: _listKey,
      initialItemCount: _tripTiles.length,
      itemBuilder: (ctx, i, animation) {
        return SlideTransition(
          position: animation.drive<Offset>(_transOffset),
          child: _tripTiles[i],
        );
      },
    );
  }
}

Delayed list items animation

import 'package:flutter/material.dart';

import '../data/trips_data.dart';
import '../models/trip.dart';
import 'trip_tile.dart';

class TripList extends StatefulWidget {
  const TripList({super.key});

  @override
  State<TripList> createState() => _TripListState();
}

class _TripListState extends State<TripList> {
  final _tripTiles = <TripTile>[];
  final _listKey = GlobalKey<AnimatedListState>();

  final _transOffset = Tween<Offset>(
    begin: const Offset(1, 0),
    end: const Offset(0, 0),
  );

  @override
  void initState() {
    super.initState();

    // Fire the `_addTrips` function after the build method is run.
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _addTrips());
  }

  void _addTrips() {
    final List<Trip> trips = tripsData;
    var ft = Future(() {});

    for (var trip in trips) {
      // Delay the list items animations.
      ft = ft.then((_) {
        return Future.delayed(const Duration(milliseconds: 30), () {
          _tripTiles.add(TripTile(trip: trip));

          final addItemIndex = _tripTiles.length - 1;
          _listKey.currentState?.insertItem(addItemIndex);
        });
      });
    }
  }

  @override
  Widget build(context) {
    return AnimatedList(
      key: _listKey,
      initialItemCount: _tripTiles.length,
      itemBuilder: (ctx, i, animation) {
        return SlideTransition(
          position: animation.drive<Offset>(_transOffset),
          child: _tripTiles[i],
        );
      },
    );
  }
}

Screen

Screen height, width

With context

// Full screen width and height
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;

// Height (without SafeArea)
var padding = MediaQuery.of(context).viewPadding;
double height1 = height - padding.top - padding.bottom;

// Height (without status bar)
double height2 = height - padding.top;

// Height (without status and toolbar)
double height3 = height - padding.top - kToolbarHeight;

Without context - Using physical size and device pixel ratio

import 'dart:ui';

var pixelRatio = window.devicePixelRatio;

 //Size in physical pixels
var physicalScreenSize = window.physicalSize;
var physicalWidth = physicalScreenSize.width;
var physicalHeight = physicalScreenSize.height;

//Size in logical pixels
var logicalScreenSize = window.physicalSize / pixelRatio;
var logicalWidth = logicalScreenSize.width;
var logicalHeight = logicalScreenSize.height;

//Padding in physical pixels
var padding = window.padding;

//Safe area paddings in logical pixels
var paddingLeft = window.padding.left / window.devicePixelRatio;
var paddingRight = window.padding.right / window.devicePixelRatio;
var paddingTop = window.padding.top / window.devicePixelRatio;
var paddingBottom = window.padding.bottom / window.devicePixelRatio;

//Safe area in logical pixels
var safeWidth = logicalWidth - paddingLeft - paddingRight;
var safeHeight = logicalHeight - paddingTop - paddingBottom;

Screen helper class

class Screen {
  static double get _ppi => (Platform.isAndroid || Platform.isIOS)? 150 : 96;
  static bool isLandscape(BuildContext c) => MediaQuery.of(c).orientation == Orientation.landscape;
  //PIXELS
  static Size size(BuildContext c) => MediaQuery.of(c).size;
  static double width(BuildContext c) => size(c).width;
  static double height(BuildContext c) => size(c).height;
  static double diagonal(BuildContext c) {
    Size s = size(c);
    return sqrt((s.width * s.width) + (s.height * s.height));
  }
  //INCHES
  static Size inches(BuildContext c) {
    Size pxSize = size(c);
    return Size(pxSize.width / _ppi, pxSize.height/ _ppi);
  }
  static double widthInches(BuildContext c) => inches(c).width;
  static double heightInches(BuildContext c) => inches(c).height;
  static double diagonalInches(BuildContext c) => diagonal(c) / _ppi;
}

For more: flutter-simplify-platform-detection-responsive-sizing

TabBar

On top

import 'package:flutter/material.dart';

import '../screens/categories_screen.dart';
import '../screens/favorites_screen.dart';

class TabsScreen extends StatefulWidget {
  const TabsScreen({Key? key}) : super(key: key);

  @override
  State<TabsScreen> createState() => _TabsScreenState();
}

class _TabsScreenState extends State<TabsScreen> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Meals'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.category), text: 'Categories'),
              Tab(icon: Icon(Icons.star), text: 'Favorites'),
            ],
          ),
        ),
        body: const TabBarView(children: [
          CategoriesScreen(),
          FavoritesScreen(),
        ]),
      ),
    );
  }
}

Constraints

Min, Max (height, width)

Container(
  color: Colors.blueAccent,
  constraints: BoxConstraints(
    minHeight: 100,
    minWidth: double.infinity,
    maxHeight: 400,
  ),
  child: ListView(
    shrinkWrap: true,
    children: <Widget>[
      ...List.generate(
        10,  // Replace this with 1, 2 to see min height works.
        (index) => Text(
          'Sample Test: ${index}',
          style: TextStyle(fontSize: 60, color: Colors.black),
        ),
      ),
    ],
  ),
),

Firebase

Collections

Add doc reference with converter

db/collections.dart:

import 'package:cloud_firestore/cloud_firestore.dart';

import '../providers/product.dart';

class Collections {
  static String products = 'products';

  static final _firestore = FirebaseFirestore.instance;

  // Firestore collections references with converter.
  static final productsColRef =
      _firestore.collection(products).withConverter<Product>(
            fromFirestore: (snapshot, options) => Product.fromDoc(snapshot),
            toFirestore: (product, options) => product.toDoc(),
          );
}

providers/product.dart:

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

class Product with ChangeNotifier {
  String id, title, description, imageUrl;
  double price;
  bool isFavorite;

  Product({
    required this.id,
    required this.title,
    required this.description,
    required this.imageUrl,
    required this.price,
    this.isFavorite = false,
  });

  Product.empty()
      : this(id: '', title: '', description: '', imageUrl: '', price: 0);

  Product.fromDoc(DocumentSnapshot<Map<String, dynamic>> doc)
      : this(
          id: doc.id,
          title: doc['title'],
          description: doc['description'],
          imageUrl: doc['imageUrl'],
          price: doc['price'] as double,
          isFavorite: doc['isFavorite'] as bool,
        );

  Map<String, dynamic> toDoc() {
    return {
      'title': title,
      'description': description,
      'imageUrl': imageUrl,
      'price': price,
      'isFavorite': isFavorite,
    };
  }

  Product copyWith({
    String? id,
    String? title,
    String? description,
    String? imageUrl,
    double? price,
  }) {
    // Hydrating the fields.
    id = id?.trim();
    title = title?.trim();
    description = description?.trim();
    imageUrl = imageUrl?.trim();

    this.id = (id == null || id.isEmpty) ? this.id : id;
    this.title = (title == null || title.isEmpty) ? this.title : title;
    this.description = (description == null || description.isEmpty)
        ? this.description
        : description;
    this.imageUrl =
        (imageUrl == null || imageUrl.isEmpty) ? this.imageUrl : imageUrl;
    this.price = (price == null || price == 0) ? this.price : price;

    return this;
  }

  void toggleFavorite() {
    isFavorite = !isFavorite;
    notifyListeners();
  }

  @override
  String toString() {
    super.toString();
    return 'Id: $id\nTitle: $title\nDescription: $description\nImage: $imageUrl\nPrice: $price\nIs favorite: $isFavorite';
  }
}

License Registry

Licensing Fonts

Ex: to include Google fonts licenses Licensing Fonts

void main() {
  LicenseRegistry.addLicense(() async* {
    final license = await rootBundle.loadString('google_fonts/OFL.txt');
    yield LicenseEntryWithLineBreaks(['google_fonts'], license);
  });

  runApp(...);
}

Useful Packages

Name Version Description
adaptive_breakpoints ^0.x.x Material Design breakpoints for responsive layouts.
adaptive_components ^0.x.x A set of widgets used to implement responsive grid layouts in Material Design.
adaptive_navigation ^0.x.x Contains a component for adaptive switching between BottomNavigationBar, NavigationRail, and Drawer.
animations ^2.x.x Makes it easy to implement a variety of animation types.
collection ^1.x.x Collections and utilities functions and classes related to collections.
cupertino_icons ^1.x.x This is an asset repo containing the default set of icon assets used by Flutter's Cupertino widgets.
dynamic_color ^1.x.x To create Material color schemes based on a platform's implementation of dynamic color.
english_words ^4.x.x Utilities for working with English words.
flutter_bloc ^8.x.x Flutter Widgets that make it easy to implement the BLoC (Business Logic Component) design pattern.
freezed_annotation ^2.x.x Annotations for the freezed code-generator. Needs freezed package.
go_router ^4.x.x A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes...
google_fonts ^3.x.x To use fonts from fonts.google.com.
material_color_utilities ^0.x.x Algorithms and utilities that power the Material Design 3 (M3) color system.
url_launcher ^6.x.x Plugin for launching a URL. Supports web, phone, SMS, and email schemes.
shared_preferences * Wraps platform-specific persistent storage for simple data. Supported data types are int, double, bool, String and List<String>.
hive * Hive is a lightweight and blazing fast key-value database written in pure Dart.

From youtube - Top 30 Flutter Tips and Tricks

Name Version Description
introduction_screen Introduction Screen allows you to have a screen on an app's first launch to.
flutter_native_splash This package automatically generates iOS, Android, and Web-native code for customizing this native splash screen background color and splash image.
flutter_launcher_icons A command-line tool which simplifies the task of updating your Flutter app's launcher icon.
google_fonts GoogleFonts
Interactive Viewer
Rich Text
Flexible
Expanded
Circle Avatar
Wrap
Fitted Box
Snackbar
Visibility
Spread operator ...
Status bar color (android)
Navigation bar color (android)
Extend body behind app bar
Safe Area
ClipRRect
Sliver app bar
Future builder
Cupertino Widgets (ios)
Platform Checking
MediaQuery
SelectableText
Hero
AnimatedIcon
Animated Container
Null-aware operator ??
Lint

Hints

Fix: CERTIFICATE_VERIFY_FAILED bad certificate

import 'dart:io';

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

import './screens/home_screen.dart';

class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          (X509Certificate cert, String host, int port) => true;
  }
}

void main() {
  // Fix in development mode: To avoid bad Certificate error
  // while caling APIs, or loading network images
  if (kDebugMode) HttpOverrides.global = MyHttpOverrides();

  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomeScreen(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

InitState

Call a method in InitState after the build method is fired

  @override
  void initState() {
    super.initState();

    // Fire the `_addTrips` function after the build method is run.
    WidgetsBinding.instance.addPostFrameCallback((_) => _addTrips());
  }

Widgets

ScaffoldMessengerWidget

import 'package:flutter/material.dart';

class ScaffoldMessengerWidget {
  BuildContext context;
  String content;
  String? label;
  VoidCallback? onPressed;
  int seconds;

  ScaffoldMessengerWidget.snackBar(
    this.context, {
    required this.content,
    this.onPressed,
    this.label,
    this.seconds = 2,
  }) {
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          content: Text(content),
          duration: Duration(seconds: seconds),
          action: (label != null)
              ? SnackBarAction(
                  label: label!,
                  onPressed: onPressed ??
                      () {
                        ScaffoldMessenger.of(context).hideCurrentSnackBar();
                      },
                )
              : null,
        ),
      );
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment