Flutter Custom Multi Selection - flutter

Question
Hi, how can I implement a custom multi selection widget in flutter? Here is an example :
I have a list of widget created with a ListView.builder and I simply want to change color based on userTap.
How can I implement this? I'm able to change color when user tap on one option but then, when another button is tapped I can't understand how to reverse the state of the old selected option.
I was searching for a solution with flutter standard state management or maybe with bloc library.
example code
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index){
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: SelectableContainer(text: 'option', index: index,),
);
},
),
);
}
}
class SelectableContainer extends StatefulWidget {
final String text;
final int index;
SelectableContainer({
#required this.text,
#required this.index
});
#override
_SelectableContainerState createState() => _SelectableContainerState();
}
class _SelectableContainerState extends State<SelectableContainer> {
bool _isSelected = false;
Color unselectedColor = Colors.white;
Color selectedColor = Colors.blue;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
setState(() {
_isSelected = true;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
height: 50,
color: _isSelected ? selectedColor : unselectedColor,
child: Center(
child: Text(widget.text),
),
),
);
}
}
Thanks

First store the selection in list when the user select an item
selectionList.add(title.id);
Then in the ListView.builder change the color of the title if it's in the selectionList
Title(color: selectionList.contain(listOfTitls[index].id)? Colors.green : Colors.White);
update
this trick will do it for you
return GestureDetector(
onTap: (){
setState(() {
_isSelected =_isSelected? false:true;//this line
});
},

In your setState method, you can just do _isSelected = !_isSelected; instead of always setting it to true. That way the tile will get unselected if _isSelected is set to true.
Although the above way wil solve your problem, there is also another widget flutter has for this same scenario called ListTile. Here is a link to the documentation. You might also want to check that out. It also has an onTap callback built into it directly.
Here is a quick app I built using ListTile. Hope it solves your problem. Cheers!
import 'package:flutter/material.dart';
class DemoClass extends StatefulWidget {
_DemoClassState createState() => _DemoClassState();
}
class _DemoClassState extends State<DemoClass> {
int _selectedIndex = -1;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: 50,
color: _selectedIndex == index ? Colors.blue : Colors.transparent,
child: ListTile(
title: Text('This is some title'),
onTap: () => setState(() {
_selectedIndex = index;
}),
),
);
}
),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Center(
child: Container(
child: DemoClass(),
),
),
);
}
}
void main() {
runApp(MyApp());
}

Related

How to make short ListView of DropDownButtons build faster?

I have a short ListView of a maximum of 10 items. Each list item will contain a DropDownButton which will hold around 1K DropDownMenuItems for selections.
In native Android, I was able to implement one that performed very smoothly, but with Flutter it takes a while to build the ListView which causes the UI to freeze.
In my case, I will need to rebuild the ListView upon every change in one of its items, so It will be a major issue.
Is there a way to make the ListView build faster, or at least be able to display a ProgressBar till it builds?
N.B: Using --profile configuration to simulate a release version improves the performance a lot, but still there is a sensed freeze.
Here's my sample code which you can directly copy/paste if you want to test it yourself.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool showList = false;
final List<DropdownMenuItem<int>> selections = List.generate(
1000,
(index) => DropdownMenuItem<int>(
value: index,
child: Text("$index"),
),
);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Container(
width: double.infinity,
child: Column(
children: [
ElevatedButton(
child: Text("toggle list visibility"),
onPressed: () {
setState(() {
showList = !showList;
});
},
),
Expanded(
child: showList
? ListView.builder(
cacheExtent: 2000,
itemCount: 10,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Container(
height: 200,
color: Colors.green,
child: Column(
children: [
Text("List Item: $index"),
DropdownButton<int>(
onChanged: (i) {},
value: 1,
items: selections,
),
],
),
),
),
);
})
: Text("List Not Built"),
),
],
),
),
),
);
}
}
Load dropdown when clicking the button.
Add this widget on your main List View
InkWell(
onTap: () {
showDialog(
context: context,
builder: (_) {
return VendorListAlert(selectVendor: selectVendorTap);
});
},
child: // create a widget, looks like your drop down
),
Handle tap event
void selectVendorTap(pass your model){
// logic
}
Sample for custom Alert
No need to create a mutable widget, the immutable widget is better.
class VendorListAlert extends StatefulWidget {
final Function selectVendor;
const VendorListAlert({Key key, this.selectVendor}) : super(key: key);
#override
_VendorListAlertState createState() => _VendorListAlertState();
}
class _VendorListAlertState extends State<VendorListAlert> {
List<UserModel> _searchVendor = [];
#override
void initState() {
super.initState();
_searchVendor = List.from(ypModel);
}
#override
Widget build(BuildContext context) {
return AlertDialog(
content: Container(
width: width,
child: ListView.builder(
shrinkWrap: true,
itemCount: _searchVendor.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: InkWell(
onTap: () {
widget.selectVendor(_searchVendor[index]);
Navigator.pop(context);
},
child:
),
);
},
),
),
);
}
}

