Flutter Tags

Snehal Masalkar
nonstopio
Published in
7 min readApr 29, 2021

--

The below gif represents how to add and remove tags in the flutter.

I have created a model “TagModel” which contains two fields.

  1. id
  2. title

Here is my model

class TagModel {
String id;
String title;

TagModel({
@required this.id,
@required this.title,
});
}

I have added sample data to the list of type TagModel called _tagsToSelect, so this is the list from where we can select the tags, we can call them suggestions.

final List<TagModel> _tagsToSelect = [
TagModel(id: '1', title: 'JavaScript'),
TagModel(id: '2', title: 'Python'),
TagModel(id: '3', title: 'Java'),
TagModel(id: '4', title: 'PHP'),
TagModel(id: '5', title: 'C#'),
TagModel(id: '6', title: 'C++'),
TagModel(id: '7', title: 'Dart'),
TagModel(id: '8', title: 'DataFlex'),
TagModel(id: '9', title: 'Flutter'),
TagModel(id: '10', title: 'Flutter Selectable Tags'),
TagModel(id: '11', title: 'Android Studio developer'),
];

After that, I have one more list which is the actual list of tags that I already have.

List<TagModel> _tags = [];

I have created a getter to get search text from the controller.

TextEditingController _searchTextEditingController =
new TextEditingController();
String get _searchText => _searchTextEditingController.text.trim();

When we create a controller then we have to add a listener for the controller in initState() method and also we have to dispose it.

refreshState(VoidCallback fn) {
if (mounted) setState(fn);
}

@override
void initState() {
super.initState();
_searchTextEditingController.addListener(() => refreshState(() {}));
}

@override
void dispose() {
super.dispose();
_searchTextEditingController.dispose();
}

If we have more than 10 to 15 tags, then we can use the search field to search the tag which we want to add to the Tags list.

Widget _buildSearchFieldWidget() {
return Container(
child: Row(
children: [
Expanded(
child: TextField(
controller: _searchTextEditingController,
decoration: InputDecoration.collapsed(
hintText: 'Search Tag',
hintStyle: TextStyle(
color: Colors.grey,
),
),
style: TextStyle(
fontSize: 16.0,
),
textInputAction: TextInputAction.search,
),
),
_searchText.isNotEmpty
? InkWell(
child: Icon(
Icons.clear,
color: Colors.grey.shade700,
),
onTap: () => _searchTextEditingController.clear(),
)
: Icon(
Icons.search,
color: Colors.grey.shade700,
),
Container(),
],
),
);
}

So the value in the search controller set to the _searchText parameter. Now we have to pass that value in the search field and search the tags from the suggestion list(_tagsToSelect).

The below method returns the search list from suggestions and displays only those tags in the suggestion which match the search criteria.

List<TagModel> _filterSearchResultList() {
if (_searchText.isEmpty) return _tagsToSelect;

List<TagModel> _tempList = [];
for (int index = 0; index < _tagsToSelect.length; index++) {
TagModel tagModel = _tagsToSelect[index];
if (tagModel.title
.toLowerCase()
.trim()
.contains(_searchText.toLowerCase())) {
_tempList.add(tagModel);
}
}

return _tempList;
}

If the search field is empty then this function returns all the tags in suggestion otherwise it will return only search tags.

Now we have to display tags, and here is my build method.

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Tags'),
backgroundColor: Colors.deepOrangeAccent,
),
body: _tagIcon(),
);
}

In _tagIcon widget I am displaying icon for the tag and actual tags.

Widget _tagIcon() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.local_offer_outlined,
color: Colors.deepOrangeAccent,
size: 25.0,
),
_tagsWidget(),
],
);
}

_tagsWidget has 3 widgets.

  1. Tags that we actually have.
  2. Search field.
  3. Suggestions.
