Flutter setState is reverting - dart

When I setState and add an image to the _images array, it appears to have added, but then it quickly reverts:
This form is loosely following Brian Egan's redux architecture example:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class Note {
final String comments;
final List<String> images;
Note({
this.comments,
this.images,
});
}
class AddNote extends StatefulWidget {
final Note note;
final bool isEditing;
AddNote({
this.note,
this.isEditing,
});
#override
_AddNoteState createState() => _AddNoteState();
}
class _AddNoteState extends State<AddNote> {
static final _scaffoldKey = GlobalKey<ScaffoldState>();
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
List<String> _images;
String _comments;
Note get _note => widget.note;
bool get _isEditing => widget.isEditing;
#override
Widget build(BuildContext context) {
_images = _note.images;
_comments = _note.comments;
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text(
_isEditing ? "Edit Note" : "Create Note",
),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
_photoPickerField(),
_notesField(),
],
),
),
),
);
}
Widget _photoPickerField() {
return GestureDetector(
onTap: _selectPicture,
child: Row(
children: <Widget>[
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 1,),
borderRadius: BorderRadius.all(const Radius.circular(10)),
),
child: SizedBox(child: Icon(Icons.camera_alt), width: 110, height: 110,)
),
] + _imagesRowItems(),
),
);
}
List<Widget> _imagesRowItems() {
return _images.map((image) {
return SizedBox(
height: 110,
width: 110,
child: Image.file(File(image), height: 110, width: 110, fit: BoxFit.cover),
);
}).toList();
}
Future _selectPicture() async {
return ImagePicker.pickImage(source: ImageSource.gallery)
.then((file) {
setState(() {
_images.add(file.path);
});
});
}
Widget _notesField() {
return TextFormField(
maxLines: 2,
keyboardType: TextInputType.multiline,
initialValue: _comments,
onSaved: (String value) => _comments = value,
);
}
}
Note that the comments field keeps its state without issue. How can I add to the images array in a way that will maintain its new state?

Your problem is that you're setting variables inside the build() method of the Widget state, but the build method is called every time you call setState() because your variables have changed, so it resets the images and comments.
To fix it, you should initialize your variables in the initState() method, like this:
class _AddNoteState extends State<AddNote> {
...
#override
void initState() {
super.initState();
_images = _note.images;
_comments = _note.comments;
}
}
And remove them from the build() method.

Related

Dynamically changing ListView of images using CachedNetworkImage

Essentially I want to do this but instead of updating a single image, I want to update a list of images. I have a ListView of images based off a List<String> of image URLs. See below:
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyAppState();
}
}
class MyAppState extends State<MyApp> {
double padding = 5.0;
String one = 'https://SOME_URL_HERE.com';
String two = 'https://SOME_URL_HERE.com';
String three = 'https://SOME_URL_HERE.com';
String four = 'https://SOME_URL_HERE.com';
List urls;
#override
Widget build(BuildContext context) {
urls = List();
urls.add(one);
urls.add(two);
return new MaterialApp(
debugShowCheckedModeBanner: false,
home: new Scaffold(
appBar: new AppBar(
title: new Text("Image list test"),
),
body: showBody(),
),
);
}
Widget showBody() {
return Padding(
padding: EdgeInsets.only(top: 15.0, left: 10.0, right: 10.0),
child: ListView(
children: <Widget>[
showItemImages(),
RaisedButton(
child: Text("Change image"),
onPressed: () {
setState(() {
urls = List();
urls.add(three);
urls.add(four);
});
},
),
Container(
child: Text('urls length: ${urls.length}'),
),
],
),
);
}
Widget showItemImages() {
return Padding(
padding: EdgeInsets.all(padding),
child: Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
height: 200,
child: urls.length > 0 //item.images.length > 0
? getImagesListView(context)
: Container(child: Text('No images yet'))),
);
}
getImagesListView(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: urls.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
width: 160.0,
child: _sizedContainer(
new CachedNetworkImage(
key: new ValueKey<String>(urls[index]),
imageUrl: urls[index],
//placeholder: (context, url) => new CircularProgressIndicator(),
),
),
);
},
);
}
Widget _sizedContainer(Widget child) {
return new SizedBox(
width: 300.0,
height: 150.0,
child: new Center(
child: child,
),
);
}
}
When I push the button, it should clear the list and add two new elements to it but it is not working
The following 3 lines:
urls = List();
urls.add(one);
urls.add(two);
should not be put inside #override widget build method. putting them there means that you re-initialize urls every time you setstate, you should put these lines inside #override initState() method.

