接触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 删除。