Jika widget khusus Anda menerima String dan TextStyle atau gaya lainnya: warna, berat, perataan teks, dll. Anda salah melakukannya! Sebaliknya, widget Anda seharusnya memiliki properti tipe Widget. Pernahkah Anda bertanya-tanya mengapa TextButton(), ListTile(), dan lainnya menerima widget (bukan string) dan bagaimana mereka menata widget Text() yang Anda masukkan ke dalamnya? — Mereka melakukannya dengan membungkus anaknya dengan DefaultTextStyle atau AnimatedDefaultTextStyle. Pendekatan ini sangat fleksibel tanpa memerlukan ratusan parameter gaya di konstruktor widget.

Dalam artikel ini, saya akan menunjukkan kepada Anda mengapa kode di sebelah kiri tidak lolos tinjauan PR saya dan masalah apa yang dipecahkan oleh kode di sebelah kanan.

Catatan:

  • _WidgetContainer cukup menambahkan warna latar belakang, dan padding, serta menampilkan turunan di Kolom. Anda akan melihat hasilnya nanti di artikel.
  • Jika Anda bertanya-tanya mengapa saya menulis context.textTheme dan bukan Theme.of(context).textTheme, Anda harus membaca artikel tentang Ekstensi yang Hilang di Flutter.


Ketika pendekatan TextStyle rusak

Pertama, bayangkan beberapa iterasi yang mungkin dilakukan oleh widget serupa dengan 2 properti String. Lihatlah seberapa cepat kode tersebut berkembang dan betapa rumitnya kode tersebut.

1. Menambahkan warna

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,
          ),
        ),
      ],
    );
  }
}

Hanya untuk menambahkan kemampuan mengubah warna teks kita perlu menambahkan 2 properti dan mengubah kode di 6 tempat!

2. Menambahkan huruf tebal dan perataan

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 properti lagi dan perubahan kode di 9 tempat!

❗️ Secara total, sudah ada 5 properti baru dan 15 perubahan sejak versi aslinya!

Keesokan harinya Anda mendapatkan permintaan lain: Deskripsi harus mendukung miring dan perataan tengah; Judul juga harus mendukung ringan dan tebal. Anda seharusnya sudah melihat ke mana arahnya — Sungguh “boolean”.

3. Melangkah ke arah yang salah ❌: TextStyle

Terkadang saya melihat widget menerima beberapa parameter TextStyle untuk teks yang berbeda, namun sayangnya, Anda masih harus meneruskan textAlign secara terpisah. Kode akan memiliki 3 properti tambahan dan perubahan di 9 tempat sejak versi aslinya:

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),
        ),
      ],
    );
  }
}

Selain itu, penyesuaiannya masih cukup terbatas. Anda tidak dapat mengubah maxLines, overflow, softWrap, dan textWidthBasis(bahkan tidak tahu apa itu 😅). Untungnya, kami punya solusinya!

Solusi ⭐: DefaultTextStyle.merge()

Sekarang saya akan menunjukkan cara terbaik untuk menangani teks di widget khusus. Mari kita ubah jenis properti String title dan String description menjadi Widget dan hapus perataannya. Daripada menggabungkan gaya teks default dengan yang disediakan dan meneruskannya ke widget Text(), kami akan menggabungkan Judul dan Deskripsi ke dalam DefaultTextStyle.merge() dan memberikan gaya di sana. Ini kodenya:

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,
        ),
      ],
    );
  }
}

Untuk penggunaan dasar, widget ini mungkin menjadi sedikit lebih sulit untuk digunakan, karena alih-alih menggunakan string sederhana, Anda sekarang harus meneruskan Text(“some string”). Tapi inilah tempat yang tepat yang memberi kita penyesuaian tanpa satu triliun tanda boolean 🎉

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,
    ),
  ),
)

Seperti yang Anda lihat, Judul memiliki ukuran headlineMedium, tetapi berwarna ungu, tebal dan rata tengah. Deskripsinya berwarna hitam (bukan black54), berukuran 16, dan memiliki garis tengah. Semuanya berfungsi seperti yang diharapkan, gaya default digabungkan dengan gaya yang diteruskan ke widget Text().

Bukan hanya TextStyle!

Dengan cara yang sama, kita juga dapat menyesuaikan widget lainnya. Misalnya, dengan IconTheme kita dapat mengatur warna default dan bahkan UKURAN untuk ikon.

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,
          ),
        ],
      ),
    );
  }
}

Selain itu, karena properti kita bertipe Widget, kita dapat meneruskan apa pun yang kita perlukan. Bahkan Row() dengan teks kaya dan ikon!

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,
    ),
  ),
)

Catatan:

  • Daripada RichText sebaiknya gunakan Text.rich(), karena yang pertama lebih berlevel rendah dan tidak menggunakan DefaultTextStyle.

Terkadang Anda benar-benar perlu meneruskan String, TextStyle, Color, atau gaya lainnya untuk widget, namun saya harap setelah mempelajari tentang DefaultTextStyle dan IconTheme, Anda akan menemukan kegunaan yang baik untuk widget tersebut! Selain itu, ada lebih banyak cara untuk memberikan gaya default untuk widget seperti mengganti Tema untuk bagian UI.

Kiat bonus ⚡️

Jika Anda tidak menyukai kode widget terakhir yang terlalu banyak bersarang, Anda dapat memperbaikinya dengan pengubah widget (mirip dengan SwiftUI) seperti ini:

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)),
    );
  }
}

Ide ini diambil dari @luke_pighetti tweet. Di sana Anda dapat menemukan lebih banyak contoh pengubah widget di Flutter!

Saya juga mengundang Anda untuk membaca artikel saya tentang Tema Kustom Flutter dengan Ekstensi Tema + Templat:



Terima kasih telah membaca. Sampai jumpa 👋