how to show two widgets one after another within some time interval in dart

I am trying to show two widgets one after another within 2 seconds of time interval.
Here, First I am trying to show the text widget and after two seconds of time interval it should be change to dot widget.
I tried but I am unable to do this. I was stuck how to return the two widgets at a time one after another.
code:
Widget _getNumberWidget(bool hasHighlight, Color color, String text) {
final textStyle = context.appThemeData.passcodeFieldStyle.numberTextStyle.textStyle.copyWith(color: color);
return Container(
height: 35,
alignment: Alignment.bottomCenter,
child: FittedBox(
fit: BoxFit.fitHeight,
child: text.isEmpty ? _getDotWidget(hasHighlight, color, text) : _showChar(hasHighlight, color, text),
),
);
}
Widget _showChar(bool hasHighlight, Color color, String text) {
final textStyle = context.appThemeData.passcodeFieldStyle.numberTextStyle.textStyle.copyWith(color: color);
AppText(
text: text,
style: context.appThemeData.passcodeFieldStyle.numberTextStyle.copyWith(textStyle: textStyle),
);
sleep(const Duration(seconds: 2));
return _getDotWidget(hasHighlight, color, text);
}
If I return the Apptext then the remaining two lines code will change to dead code. Can any one suggest me how to do this. Thanks
Use state, define variable:
bool _showDotWidget = false;
Instead of sleep, use Future.delayed
Widget _showChar(bool hasHighlight, Color color, String text) {
if (_showDotWidget)
return _getDotWidget(hasHighlight, color, text);
else {
Future.delayed(Duration(seconds:2), () {
setState({
_showDotWidget = true;
});
});
final textStyle = context.appThemeData.passcodeFieldStyle.numberTextStyle.textStyle.copyWith(color: color);
return AppText(
text: text,
style: context.appThemeData.passcodeFieldStyle.numberTextStyle.copyWith(textStyle: textStyle),
);
}
}
Check my simple implementation using Future.delayed, which fires setState after 5 seconds to set a value (isElapsed=true).
class _ShowWidgetsState extends State<ShowWidgets> {
bool isElapsed = false;
#override
Widget build(BuildContext context) {
//Delay for 5 seconds before setting
//isElapsed to true
Future.delayed(Duration(seconds: 5), () {
print('Displaying');
setState(() {
isElapsed = true;
});
});
return Scaffold(
appBar: AppBar(
title: Text('Show Widgets'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {},
child: Text('First Widget'),
),
SizedBox(height: 10.0),
Container(
alignment: Alignment.center,
//Use Visibility Widget to show
child: Visibility(
visible: isElapsed,
child: ElevatedButton(
child: Text('Second Widget'),
onPressed: () {},
),
)),
],
),
);
}
You can use FutureBuilder like this
import 'package:flutter/material.dart';
class ShowWidgetWithInterval extends StatelessWidget {
Future<bool> _setInterval() async {
await Future.delayed(Duration(seconds: 2));
return true;
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _setInterval(),
builder: (context, intervalSnapshot) {
if (!intervalSnapshot.hasData) {
return Text('Widget 1');
}
return Text('Widget 2');
},
);
}
}
You can use FutureBuilder and customize StatelessWidget insead of a function:
FutureBuilder(
future: Future.delayed(const Duration(seconds: 3)),
builder: (c, s) => s.connectionState == ConnectionState.done
? Text("Dot widget with text: $text")
// Pass here SizedBox.shrink(); in case you do not want to render anithing
: const Text("Dot widget is loading..."));
Full code to reproduce:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String? text;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
text = 'Some text';
});
},
),
body: Center(
child: Container(
padding: const EdgeInsets.all(4),
color: Colors.lightGreen, child: NumberWidget(text: text))),
),
);
}
}
class NumberWidget extends StatelessWidget {
final bool hasHighlight;
final Color color;
final String? text;
const NumberWidget(
{Key? key,
this.hasHighlight = false,
this.color = Colors.lightGreen,
this.text})
: super(key: key);
//Helper function for a text variable.
bool isEmpty(String? s) => s == null || s.trim().isEmpty;
#override
Widget build(BuildContext context) {
if (isEmpty(text)) {
return const Text("Text is empty");
}
return FutureBuilder(
future: Future.delayed(const Duration(seconds: 3)),
builder: (c, s) => s.connectionState == ConnectionState.done
? Text("Dot widget with text: $text")
// Pass here SizedBox.shrink(); in casde you do not want to render anithing
: const Text("Dot widget is loading..."));
}
}
Result:

