DEV Community

Cover image for CustomText - Custom Widget Series
Abdur Rafay Saleem
Abdur Rafay Saleem

Posted on

CustomText - Custom Widget Series

Welcome to another article for my Custom Widgets Series. In this series I share some tips and code about creating reusable and advanced flutter widgets. These widgets are used by me and many professionals out there in production level flutter apps. Now they are available to you!

Table of Contents

  1. Introduction
  2. Usage
  3. Code
  4. Credits

Introduction

Are you guys tired of writing down the Text widget in flutter with tedious amount of boilerplate for even the simplest of styling? Do you wish you didn't have to touch the TextStyle for just changing one or two fields? Now there is a way to quickly create fixed style texts, with much simpler properties.

Moreover, I am sure you have struggled with the RichText API to just print simple multi styled text or make a certain link clickable without affecting the rest of the para. This widget introduces simple and intuitive ways to make that happen. You won't ever need to touch RichText again.

CustomText ✨

A wrapper around Text widget to directly pass in frequent properties like:

  • color
  • font size
  • font weight
  • max lines

etc. without creating seperate TextStyle. You can see the it in the example below.

CustomText(
  message ?? 'FindingJobsNearYou',
  fontSize: 18,
  letterSpacing: 0.3,
  color: context.colorScheme.onPrimary,
  fontWeight: FontWeight.bold,
),
Enter fullscreen mode Exit fullscreen mode

Style Factories

Moreover, it has factories for different styles like .body(), .subtitle(), and .title(). They have certain font sizes, weights and colors predefined according to my choice so I can use them in a plug n play fashion.

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    // Title
    CustomText.body(
      title,
      fontWeight: FontWeight.bold,
    ),

    Insets.gapH10,

    // Message
    CustomText.subtitle(
      message,
      maxLines: 3,
    ),

    Insets.gapH10,

    // Time
    CustomText.label(
      time,
      color: context.colorScheme.onSurface.withValues(alpha: 0.6),
    ),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Rich text

Then comes in the heavy lifting factory .rich() for the annoyingly complex RichText.

An image of custom rich text widget in flutter

See how simple the API is? All you have to do is use .rich() factory. You no longer need to fight between Text and TextSpan. Just pass in instance of another CustomText as textWidget. Also, notice the Insets.gap widget? It is a simple SizedBox. You can pass any widget in between the text without worrying about WidgetSpan. And just like that you can create complex text widgets.

Links and clickable text

Very often you want to make a text clickable with some custom callback. You can wrap it with InkWell or similar widgets but it is cumbersome. And, it is even more difficult to make only a part of the text clickable.

Now just pass in isLink: true and onLinkTap() callback to the CustomText or simply use the .link() constructor. See the below example of creating a complex hyperlinked legal statement.

// Legal Links
CustomText.rich(
  text: 'By signing you agree to the ',
  maxLines: 2,
  lineHeight: 1.7,
  fontSize: 14,
  letterSpacing: -0.1,
  textAlign: TextAlign.center,
  children: [
    CustomText.link(
      'Terms',
      fontWeight: FontWeight.bold,
      fontSize: 14,
      letterSpacing: -0.1,
      onLinkTap: () {
        AppUtils.openUrl(AppConstants.termsOfServiceUrl);
      },
    ),
    CustomText.body(
      ' and ',
      fontSize: 14,
      letterSpacing: -0.1,
    ),
    CustomText.link(
      'Privacy Policy',
      fontSize: 14,
      letterSpacing: -0.1,
      fontWeight: FontWeight.bold,
      onLinkTap: () {
        AppUtils.openUrl(AppConstants.privacyPolicyUrl);
      },
    ),
  ],
),
Enter fullscreen mode Exit fullscreen mode

Code

