[Flutter] ทำ Custom Widgetโดยการ Override

Moo Thanathorn
Mintelligence
Published in
4 min readJan 16, 2024

อย่างที่ทราบกัน Flutter เป็น Cross platform app development framework จึงมี component มาให้ใช้สะดวกสบายมาก อย่าง material (Andorid style) หรือ cupertino (Ios style) แต่ถ้าเราอยากได้หน้าตาเฉพาะก็สามารถทำได้

import 'package:flutter/material.dart';

void main() {
runApp(const MainApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: const Icon(Icons.abc),
title: const Text("Play Gorund", textAlign: TextAlign.center),
),
body: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ExpansionTile(
title: Text("Expansion Tile ${index + 1}"),
children: [
Container(
padding: const EdgeInsets.all(10),
child: Text("Children ${index + 1}"),
),
],
);
})),
);
}
}

ผมจะใช้ class ExpansionTile เป็นตัวอย่าง จากด้านบนธรรมดาจะหน้าตาตามภาพ และ เราสามารถใส่ props ตกแต่งหน้าตาตามที่เราต้องการจะได้แบบนี้


class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
Widget content = Container(
padding: const EdgeInsets.all(10),
child: const Text("Content Content Content"),
);

return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: const Icon(Icons.abc),
title: const Text("Play Gorund", textAlign: TextAlign.center),
),
body: ListView(
children: [
ExpansionTile(
// เพิ่ม icon ด้านหน้า
leading: const Icon(
Icons.warning_amber,
size: 24,
color: Colors.amber,
),
// เพิ่มสี ปรับขนาด และความหนาของ title
title: const Text(
"Waring",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.amber,
),
),
// เพิ่มสี bg ตอน expanded
backgroundColor: Colors.amber[50],
// เพิ่มสี icon ตอน expanded
iconColor: Colors.amber,
// ลบ border
shape: const Border.fromBorderSide(BorderSide.none),
// เพิ่ม title padding
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
// เพิ่ม children data
children: [content],
),
ExpansionTile(
leading: const Icon(
Icons.check_circle_outline_outlined,
size: 24,
color: Colors.green,
),
title: const Text(
"Success",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.green,
),
),
backgroundColor: Colors.green[50],
iconColor: Colors.green,
shape: const Border.fromBorderSide(BorderSide.none),
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
children: [content],
),
ExpansionTile(
leading: const Icon(
Icons.dangerous_outlined,
size: 24,
color: Colors.red,
),
title: const Text(
"Alert",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.red,
),
),
backgroundColor: Colors.red[50],
iconColor: Colors.red,
shape: const Border.fromBorderSide(BorderSide.none),
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
children: [content],
),
ExpansionTile(
leading: const Icon(
Icons.dangerous_outlined,
size: 24,
color: Colors.blue,
),
title: const Text(
"Primary",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.blue,
),
),
backgroundColor: Colors.blue[50],
iconColor: Colors.blue,
shape: const Border.fromBorderSide(BorderSide.none),
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
children: [content],
),
],
),
),
);
}
}

จากที่เห็น code ด้านบนยาวพรืดเลยและ มีการใช้งานค่าซ้ำๆ กันเช่น
shape: Border.fromBorderSide(BorderSide.none)
tilePadding: EdgeInsets.symmetric(horizontal: 24)
เราสามารถลดการใช้ซ้ำได้โดยอาจจะประการ ShapeBorder ไว้ด้านบนแล้วเรียกใช้
หรือ เก็บ props ใส่ Map แล้ววน loop แต่ผมจะทำการแยก Widget ออกมาแบบนี้

// แยก class มาเป็น ExpansionTileCustom อยู่ใน main.dart
class ExpansionTileCustom extends StatelessWidget {
final MaterialColor? color;
final Widget? content;
final IconData? icon;
final String titleName;

const ExpansionTileCustom({
super.key,
this.color,
this.content,
this.icon,
required this.titleName,
});

@override
Widget build(BuildContext context) {
return ExpansionTile(
leading: Icon(icon, size: 24, color: color),
title: Text(
titleName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: color,
),
),
backgroundColor: color?[50],
iconColor: color,
shape: const Border.fromBorderSide(BorderSide.none),
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
children: [content!],
);
}
}
// main.dart
class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
Widget content = Container(
padding: const EdgeInsets.all(10),
child: const Text("Content Content Content"),
);