When I add elements to listview, how to update listview in Flutter?

I am new to flutter and I would like to add element every 5 seconds to my list view. I have list view and I think I have the true adding method. However, I do not know how to update my list view every 5 seconds.
void randomCity(){
List <int> colors = [yellow,green,blue,red,black,white];
List <String> countryNames = ["Gdańsk","Warszawa","Poznań","Białystok","Wrocław","Katowice","Kraków"];
List <String> countryImages = [gdanskPic,warszawaPic,poznanPic,bialystokPic,wroclawPic,katowicePic,krakowPic];
Random random = new Random();
DateTime now = new DateTime.now();
Future.delayed(Duration(seconds: 5), (){
setState(() {
int randomCity = random.nextInt(countryNames.length);
int randomColor = random.nextInt(colors.length);
countrylist.add(Country(
countryNames[randomCity], countryImages[randomCity],
colors[randomColor], now.toString()));
});
});
}
In this code I am adding new element to my list view.
randomCity();
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
backgroundColor: Colors.grey[100],
elevation: 0.0,
title: Text(
"Random City App",
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
centerTitle: true,
actions: <Widget>[
IconButton(
icon: Icon(
Icons.add,
color: Colors.black,
size: 32,
),
onPressed: () {})
],
),
body: ListView.builder(
itemCount: countrylist.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => CountryDetails(countryName: countrylist[index].name,
appBarColor: countrylist[index].color, date: countrylist[index].date, image: countrylist[index].image,))
);
},
title: Text(countrylist[index].name + " ${countrylist[index].date}"),
tileColor: Color(countrylist[index].color),
),
);
},
));
}
And this is my ListView.Builder.
You have to convert your widget into StatefulWidget and then rebuild it with setState (more info on ways to manage state https://flutter.dev/docs/development/data-and-backend/state-mgmt/options)
class MyApp extends StatelessWidget { // your main widget
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: MyWidget(),
),
);
}
}
class MyWidget extends StatefulWidget { // create new StatefulWidget widget
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
List<Country> countrylist = []; // mover other variables in here
...
void randomCity(){
...
setState(() {}); // this will rebuild your widget again and again
}
#override
Widget build(BuildContext context) {
Future.delayed(Duration(seconds: 5), (){
randomCity();
});
return ListView.builder(
itemCount: countrylist.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {},
title: Text(countrylist[index]),
),
);
},
);
}
}
You have to tell the ListView to rebuild which you can do with the setState method (if you are using a StefulWidget). Also, I would use Timer instead of Future.delayed for periodic updates. Here would be a simplified example of your usecase:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Timer timer;
final countryNames = ['Germany', 'Croatia', 'Turkey', 'USA'];
List<String> countryList = [];
#override
void initState() {
Timer.periodic(Duration(seconds: 5), (timer) {
int randomCity = Random().nextInt(countryNames.length);
countryList.add(countryNames[randomCity]);
setState(() {});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List Updater'),
),
body: ListView.builder(
itemBuilder: (context, index) {
return Card(
child: Text(countryList[index]),
);
},
itemCount: countryList.length,
),
);
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
}