Widget _tagsWidget() {
return Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Tags',
style: TextStyle(
fontSize: 20.0,
color: Colors.black,
),
),
),
_tags.length > 0
? Column(children: [
Wrap(
alignment: WrapAlignment.start,
children: _tags
.map((tagModel) => tagChip(
tagModel: tagModel,
onTap: () => _removeTag(tagModel),
action: 'Remove',
))
.toSet()
.toList(),
),
])
: Container(),
_buildSearchFieldWidget(),
_displayTagWidget(),
],
),
);
}
}
  1. In the first section, I am displaying the actual list of tags that I already have.

So my single tagChip is as follows.

Widget tagChip({
tagModel,
onTap,
action,
}) {
return InkWell(
onTap: onTap,
child: Stack(
children: [
Container(
padding: EdgeInsets.symmetric(
vertical: 5.0,
horizontal: 5.0,
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 10.0,
),
decoration: BoxDecoration(
color: Colors.deepOrangeAccent,
borderRadius: BorderRadius.circular(100.0),
),
child: Text(
'${tagModel.title}',
style: TextStyle(
color: Colors.white,
fontSize: 15.0,
),
),
),
),
Positioned(
right: 0,
child: CircleAvatar(
backgroundColor: Colors.orange.shade600,
radius: 8.0,
child: Icon(
Icons.clear,
size: 10.0,
color: Colors.white,
),
),
)
],
));
}

onTap function in tagChip is used to add or remove tag onTap of that tag. In the first section, I already have tags so I can remove tags that I don't want in my list. So I will pass _removeTag() function as the onTap function, which means if I tap on the tag from the first section then that tag is removed from the list that I already have.

_removeTag(tagModel) async {
if (_tags.contains(tagModel)) {
setState(() {
_tags.remove(tagModel);
});
}
}

2. The Second section is searchField and for that my widget is _buildSearchFieldWidget() and I have already explained that widget above.

3. The third section is the suggestion tags.

_displayTagWidget() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: _filterSearchResultList().isNotEmpty
? _buildSuggestionWidget()
: Text('No Labels added'),
);
}

So in _displayTagWidget() function, I am displaying tags that are getting from _filterSearchResultList(), basically, I am displaying searched tags and if searched Text is empty then I am displaying all the tags in suggestion.

Widget _buildSuggestionWidget() {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (_filterSearchResultList().length != _tags.length) Text('Suggestions'),
Wrap(
alignment: WrapAlignment.start,
children: _filterSearchResultList()
.where((tagModel) => !_tags.contains(tagModel))
.map((tagModel) => tagChip(
tagModel: tagModel,
onTap: () => _addTags(tagModel),
action: 'Add',
))
.toList(),
),
]);
}

In the suggestion section, I am displaying only those tags which are not in the list which I already have i.e in the first section. When I tap on the tag from the suggestion section then at that time I am adding that in the first section.

so _addTag() method is as follows.

_addTags(tagModel) async {
if (!_tags.contains(tagModel))
setState(() {
_tags.add(tagModel);
});
}

Here is the full code for working tags.

import 'package:flutter/material.dart';
import 'package:flutter_tags/flutter_tags.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Tags Demo',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
),
home: MyHomePage(title: 'Flutter Tags'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
List<TagModel> _tags = [];
TextEditingController _searchTextEditingController =
new TextEditingController();

String get _searchText => _searchTextEditingController.text.trim();

final List<TagModel> _tagsToSelect = [
TagModel(id: '1', title: 'JavaScript'),
TagModel(id: '2', title: 'Python'),
TagModel(id: '3', title: 'Java'),
TagModel(id: '4', title: 'PHP'),
TagModel(id: '5', title: 'C#'),
TagModel(id: '6', title: 'C++'),
TagModel(id: '7', title: 'Dart'),
TagModel(id: '8', title: 'DataFlex'),
TagModel(id: '9', title: 'Flutter'),
TagModel(id: '10', title: 'Flutter Selectable Tags'),
TagModel(id: '11', title: 'Android Studio developer'),
];
refreshState(VoidCallback fn) {
if (mounted) setState(fn);
}

@override
void initState() {
super.initState();
_searchTextEditingController.addListener(() => refreshState(() {}));
}

@override
void dispose() {
super.dispose();
_searchTextEditingController.dispose();
}



List<TagModel> _filterSearchResultList() {
if (_searchText.isEmpty) return _tagsToSelect;

List<TagModel> _tempList = [];
for (int index = 0; index < _tagsToSelect.length; index++) {
TagModel tagModel = _tagsToSelect[index];
if (tagModel.title
.toLowerCase()
.trim()
.contains(_searchText.toLowerCase())) {
_tempList.add(tagModel);
}
}

return _tempList;
}

_addTags(tagModel) async {
if (!_tags.contains(tagModel))
setState(() {
_tags.add(tagModel);
});
}

_removeTag(tagModel) async {
if (_tags.contains(tagModel)) {
setState(() {
_tags.remove(tagModel);
});
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Tags'),
backgroundColor: Colors.deepOrangeAccent,
),
body: _tagIcon(),
);
}