How to use SharedPreferences in Bloc Pattern?

I am trying to use shared preference in my app with the bloc pattern.
Following is my code
class PrefsStats {
final bool isMale;
final String name;
final int age;
PrefsStats(this.isMale, this.name, this.age);
}
class PrefsBloc {
final _changePrefernce = BehaviorSubject<PrefsStats>();
Function(PrefsStats) get changePrefs => _changePrefernce.sink.add;
Stream<PrefsStats> get prefrence => _changePrefernce.stream;
SharedPreferences sPrefs;
dispose(){
_changePrefernce?.close();
}
PrefsBloc(){
_loadSharedPreferences();
}
Future<void> _loadSharedPreferences() async {
sPrefs = await SharedPreferences.getInstance();
final namePref = sPrefs.getString("name") ?? "";
final malePref = sPrefs.getBool("male") ?? false;
final agePref = sPrefs.getInt("age") ?? 0;
_changePrefernce.add(PrefsStats(malePref,namePref,agePref));
}
}
final prefsBloc = PrefsBloc();
I just want to insert data using one button and get data using another button from SharedPreferences
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.changePrefs(PrefsStats(true, "argo", 21));
},
child: Text("Insert Data"),
),
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.prefrence.forEach((data){
print(data.name);
});
},
child: Text("Get Data"),
),
SizedBox(
height: 20,
),
],
)),
),
);
}
#override
void dispose() {
prefsBloc?.dispose();
super.dispose();
}
}
Whenever I close my app and reopen it again and I click get data button at the start even before inserting data, I get default values. I know I am not assigning keys at the time of setting value, which is causing the confusion of how to use shared preferences with bloc. And the other problem is whenever I set data, the code inside get data button gets called even before pressing get data which I fail to understand.
There exits two places on your code that must be fixed.
First of all, in your BloC class, your stream must Listen whenever a sink is added,
.
.
.
PrefsBloc(){
_loadSharedPreferences();
_changePrefernce.stream.listen(_newFunction);
}
void _newFunction(PrefsStats stats){
if (states != null) {
if (sPrefs != null) {
sPrefs.setString("name", states.name);
sPrefs.setInt("age", states.age);
sPrefs.setBool("male", states.isMale);
sPrefs.commit();
}
}
}
Second place is in _MyAppState class, in the build function you have to wrap Scaffold with a StreamBuilder,
class _MyHomePageState extends State<MyHomePage> {
String textAge = "";
#override
Widget build(BuildContext context) {
return MaterialApp(
home: StreamBuilder(
stream: prefsBloc.prefrence,
builder: (context, AsyncSnapshot<PrefsStats> snapshot) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
Text((snapshot.data != null) ? snapshot.data.name : ""),
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.changePrefs(PrefsStats(
true,
textAge.toString(),
21,
));
},
child: Text("Insert Data"),
),
TextFormField(
initialValue: (snapshot.data != null) ? snapshot.data.name : "",
onFieldSubmitted: (value) {
textAge = value;
},
),
Text(textAge),
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.prefrence.forEach((data) {
print(data.name);
setState(() {
textAge = data.name;
});
});
},
child: Text("Get Data"),
),
SizedBox(
height: 20,
),
],
)),
);
},
));
}

how to execute the VoidCallback in flutter

