接触flutter一段时间,用flutter做过一些demo项目,也看了一些flutter的源码,对flutter的组件体系有了一些了解,这里总结一下flutter自定义组件的最佳实践。
在flutter上开发自定义组件,实际上有两种方式,一种是继承StatelessWidget
或StatefulWidget
,另一种是使用RenderObject
。继承StatelessWidget
或StatefulWidget
是最常用的方式,因为它们提供了一些现成的方法和属性,可以方便地实现组件的生命周期和状态管理。而使用RenderObject
则需要自己实现一些方法和属性,比较复杂,一般用于实现一些复杂的自定义组件。
我们来分别看看这两种方式的实现。
StatelessWidget
或StatefulWidget
继承StatelessWidget
或StatefulWidget
是最常用的方式,因为它们提供了一些现成的方法和属性,可以方便地实现组件的生命周期和状态管理。下面是一个简单的例子,实现一个Counter
组件,这个组件可以显示一个计数器,用户可以点击按钮来增加计数器的值。
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: _increment,
child: Text('Increment'),
),
],
);
}
}
在这个例子中,我们定义了一个Counter
组件,它继承自StatefulWidget
,并实现了一个_CounterState
类,这个类继承自State
,用来管理组件的状态。在_CounterState
类中,我们定义了一个_count
变量来保存计数器的值,以及一个_increment
方法来增加计数器的值。在build
方法中,我们使用Column
组件来显示计数器的值和一个按钮,用户可以点击按钮来增加计数器的值。
RenderObject
使用RenderObject
是一种更底层的方式,它可以让我们更加灵活地控制组件的布局和绘制。下面是一个简单的例子,实现一个钟表组件,这个组件可以显示当前时间。
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Clock extends SingleChildRenderObjectWidget {
Clock({Widget child}) : super(child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderClock();
}
}
class RenderClock extends RenderBox {
DateTime _dateTime = DateTime.now();
Timer _timer;
RenderClock() {
_timer = Timer.periodic(Duration(seconds: 1), (Timer t) {
_dateTime = DateTime.now();
this.markNeedsPaint();
});
}
@override
void performLayout() {
size = Size(200, 200);
}
@override
void paint(PaintingContext context, Offset offset) {
final Paint paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
final center = size.center(offset);
final radius = size.shortestSide / 2;
context.canvas.drawCircle(center, radius, paint);
// Draw the hour hand.
final hourHandLength = radius * 0.5;
final hourHandRadians = ((_dateTime.hour % 12) + _dateTime.minute / 60) * 2 * math.pi / 12 - math.pi / 2;
context.canvas.drawLine(
center,
Offset(center.dx + hourHandLength * math.cos(hourHandRadians),
center.dy + hourHandLength * math.sin(hourHandRadians)),
paint..strokeWidth = 6.0,
);
// Draw the minute hand.
final minuteHandLength = radius * 0.8;
final minuteHandRadians = (_dateTime.minute + _dateTime.second / 60) * 2 * math.pi / 60 - math.pi / 2;
context.canvas.drawLine(
center,
Offset(center.dx + minuteHandLength * math.cos(minuteHandRadians),
center.dy + minuteHandLength * math.sin(minuteHandRadians)),
paint..strokeWidth = 4.0,
);
// Draw the second hand.
final secondHandLength = radius * 0.9;
final secondHandRadians = _dateTime.second * 2 * math.pi / 60 - math.pi / 2;
context.canvas.drawLine(
center,
Offset(center.dx + secondHandLength * math.cos(secondHandRadians),
center.dy + secondHandLength * math.sin(secondHandRadians)),
paint..color = Colors.red..strokeWidth = 2.0,
);
}
@override
void detach() {
_timer.cancel();
super.detach();
}
}
效果如下图所示:
图 0
上面给出了两种方式实现自定义组件的例子,第一种方式是继承StatelessWidget
或StatefulWidget
,第二种方式是使用RenderObject
。在实际开发中,我们可能需要遵循一些最佳实践,来提高组件的性能和可维护性。这里主要讲一下组件的封装、布局和文档吧。
在flutter中,组件的封装是常有的是,虽然说大部分时候flutter的组件库已经提供了我们需要的组件,但是有时候我们还是需要自定义一些组件来满足我们的需求。在封装组件时,我们应该遵循以下几个原则:
下面,我们来一一个简单的例子,比如,我们要实现一个日历组件,这个日历组件可以显示当前月份的日历,并且可以选择日期。我们可以将这个日历组件封装成一个Calendar
组件,这个组件可以接受一个DateTime
类型的参数,用来指定当前月份的日期。这个Calendar
组件可以包含一个MonthView
组件和一个WeekView
组件,MonthView
组件用来显示当前月份的日历,WeekView
组件用来显示星期几。这样,我们就将日历组件封装成了一个Calendar
组件,用户只需要传入一个DateTime
类型的参数,就可以使用这个日历组件了。让我来看看,如何遵循上面的原则来封装这个日历组件。
class Calendar extends StatelessWidget {
final DateTime date;
Calendar({this.date});
@override
Widget build(BuildContext context) {
return Column(
children: [
MonthView(date: date),
WeekView(),
],
);
}
}
class MonthView extends StatelessWidget {
final DateTime date;
MonthView({this.date});
@override
Widget build(BuildContext context) {
return Container(
child: Text('Month View'),
);
}
}
class WeekView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Text('Week View'),
);
}
}
这个例子我们来看看以上原则的应用:单一职责原则(Calendar、MonthView和WeekView各自负责一项功能)、高内聚低耦合(Calendar组件由MonthView和WeekView组成,但它们之间的耦合度很低)、易用性(使用Calendar组件只需要传入一个DateTime参数)、可定制性(可以通过修改MonthView和WeekView的实现来定制组件的表现)和易扩展性(可以通过添加更多的子组件来扩展Calendar组件的功能)。
一个好的布局可以提高组件的性能和用户体验,有些组件在涉及之初就需要考虑响应式布局,这样可以适应不同的屏幕尺寸和分辨率。在布局组件时,我们应该遵循以下几个原则:
下面,我们就来看看另外一个另外一个例子,现在大家都喜欢玩大语言模型聊天对话应用,又要支持图片又要支持文字,我们可以封装一个ChatMessage
组件,这个组件可以显示用户发送的消息,可以是文字也可以是图片。这个ChatMessage
组件可以包含一个TextMessage
组件和一个ImageMessage
组件,TextMessage
组件用来显示文字消息,ImageMessage
组件用来显示图片消息。这样,我们就将聊天消息组件封装成了一个ChatMessage
组件,用户只需要传入一个Message
对象,就可以使用这个聊天消息组件了。让我来看看,如何遵循上面的原则来布局这个聊天消息组件。
class ChatMessage extends StatelessWidget {
final Message message;
ChatMessage({this.message});
@override
Widget build(BuildContext context) {
return Flexible(
child: message.type == MessageType.text
? TextMessage(text: message.text)
: ImageMessage(image: message.image),
);
}
}
class TextMessage extends StatelessWidget {
final String text;
TextMessage({this.text});
@override
Widget build(BuildContext context) {
return Semantics(
child: Text(text),
label: 'Text message',
);
}
}
class ImageMessage extends StatelessWidget {
final String image;
ImageMessage({this.image});
@override
Widget build(BuildContext context) {
return Semantics(
child: Image.network(image),
label: 'Image message',
);
}
}
在这个例子中,ChatMessage组件使用了Flexible来自动调整其大小,以适应不同的屏幕尺寸和分辨率(灵活性和响应式)。同时,我们避免了不必要的嵌套和容器使用,以提高性能。另外,我们使用了Semantics组件来提供辅助功能,以提高可访问性。
一个组件研发完成后,我们应该为组件编写文档,以便其他开发者能够快速了解组件的用法和功能。没有文档的组件是没有人敢用的,现在的开发者都是懒得,不想花费过多的时间和精力来学习如何使用组件。在编写组件文档时,我们应该遵循以下几个原则:
今天就扯这么多吧,希望对大家有所帮助。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。