A highly customizable Flutter widget that makes it easy to render QR codes. Built on top of the qr package.
If you want to say thank you, star us on GitHub or like us on pub.dev.
First, follow the package installation instructions and add a PrettyQrView
widget to your app:
PrettyQrView.data(
data: 'lorem ipsum dolor sit amet',
decoration: const PrettyQrDecoration(
image: PrettyQrDecorationImage(
image: AssetImage('images/flutter.png'),
),
),
)
If you want to pass non-string data or want to specify a QR version, consider using the default PrettyQrView
constructor:
@protected
late QrImage qrImage;
@override
void initState() {
super.initState();
final qrCode = QrCode(
8,
QrErrorCorrectLevel.H,
)..addData('lorem ipsum dolor sit amet');
qrImage = QrImage(qrCode);
}
@override
Widget build(BuildContext context) {
return PrettyQrView(
qrImage: qrImage,
decoration: const PrettyQrDecoration(),
);
}
Note: Do not create QrImage
inside the build
method; otherwise, you may have an undesired jank in the UI thread.
You can save the QR code as an image using the toImage or toImageAsBytes extension methods that apply to QrImage
. Optionally, the configuration
parameter may be used to set additional saving options, such as pixel ratio or text direction.
final qrCode = QrCode.fromData(
data: 'lorem ipsum dolor sit amet',
errorCorrectLevel: QrErrorCorrectLevel.H,
);
final qrImage = QrImage(qrCode);
final qrImageBytes = await qrImage.toImageAsBytes(
size: 512,
format: ImageByteFormat.png,
decoration: const PrettyQrDecoration(),
);
See the example
folder for more code samples of the various possibilities.
Contributions are welcomed!
Here is a curated list of how you can help:
Run this command:
With Flutter:
$ flutter pub add pretty_qr_code
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
pretty_qr_code: ^3.3.0
Alternatively, your editor might support flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'package:flutter/material.dart';
import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'package:pretty_qr_code_example/features/io_save_image.dart'
if (dart.library.html) 'package:pretty_qr_code_example/features/web_save_image.dart';
void main() {
runApp(const PrettyQrExampleApp());
}
class PrettyQrExampleApp extends StatelessWidget {
const PrettyQrExampleApp({
super.key,
});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.black,
),
),
home: const PrettyQrHomePage(),
);
}
}
class PrettyQrHomePage extends StatefulWidget {
const PrettyQrHomePage({
super.key,
});
@override
State<PrettyQrHomePage> createState() => _PrettyQrHomePageState();
}
class _PrettyQrHomePageState extends State<PrettyQrHomePage> {
@protected
late QrCode qrCode;
@protected
late QrImage qrImage;
@protected
late PrettyQrDecoration decoration;
@override
void initState() {
super.initState();
qrCode = QrCode.fromData(
data: 'https://pub.dev/packages/pretty_qr_code',
errorCorrectLevel: QrErrorCorrectLevel.H,
);
qrImage = QrImage(qrCode);
decoration = const PrettyQrDecoration(
shape: PrettyQrSmoothSymbol(
color: _PrettyQrSettings.kDefaultQrDecorationBrush,
),
image: _PrettyQrSettings.kDefaultQrDecorationImage,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Pretty QR Code'),
),
body: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 1024,
),
child: LayoutBuilder(
builder: (context, constraints) {
final safePadding = MediaQuery.of(context).padding;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (constraints.maxWidth >= 720)
Flexible(
flex: 3,
child: Padding(
padding: EdgeInsets.only(
left: safePadding.left + 24,
right: safePadding.right + 24,
bottom: 24,
),
child: _PrettyQrAnimatedView(
qrImage: qrImage,
decoration: decoration,
),
),
),
Flexible(
flex: 2,
child: Column(
children: [
if (constraints.maxWidth < 720)
Padding(
padding: safePadding.copyWith(
top: 0,
bottom: 0,
),
child: _PrettyQrAnimatedView(
qrImage: qrImage,
decoration: decoration,
),
),
Expanded(
child: SingleChildScrollView(
padding: safePadding.copyWith(top: 0),
child: _PrettyQrSettings(
decoration: decoration,
onChanged: (value) => setState(() {
decoration = value;
}),
onExportPressed: (size) {
return qrImage.exportAsImage(
context,
size: size,
decoration: decoration,
);
},
),
),
),
],
),
),
],
);
},
),
),
),
);
}
}
class _PrettyQrAnimatedView extends StatefulWidget {
@protected
final QrImage qrImage;
@protected
final PrettyQrDecoration decoration;
const _PrettyQrAnimatedView({
required this.qrImage,
required this.decoration,
});
@override
State<_PrettyQrAnimatedView> createState() => _PrettyQrAnimatedViewState();
}
class _PrettyQrAnimatedViewState extends State<_PrettyQrAnimatedView> {
@protected
late PrettyQrDecoration previosDecoration;
@override
void initState() {
super.initState();
previosDecoration = widget.decoration;
}
@override
void didUpdateWidget(
covariant _PrettyQrAnimatedView oldWidget,
) {
super.didUpdateWidget(oldWidget);
if (widget.decoration != oldWidget.decoration) {
previosDecoration = oldWidget.decoration;
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<PrettyQrDecoration>(
tween: PrettyQrDecorationTween(
begin: previosDecoration,
end: widget.decoration,
),
curve: Curves.ease,
duration: const Duration(
milliseconds: 240,
),
builder: (context, decoration, child) {
return PrettyQrView(
qrImage: widget.qrImage,
decoration: decoration,
);
},
),
);
}
}
class _PrettyQrSettings extends StatefulWidget {
@protected
final PrettyQrDecoration decoration;
@protected
final Future<String?> Function(int)? onExportPressed;
@protected
final ValueChanged<PrettyQrDecoration>? onChanged;
@visibleForTesting
static const kDefaultQrDecorationImage = PrettyQrDecorationImage(
image: AssetImage('images/flutter.png'),
position: PrettyQrDecorationImagePosition.embedded,
);
@visibleForTesting
static const kDefaultQrDecorationBrush = Color(0xFF74565F);
const _PrettyQrSettings({
required this.decoration,
this.onChanged,
this.onExportPressed,
});
@override
State<_PrettyQrSettings> createState() => _PrettyQrSettingsState();
}
class _PrettyQrSettingsState extends State<_PrettyQrSettings> {
@protected
late final TextEditingController imageSizeEditingController;
@override
void initState() {
super.initState();
imageSizeEditingController = TextEditingController(
text: ' 512w',
);
}
@protected
int get imageSize {
final rawValue = imageSizeEditingController.text;
return int.parse(rawValue.replaceAll('w', '').replaceAll(' ', ''));
}
@protected
Color get shapeColor {
var shape = widget.decoration.shape;
if (shape is PrettyQrSmoothSymbol) return shape.color;
if (shape is PrettyQrRoundedSymbol) return shape.color;
return Colors.black;
}
@protected
bool get isRoundedBorders {
var shape = widget.decoration.shape;
if (shape is PrettyQrSmoothSymbol) {
return shape.roundFactor > 0;
} else if (shape is PrettyQrRoundedSymbol) {
return shape.borderRadius != BorderRadius.zero;
}
return false;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
LayoutBuilder(
builder: (context, constraints) {
return PopupMenuButton(
onSelected: changeShape,
constraints: BoxConstraints(
minWidth: constraints.maxWidth,
),
initialValue: widget.decoration.shape.runtimeType,
itemBuilder: (context) {
return [
const PopupMenuItem(
value: PrettyQrSmoothSymbol,
child: Text('Smooth'),
),
const PopupMenuItem(
value: PrettyQrRoundedSymbol,
child: Text('Rounded rectangle'),
),
];
},
child: ListTile(
leading: const Icon(Icons.format_paint_outlined),
title: const Text('Style'),
trailing: Text(
widget.decoration.shape is PrettyQrSmoothSymbol
? 'Smooth'
: 'Rounded rectangle',
style: Theme.of(context).textTheme.titleSmall,
),
),
);
},
),
LayoutBuilder(
builder: (context, constraints) {
return PopupMenuButton(
onSelected: toggleColor,
constraints: BoxConstraints(
minWidth: constraints.maxWidth,
),
initialValue:
shapeColor == _PrettyQrSettings.kDefaultQrDecorationBrush,
itemBuilder: (context) {
return [
const PopupMenuItem(
value: true,
child: Text('Color'),
),
const PopupMenuItem(
value: false,
child: Text('Gradient'),
),
];
},
child: ListTile(
leading: const Icon(Icons.color_lens_outlined),
title: const Text('Brush'),
trailing: Text(
shapeColor == _PrettyQrSettings.kDefaultQrDecorationBrush
? 'Color'
: 'Gradient',
style: Theme.of(context).textTheme.titleSmall,
),
),
);
},
),
SwitchListTile.adaptive(
value: isRoundedBorders,
onChanged: (value) => toggleRoundedCorners(),
secondary: const Icon(Icons.rounded_corner),
title: const Text('Rounded corners'),
),
const Divider(),
SwitchListTile.adaptive(
value: widget.decoration.image != null,
onChanged: (value) => toggleImage(),
secondary: Icon(
widget.decoration.image != null
? Icons.image_outlined
: Icons.hide_image_outlined,
),
title: const Text('Image'),
),
if (widget.decoration.image != null)
ListTile(
enabled: widget.decoration.image != null,
leading: const Icon(Icons.layers_outlined),
title: const Text('Image position'),
trailing: PopupMenuButton(
onSelected: changeImagePosition,
initialValue: widget.decoration.image?.position,
itemBuilder: (context) {
return [
const PopupMenuItem(
value: PrettyQrDecorationImagePosition.embedded,
child: Text('Embedded'),
),
const PopupMenuItem(
value: PrettyQrDecorationImagePosition.foreground,
child: Text('Foreground'),
),
const PopupMenuItem(
value: PrettyQrDecorationImagePosition.background,
child: Text('Background'),
),
];
},
),
),
if (widget.onExportPressed != null) ...[
const Divider(),
ListTile(
leading: const Icon(Icons.save_alt_outlined),
title: const Text('Export'),
onTap: () async {
final path = await widget.onExportPressed?.call(imageSize);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(path == null ? 'Saved' : 'Saved to $path'),
),
);
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
PopupMenuButton(
initialValue: imageSize,
onSelected: (value) {
imageSizeEditingController.text = ' ${value}w';
setState(() {});
},
itemBuilder: (context) {
return [
const PopupMenuItem(
value: 256,
child: Text('256w'),
),
const PopupMenuItem(
value: 512,
child: Text('512w'),
),
const PopupMenuItem(
value: 1024,
child: Text('1024w'),
),
];
},
child: SizedBox(
width: 72,
height: 36,
child: TextField(
enabled: false,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
controller: imageSizeEditingController,
decoration: InputDecoration(
filled: true,
counterText: '',
contentPadding: EdgeInsets.zero,
fillColor: Theme.of(context).colorScheme.background,
disabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.onBackground,
),
),
),
),
),
),
],
),
),
],
],
);
}
@protected
void changeShape(
final Type type,
) {
var shape = widget.decoration.shape;
if (shape.runtimeType == type) return;
if (shape is PrettyQrSmoothSymbol) {
shape = PrettyQrRoundedSymbol(color: shapeColor);
} else if (shape is PrettyQrRoundedSymbol) {
shape = PrettyQrSmoothSymbol(color: shapeColor);
}
widget.onChanged?.call(widget.decoration.copyWith(shape: shape));
}
@protected
void toggleColor(bool value) {
var shape = widget.decoration.shape;
var color = value
? _PrettyQrSettings.kDefaultQrDecorationBrush
: PrettyQrBrush.gradient(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.teal[200]!,
Colors.blue[200]!,
Colors.red[200]!,
],
),
);
if (shape is PrettyQrSmoothSymbol) {
shape = PrettyQrSmoothSymbol(
color: color,
roundFactor: shape.roundFactor,
);
} else if (shape is PrettyQrRoundedSymbol) {
shape = PrettyQrRoundedSymbol(
color: color,
borderRadius: shape.borderRadius,
);
}
widget.onChanged?.call(widget.decoration.copyWith(shape: shape));
}
@protected
void toggleRoundedCorners() {
var shape = widget.decoration.shape;
if (shape is PrettyQrSmoothSymbol) {
shape = PrettyQrSmoothSymbol(
color: shape.color,
roundFactor: isRoundedBorders ? 0 : 1,
);
} else if (shape is PrettyQrRoundedSymbol) {
shape = PrettyQrRoundedSymbol(
color: shape.color,
borderRadius: isRoundedBorders
? BorderRadius.zero
: const BorderRadius.all(Radius.circular(10)),
);
}
widget.onChanged?.call(widget.decoration.copyWith(shape: shape));
}
@protected
void toggleImage() {
const defaultImage = _PrettyQrSettings.kDefaultQrDecorationImage;
final image = widget.decoration.image != null ? null : defaultImage;
widget.onChanged?.call(PrettyQrDecoration(
image: image,
shape: widget.decoration.shape,
background: widget.decoration.background,
));
}
@protected
void changeImagePosition(
final PrettyQrDecorationImagePosition value,
) {
final image = widget.decoration.image?.copyWith(position: value);
widget.onChanged?.call(widget.decoration.copyWith(image: image));
}
@override
void dispose() {
imageSizeEditingController.dispose();
super.dispose();
}
}
View on GitHub: https://github.com/promops/flutter_pretty_qr