Flutter is a popular mobile app development framework that allows developers to build beautiful, high-performance apps for iOS, Android, and the web. One of the most powerful features of Flutter is its ability to create custom widgets that allow developers to build unique user interfaces and add custom functionality to their apps.
In this blog post, we'll explore how to create custom widgets in Flutter using the widget composition technique, as well as how to use low-level widgets such as RenderObjectWidget, CustomPaint, CustomClipper, and more to achieve complex effects.
Widget Composition
Widget composition is the process of creating new widgets by combining existing ones. This technique is essential in Flutter because almost everything in Flutter is a widget. By combining different widgets, you can create complex and powerful user interfaces.
Let's take a simple example of a custom widget that combines two existing widgets: a text widget and an icon widget. We'll call this custom widget "IconText". Here's how you can create it:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Center(
child:IconText(icon: Icons.add, text: 'Nachiketa',)
),
),
);
}
}
class IconText extends StatelessWidget {
final IconData icon;
final String text;
IconText({required this.icon, required this.text});
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon),
const SizedBox(width: 8),
Text(text),
],
);
}
}
In this example, we define a new widget called "IconText" that takes two parameters: an icon and some text. The build method of this widget returns a row that contains an icon widget and a text widget. We also add some spacing between the icon and the text using the SizedBox widget.
Now, we can use this custom widget just like any other widget in Flutter:
IconText(icon: Icons.star, text: 'Favorite')
Low-Level Widgets
While widget composition is powerful and versatile, sometimes you need more control over the rendering process. That's where low-level widgets like RenderObjectWidget, CustomPaint, CustomClipper, and more come in.
RenderObjectWidget
The RenderObjectWidget is the lowest level widget in Flutter. It allows you to create custom widgets by defining your own rendering logic. This widget is useful when you need to create custom UI elements that aren't possible with the existing widgets.
Here's an example of a custom widget that uses the RenderObjectWidget to draw a simple shape:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Center(
child:CustomShape(child: Container(),)
),
),
);
}
}
class CustomShape extends SingleChildRenderObjectWidget {
CustomShape({Key? key, required Widget child}) :
super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomShape();
}
}
class RenderCustomShape extends RenderProxyBox {
@override
void paint(PaintingContext context, Offset offset) {
final size = child!.size;
final paint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xff44c7c7),
Color(0xff3c3e5b),
],
).createShader(Rect.fromLTWH(0, 0,
size.width, size.height))
..style = PaintingStyle.fill;
final path = Path()
..moveTo(size.width / 4, 0)
..lineTo(size.width * 0.75, 0)
..cubicTo(size.width, size.height / 6,
size.width, size.height / 3, size.width * 0.75, size.height / 2)
..lineTo(size.width / 4, size.height / 2)
..cubicTo(0, size.height / 3, 0,
size.height / 6, size.width / 4, 0)
..close();
context.canvas.drawPath(path, paint);
super.paint(context, offset);
}
}
class CustomShapeDesign extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 200,
child: CustomShape(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Custom Shape Design',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 8),
Text(
'Nachiketa',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
],
),
),
),
);
}
}
In this example, we define a custom widget called "CustomShape" that uses the RenderObjectWidget to draw a custom shape. We also define a new class called "RenderCustomShape" that extends RenderProxyBox, which allows us to draw on top of a child widget.
CustomPaint
The CustomPaint widget is a higher-level widget that uses the RenderObjectWidget internally to provide a convenient way to create custom painting effects. It allows you to paint on a canvas using a custom painter object.
Here's an example of a custom widget that uses CustomPaint to draw a gradient background:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Center(
child:GradientBackground(child: Container(),)
),
),
);
}
}
class GradientBackground extends StatelessWidget {
final Widget child;
GradientBackground({required this.child});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: GradientPainter(),
child: child,
);
}
}
class GradientPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final gradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue, Colors.green],
);
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
final paint = Paint()..shader = gradient.createShader(rect);
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
In this example, we define a custom widget called "GradientBackground" that uses CustomPaint to draw a gradient background. We also define a new class called "GradientPainter" that extends CustomPainter and implements the painting logic.
CustomClipper
The CustomClipper widget allows you to clip child widgets using a custom shape. It works by defining a custom path that determines the shape of the clipping region.
Here's an example of a custom widget that uses CustomClipper to clip a child widget to a rounded rectangle:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Center(
child:MyHomePage()
),
),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter'),
),
body: Center(
child: Container(
height: 300.0,
width: 300.0,
child: ClipPath(
clipper: RoundedClipper(),
child: CustomPaint(
painter: GradientPainter(),
child: Center(
child: Text(
'Flutter Solution',
style: TextStyle(fontSize: 24.0, color: Colors.white),
),
),
),
),
),
),
);
}
}
class RoundedClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0, size.height * 0.4);
final firstControlPoint = Offset(size.width * 0.25, size.height * 0.25);
final firstEndPoint = Offset(size.width * 0.5, 0);
path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy,
firstEndPoint.dx, firstEndPoint.dy);
final secondControlPoint =
Offset(size.width * 0.75, size.height * 0.25);
final secondEndPoint = Offset(size.width, size.height * 0.4);
path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy,
secondEndPoint.dx, secondEndPoint.dy);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
class GradientPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final gradient = LinearGradient(
colors: [
Colors.blue.shade800,
Colors.lightBlue.shade400,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.9],
);
final paint = Paint()..shader = gradient.createShader(Offset.zero & size);
final path = Path()
..lineTo(0, size.height * 0.4)
..quadraticBezierTo(
size.width * 0.25,
size.height * 0.25,
size.width * 0.5,
0,
)
..quadraticBezierTo(
size.width * 0.75,
size.height * 0.25,
size.width,
size.height * 0.4,
)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
In this example, we define a custom widget called "RoundedContainer" that uses CustomClipper to clip a child widget to a rounded rectangle. We also define a new class called "RoundedClipper" that extends CustomClipper<Path> and implements the clipping logic.
Conclusion
In this blog post, we've explored how to create custom widgets in Flutter using the widget composition technique and how to use low-level widgets such as RenderObjectWidget, CustomPaint, CustomClipper, and more to achieve complex effects. By using these techniques, you can create powerful and unique user interfaces that are tailored to your app's needs.
0 Comments