Last active
July 5, 2022 10:59
-
-
Save Ahmadre/2b124875ed5adbd43de6d73b3d25f3cf to your computer and use it in GitHub Desktop.
Generic Types in Flutter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
const Color darkBlue = Color.fromARGB(255, 18, 32, 47); | |
/// Main | |
void main() { | |
runApp(GenericTypesApp()); | |
} | |
/// EOF Main | |
/// Models | |
class Product { | |
final String title; | |
final String? description; | |
final double price; | |
final String? size; | |
final Color? color; | |
Product({ | |
required this.title, | |
this.description, | |
required this.price, | |
this.size, | |
this.color, | |
}); | |
Product copyWith({ | |
final String? title, | |
final String? description, | |
final double? price, | |
final String? size, | |
final Color? color, | |
}) { | |
return Product( | |
title: title ?? this.title, | |
description: description ?? this.description, | |
price: price ?? this.price, | |
size: size ?? this.size, | |
color: color ?? this.color, | |
); | |
} | |
@override | |
String toString() { | |
return 'Product(title: $title, description: $description, price: $price, size: $size, color: $color)'; | |
} | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) return true; | |
return other is Product && | |
other.title == title && | |
other.description == description && | |
other.price == price && | |
other.size == size && | |
other.color == color; | |
} | |
@override | |
int get hashCode { | |
return title.hashCode ^ | |
description.hashCode ^ | |
price.hashCode ^ | |
size.hashCode ^ | |
color.hashCode; | |
} | |
} | |
enum Size { | |
xs('XS'), | |
s('S'), | |
m('M'), | |
l('L'), | |
xl('XL'), | |
xxl('XXL'); | |
final String value; | |
const Size(this.value); | |
} | |
/// EOF Models | |
/// App | |
class GenericTypesApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData.dark().copyWith( | |
scaffoldBackgroundColor: darkBlue, | |
), | |
debugShowCheckedModeBanner: false, | |
home: const ProductPage(), | |
); | |
} | |
} | |
/// EOF App | |
class ProductPage extends StatefulWidget { | |
const ProductPage({ | |
Key? key, | |
}) : super(key: key); | |
@override | |
ProductPageState createState() => ProductPageState(); | |
} | |
class ProductPageState extends State<ProductPage> { | |
Product product = Product( | |
title: 'Dash Holiday Shirt', | |
price: 20, | |
color: Colors.black, | |
); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Shirt(product: product), | |
const SizedBox( | |
height: 25, | |
), | |
Text( | |
product.title, | |
style: Theme.of(context).textTheme.headline5, | |
), | |
const SizedBox( | |
height: 15, | |
), | |
Text( | |
'${product.price.toString()} €', | |
style: Theme.of(context).textTheme.headline5, | |
), | |
const SizedBox( | |
height: 25, | |
), | |
SingleOptionsPicker<String>( | |
options: { | |
Size.xs.value: 1, | |
Size.s.value: 3, | |
Size.m.value: 5, | |
Size.l.value: 10, | |
Size.xl.value: 7, | |
Size.xxl.value: 15, | |
}, | |
onSingleOptionSet: (String option) => setState(() { | |
product = product.copyWith(size: option); | |
}), | |
), | |
const SizedBox( | |
height: 15, | |
), | |
SingleOptionsPicker<Color>( | |
options: { | |
Colors.black: 3, | |
Colors.blue: 1, | |
Colors.white: 4, | |
Colors.red: 10, | |
Colors.yellow: 5, | |
Colors.green: 3, | |
}, | |
onSingleOptionSet: (Color option) => setState(() { | |
product = product.copyWith(color: option); | |
}), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class SingleOptionsPicker<T> extends StatelessWidget { | |
const SingleOptionsPicker({ | |
Key? key, | |
required this.options, | |
required this.onSingleOptionSet, | |
}) : super(key: key); | |
final Map<T, int> options; | |
final ValueChanged<T> onSingleOptionSet; | |
@override | |
Widget build(BuildContext context) { | |
return OptionsPicker<T>( | |
options: options, | |
onOptionSet: (Set<T> options) => onSingleOptionSet(options.first), | |
allowMultiple: false, | |
); | |
} | |
} | |
class OptionsPicker<T> extends StatefulWidget { | |
const OptionsPicker({ | |
Key? key, | |
required this.options, | |
required this.onOptionSet, | |
this.allowMultiple = false, | |
}) : super(key: key); | |
final Map<T, int> options; | |
final ValueChanged<Set<T>> onOptionSet; | |
final bool allowMultiple; | |
@override | |
OptionsPickerState<T> createState() => OptionsPickerState<T>(); | |
} | |
class OptionsPickerState<T> extends State<OptionsPicker<T>> { | |
late Set<int> selectedOptions; | |
late List<T> optionsList; | |
@override | |
void initState() { | |
super.initState(); | |
selectedOptions = <int>{0}; | |
optionsList = widget.options.keys.toList(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final toggleContent = <Widget>[]; | |
widget.options.forEach( | |
(T key, int quantity) { | |
toggleContent.add( | |
Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), | |
child: key is! Color | |
? Text( | |
(key as String).toUpperCase(), | |
style: TextStyle( | |
color: Theme.of(context).colorScheme.secondary, | |
), | |
) | |
: Container( | |
width: 50, | |
height: 50, | |
decoration: BoxDecoration( | |
color: key, | |
shape: BoxShape.circle, | |
), | |
), | |
), | |
); | |
}, | |
); | |
final List<bool> selectedIndices = List.generate( | |
optionsList.length, | |
(int index) => selectedOptions.contains(index), | |
); | |
return ToggleButtons( | |
isSelected: selectedIndices, | |
renderBorder: T is! Color, | |
borderColor: T is Color ? Colors.transparent : null, | |
borderRadius: BorderRadius.circular(15), | |
children: toggleContent, | |
onPressed: (int index) { | |
if (!widget.allowMultiple) { | |
if (selectedOptions.contains(index)) { | |
return; | |
} | |
selectedOptions | |
..clear() | |
..add(index); | |
} else { | |
if (selectedOptions.contains(index)) { | |
if (selectedOptions.length > 1) { | |
selectedOptions.remove(index); | |
} | |
} else { | |
selectedOptions.add(index); | |
} | |
} | |
final Set<T> selectedSet = {}; | |
for (int i in selectedOptions) { | |
selectedSet.add(optionsList[i]); | |
} | |
widget.onOptionSet(selectedSet); | |
}, | |
); | |
} | |
} | |
class Shirt extends StatelessWidget { | |
const Shirt({ | |
Key? key, | |
required this.product, | |
}) : super(key: key); | |
final Product product; | |
final String shirtSource = | |
'https://firebasestorage.googleapis.com/v0/b/flutter-course-app-logik.appspot.com/o/shirt.png?alt=media&token=c909366a-6914-4e33-928b-6180bb5e842f'; | |
final String dashSource = | |
'https://firebasestorage.googleapis.com/v0/b/flutter-course-app-logik.appspot.com/o/dash.png?alt=media&token=07ef5f44-6162-47f0-a86b-78dea6e3c75c'; | |
final double colorIntensity = .7; | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
alignment: Alignment.center, | |
children: [ | |
Image.network( | |
shirtSource, | |
color: product.color?.withOpacity(colorIntensity), | |
colorBlendMode: BlendMode.srcATop, | |
), | |
Center( | |
child: Transform.translate( | |
offset: const Offset(0, -80), | |
child: Opacity( | |
opacity: .9, | |
child: Image.network( | |
dashSource, | |
width: 250, | |
height: 250, | |
), | |
), | |
), | |
), | |
], | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment