- Update Gradle
- Firebase configuration
- Configurations - files
- Method extensions
- Hooks - flutter_hooks extension
- Theming
- Routing
- Animations
- Screen
- TabBar
- Constraints
- Firebase
- License Registry
- Useful Packages
- Hints
- Widgets
- Gradle plug-in versions list.
- Update the Android Gradle plugin Compatible Gradle versions with Gradle plugin version.
- Gradle distributions.
- Kotlin plugin version.
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
To fix: PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 10: , null, null).
- Open terminal inside your flutter project
cd android
./gradlew signingReport
orgradlew signingReport
, To get SHA1 and SHA256 keys.- Add those keys to your firebase project:
Project settings -> general -> your Apps -> Android Apps -> Add fingerprint
. - Download and replace the
google-services.json
. 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.
-
Configure your Firebase project
-
Select IOS
-
Enter your Bundle ID
-
Download
credetials
. -
Download and replace
GoogleService-info.plist
. -
Add this to your
info.plist
inios\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>
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>
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;
}
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,
);
}
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
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;
}
}
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,
);
}
}
extension CompactMap<T> on Iterable<T?> {
Iterable<E> compactMap<E>([E? Function(T?)? transform]) {
return map(transform ?? (e) => e).where((e) => e != null).cast();
}
}
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;
}
}
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'),
);
}
}
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}'),
],
),
);
}
}
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!,
),
),
],
),
);
}
}
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())),
);
}
}
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}'),
);
}),
),
),
],
),
);
}
}
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),
),
),
),
),
),
),
],
),
);
}
}
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),
),
),
),
);
},
),
),
);
}
}
/* 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)
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?>;
- Repo: flutter_animations
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!'),
)
],
),
),
);
}
}
- Repo: flutter_animations
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,
),
),
);
}
}
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.
- Repo: flutter_animations
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),
),
),
],
),
),
);
}
}
- Repo: flutter_animations
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: () {},
);
}
}
- Repo: flutter_animations
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,
);
},
);
}
}
- Repo: flutter_animations
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,
);
},
);
}
}
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],
);
},
);
}
}
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],
);
},
);
}
}
// 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;
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;
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
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(),
]),
),
);
}
}
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),
),
),
],
),
),
- Repo: flutter_myshop
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';
}
}
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(...);
}
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. |
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 |
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,
),
);
}
@override
void initState() {
super.initState();
// Fire the `_addTrips` function after the build method is run.
WidgetsBinding.instance.addPostFrameCallback((_) => _addTrips());
}
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,
),
);
}
}