I'm trying to test the VoidCallback so I created the main file, that have a function called from a flat button in the widget, which is in a separate file, but did not work.
main.dart
import 'package:flutter/material.dart';
import 'controller_test.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Retrieve Text Input',
home: MyCustomForm(),
);
}
}
// Define a Custom Form Widget
class MyCustomForm extends StatefulWidget {
#override
_MyCustomFormState createState() => _MyCustomFormState();
}
class _MyCustomFormState extends State<MyCustomForm> {
final myController = TextEditingController();
#override
void initState() {
super.initState();
myController.addListener(_printLatestValue);
}
_printLatestValue() {
print("Second text field: ${myController.text}");
}
_test() {
print("hi there");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Retrieve Text Input'),
),
body: Con(_test, myController)
);
}
}
controller_test.dart
import 'package:flutter/material.dart';
class Con extends StatelessWidget {
Con(this.clickCallback, this.tc);
final TextEditingController tc;
final VoidCallback clickCallback;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
onChanged: (text) {
print("First text field: $text");
},
),
TextField(
controller: tc,
),
FlatButton(
onPressed: () => clickCallback,
child: Text("click me"),
)
],
),
);
}
}
When I click the FlatButton in the widget, nothing is happening, I was expecting hi there to be printed
there are two options here.
onPressed: () => fun() is like onPressed argument is an anonymous method that calls fun.
onPressed: fun is like onPressed argument is the function fun.
I just found it in another answer here
I was missing the (), so correct call is:
FlatButton(
onPressed: () => clickCallback(),
child: Text("click me"),
)
You can get callback from stateless widget to your current page by using Voidcallback class.
Just add this custom widget in your current page (widget.build()
function)
DefaultButton(
buttonText: Constants.LOGIN_BUTTON_TEXT,
onPressed: () => validateInputFields(),
size: size,
);
My custom widget class is
class DefaultButton extends StatelessWidget {
DefaultButton({this.buttonText, this.onPressed, this.size});
final String buttonText;
final VoidCallback onPressed;
final Size size;
#override
Widget build(BuildContext context) {
return MaterialButton(
minWidth: size.width,
onPressed: () => onPressed(), //callback to refered page
padding: EdgeInsets.all(0.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DEFAULT_BORDER_RADIUS),
),
child: Ink(
width: size.width,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
SECONDARY_COLOR_SHADE_LITE,
SECONDARY_COLOR_SHADE_DARK,
],
),
borderRadius: BorderRadius.circular(DEFAULT_BORDER_RADIUS),
),
child: Padding(
padding: EdgeInsets.only(left: 20, right: 20, top: 12, bottom: 12),
child: Text(
buttonText,
style: Theme.of(context).textTheme.button,
textAlign: TextAlign.center,
)),
),
);
}
}
In your callback make sure to call setState if any variables change. I repopulate an list in my provider and then use assign that list to the variable list which I convert into a list of cards. The variable list needs state refreshed to see it.

How to Conditionally Route Using One Button? (Flutter)