Here is the crux of it all, the code for this amazing widget. I decided to place it at the end to avoid confusion, because it was important to see the usage and benefits before the implementation. I have included it as gist so I can update it in the future, if needed.

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
// Helpers
import '../../helpers/constants/constants.dart';
import '../../helpers/extensions/extensions.dart';
class CustomText extends StatelessWidget {
final String text;
final TextAlign? textAlign;
final TextOverflow overflow;
final FontWeight? fontWeight;
final double? fontSize;
final double? lineHeight;
final bool? softWrap;
final int? maxLines;
final Color? color;
final List<Widget>? textSpans;
final TextStyle? style;
final bool isLink;
final VoidCallback? onLinkTap;
final bool useSecondaryFont;
final double? letterSpacing;
final String? fontFamily;
const CustomText(
this.text, {
super.key,
this.textAlign,
this.fontFamily,
this.textSpans,
this.lineHeight,
this.useSecondaryFont = false,
this.isLink = false,
this.onLinkTap,
this.overflow = TextOverflow.ellipsis,
this.fontWeight,
this.fontSize,
this.letterSpacing,
this.color,
this.style,
this.maxLines,
this.softWrap,
});
const CustomText.heading(
this.text, {
super.key,
this.letterSpacing,
this.color,
this.maxLines,
this.useSecondaryFont = false,
this.fontSize = 28,
this.fontWeight = FontWeight.bold,
this.fontFamily,
}) : textAlign = null,
overflow = TextOverflow.ellipsis,
softWrap = null,
textSpans = null,
style = null,
isLink = false,
lineHeight = null,
onLinkTap = null;
const CustomText.title(
this.text, {
super.key,
this.color,
this.letterSpacing,
this.maxLines,
this.useSecondaryFont = false,
this.fontSize = 22,
this.fontWeight = FontWeight.w600,
this.fontFamily,
}) : textAlign = null,
overflow = TextOverflow.ellipsis,
softWrap = null,
textSpans = null,
style = null,
isLink = false,
lineHeight = null,
onLinkTap = null;
const CustomText.body(
this.text, {
super.key,
this.color,
this.letterSpacing,
this.maxLines,
this.useSecondaryFont = false,
this.fontSize = 16,
this.fontWeight,
this.fontFamily,
}) : textAlign = null,
overflow = TextOverflow.ellipsis,
softWrap = null,
textSpans = null,
style = null,
isLink = false,
lineHeight = null,
onLinkTap = null;
const CustomText.subtitle(
this.text, {
super.key,
this.color,
this.maxLines,
this.letterSpacing,
this.useSecondaryFont = false,
this.fontSize = 14,
this.fontWeight,
this.fontFamily,
}) : textAlign = null,
overflow = TextOverflow.ellipsis,
softWrap = null,
textSpans = null,
style = null,
isLink = false,
lineHeight = null,
onLinkTap = null;
const CustomText.label(
this.text, {
super.key,
this.color,
this.letterSpacing,
this.maxLines,
this.useSecondaryFont = false,
this.fontSize = 12,
this.fontWeight = FontWeight.w300,
this.fontFamily,
}) : textAlign = null,
overflow = TextOverflow.ellipsis,
softWrap = null,
textSpans = null,
style = null,
isLink = false,
lineHeight = null,
onLinkTap = null;
const CustomText.link(
this.text, {
required this.onLinkTap,
super.key,
this.color,
this.letterSpacing,
this.maxLines,
this.useSecondaryFont = false,
this.fontSize = 16,
this.fontWeight,
this.fontFamily,
}) : isLink = true,
textAlign = null,
overflow = TextOverflow.ellipsis,
softWrap = null,
textSpans = null,
lineHeight = null,
style = null;
factory CustomText.rich({
required List<Widget> children,
String? text,
CustomText? textWidget,
double? letterSpacing,
TextAlign? textAlign,
Color? color,
bool useSecondaryFont = false,
FontWeight? fontWeight,
int? maxLines,
double? lineHeight,
double fontSize = 16,
}) {
assert(
text != null || textWidget != null,
'Text or textWidget must be provided',
);
assert(
children.every((element) => element is CustomText || element is SizedBox),
'Children must be CustomText or SizedBox',
);
return CustomText(
text ?? textWidget?.text ?? '',
textSpans: children,
textAlign: textWidget?.textAlign ?? textAlign,
useSecondaryFont: useSecondaryFont,
maxLines: textWidget?.maxLines ?? maxLines,
letterSpacing: textWidget?.letterSpacing ?? letterSpacing,
fontWeight: textWidget?.fontWeight ?? fontWeight,
lineHeight: textWidget?.lineHeight ?? lineHeight,
fontSize: textWidget?.fontSize ?? fontSize,
color: textWidget?.color ?? color,
);
}
CustomText copyWith({
TextAlign? textAlign,
TextOverflow? overflow,
FontWeight? fontWeight,
double? fontSize,
bool? softWrap,
int? maxLines,
Color? color,
TextStyle? style,
bool? useSecondaryFont,
double? lineHeight,
double? letterSpacing,
String? fontFamily,
}) {
return CustomText(
text,
textAlign: textAlign ?? this.textAlign,
overflow: overflow ?? this.overflow,
fontWeight: fontWeight ?? this.fontWeight,
fontSize: fontSize ?? this.fontSize,
softWrap: softWrap ?? this.softWrap,
maxLines: maxLines ?? this.maxLines,
color: color ?? this.color,
style: style ?? this.style,
lineHeight: lineHeight ?? this.lineHeight,
useSecondaryFont: useSecondaryFont ?? this.useSecondaryFont,
letterSpacing: letterSpacing ?? this.letterSpacing,
fontFamily: fontFamily ?? this.fontFamily,
);
}
TextStyle _buildTextStyleFromArgs(
BuildContext context, {
CustomText? otherText,
}) {
return TextStyle(
fontSize: otherText?.fontSize ?? fontSize,
fontWeight: otherText?.fontWeight ?? fontWeight,
height: otherText?.lineHeight ??
lineHeight ??
context.textTheme.bodyMedium?.height,
fontFamily: (otherText?.fontFamily ?? fontFamily) ??
(otherText?.useSecondaryFont ?? useSecondaryFont
? AppTypography.secondaryFontFamily
: null),
color: otherText?.color ?? color,
letterSpacing: otherText?.letterSpacing ?? letterSpacing,
);
}
TapGestureRecognizer _buildLinkTapRecognizer() {
return TapGestureRecognizer()..onTap = onLinkTap;
}
@override
Widget build(BuildContext context) {
return Text.rich(
TextSpan(
text: text,
recognizer: isLink ? _buildLinkTapRecognizer() : null,
children: textSpans
?.map(
(e) => switch (e) {
CustomText(isLink: false) => TextSpan(
text: e.text,
style: e.style ??
style ??
_buildTextStyleFromArgs(context, otherText: e),
),
CustomText(isLink: true) => TextSpan(
text: e.text,
style: e.style ??
style ??
_buildTextStyleFromArgs(context, otherText: e),
recognizer: e._buildLinkTapRecognizer(),
),
_ => WidgetSpan(child: e),
},
)
.toList() ??
[],
),
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
style: style ?? _buildTextStyleFromArgs(context),
);
}
}

Credits

If you like it, please keep following me as I have around 40 such widgets and I will post many more of these.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay