หากวิดเจ็ตที่กำหนดเองของคุณยอมรับ String และ TextStyle หรือรูปแบบอื่นๆ เช่น สี น้ำหนัก จัดแนวข้อความ ฯลฯ คุณกำลังทำผิด! วิดเจ็ตของคุณควรมีคุณสมบัติประเภท Widget แทน คุณเคยสงสัยหรือไม่ว่าทำไม TextButton(), ListTile() และวิดเจ็ตอื่นๆ จึงยอมรับวิดเจ็ต (ไม่ใช่สตริง) และวิดเจ็ตเหล่านี้จัดรูปแบบวิดเจ็ต Text() ที่คุณส่งเข้าไปอย่างไร — พวกเขาทำได้โดยห่อลูกด้วย DefaultTextStyle หรือ AnimatedDefaultTextStyle วิธีการนี้มีความยืดหยุ่นมากโดยไม่ต้องใช้พารามิเตอร์การจัดรูปแบบหลายร้อยรายการในตัวสร้างวิดเจ็ต

ในบทความนี้ ฉันจะแสดงให้คุณเห็นว่าเหตุใดโค้ดทางด้านซ้ายจึงไม่ผ่านการตรวจสอบ PR ของฉัน และโค้ดทางด้านขวาจะแก้ปัญหาอะไรได้บ้าง

หมายเหตุ:

  • _WidgetContainer เพียงเพิ่มสีพื้นหลังและช่องว่างภายใน และแสดงลูกในคอลัมน์ คุณจะเห็นผลลัพธ์ในบทความต่อไป
  • หากคุณสงสัยว่าทำไมฉันถึงเขียน context.textTheme และไม่ใช่ Theme.of(context).textTheme คุณควรอ่านบทความเรื่อง "ส่วนขยายที่ขาดหายไปใน Flutter"


เมื่อแนวทาง TextStyle พัง

ขั้นแรก ลองจินตนาการถึงการวนซ้ำหลายครั้งที่วิดเจ็ตที่คล้ายกันซึ่งมีคุณสมบัติ 2 String อาจผ่านไปได้ ดูว่าโค้ดจะเติบโตเร็วแค่ไหนและซับซ้อนแค่ไหน

1. การเพิ่มสีสัน

class CustomWidgetWithColor extends StatelessWidget {
  const CustomWidgetWithColor({
    super.key,
    required this.title,
    // 1
    this.titleColor,
    required this.description,
    // 2
    this.descriptionColor,
  });

  final String title;
  // 3
  final Color? titleColor;
  final String description;
  // 4
  final Color? descriptionColor;

  @override
  Widget build(BuildContext context) {
    return _WidgetContainer(
      children: [
        Text(
          title,
          style: context.textTheme.headlineMedium?.copyWith(
            // 5
            color: titleColor ?? Colors.black,
          ),
        ),
        Text(
          description,
          style: context.textTheme.bodySmall?.copyWith(
            // 6
            color: descriptionColor ?? Colors.black54,
          ),
        ),
      ],
    );
  }
}

เพียงเพื่อเพิ่มความสามารถในการเปลี่ยนสีข้อความ เราจำเป็นต้องเพิ่ม 2 คุณสมบัติและเปลี่ยนโค้ดใน 6 ตำแหน่ง!

2. การเพิ่มตัวหนาและการจัดตำแหน่ง

class CustomWidgetWithEverything extends StatelessWidget {
  const CustomWidgetWithEverything({
    super.key,
    required this.title,
    this.titleColor,
    // 1
    this.isTitleBold = false,
    // 2
    this.titleAlignment = TextAlign.start,
    required this.description,
    this.descriptionColor,
    // 3
    this.isDescriptionBold = false,
  });

  final String title;
  final Color? titleColor;
  // 4
  final bool isTitleBold;
  // 5
  final TextAlign titleAlignment;
  final String description;
  final Color? descriptionColor;
  // 6
  final bool isDescriptionBold;