I have a scene (collections.dart) that takes an index of several other scenes/files in a PageView.builder. You can swipe between scenes from the collections.dart file. Also in collections.dart is a button.
I want it to be the case that if you click on the button, and the current scene being shown through collections.dart is, for example, FirstScreen, then I can route to a table I have built specifically for first.dart, with the same being true for all other scenes in the index.
I have tried to accomplish this by a conditional statement in the onPressed argument, but no success yet. There is no error, it just takes no action. Here is the code in its entirety for collections.dart (including the unsuccessful conditional statement for onPressed):
import 'package:flutter/material.dart';
import 'package:circle_indicator/circle_indicator.dart';
import 'first.dart';
import 'second.dart';
import 'third.dart';
import 'fourth.dart';
import 'fifth.dart';
import 'sixth.dart';
import 'seventh.dart';
import 'eighth.dart';
import 'ninth.dart';
import 'tenth.dart';
class CollectionsScreen extends StatelessWidget {
#override
Widget build(BuildContext context){
return Collections();
}
}
class Collections extends StatefulWidget {
#override
CollectionsState createState() => CollectionsState();
}
class CollectionsState extends State<Collections> {
FirstScreen one;
SecondScreen two;
ThirdScreen three;
FourthScreen four;
FifthScreen five;
SixthScreen six;
SeventhScreen seven;
EighthScreen eight;
NinthScreen nine;
TenthScreen ten;
List<Widget> pages;
#override
void initState() {
one = FirstScreen();
two = SecondScreen();
three = ThirdScreen();
four = FourthScreen();
five = FifthScreen();
six = SixthScreen();
seven = SeventhScreen();
eight = EighthScreen();
nine = NinthScreen();
ten = TenthScreen();
pages = [one, two, three, four, five, six, seven, eight, nine, ten];
super.initState();
}
final PageController controller = new PageController();
#override
Widget build(BuildContext context){
return new Stack(
children: <Widget>[
new Scaffold(
body: new Container(
child: new PageView.builder( //Swipe Between Pages
controller: controller,
itemCount: 10,
itemBuilder: (context, index){
return pages[index];
}
),
),
),
new Container( //CircleIndicator
child: new CircleIndicator(controller, 10, 8.0, Colors.white70, Colors.white,),
alignment: Alignment(0.0, 0.9),
),
new Container( //Button
alignment: Alignment(0.0, 0.65),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new Container(
child: new RaisedButton(
elevation: 4.0,
child: new Text(
'SHOW ME',
style: new TextStyle(
fontWeight: FontWeight.w900,
fontSize: 22.0,
),
),
color: Color(0xFF70E0EF),
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(7.5)
),
//This is the conditional statement I'm talking about
onPressed: () {
new PageView.builder(
controller: controller,
itemBuilder: (context, index) {
if (pages[index] == one){
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new FirstTable()),
);
}
else if (pages[index] == two){
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new SecondTable()),
);
}
else {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new ThirdTable()),
);
}
}
);
},
),
width: 150.0,
height: 60.0,
),
],
),
),
],
);
}
}
The "Table" classes I'm referring to in the conditional statement are in the files for first.dart, second.dart, etc. Here is the file for first.dart. For the moment, the code is identical between all these files (first.dart, second.dart, etc.):
import 'package:flutter/material.dart';
class FirstScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new First();
}
}
class First extends StatefulWidget {
#override
FirstState createState() => FirstState();
}
class FirstState extends State<First>{
#override
Widget build(BuildContext context) {
double fontSize = MediaQuery.of(context).size.height;
double fontSizeFractional = fontSize * 0.07;
return Scaffold(
body: new Stack(
fit: StackFit.passthrough,
children: [
new Container( //Background
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('assets/FirstBG.png'),
fit: BoxFit.cover
),
),
),
new Container( //Title
margin: EdgeInsets.all(40.0),
alignment: new Alignment(0.0, -0.70),
child: new Text(
'FIRST',
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: fontSizeFractional,
color: Colors.white,
fontFamily: 'baron neue',
),
),
),
],
),
);
}
}
class FirstTable extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Table();
}
}
class Table extends StatefulWidget {
#override
TableState createState() => TableState();
}
class TableState extends State<Table>{
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: new RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: new Text(
'Go Back',
),
),
),
);
}
}
I have a theory that the reason it isn't working is that collections.dart isn't actually caching any data as to what page of the index it is on (that could be totally wrong, though). Curious to hear your ideas!
Your problem is that you should directly use controller.page inside the onPressed of your button. instead of instantiating a widget.
Although ultimately you should hide an abstract layer between your gallery class and the list of items.
To do that you can create a custom class which will hols all informations about a gallery item :
#immutable
class GalleryItem {
final Widget content;
final Widget details;
GalleryItem({#required this.content, this.details}) : assert(content != null);
}
Your gallery will then take a list of such class as parameter. And do it's job with these.
Ideally you want to use your gallery like this :
Gallery(
items: [
GalleryItem(
content: Container(
color: Colors.red,
),
details: Text("red"),
),
GalleryItem(
content: Container(
color: Colors.blue,
),
details: Text("blue"),
),
],
),
The code of such gallery would be :
class Gallery extends StatefulWidget {
final List<GalleryItem> items;
Gallery({#required this.items, Key key})
: assert(items != null),
super(key: key);
#override
_GalleryState createState() => _GalleryState();
}
class _GalleryState extends State<Gallery> {
final PageController pageController = PageController();
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: PageView(
children: widget.items.map((item) => item.content).toList(),
controller: pageController,
),
),
RaisedButton(
onPressed: showContentDetails,
child: Text("More info"),
)
],
);
}
void showContentDetails() {
final index = pageController.page.round();
if (widget.items[index]?.details != null) {
showDialog(
context: context,
builder: (_) =>
GalleryItemDetails(details: widget.items[index].details),
);
}
}
}
class GalleryItemDetails extends StatelessWidget {
final Widget details;
GalleryItemDetails({#required this.details, Key key})
: assert(details != null),
super(key: key);
#override
Widget build(BuildContext context) {
return Dialog(
child: details,
);
}
}

Adding a Chip to a Material TextField

Say we created a Chip object and TextField object like below. How do you add a Chip to the inside of the TextField?
new Chip(
label: new Text('Peyton Smith'),
)
new TextField(
)
Is it possible to combine them to get something like in the Material spec where typing in something into a Material TextField adds a Chip?
What you are looking for was actually provided in the comment.
Here is a simple implementation using InputChip:
InputChip(
avatar: CircleAvatar(
backgroundColor: Colors.grey.shade800,
child: Text('AB'),
),
label: Text('Aaron Burr'),
onPressed: () {
print('I am the one thing in life.');
}
)
A material design input chip.
Input chips represent a complex piece of information, such as an
entity (person, place, or thing) or conversational text, in a compact
form.
Input chips can be made selectable by setting
onSelected,
deletable by setting
onDeleted,
and pressable like a button with
onPressed.
They have a
label,
and they can have a leading icon (see
avatar)
and a trailing icon
(deleteIcon).
Colors and padding can be customized.
Here is my simple interpretation of InputChip:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: _toContainer(),
),
),
Divider(
color: Colors.blueGrey,
height: 10.0,
),
Align(
alignment: Alignment.centerLeft,
child: _subjectContainer(),
),
Divider(
color: Colors.blueGrey,
height: 10.0,
),
Align(
alignment: Alignment.centerLeft,
child: _messageContainer(),
),
],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
Widget _messageContainer() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Text(
'Message',
style: TextStyle(color: Colors.black, fontSize: 18.0),
),
),
);
}
Widget _toContainer() {
return Wrap(
spacing: 5.0,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8.0, right: 8.0),
child: Container(
child: Text(
'To',
style: TextStyle(color: Colors.black, fontSize: 18.0),
),
),
),
Container(
child: _profileChips("Scott Hill",
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"),
),
],
);
}
Widget _subjectContainer() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Text(
'Subject',
style: TextStyle(color: Colors.black, fontSize: 18.0),
),
),
);
}
Widget _profileChips(String myName, String myImage) {
return Material(
child: InputChip(
avatar: CircleAvatar(
backgroundColor: Colors.blueGrey,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(myImage),
)),
),
),
label: Text(myName),
labelStyle: TextStyle(
color: Colors.black, fontSize: 14.0, fontWeight: FontWeight.bold),
onPressed: () {},
onDeleted: () {},
),
);
}
}
Output:
And for a fully functional example, I've tested the answer in here.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// See: https://twitter.com/shakil807/status/1042127387515858949
// https://github.com/pchmn/MaterialChipsInput/tree/master/library/src/main/java/com/pchmn/materialchips
// https://github.com/BelooS/ChipsLayoutManager
void main() => runApp(ChipsDemoApp());
class ChipsDemoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.indigo,
accentColor: Colors.pink,
),
home: DemoScreen(),
);
}
}
class DemoScreen extends StatefulWidget {
#override
_DemoScreenState createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Material Chips Input'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: const InputDecoration(hintText: 'normal'),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChipsInput<AppProfile>(
decoration: InputDecoration(
prefixIcon: Icon(Icons.search), hintText: 'Profile search'),
findSuggestions: _findSuggestions,
onChanged: _onChanged,
chipBuilder: (BuildContext context,
ChipsInputState<AppProfile> state, AppProfile profile) {
return InputChip(
key: ObjectKey(profile),
label: Text(profile.name),
avatar: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
onDeleted: () => state.deleteChip(profile),
onSelected: (_) => _onChipTapped(profile),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
suggestionBuilder: (BuildContext context,
ChipsInputState<AppProfile> state, AppProfile profile) {
return ListTile(
key: ObjectKey(profile),
leading: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
title: Text(profile.name),
subtitle: Text(profile.email),
onTap: () => state.selectSuggestion(profile),
);
},
),
),
),
],
),
);
}
void _onChipTapped(AppProfile profile) {
print('$profile');
}
void _onChanged(List<AppProfile> data) {
print('onChanged $data');
}
Future<List<AppProfile>> _findSuggestions(String query) async {
if (query.length != 0) {
return mockResults.where((profile) {
return profile.name.contains(query) || profile.email.contains(query);
}).toList(growable: false);
} else {
return const <AppProfile>[];
}
}
}
// -------------------------------------------------
const mockResults = <AppProfile>[
AppProfile('Stock Man', 'stock#man.com',
'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
AppProfile('Paul', 'paul#google.com',
'https://mbtskoudsalg.com/images/person-stock-image-png.png'),
AppProfile('Fred', 'fred#google.com',
'https://media.istockphoto.com/photos/feeling-great-about-my-corporate-choices-picture-id507296326'),
AppProfile('Bera', 'bera#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('John', 'john#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Thomas', 'thomas#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Norbert', 'norbert#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Marina', 'marina#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
];
class AppProfile {
final String name;
final String email;
final String imageUrl;
const AppProfile(this.name, this.email, this.imageUrl);
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppProfile &&
runtimeType == other.runtimeType &&
name == other.name;
#override
int get hashCode => name.hashCode;
#override
String toString() {
return 'Profile{$name}';
}
}
// -------------------------------------------------
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(
BuildContext context, ChipsInputState<T> state, T data);
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
Key key,
this.decoration = const InputDecoration(),
#required this.chipBuilder,
#required this.suggestionBuilder,
#required this.findSuggestions,
#required this.onChanged,
this.onChipTapped,
}) : super(key: key);
final InputDecoration decoration;
final ChipsInputSuggestions findSuggestions;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T> onChipTapped;
final ChipsBuilder<T> chipBuilder;
final ChipsBuilder<T> suggestionBuilder;
#override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>>
implements TextInputClient {
static const kObjectReplacementChar = 0xFFFC;
Set<T> _chips = Set<T>();
List<T> _suggestions;
int _searchId = 0;
FocusNode _focusNode;
TextEditingValue _value = TextEditingValue();
TextInputConnection _connection;
String get text => String.fromCharCodes(
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
);
bool get _hasInputConnection => _connection != null && _connection.attached;
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void selectSuggestion(T data) {
setState(() {
_chips.add(data);
_updateTextInputState();
_suggestions = null;
});
widget.onChanged(_chips.toList(growable: false));
}
void deleteChip(T data) {
setState(() {
_chips.remove(data);
_updateTextInputState();
});
widget.onChanged(_chips.toList(growable: false));
}
#override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChanged);
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
_closeInputConnectionIfNeeded();
}
setState(() {
// rebuild so that _TextCursor is hidden.
});
}
#override
void dispose() {
_focusNode?.dispose();
_closeInputConnectionIfNeeded();
super.dispose();
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection = TextInput.attach(this, TextInputConfiguration());
_connection.setEditingState(_value);
}
_connection.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_connection.close();
_connection = null;
}
}
#override
Widget build(BuildContext context) {
var chipsChildren = _chips
.map<Widget>(
(data) => widget.chipBuilder(context, this, data),
)
.toList();
final theme = Theme.of(context);
chipsChildren.add(
Container(
height: 32.0,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
text,
style: theme.textTheme.subtitle1.copyWith(
height: 1.5,
),
),
_TextCaret(
resumed: _focusNode.hasFocus,
),
],
),
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
//mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: InputDecorator(
decoration: widget.decoration,
isFocused: _focusNode.hasFocus,
isEmpty: _value.text.length == 0,
child: Wrap(
children: chipsChildren,
spacing: 4.0,
runSpacing: 4.0,
),
),
),
Expanded(
child: ListView.builder(
itemCount: _suggestions?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
return widget.suggestionBuilder(
context, this, _suggestions[index]);
},
),
),
],
);
}
#override
void updateEditingValue(TextEditingValue value) {
final oldCount = _countReplacements(_value);
final newCount = _countReplacements(value);
setState(() {
if (newCount < oldCount) {
_chips = Set.from(_chips.take(newCount));
}
_value = value;
});
_onSearchChanged(text);
}
int _countReplacements(TextEditingValue value) {
return value.text.codeUnits
.where((ch) => ch == kObjectReplacementChar)
.length;
}
#override
void performAction(TextInputAction action) {
_focusNode.unfocus();
}
void _updateTextInputState() {
final text =
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
_value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
composing: TextRange(start: 0, end: text.length),
);
_connection.setEditingState(_value);
}
void _onSearchChanged(String value) async {
final localId = ++_searchId;
final results = await widget.findSuggestions(value);
if (_searchId == localId && mounted) {
setState(() => _suggestions = results
.where((profile) => !_chips.contains(profile))
.toList(growable: false));
}
}
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _TextCaret extends StatefulWidget {
const _TextCaret({
Key key,
this.duration = const Duration(milliseconds: 500),
this.resumed = false,
}) : super(key: key);
final Duration duration;
final bool resumed;
#override
_TextCursorState createState() => _TextCursorState();
}
class _TextCursorState extends State<_TextCaret>
with SingleTickerProviderStateMixin {
bool _displayed = false;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer.periodic(widget.duration, _onTimer);
}
void _onTimer(Timer timer) {
setState(() => _displayed = !_displayed);
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FractionallySizedBox(
heightFactor: 0.7,
child: Opacity(
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
child: Container(
width: 2.0,
color: theme.primaryColor,
),
),
);
}
}
Working output:
Aside from the samples above, you have the option to use flutter_chips_input plugin.
Flutter library for building input fields with InputChips as input
options.
Here is an example:

Resources