return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: const Icon(Icons.abc),
title: const Text("Play Gorund", textAlign: TextAlign.center),
),
body: ListView(
children: [
ExpansionTileCustom(
titleName: "Waring",
color: Colors.amber,
icon: Icons.warning_amber,
content: content,
),
ExpansionTileCustom(
titleName: "Success",
color: Colors.green,
icon: Icons.check_circle_outline_outlined,
content: content,
),
ExpansionTileCustom(
titleName: "Alert",
color: Colors.red,
icon: Icons.dangerous_outlined,
content: content,
),
ExpansionTileCustom(
titleName: "Primary",
color: Colors.blue,
icon: Icons.category_outlined,
content: content,
),
],
),
),
);
}
}

ทีนี้ Listview ของเราก็จะดูง่ายขึ้น ถ้าเราอยากเพิ่ม props อะไรก็ไปประกาศที่
class ExpansionTileCustom ถ้าใช้หลายตัวก็ต้องเพิ่มหลายตัว เรามีวิธีง่ายกว่านั้นนั่นก็คือการ Override Widget โดยใช้ extends

class ExpansionTileCustom extends ExpansionTile {
final MaterialColor? color;
final IconData? icon;
final String titleName;

ExpansionTileCustom({
super.key,
super.children,
super.subtitle,
super.textColor,
this.color,
this.icon,
required this.titleName,
}) : super(
leading: Icon(icon, size: 24, color: color),
title: Text(
titleName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: color,
),
),
backgroundColor: color?[50],
iconColor: color,
shape: const Border.fromBorderSide(BorderSide.none),
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
);
}

จากที่เห็นเราเรียกใช้ field หรือ method จาก class ExpansionTile โดยใช้ super
และไม่ต้องไปประกาศ field ใหม่ เว้นแต่อยากได้ field ที่เจาะจง type หรือ ชื่อ field ซ้ำกับ super class ก็ต้องประกาศชื่อ field ใหม่ ตามที่ Linter rule แนะนำ

หวังว่าบทความนี้จะเป็นประโยชน์ได้บ้างนะครับ กว่าจะได้มาเขียนบทความนี้ ผมก็เขียน Flutter แบบหน้ามืดตามัวมาสักพักเลย จากที่เขียน React อยู่ประจำ ก็ได้มาจับ project ใหม่ ต้องรีบ Learn รีบ Launch เป็นมือใหม่ที่ยังไม่รู้ลึกเท่าไหร่
(จริงๆ ก็เขียนมาหลายเดือนอยู่ 🤣 ) แนะนำติชมกันมาได้เลยครับ

// main.dart full code

import 'package:flutter/material.dart';

void main() {
runApp(const MainApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
Widget content = Container(
padding: const EdgeInsets.all(10),
child: const Text("Content Content Content"),
);

return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: const Icon(Icons.abc),
title: const Text("Play Gorund", textAlign: TextAlign.center),
),
body: ListView(
children: [
ExpansionTileCustom(
titleName: "Waring",
color: Colors.amber,
icon: Icons.warning_amber,
children: [content],
),
ExpansionTileCustom(
titleName: "Success",
color: Colors.green,
icon: Icons.check_circle_outline_outlined,
children: [content],
),
ExpansionTileCustom(
titleName: "Alert",
color: Colors.red,
icon: Icons.dangerous_outlined,
children: [content],
),
ExpansionTileCustom(
titleName: "Promary",
color: Colors.blue,
icon: Icons.category_outlined,
children: [content],
),
],
),
),
);
}
}

class ExpansionTileCustom extends ExpansionTile {
final MaterialColor? color;
final IconData? icon;
final String titleName;
// @override
// final String? title;

ExpansionTileCustom({
super.key,
super.children,
super.subtitle,
super.textColor,
this.color,
this.icon,
// required super.title,
required this.titleName,
}) : super(
leading: Icon(icon, size: 24, color: color),
title: Text(
titleName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: color,
),
),
backgroundColor: color?[50],
iconColor: color,
shape: const Border.fromBorderSide(BorderSide.none),
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
);
}

--

--