  @override
  Widget build(BuildContext context) {
    return _WidgetContainer(
      children: [
        Text(
          title,
          // 7
          textAlign: titleAlignment,
          style: context.textTheme.headlineMedium?.copyWith(
            color: titleColor ?? Colors.black,
            // 8
            fontWeight: isTitleBold ? FontWeight.bold : FontWeight.normal,
          ),
        ),
        Text(
          description,
          style: context.textTheme.bodySmall?.copyWith(
            color: descriptionColor ?? Colors.black54,
            // 9
            fontWeight: isDescriptionBold ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ],
    );
  }
}

คุณสมบัติเพิ่มเติม 3 รายการ และการเปลี่ยนแปลงโค้ดใน 9 ตำแหน่ง!

❗️ โดยรวมแล้วมี คุณสมบัติใหม่ 5 รายการและการเปลี่ยนแปลง 15 รายการ นับตั้งแต่เวอร์ชันดั้งเดิม!

วันถัดไปคุณได้รับคำขออื่น: คำอธิบายควรรองรับการจัดตำแหน่งตัวเอียงและกึ่งกลาง ชื่อเรื่องควรสนับสนุนแสงนอกเหนือจากตัวหนา คุณควรเห็นแล้วว่ามันจะไปอยู่ที่ไหน — ไปสู่นรก "บูลีน"

3.ก้าวไปผิดทาง ❌: TextStyle

บางครั้งฉันเห็นว่าวิดเจ็ตยอมรับพารามิเตอร์ TextStyle หลายตัวสำหรับข้อความที่แตกต่างกัน แต่น่าเสียดายที่คุณยังต้องผ่าน textAlign แยกกัน โค้ดจะมีคุณสมบัติเพิ่มเติม 3 รายการและการเปลี่ยนแปลงใน 9 ตำแหน่ง ตั้งแต่เวอร์ชันดั้งเดิม:

class CustomWidgetWithTextStyle extends StatelessWidget {
  const CustomWidgetWithTextStyle({
    super.key,
    required this.title,
    // 1
    this.titleStyle,
    // 2
    this.titleAlignment = TextAlign.start,
    required this.description,
    // 3
    this.descriptionStyle,
  });

  final String title;
  // 4
  final TextStyle? titleStyle;
  // 5
  final TextAlign? titleAlignment;
  final String description;
  // 6
  final TextStyle? descriptionStyle;

  @override
  Widget build(BuildContext context) {
    return _WidgetContainer(
      children: [
        Text(
          title,
          // 7
          textAlign: titleAlignment,
          style: context.textTheme.headlineMedium
              ?.copyWith(color: Colors.black)
              // 8
              .merge(titleStyle),
        ),
        Text(
          description,
          style: context.textTheme.bodySmall
              ?.copyWith(color: Colors.black54)
              // 9
              .merge(descriptionStyle),
        ),
      ],
    );
  }
}

นอกจากนี้ การปรับแต่งยังค่อนข้าง จำกัด คุณไม่สามารถเปลี่ยน maxLines, overflow, softWrap และ textWidthBasis(ไม่รู้ด้วยซ้ำว่ามันคืออะไร 😅) โชคดีที่เรามีวิธีแก้ปัญหา!

โซลูชัน ⭐: DefaultTextStyle.merge()

ตอนนี้ฉันจะแสดงวิธีที่ดีที่สุดในการจัดการข้อความในวิดเจ็ตที่กำหนดเอง มาเปลี่ยนประเภทของคุณสมบัติ String title และ String description เป็น Widget และลบการจัดตำแหน่งออก แทนที่จะรวมรูปแบบข้อความเริ่มต้นเข้ากับวิดเจ็ตที่มีให้และส่งต่อไปยังวิดเจ็ต Text() เราจะรวมชื่อและคำอธิบายไว้ใน DefaultTextStyle.merge() และจัดเตรียมการจัดรูปแบบไว้ที่นั่น นี่คือรหัส:

class TheBestCustomWidget extends StatelessWidget {
  const TheBestCustomWidget({
    super.key,
    required this.title,
    required this.description,
  });

  final Widget title;
  final Widget description;

  @override
  Widget build(BuildContext context) {
    return _WidgetContainer(
      children: [
        DefaultTextStyle.merge(
          style: context.textTheme.headlineMedium?.copyWith(color: Colors.black),
          child: title,
        ),
        DefaultTextStyle.merge(
          style: context.textTheme.bodySmall?.copyWith(color: Colors.black54),
          child: description,
        ),
      ],
    );
  }
}

สำหรับการใช้งานขั้นพื้นฐาน วิดเจ็ตนี้อาจใช้งานยากขึ้นเล็กน้อย เนื่องจากแทนที่จะใช้สตริงธรรมดา ตอนนี้คุณควรผ่าน Text(“some string”) แต่นี่คือสถานที่ที่ทำให้เราปรับแต่งได้โดยไม่ต้องใช้แฟล็กบูลีนนับล้านล้าน 🎉

TheBestCustomWidget(
  title: Text(
    'The Best Title',
    textAlign: TextAlign.center,
    style: TextStyle(
      color: Colors.purple,
      fontWeight: FontWeight.bold,
    ),
  ),
  description: Text(
    'DefaultTextStyle.merge() is the best!',
    textAlign: TextAlign.center,
    style: TextStyle(
      color: Colors.black,
      fontSize: 16,
    ),
  ),
)

อย่างที่คุณเห็น ชื่อมีขนาด headlineMedium แต่มีสีม่วง มีตัวหนาและอยู่ตรงกลาง คำอธิบายเป็นสีดำ (ไม่ใช่ black54) มีขนาด 16 และมีการจัดตำแหน่งตรงกลาง ทุกอย่างทำงานตามที่คาดไว้ สไตล์เริ่มต้นจะผสานกับสไตล์ที่ส่งผ่านไปยังวิดเจ็ต Text()

ไม่ใช่แค่ TextStyle เท่านั้น!

ในทำนองเดียวกัน เรายังปรับแต่งวิดเจ็ตอื่นๆ ได้ด้วย ตัวอย่างเช่น ด้วย IconTheme เราสามารถตั้งค่าสีเริ่มต้นและแม้แต่ SIZE สำหรับไอคอนได้

class TheBestCustomWidgetWithIconsSupport extends StatelessWidget {
  const TheBestCustomWidgetWithIconsSupport({
    super.key,
    required this.title,
    required this.description,
  });

  final Widget title;
  final Widget description;

  @override
  Widget build(BuildContext context) {
    // 1. IconTheme
    return IconTheme.merge(
      // 2. IconThemeData
      data: const IconThemeData(
        color: Colors.red,
        size: 32,
      ),
      child: _WidgetContainer(
        children: [
          DefaultTextStyle.merge(
            style: context.textTheme.headlineMedium?.copyWith(color: Colors.black),
            child: title,
          ),
          DefaultTextStyle.merge(
            style: context.textTheme.bodySmall?.copyWith(color: Colors.black54),
            child: description,
          ),
        ],
      ),
    );
  }
}

นอกจากนี้ เนื่องจากคุณสมบัติของเราเป็นประเภท Widget เราจึงสามารถส่งผ่าน อะไรก็ได้ที่เราต้องการ แม้แต่ Row() ด้วยข้อความและไอคอนที่หลากหลาย!

TheBestCustomWidgetWithIconsSupport(
  // Row
  title: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      // Rich text
      Text.rich(
        TextSpan(
          children: [
            TextSpan(
              text: 'The',
              style: TextStyle(color: Colors.purple),
            ),
            TextSpan(
              text: ' Best ',
              style: TextStyle(fontStyle: FontStyle.italic),
            ),
            TextSpan(text: 'Title!'),
          ],
          style: TextStyle(
            color: Colors.purple,
            fontWeight: FontWeight.bold,
          ),
        ),
        textAlign: TextAlign.center,
      ),
      SizedBox(width: 5),
      // Icon
      Icon(Icons.favorite),
    ],
  ),
  description: Text(
    'DefaultTextStyle & IconTheme are the best!',
    textAlign: TextAlign.center,
    style: TextStyle(
      color: Colors.black,
      fontWeight: FontWeight.normal,
      fontSize: 16,
    ),
  ),
)

