flutter_taggable 0.0.2
flutter_taggable: ^0.0.2 copied to clipboard
A lightweight extension to TextEditingController that allows for tagging/mentioning users, with a focus on storing tags in a database-friendly format.
A lightweight extension to TextEditingController that allows for tagging/mentioning users and the like, with a focus on storing tags in a database-friendly format. Tagging behaviour is similar to that of popular platforms like WhatsApp and Instagram.

Motivation #
There are many packages that allow for tagging/mentioning users and other entities in a text field, but not all are suitable for storing the tags in such a way that allows for easy retrieval and parsing. Imagine the following scenario:
A user writes and posts the comment
@Ada Lovelace check out this package!
. Suppose the backend server needs to parse this comment to send a notification to Ada. If the comment is stored as-is, two issues arise:
- It is unclear where the tag ends. Is it
@Ada
,@Ada Lovelace
or perhaps@Ada Lovelace check
? It would be both difficult and inefficient to determine the exact tag.- If the user Ada changes her username to
@Ada King
, the comment would no longer tag her, as the tag is hardcoded as@Ada Lovelace
.
Typically, any taggable entity has a unique identifier, such as a user ID. This package allows for tagging users such that the frontend display is user-friendly, while providing a database-friendly format for easy parsing and retrieval.
Features #
- Lightweight: This package can be used with the standard TextField widget - you only need to replace the
TextEditingController
with aTagTextEditingController
. - End-user friendly: The controller handles the tagging logic, so you can focus on the UI. For example, backspacing over a tag deletes the entire tag, not just one character.
- Customizable tag format: Specify both the frontend format (e.g.
@Ada Lovelace
) and the backend format (e.g.@123
). This package can also support multiple types of tagging (e.g.@Ada Lovelace
and#MyTrendingTopic
, see the video above). - Customizable search: Specify how the package should search for tags, as well as how the options should be displayed.
- Type annotations: Give a type to the
TagTextEditingController
for better type safety and code completion when you define your callbacks.
Getting started #
This package assumes that your 'taggable' data comes in three forms:
- A dataclass-like object that represents the taggable entity.
- A representation of this entity that can be displayed in the UI.
- A representation of this entity that can be stored in the database.
In the example, we use the following class to represent a taggable entity:
class Taggable {
const Taggable({required this.id, required this.name});
final String id;
final String name;
}
The id
is the unique identifier of the taggable entity, and the name
is the string that will be displayed in the UI. Comparing this to WhatsApp, the taggable entity would be a user, the id
would be the user's phone number, and the name
would be the user's name.
The package supports having multiple types of taggable entities. However, to make the code less verbose, we recommend creating a class that extends Taggable
for each type of taggable entity. For example:
class User extends Taggable {
const User({required super.id, required super.name});
}
class Topic extends Taggable {
const Topic({required super.id, required super.name});
}
Alternatively, you can make the Taggable class have abstract methods that return the frontend and backend representations of the taggable entity, to be implemented by the subclasses.
Usage #
Installation #
Add the package to your project with the following command:
flutter pub add taggable
Basic usage #
To use the package, you need to replace the default TextEditingController
with a TagTextEditingController
and specify a couple of parameters;
import 'package:flutter/material.dart';
import 'package:taggable/taggable.dart';
class TaggableExample extends StatefulWidget {
@override
_TaggableExampleState createState() => _TaggableExampleState();
}
class _TaggableExampleState extends State<TaggableExample> {
late TagTextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TagTextEditingController<Taggable>(
searchTaggables: searchTaggables,
buildTaggables: buildTaggables,
toFrontendConverter: (taggable) => taggable.name,
toBackendConverter: (taggable) => taggable.id,
tagStyles: {
'@': const TextStyle(color: Colors.blue),
'#': const TextStyle(color: Colors.green),
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Taggable Example'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Type @ to tag a user or # to tag a topic',
),
),
),
);
}
}
Note how the controller is type-annotated with the Taggable
class. In the example above, we specify the following parameters (with T being the type of the taggable entity):
FutureOr<Iterable<T>> Function(String prefix, String? query) searchTaggables
This function is called whenever the user types a tag. It should return a list of taggable entities that match the query. The prefix
parameter is the character that the user typed to start the tag, and the query
parameter is the text that the user typed after the prefix. For example, if the user types @Ada
, the prefix
would be @
and the query
would be Ada
. The return type is of FutureOr
because the function can be both synchronous (if the data is already available) or asynchronous (if the data needs to be fetched from somewhere).
Future<T?> Function(FutureOr<Iterable<T>> taggables) buildTaggables
This function takes the list of taggable entities that match the query and builds the UI representation of these entities. In the example, an OverlayEntry
is used to display the options as a list. Because users typically select an option by tapping on it, the return type is a Future. The example uses a Completer to return the selected taggable entity, since an OverlayEntry
does not return a value.
String Function(T taggable) toFrontendConverter
This function converts the taggable entity to a string that will be displayed in the UI. In the example, we use the name
field of the Taggable
class.
String Function(T taggable) toBackendConverter
This function converts the taggable entity to a string that will be stored in the database. In the example, we use the id
field of the Taggable
class.
Map<String, TextStyle?> tagStyles
This map specifies the styles that should be applied to the tags. The keys are the prefixes that the user can type to start a tag, and the values are the styles that should be applied to the tags. In the example, we use the @
prefix for users and the #
prefix for topics.
Conversion #
There are three processes in which the taggable entities are converted:
- Saving text with tags: When a piece of text containing tags needs to be saved, you can use the
backendTextFormat
getter of theTagTextEditingController
to get the text in the backend format. - Populating the text field: When you have a piece of text in the backend format and want to populate the text field with it, you can use the
setInitialText
method of theTagTextEditingController
. Besides the text, you need to provide functionbackendToTaggable
that converts the backend format to the taggable entity. This function has the following signature:
FutureOr<T?> Function(String prefix, String backendString) backendToTaggable
- Displaying tags outside the text field: Typically, content created with the
TagTextEditingController
will be displayed in a different widget, such as a comment section. This package does not provide a built-in way to display tags outside the text field, as the display of such content is highly dependent on the UI of your app. However, many of the conversion functions used in theTagTextEditingController
can be reused to display tags outside the text field. See the example for one way to display tags outside the text field.
Additional information #
Limitations #
- Whitespace: because of the focus on database-friendly storage, tags are currently required to have whitespace before and after them, with the exception of tags that are the first or last characters in the text field. This is to make tags easier to parse and retrieve from the database. For example, the text
Hi @Ada Lovelace how are you?
is valid, butHi@Ada Lovelace, how are you?
is not.
Contributions #
Contributions are welcome! If you have any suggestions or improvements, please open an issue or a pull request on the GitHub repository.