How to toggle boolean in listview item independently?

I am trying to change the color of a container with a button press. It looks at the boolean value and determines the color. How can I change the boolean for each individual listview item independently?
I am using provider for state management also. This is just an example of the type of thing I am trying to do.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
bool colorToggle = false;
return MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, i) {
return Container(
width: 100,
height: 100,
color: colorToggle ? Colors.blue : Colors.green,
child: GestureDetector(
onTap: () {
//how to change the colorToggle independendently with each listview item?
print('tapped');
},
child: Text('change color'),
),
);
},
),
),
);
}
}
You cannot achieve this with with a single boolean value.
You can use a List instead. Add the index of the item to the List once an item is tapped. Then check the List for the item's index to set color
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List toggled = List();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, i) {
return Container(
width: 100,
height: 100,
color: toggled.contains(i) ? Colors.blue : Colors.green,
child: GestureDetector(
onTap: () {
toggled.add(i);
setState(() {});
},
child: Text('change color'),
),
);
},
),
),
);
}
}

Animate elements in ListView on initialization

I want to achieve something like below (animation style doesn't matter, I'm looking for the way to do this)
However, all resources and question only explain how to create
item addition or removal animations.
My current code (I use BLoC pattern)
class _MembersPageState extends State<MembersPage> {
#override
Widget build(BuildContext context) {
return BlocProvider<MembersPageBloc>(
create: (context) =>
MembersPageBloc(userRepository: UserRepository.instance)..add(MembersPageShowed()),
child: BlocBuilder<MembersPageBloc, MembersPageState>(
builder: (context, state) {
if (state is MembersPageSuccess) {
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (context, index) {
User user = state.users[index];
return ListTile(
isThreeLine: true,
leading: Icon(Icons.person, size: 36),
title: Text(user.name),
subtitle: Text(user.username),
onTap: () => null,
);
},
);
} else
return Text("I don't care");
},
),
);
}
}
Widgets like AnimatedOpacity and AnimatedPositioned can be used to achieve this. However, lifecycle of the children widgets in a ListView is a bit complex. They get destroyed and recreated according to the scroll position. If the child widget has an animation that starts on initialization, it will reanimate whenever the child gets visible to the UI.
Here is my hacky solution. I used a static boolean to indicate whether it's the first time or recreation state and simply ignore the recration. You can copy and try it in Dartpad.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.brown,
body: ListView(
children: List.generate(
25,
(i) => AnimatedListItem(i, key: ValueKey<int>(i)),
),
),
),
);
}
}
class AnimatedListItem extends StatefulWidget {
final int index;
const AnimatedListItem(this.index, {Key? key}) : super(key: key);
#override
State<AnimatedListItem> createState() => _AnimatedListItemState();
}
class _AnimatedListItemState extends State<AnimatedListItem> {
bool _animate = false;
static bool _isStart = true;
#override
void initState() {
super.initState();
if (_isStart) {
Future.delayed(Duration(milliseconds: widget.index * 100), () {
setState(() {
_animate = true;
_isStart = false;
});
});
} else {
_animate = true;
}
}
#override
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: const Duration(milliseconds: 1000),
opacity: _animate ? 1 : 0,
curve: Curves.easeInOutQuart,
child: AnimatedPadding(
duration: const Duration(milliseconds: 1000),
padding: _animate
? const EdgeInsets.all(4.0)
: const EdgeInsets.only(top: 10),
child: Container(
constraints: const BoxConstraints.expand(height: 100),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.index.toString(),
style: const TextStyle(fontSize: 24),
),
),
),
),
),
);
}
}
I created a gist on dartpad shows how to add initial animation for ListView
The key point is to create an AnimationController for each list item, cache it, and clean up when animation is complete.