หมายเหตุ:

  • แทนที่จะใช้ RichText คุณควรใช้ Text.rich() เนื่องจากอันแรกเป็นระดับต่ำมากกว่าและไม่ใช้ DefaultTextStyle

บางครั้งคุณจะต้องผ่าน String, TextStyle, Color หรือสไตล์อื่นสำหรับวิดเจ็ต แต่ฉันหวังว่าหลังจากเรียนรู้เกี่ยวกับ DefaultTextStyle และ IconTheme แล้ว คุณจะพบว่ามีประโยชน์สำหรับวิดเจ็ตเหล่านี้! นอกจากนี้ยังมีวิธีอื่นๆ อีกมากมายในการจัดเตรียมสไตล์เริ่มต้นสำหรับวิดเจ็ต เช่น การแทนที่ธีมสำหรับส่วนของ UI

ทิปโบนัส⚡️

หากคุณไม่ชอบที่โค้ดวิดเจ็ตสุดท้ายมีการซ้อนมากเกินไป คุณสามารถแก้ไขได้ด้วยตัวแก้ไขวิดเจ็ต (คล้ายกับ "SwiftUI") ดังนี้:

class TheBestCustomWidgetWithExtensions extends StatelessWidget {
  const TheBestCustomWidgetWithExtensions({
    super.key,
    required this.title,
    required this.description,
  });

  final Widget title;
  final Widget description;

  @override
  Widget build(BuildContext context) {
    return _WidgetContainer(
      children: [
        title
            .textStyle(context.textTheme.headlineMedium)
            .foregroundColor(Colors.black),
        description
            .textStyle(context.textTheme.bodySmall)
            .foregroundColor(Colors.black54),
      ],
    );
  }
}

extension on Widget {
  Widget textStyle(TextStyle? style, {TextAlign? align}) {
    return DefaultTextStyle.merge(
      style: style,
      textAlign: align,
      child: this,
    );
  }

  Widget foregroundColor(Color color) {
    return IconTheme.merge(
      data: IconThemeData(color: color),
      child: textStyle(TextStyle(color: color)),
    );
  }
}

แนวคิดนี้นำมาจากทวีต “@luke_pighetti” คุณจะพบตัวอย่างเพิ่มเติมของตัวดัดแปลงวิดเจ็ตใน Flutter!

นอกจากนี้ ฉันขอเชิญคุณอ่านบทความของฉันเกี่ยวกับ Flutter Custom Theme พร้อม ThemeExtensions + Templates:



ขอขอบคุณที่อ่าน บาย บะบาย