I need to implement searchview in toolbar my app to filter a list view:
With the help #aziza answer i write detail code snippet of search view with list filter below. it will help for others
import 'package:flutter/material.dart';
class SearchList extends StatefulWidget {
SearchList({ Key key }) : super(key: key);
#override
_SearchListState createState() => new _SearchListState();
}
class _SearchListState extends State<SearchList>
{
Widget appBarTitle = new Text("Search Sample", style: new TextStyle(color: Colors.white),);
Icon actionIcon = new Icon(Icons.search, color: Colors.white,);
final key = new GlobalKey<ScaffoldState>();
final TextEditingController _searchQuery = new TextEditingController();
List<String> _list;
bool _IsSearching;
String _searchText = "";
_SearchListState() {
_searchQuery.addListener(() {
if (_searchQuery.text.isEmpty) {
setState(() {
_IsSearching = false;
_searchText = "";
});
}
else {
setState(() {
_IsSearching = true;
_searchText = _searchQuery.text;
});
}
});
}
#override
void initState() {
super.initState();
_IsSearching = false;
init();
}
void init() {
_list = List();
_list.add("Google");
_list.add("IOS");
_list.add("Andorid");
_list.add("Dart");
_list.add("Flutter");
_list.add("Python");
_list.add("React");
_list.add("Xamarin");
_list.add("Kotlin");
_list.add("Java");
_list.add("RxAndroid");
}
#override
Widget build(BuildContext context) {
return new Scaffold(
key: key,
appBar: buildBar(context),
body: new ListView(
padding: new EdgeInsets.symmetric(vertical: 8.0),
children: _IsSearching ? _buildSearchList() : _buildList(),
),
);
}
List<ChildItem> _buildList() {
return _list.map((contact) => new ChildItem(contact)).toList();
}
List<ChildItem> _buildSearchList() {
if (_searchText.isEmpty) {
return _list.map((contact) => new ChildItem(contact))
.toList();
}
else {
List<String> _searchList = List();
for (int i = 0; i < _list.length; i++) {
String name = _list.elementAt(i);
if (name.toLowerCase().contains(_searchText.toLowerCase())) {
_searchList.add(name);
}
}
return _searchList.map((contact) => new ChildItem(contact))
.toList();
}
}
Widget buildBar(BuildContext context) {
return new AppBar(
centerTitle: true,
title: appBarTitle,
actions: <Widget>[
new IconButton(icon: actionIcon, onPressed: () {
setState(() {
if (this.actionIcon.icon == Icons.search) {
this.actionIcon = new Icon(Icons.close, color: Colors.white,);
this.appBarTitle = new TextField(
controller: _searchQuery,
style: new TextStyle(
color: Colors.white,
),
decoration: new InputDecoration(
prefixIcon: new Icon(Icons.search, color: Colors.white),
hintText: "Search...",
hintStyle: new TextStyle(color: Colors.white)
),
);
_handleSearchStart();
}
else {
_handleSearchEnd();
}
});
},),
]
);
}
void _handleSearchStart() {
setState(() {
_IsSearching = true;
});
}
void _handleSearchEnd() {
setState(() {
this.actionIcon = new Icon(Icons.search, color: Colors.white,);
this.appBarTitle =
new Text("Search Sample", style: new TextStyle(color: Colors.white),);
_IsSearching = false;
_searchQuery.clear();
});
}
}
class ChildItem extends StatelessWidget {
final String name;
ChildItem(this.name);
#override
Widget build(BuildContext context) {
return new ListTile(title: new Text(this.name));
}
}
Output :
You just need to alternate between the state whenever the user taps on the icon. Beside a little bit of refactoring an code cleaning on your side, this simple example should get you going.
class SearchAppBar extends StatefulWidget {
#override
_SearchAppBarState createState() => new _SearchAppBarState();
}
class _SearchAppBarState extends State<SearchAppBar> {
Widget appBarTitle = new Text("AppBar Title");
Icon actionIcon = new Icon(Icons.search);
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
centerTitle: true,
title:appBarTitle,
actions: <Widget>[
new IconButton(icon: actionIcon,onPressed:(){
setState(() {
if ( this.actionIcon.icon == Icons.search){
this.actionIcon = new Icon(Icons.close);
this.appBarTitle = new TextField(
style: new TextStyle(
color: Colors.white,
),
decoration: new InputDecoration(
prefixIcon: new Icon(Icons.search,color: Colors.white),
hintText: "Search...",
hintStyle: new TextStyle(color: Colors.white)
),
);}
else {
this.actionIcon = new Icon(Icons.search);
this.appBarTitle = new Text("AppBar Title");
}
});
} ,),]
),
);
}
}
Screenshot (Null safe):
You should use SearchDelegate which comes out of the box with Flutter. Here is a small video how it works:
Full code:
class SearchPage extends StatefulWidget {
#override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
String? _result;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search')),
body: Center(
child: Column(
children: <Widget>[
Text(_result ?? '', style: TextStyle(fontSize: 18)),
ElevatedButton(
onPressed: () async {
var result = await showSearch<String>(
context: context,
delegate: CustomDelegate(),
);
setState(() => _result = result);
},
child: Text('Search'),
),
],
),
),
);
}
}
class CustomDelegate extends SearchDelegate<String> {
List<String> data = nouns.take(100).toList();
#override
List<Widget> buildActions(BuildContext context) => [IconButton(icon: Icon(Icons.clear), onPressed: () => query = '')];
#override
Widget buildLeading(BuildContext context) => IconButton(icon: Icon(Icons.chevron_left), onPressed: () => close(context, ''));
#override
Widget buildResults(BuildContext context) => Container();
#override
Widget buildSuggestions(BuildContext context) {
var listToShow = data;
if (query.isNotEmpty)
listToShow = data.where((e) => e.contains(query)).toList();
return ListView.builder(
itemCount: listToShow.length,
itemBuilder: (_, i) {
var noun = listToShow[i];
return ListTile(
title: Text(noun),
onTap: () => close(context, noun),
);
},
);
}
}
If you want a simple search bar, you can do it with a customized TextField
import 'package:flutter/material.dart';
class SearchBar extends StatelessWidget {
final void Function(String) onTextChange;
SearchBar({ this.onTextChange });
#override
Widget build(BuildContext context) {
return Container(
height: 50,
padding: EdgeInsets.all(8),
child: TextField(
onChanged: onTextChange,
decoration: InputDecoration(
fillColor: Colors.black.withOpacity(0.1),
filled: true,
prefixIcon: Icon(Icons.search),
hintText: 'Search something ...',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
contentPadding: EdgeInsets.zero
)
)
);
}
}
You can do by edit leading, title and actions of AppBar. As you can see bellow.
appBar: new AppBar(
leading: _isSearching ? const BackButton() : null,
title: _isSearching ? _buildSearchField() : _buildTitle(context),
actions: _buildActions(),
),
You can see it here in detail. They guys have built a simple demo for that.
Related
Want to clone playstore front page appbar scrolling functionality in flutter. Playstore Appbar I'm trying to make a screen which contains static Tabs under SliverAppBar bottom property. I need to create dynamic Tabs and TabBarViews whenever i clicked onto any parent static tab. I did it successfully but i'm facing some problems.
When i click onto any parent tab, I would try to initialize the tabController but the currentIndex remains the same as it was into the previous parent tab.First Tab Second Tab
Each tab body must save its scrolling position.
This my screen code.
class DynamicTabContent {
IconData icon;
String tooTip;
DynamicTabContent.name(this.icon, this.tooTip);
}
int currentTabBlue = 0;
class TestAppHomePage extends StatefulWidget {
#override
TestAppHomePageState createState() => new TestAppHomePageState();
}
class TestAppHomePageState extends State<TestAppHomePage>
with TickerProviderStateMixin {
List<DynamicTabContent> myList = new List();
ScrollController _scrollController = new ScrollController();
TabController _tabControllerBlue;
TabController _tabController;
handleTabChange() {
currentTabBlue = _tabControllerBlue.index;
print("CurrentTab = " + currentTabBlue.toString());
if (_tabControllerBlue.index == 0) {
setState(() {
myList.clear();
myList.add(new DynamicTabContent.name(Icons.favorite, "Favorited"));
myList
.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
this._tabController = new TabController(initialIndex: 0, length: 15, vsync: this);
});
} else if (_tabControllerBlue.index == 1) {
setState(() {
myList.clear();
myList.add(new DynamicTabContent.name(Icons.favorite, "Favorited"));
myList
.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
myList
.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
this._tabController = new TabController(initialIndex: 0, length: 15, vsync: this);
});
} else if (_tabControllerBlue.index == 2) {
setState(() {
myList.clear();
myList.add(new DynamicTabContent.name(Icons.favorite, "Favorited"));
myList.add(new DynamicTabContent.name(Icons.favorite, "Favorited"));
myList
.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
myList
.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
myList
.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
this._tabController = new TabController(initialIndex: 0, length: 15, vsync: this);
});
}
}
#override
void initState() {
print("initState = TestAppHomePage");
myList.add(new DynamicTabContent.name(Icons.favorite, "Favorited"));
myList.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
_tabControllerBlue =
new TabController(initialIndex: 0, length: 3, vsync: this);
_tabControllerBlue.addListener(handleTabChange);
_tabController =
new TabController(initialIndex: 0, length: myList.length, vsync: this);
}
#override
void dispose() {
print("dispose");
// _tabController.removeListener(handleTabChange);
// _tabController.dispose();
super.dispose();
}
Future<void> executeAfterBuild() async {
print("Build: Called Back");
}
#override
Widget build(BuildContext context) {
executeAfterBuild();
return new Scaffold(
body: new NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Colors.white,
),
onPressed: null,
),
title: Text('Kitchen'),
floating: true,
pinned: true,
bottom: TabBar(
controller: _tabControllerBlue,
tabs: [
Tab(icon: Icon(Icons.lightbulb_outline), text: "Tab 1"),
Tab(icon: Icon(Icons.lightbulb_outline), text: "Tab 2"),
Tab(icon: Icon(Icons.lightbulb_outline), text: "Tab 3"),
],
),
),
new SliverPersistentHeader(
pinned: true,
delegate: TestTabBarDelegate(controller: _tabController),
),
];
},
body: new TestHomePageBody(
tabController: _tabController,
scrollController: _scrollController,
myList: myList,
),
),
);
}
}
class TestHomePageBody extends StatefulWidget {
TestHomePageBody({this.tabController, this.scrollController, this.myList});
final TabController tabController;
final ScrollController scrollController;
final List<DynamicTabContent> myList;
State<StatefulWidget> createState() {
return TestHomePageBodyState();
}
}
class TestHomePageBodyState extends State<TestHomePageBody> {
Key _key = new PageStorageKey({});
bool _innerListIsScrolled = false;
void _updateScrollPosition() {
if (!_innerListIsScrolled &&
widget.scrollController.position.extentAfter == 0.0) {
setState(() {
_innerListIsScrolled = true;
print("_innerListIsScrolled = true");
});
} else if (_innerListIsScrolled &&
widget.scrollController.position.extentAfter > 0.0) {
setState(() {
_innerListIsScrolled = false;
print("_innerListIsScrolled = false");
// Reset scroll positions of the TabBarView pages
_key = new PageStorageKey({});
});
}
}
#override
void initState() {
widget.scrollController.addListener(_updateScrollPosition);
print("initState = TestHomePageBodyState");
super.initState();
}
#override
void dispose() {
widget.scrollController.removeListener(_updateScrollPosition);
super.dispose();
}
#override
Widget build(BuildContext context) {
return new TabBarView(
controller: widget.tabController,
key: _key,
children: widget.myList.isEmpty
? <Widget>[]
: widget.myList.map(
(dynamicContent) {
return new Card(
child: new Column(
children: <Widget>[
new Container(
height: 450.0,
width: 300.0,
child: new IconButton(
icon: new Icon(dynamicContent.icon, size: 100.0),
tooltip: dynamicContent.tooTip,
onPressed: null,
),
),
Text(dynamicContent.tooTip),
],
),
);
},
).toList(),
);
}
}
class TestTabBarDelegate extends SliverPersistentHeaderDelegate {
TestTabBarDelegate({this.controller});
final TabController controller;
#override
double get minExtent => kToolbarHeight;
#override
double get maxExtent => kToolbarHeight;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: Theme.of(context).cardColor,
height: kToolbarHeight,
child: new TabBar(
controller: controller,
isScrollable: true,
labelColor: Theme.of(context).accentColor,
indicatorSize: TabBarIndicatorSize.label,
key: new PageStorageKey<Type>(TabBar),
indicatorColor: Theme.of(context).accentColor,
tabs: List<Widget>.generate(controller.length, (int index) {
print(controller.length);
return new Tab(text: "Excluded Discounted Deals");
}),
),
);
}
#override
bool shouldRebuild(covariant TestTabBarDelegate oldDelegate) {
return oldDelegate.controller != controller;
}
}
One way to solve it is using PageStorage. eg.
In the onTap method you will need to write the state, and when building the widget you will need to read the state, something as follow:
// Reading state:
var tabInternalIndex = PageStorage.of(context).readState(context, identifier: ValueKey('tab3'));
_currentTab = tabInternalIndex == null ? _currentTab : tabInternalIndex;
// Writing State
onTap: (int index) {
setState(() {
_currentTab = index;
PageStorage.of(context).writeState(context, index,
identifier: ValueKey('tab3'));
});
}
Please notice that identifier for every main tab has to be different.
Complete example:
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 DefaultTabController(
length: 3,
initialIndex: 1,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.mail)),
Tab(icon: Icon(Icons.contacts)),
Tab(icon: Icon(Icons.info)),
],
),
title: Text('Sample tabs'),
),
body: TabBarView(children: <Widget>[
SubTabs1(),
SubTabs2(),
SubTabs3()
]),
),
);
}
}
class SubTabs1 extends StatefulWidget {
#override
_SubTabs1State createState() => _SubTabs1State();
}
class _SubTabs1State extends State<SubTabs1> {
int _currentTab = 0;
#override
Widget build(BuildContext context) {
var tabInternalIndex = PageStorage.of(context)
.readState(context, identifier: ValueKey('tab1'));
_currentTab = tabInternalIndex == null ? _currentTab : tabInternalIndex;
return DefaultTabController(
initialIndex: _currentTab,
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
onTap: (int index) {
setState(() {
_currentTab = index;
PageStorage.of(context).writeState(context, index,
identifier: ValueKey('tab1'));
});
},
tabs: [
Tab(icon: Icon(Icons.delete)),
Tab(icon: Icon(Icons.delete_forever)),
Tab(icon: Icon(Icons.delete_outline)),
],
),
),
body: TabBarView(
children: <Widget>[
Container(
color: Colors.green[100],
child: Text('Child 5'),
),
Container(
color: Colors.green[300],
child: Text('Child 6'),
),
Container(
color: Colors.green[600],
child: Text('Child 7'),
)
],
)));
}
}
class SubTabs2 extends StatefulWidget {
#override
_SubTabs2State createState() => _SubTabs2State();
}
class _SubTabs2State extends State<SubTabs2> {
int _currentTab = 0;
#override
Widget build(BuildContext context) {
var tabInternalIndex = PageStorage.of(context)
.readState(context, identifier: ValueKey('tab2'));
_currentTab = tabInternalIndex == null ? _currentTab : tabInternalIndex;
return DefaultTabController(
initialIndex: _currentTab,
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
onTap: (int index) {
setState(() {
_currentTab = index;
PageStorage.of(context).writeState(context, index,
identifier: ValueKey('tab2'));
});
},
tabs: [
Tab(icon: Icon(Icons.alarm_add)),
Tab(icon: Icon(Icons.alarm_off)),
Tab(icon: Icon(Icons.alarm_on)),
],
),
),
body: TabBarView(
children: <Widget>[
Container(
color: Colors.yellow[100],
child: Text('Child 5'),
),
Container(
color: Colors.yellow[300],
child: Text('Child 6'),
),
Container(
color: Colors.yellow[600],
child: Text('Child 7'),
)
],
)));
}
}
class SubTabs3 extends StatefulWidget {
#override
_SubTabs3State createState() => _SubTabs3State();
}
class _SubTabs3State extends State<SubTabs3> {
int _currentTab = 0;
#override
Widget build(BuildContext context) {
// Reading state:
var tabInternalIndex = PageStorage.of(context).readState(context, identifier: ValueKey('tab3'));
_currentTab = tabInternalIndex == null ? _currentTab : tabInternalIndex;
return DefaultTabController(
initialIndex: _currentTab,
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
onTap: (int index) {
setState(() {
_currentTab = index;
PageStorage.of(context).writeState(context, index,
identifier: ValueKey('tab3'));
});
},
tabs: [
Tab(icon: Icon(Icons.ac_unit)),
Tab(icon: Icon(Icons.accessible)),
Tab(icon: Icon(Icons.airport_shuttle)),
],
),
),
body: TabBarView(
children: <Widget>[
Container(
color: Colors.pink[100],
child: Text('Child 1'),
),
Container(
color: Colors.pink[300],
child: Text('Child 2'),
),
Container(
color: Colors.pink[600],
child: Text('Child 3'),
)
],
)));
}
}
Hope this help.
How to fix this For loop not working error? for loop only work once in Flutter
It's a simple login form. If username and password matched go to user
page else go to admin page.
method code:
checkLogin(){
setState(() {
for(var c=0;c < global.user_name_arr.length-1 ; c++){
if(global.user_name_arr[c]==myController.text&&global.user_password_arr[c]==myControllerPwd.text)
Navigator.push(context, MaterialPageRoute(builder: (context)=>user()),);
else
Navigator.push(context, MaterialPageRoute(builder:(context)=>admin()),); }
}); }
RaiseButton code:
new RaisedButton(
child:new Text("Click"),
onPressed:checkLogin,
)
global.dart
library user_login.globlas;
var user_name_arr=['bhanuka','isuru','sampath'];
var user_password_arr=['1234','123','12'];
First off, let's refactor your code :) Create a user class like so:
class User {
final String name;
final String password;
User(this.name, this.password);
}
Next, fix your global user collection:
final validUsers = [User('bhanuka', '1234'), User('isuru', '123'), User('sampath', '12')];
Now, use this code to perform correct navigation:
checkLogin() {
if (validUsers.indexWhere((user) => user.name == myController.text && user.password == myControllerPwd.text) >= 0) {
Navigator.push(context, MaterialPageRoute(builder: (context)=>user()),);
} else {
Navigator.push(context, MaterialPageRoute(builder:(context)=>admin()),);
}
}
There are better ways to do this comparison but I guess it's good enough for your use case.
here you are using if else so that condition is right or wrong one of the part is executed.
import 'package:flutter/material.dart';
void main() => runApp(new MaterialApp(
title: 'Forms in Flutter',
home: new LoginPage(),
));
class LoginPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _LoginPageState();
}
class _LoginData {
String email = '';
String password = '';
}
class _LoginPageState extends State<LoginPage> {
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
_LoginData _data = new _LoginData();
var user_name_arr = ['bhanuka', 'isuru', 'sampath'];
var user_password_arr = ['1234', '123', '12'];
var p;
void submit() {
if (this._formKey.currentState.validate()) {
_formKey.currentState.save(); // Save our form now.
if (user_name_arr.contains(_data.email)) {
p = user_name_arr.indexOf(_data.email);
if (user_password_arr.elementAt(p) == _data.password) {
Navigator.push(context, MaterialPageRoute(builder: (context)=>user()),);
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => admin()),
);
}
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => admin()),
);
}
}
}
#override
Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
return new Scaffold(
appBar: new AppBar(
title: new Text('Login'),
),
body: new Container(
padding: new EdgeInsets.all(20.0),
child: new Form(
key: this._formKey,
child: new ListView(
children: <Widget>[
new TextFormField(
keyboardType: TextInputType
.emailAddress, // Use email input type for emails.
decoration: new InputDecoration(
hintText: 'you#example.com',
labelText: 'E-mail Address'),
onSaved: (String value) {
this._data.email = value;
}),
new TextFormField(
obscureText: true, // Use secure text for passwords.
decoration: new InputDecoration(
hintText: 'Password', labelText: 'Enter your password'),
onSaved: (String value) {
this._data.password = value;
}),
new Container(
width: screenSize.width,
child: new RaisedButton(
child: new Text(
'Login',
style: new TextStyle(color: Colors.white),
),
onPressed: this.submit,
color: Colors.blue,
),
margin: new EdgeInsets.only(top: 20.0),
)
],
),
)),
);
}
}
class user extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(child: new Text("user")),
),
);
}
}
class admin extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(child: new Text("admin")),
),
);
}
}
I have a list of stateful widgets where the user can add, remove, and interact with items in the list. Removing items from the list causes subsequent items in the list to rebuild as they shift to fill the deleted row. This results in a loss of state data for these widgets - though they should remain unaltered other than their location on the screen. I want to be able to maintain state for the remaining items in the list even as their position changes.
Below is a simplified version of my app which consists primarily of a list of StatefulWidgets. The user can add items to the list ("tasks" in my app) via the floating action button or remove them by swiping. Any item in the list can be highlighted by tapping the item, which changes the state of the background color of the item. If multiple items are highlighted in the list, and an item (other than the last item in the list) is removed, the items that shift to replace the removed item lose their state data (i.e. the background color resets to transparent). I suspect this is because _taskList rebuilds since I call setState() to update the display after a task is removed. I want to know if there is a clean way to maintain state data for the remaining tasks after a task is removed from _taskList.
void main() => runApp(new TimeTrackApp());
class TimeTrackApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Time Tracker',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new TimeTrackHome(title: 'Task List'),
);
}
}
class TimeTrackHome extends StatefulWidget {
TimeTrackHome({Key key, this.title}) : super(key: key);
final String title;
#override
_TimeTrackHomeState createState() => new _TimeTrackHomeState();
}
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItem> _taskList = new List<TaskItem>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItem(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItem>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index],
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}
class TaskItem extends StatefulWidget {
final String name;
TaskItem({Key key, this.name}) : super(key: key);
TaskItem.from(TaskItem other) : name = other.name;
#override
State<StatefulWidget> createState() => new _TaskState();
}
class _TaskState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
Color _color = Colors.transparent;
void _highlightTask() {
setState(() {
if(_color == Colors.transparent) {
_color = Colors.greenAccent;
}
else {
_color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: _color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
I ended up solving the problem by creating an intermediate class which contains a reference to the StatefulWidget and transferred over all the state variables. The State class accesses the state variables through a reference to the intermediate class. The higher level widget that contained and managed a List of the StatefulWidget now access the StatefulWidget through this intermediate class. I'm not entirely confident in the "correctness" of my solution as I haven't found any other examples of this, so I am still open to suggestions.
My intermediate class is as follows:
class TaskItemData {
// StatefulWidget reference
TaskItem widget;
Color _color = Colors.transparent;
TaskItemData({String name: "",}) {
_color = Colors.transparent;
widget = TaskItem(name: name, stateData: this,);
}
}
My StatefulWidget and its corresponding State classes are nearly unchanged, except that the state variables no longer reside in the State class. I also added a reference to the intermediate class inside my StatefulWidget which gets initialized in the constructor. Previous uses of state variables in my State class now get accessed through the reference to the intermediate class. The modified StatefulWidget and State classes is as follows:
class TaskItem extends StatefulWidget {
final String name;
// intermediate class reference
final TaskItemData stateData;
TaskItem({Key key, this.name, this.stateData}) : super(key: key);
#override
State<StatefulWidget> createState() => new _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
void _highlightTask() {
setState(() {
if(widget.stateData._color == Colors.transparent) {
widget.stateData._color = Colors.greenAccent;
}
else {
widget.stateData._color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: widget.stateData._color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
The widget containing the List of TaskItem objects has been replaced with a List of TaskItemData. The ListViewBuilder child now accesses the TaskItem widget through the intermediate class (i.e. child: _taskList[index], has changed to child: _taskList[index].widget,). It is as follows:
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItemData> _taskList = new List<TaskItemData>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItemData(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItemData>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index].widget,
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}
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:
I'm just starting with Flutter, finished the first codelab and tried to add some simple functionality to it.
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
theme: new ThemeData(primaryColor: Colors.deepOrange),
home: new RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
#override
createState() => new RandomWordsState();
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = new TextStyle(fontSize: 18.0);
final _saved = new Set<WordPair>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.list),
onPressed: _pushSaved,
)
],
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return new Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
},
);
}
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new IconButton(
icon: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border),
color: alreadySaved ? Colors.red : null,
onPressed: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
));
}
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return _buildRow(pair);
// new ListTile(
// title: new Text(
// pair.asPascalCase,
// style: _biggerFont,
// ),
// );
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
},
),
);
}
}
In the Save suggestions screen I built the same row as in the Sugestions Screen.
In the Saved Sugstions screen when you click the heart icon the element is removed from the array of saved items but the screen is not re-rendered.
what am I doing wrong here?
thanks!
Update
Actually your app is working perfectly fine with me :/
Because you are not communicating the state change with the icon change. You are already changing state based on alreadySaved, notice how you managed setState()
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
In the previous block you are only removing or adding to your favourite list based on the boolean value of alreadySaved and you are not telling setState to change anything else. That is why the following does not produce a re-render even though alreadySaved is switching values
///These two lines do not know what is happening
icon: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border),
color: alreadySaved ? Colors.red : null,
So you can instead do the following
icon: new Icon(_whichIcon), //initialized var _whichIcon = Icons.favorite_border
color: _whichIconColor, //Initialized var _whichIconColor = Colors.transparent
And your setState would be:
setState(() {
if (alreadySaved) {
_saved.remove(pair);
_whichIcon = Icons.favorite_border ;
_whichIconColor = Colors.transparent;
} else {
_saved.add(pair);
_whichIcon = Icons.favorite ;
_whichIconColor = Colors.red;
}
});
Or simpler you can do it like this, and keep your icon logic unchanged:
bool alreadySaved = false;
...
setState(() {
if (_saved.contains(pair)) {
_saved.remove(pair);
alreadySaved = false;
} else {
_saved.add(pair);
alreadySaved = true;
}
});