Widget _tagIcon() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.local_offer_outlined,
color: Colors.deepOrangeAccent,
size: 25.0,
),
_tagsWidget(),
],
);
}

_displayTagWidget() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: _filterSearchResultList().isNotEmpty
? _buildSuggestionWidget()
: Text('No Labels added'),
);
}

Widget _buildSuggestionWidget() {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (_filterSearchResultList().length != _tags.length) Text('Suggestions'),
Wrap(
alignment: WrapAlignment.start,
children: _filterSearchResultList()
.where((tagModel) => !_tags.contains(tagModel))
.map((tagModel) => tagChip(
tagModel: tagModel,
onTap: () => _addTags(tagModel),
action: 'Add',
))
.toList(),
),
]);
}

Widget tagChip({
tagModel,
onTap,
action,
}) {
return InkWell(
onTap: onTap,
child: Stack(
children: [
Container(
padding: EdgeInsets.symmetric(
vertical: 5.0,
horizontal: 5.0,
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 10.0,
),
decoration: BoxDecoration(
color: Colors.deepOrangeAccent,
borderRadius: BorderRadius.circular(100.0),
),
child: Text(
'${tagModel.title}',
style: TextStyle(
color: Colors.white,
fontSize: 15.0,
),
),
),
),
Positioned(
right: 0,
child: CircleAvatar(
backgroundColor: Colors.orange.shade600,
radius: 8.0,
child: Icon(
Icons.clear,
size: 10.0,
color: Colors.white,
),
),
)
],
));
}

Widget _buildSearchFieldWidget() {
return Container(
padding: EdgeInsets.only(
left: 20.0,
top: 10.0,
bottom: 10.0,
),
margin: EdgeInsets.only(
left: 20.0,
right: 20.0,
top: 20.0,
bottom: 5.0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(5.0),
),
border: Border.all(
color: Colors.grey.shade500,
width: 1,
),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _searchTextEditingController,
decoration: InputDecoration.collapsed(
hintText: 'Search Tag',
hintStyle: TextStyle(
color: Colors.grey,
),
),
style: TextStyle(
fontSize: 16.0,
),
textInputAction: TextInputAction.search,
),
),
_searchText.isNotEmpty
? InkWell(
child: Icon(
Icons.clear,
color: Colors.grey.shade700,
),
onTap: () => _searchTextEditingController.clear(),
)
: Icon(
Icons.search,
color: Colors.grey.shade700,
),
Container(),
],
),
);
}

Widget _tagsWidget() {
return Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Tags',
style: TextStyle(
fontSize: 20.0,
color: Colors.black,
),
),
),
_tags.length > 0
? Column(children: [
Wrap(
alignment: WrapAlignment.start,
children: _tags
.map((tagModel) => tagChip(
tagModel: tagModel,
onTap: () => _removeTag(tagModel),
action: 'Remove',
))
.toSet()
.toList(),
),
])
: Container(),
_buildSearchFieldWidget(),
_displayTagWidget(),
],
),
);
}
}

class TagModel {
String id;
String title;

TagModel({
@required this.id,
@required this.title,
});
}

In this way, we can add or remove tags from the list.

--

--