Issue in custom flutter search widget's search results selection - flutter web - flutter

I build a simple search widget for flutter web. Everything working fine but after I got the search results, I have to click twice on the result to select a specific search result. Please help me to figure out the problem. I tried for several day but no luck. I'm using flutter 2.5.2 version.
darpad link to run the code
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(MaterialApp(
home: SearchView(),
));
}
class SearchView extends StatefulWidget {
#override
State<SearchView> createState() => _SearchViewState();
}
class _SearchViewState extends State<SearchView> {
String searchResult = '';
final textController = TextEditingController();
final List<String> data = ['Result 1', 'Result 2', 'Result 3', 'Result 4'];
Future<List<String>> loadData() {
return Future.delayed(Duration(seconds: 1), () {
if (this.textController.text.trim().length != 0) {
return data;
} else {
return [];
}
});
}
#override
void initState() {
this.textController.addListener(this._onTextChanged);
super.initState();
}
void _onTextChanged() {
print('text cahnged');
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Brand'),
),
body: SingleChildScrollView(
child: Column(
children: [
TextFormField(
controller: this.textController,
),
FutureBuilder(
future: loadData(),
builder:
(BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
print("future build");
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (String result in snapshot.data)
InkWell(
onTap: () {
print('Clicked');
setState(() {
this.textController.clear();
this.searchResult = result;
});
},
child: Text(result),
),
],
);
} else {
return CircularProgressIndicator();
}
},
),
Text('Search result is ${searchResult}'),
],
),
),
);
}
}
Please help me to fix this issue. Thank you and have a nice day

This weird behavior happens because of a flutter issue. Before flutter version 2.5, Text change listeners only listen for text changes. But from version 2.5, listeners also listen for focus change events. Need to use onChanged method in TextFormField until flutter fix the issue.

Related

Waiting for Async Data in InitState

I need to get data from a Future to a Stateful widget before it displays on startup. I have tried async/await, FutureBuilder, and the Sync package implementing a WaitGroup within the initState method; however, nothing I do waits for the data to return from the Future before it renders the screen.
In the below examples, I have a simple String strName that I initialize to "Default Name" that I am using for testing and displaying in the Scaffold. It only displays the initialized "Default Name," and not the name returned from the Future. The closest I got was using a FutureBuilder, at least it updated the screen after the initialized "Default Name" was shown. However, I need to get the data prior to the screen rendering. Does anyone have any ideas?
Here's an example of what I tried with Sync WaitGroup:
class _MyHomePageState extends State<MyHomePage> {
String strName = "Default Name";
Future<String> _getName() async {
var name = await Future<String>.delayed(const Duration(seconds: 5), () => "New Name");
return name;
}
#override
void initState() {
WaitGroup wg = WaitGroup();
wg.add(1);
Future<String> futureName = _getName();
futureName.then(
(value) {
strName = value;
wg.done();
},
);
wg.wait();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(strName),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
strName,
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
}
This is what my async/await method looked like:
class _MyHomePageState extends State<MyHomePage> {
String strName = "Default Name";
Future<String> _getName() async {
var name = await Future<String>.delayed(const Duration(seconds: 5), () => "Jimbo");
return name;
}
#override
void initState() {
Future<String> futureName = _getName();
futureName.then(
(value) {
strName = value;
},
);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(strName),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
strName,
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
}
I've never worked with a language where there asynchronous is the default structure of so many parts. How do you deal with making async synchronous in Dart? I haven't even got into the SQLite and HTTP part of it, and it is killing me. I've been at it for four days and got so frustrated I almost broke a keyboard yesterday.
The best is to use a loading screen while fetching your data
and use snapshot.data
full implementation using FutureBuilder:
class _MyHomePageState extends State<MyHomePage> {
String strName = "Default Name";
Future<String> _getName() async {
var name = await Future<String>.delayed(
const Duration(seconds: 5), () => "New Name");
return name;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(strName),
),
body: FutureBuilder<String>(
future: _getName(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
snapshot.data!,
style: Theme.of(context).textTheme.headline4,
),
],
);
}
return Center(
child: CircularProgressIndicator(),
);
}),
);
}
}
This is really a bad practice
but if you really need to resolve some future data before the app renders you can use the void main() method.
void main()async {
Future<String> futureName = _getName();
futureName.then(
(value) {
strName = value;
runApp(MyApp());
},
);
}

Rebuild widget inside stack using bloc?

Is it possible to rebuild widget inside a stack using BlocBuilder? I tried a simple code below but it only rebuild the widget once. I want to rebuild the widget inside the stack which is the GoogleMapDetailsList and pass the search value. GoogleMapDetailsList also has different bloc that triggers in its InitState.
Column(
children: [
searchWidget(context),
Expanded(
child: Stack(
children: [
googleMap(),
BlocBuilder<GoogleMapSearchBloc, GoogleMapSearchState>(
builder: (context, state) {
if (state is GoogleMapSearchInProgressState) {
return GoogleMapDetailsList(
search: search,
goToPlaceCallback: _goToPlace,
);
} else if (state is GoogleMapSearchLoadInProgressState) {
return CircularProgressIndicator();
} else {
return Container();
}
},
),
],
),
),
],
),
Row searchWidget(BuildContext context) {
return Row(
children: [
ButtonTemplate(
textPaddingInset: EdgeInsets.zero,
text: 'Search',
buttonColor: Theme.of(context).accentColor,
onPressed: () {
setState(() {
searchResult = true;
});
BlocProvider.of<GoogleMapSearchBloc>(context)
.add(StartSearchEvent());
})
],
}
//BLOC
class GoogleMapSearchBloc
extends Bloc<GoogleMapSearchEvent, GoogleMapSearchState> {
GoogleMapSearchBloc() : super(GoogleMapSearchInitialState());
#override
Stream<GoogleMapSearchState> mapEventToState(
GoogleMapSearchEvent event,
) async* {
if (event is StartSearchEvent) {
yield GoogleMapSearchLoadInProgressState();
Future.delayed(const Duration(seconds: 5), () {});
yield GoogleMapSearchInProgressState();
}
}
}
I solved it by overriding didUpdateWidget() in GoogleMapDetailsList. I saw it in this answer. I also want to know if there is another way of doing this.
#override
void didUpdateWidget(GoogleMapDetailsList oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.search != widget.search) {
BlocProvider.of<GoogleMapPlaceSearchBloc>(context)
.add(RefreshGoogleMapPlaceSearchEvent());
}
}
The app gets notified only once in this case. To subscribe for further changes use listen:true.
BlocProvider.of<GoogleMapSearchBloc>(context, listen:true)
.add(StartSearchEvent());

How to automatically scroll through all the ListTiles in the Listview.seperated in Flutter?

Scroll automatically (without any user interaction) through all the ListTiles in the Listview using a Timer in flutter. The below method makes only one ListTile to animate but I want to animate all the ListTiles from top to bottom one by one and again from bottom to top one by one.
The below is the Listview:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: FutureBuilder(
future: fetchNews(),
builder: (context, snap) {
if (snap.hasData) {
news = snap.data;
return ListView.separated(
//controller: _controller,
scrollDirection: scrollDirection,
controller: controller,
itemBuilder: (context, i) {
final NewsModel _item = news[i];
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
index: i,
child: ListTile(
title: Text('${_item.title}'),
subtitle: Text(
'${_item.description}',
// maxLines: 1,
//overflow: TextOverflow.ellipsis,
),
),
);
},
separatorBuilder: (context, i) => Divider(),
itemCount: news.length,
);
} else if (snap.hasError) {
return Center(
child: Text(snap.error.toString()),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
),
);
}
}
This is the automatic scrolling i have tried:
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 2), (Timer t) async {
await controller.scrollToIndex(1,
preferPosition: AutoScrollPosition.begin);
});
Here is a solution assuming that all your items in the ListView have the same itemExtent.
In this solution, I highlight the current Item as selected. You could also want to stop autoscrolling as soon as you reach the bottom of the list.
Full source code
import 'dart:async';
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part '66455867.auto_scroll.freezed.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
Future<List<News>> _fetchNews() async => dummyData;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('News')),
body: FutureBuilder(
future: _fetchNews(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return NewsList(newsList: snapshot.data);
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
} else {
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
class NewsList extends StatefulWidget {
final List<News> newsList;
const NewsList({
Key key,
this.newsList,
}) : super(key: key);
#override
_NewsListState createState() => _NewsListState();
}
class _NewsListState extends State<NewsList> {
ScrollController _scrollController = ScrollController();
Timer _timer;
double _itemExtent = 100.0;
Duration _scrollDuration = Duration(milliseconds: 300);
Curve _scrollCurve = Curves.easeInOut;
int _autoScrollIncrement = 1;
int _currentScrollIndex = 0;
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
_autoScrollIncrement = _currentScrollIndex == 0
? 1
: _currentScrollIndex == widget.newsList.length - 1
? -1
: _autoScrollIncrement;
_currentScrollIndex += _autoScrollIncrement;
_animateToIndex(_currentScrollIndex);
setState(() {});
});
}
void _animateToIndex(int index) {
_scrollController.animateTo(
index * _itemExtent,
duration: _scrollDuration,
curve: _scrollCurve,
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ListView(
controller: _scrollController,
itemExtent: _itemExtent,
children: widget.newsList
.map((news) => ListTile(
title: Text(news.title),
subtitle: Text(
news.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
selected: widget.newsList[_currentScrollIndex].id == news.id,
selectedTileColor: Colors.amber.shade100,
))
.toList(),
);
}
}
#freezed
abstract class News with _$News {
const factory News({int id, String title, String description}) = _News;
}
final faker = Faker();
final dummyData = List.generate(
10,
(index) => News(
id: faker.randomGenerator.integer(99999999),
title: faker.sport.name(),
description: faker.lorem.sentence(),
),
);
Packages used in the solution:
freeze for the News Domain Class
build_runner to generate the freezed code
faker to generate the list of random news
UPDATE : Scroll only once
To stop the autoscrolling at the bottom of the listview, you just need to modify the initState method:
int _currentScrollIndex;
News _selectedNews;
#override
void initState() {
super.initState();
_currentScrollIndex = -1;
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
setState(() {
if (_currentScrollIndex == widget.newsList.length - 1) {
_timer.cancel();
_selectedNews = null;
} else {
_selectedNews = widget.newsList[++_currentScrollIndex];
_animateToIndex(_currentScrollIndex);
}
});
});
}
We don't need the scroll direction defined as _autoScrollIncrement. However, I would introduce a new _selectedNews to easily unselect the last News item when we arrive at the bottom of the list. The selected flag of our ListTile would then become:
#override
Widget build(BuildContext context) {
return ListView(
[...]
children: widget.newsList
.map((news) => ListTile(
[...]
selected: _selectedNews?.id == news.id,
[...]
))
.toList(),
);
}

How to get value from an object which in the state (flutter_bloc)

in builder method I reach the value of state like
return BlocBuilder<UsersOwnProfileBloc, UsersOwnProfileState>(
cubit: widget.bloc,
builder: (context, state) {
if (state is FetchedUserSettingState) {
bool account = state.userSettings.publicAccount
}
But I need to get the values from initState. I need to set the values of the widget. I tried something like this but I got error
#override
void initState() {
super.initState();
UsersOwnProfileState state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
}
Can anyone show me how to get state value in initState?
class UserSettingPage extends StatefulWidget {
final UsersOwnProfileBloc bloc;
const UserSettingPage({Key key, this.bloc}) : super(key: key);
#override
_UserSettingPageState createState() => _UserSettingPageState();
}
class _UserSettingPageState extends State<UserSettingPage> {
bool newListingAlert;
bool listingForSearchAlert;
bool searchForListingAlert;
bool followAlert;
bool publicAccount;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
});
}
#override
Widget build(BuildContext context) {
return BlocBuilder<UsersOwnProfileBloc, UsersOwnProfileState>(
cubit: widget.bloc,
builder: (context, state) {
if (state is FetchedUserSettingState) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(25.h),
child: ListingEditAppBar(
onCancel: () {
widget.bloc.add(FetchUserEvent(userId: CurrentUser.currentUser.id));
Navigator.pop(context);
},
),
),
body: Column(
children: [
PageTitle(title: "Preferences"),
Expanded(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Profilim herkese açık"),
Switch(
onChanged: (value) {
setState(() {
publicAccount = value;
});
},
value: publicAccount,
)
],
),
)
],
),
)
],
),
);
}
return MyProgressIndicator();
},
);
}
}
I have added the whole code. I am getting the following error.
Failed assertion: boolean expression must not be null
The relevant error-causing widget was
Switch
If you would like to access the state within initState you will need to use WidgetsBinding to access this. However, using this ensures that your widget is built and then triggers the method to get the value. It will be faster to just use the BlocBuilder, Watch, or Select to get the value you are looking for.
But to answer your question, you can do the following
WidgetsBinding.instance.addPostFrameCallback((_) {
final state = BlocProvider.of<UsersOwnProfileBloc>(context).state;
if (state is FetchedUserSettingState) {
publicAccount = state.userSettings.publicAccount;
}
});

Is there an equivalent widget in flutter to the "select multiple" element in HTML

I am searching for a widget in flutter that is equal to
<select multiple=""></select>
in flutter.
An example implementation (for the web) is MaterializeCSS Select Multiple
As seen above I should be able to provide a list of items (with some of them preselected) and at the end retrieve a list of selected items or a map or something else.
An example implementation or a link to a documentation is very appreciated.
I don't think that a widget like that currently exists in Flutter, but you can build one yourself.
On mobile phones with limited screen space it would probably make sense to display a dialog with a submit button, like this native Android dialog.
Here is a rough sketch how to implement such a dialog in less than 100 lines of code:
class MultiSelectDialogItem<V> {
const MultiSelectDialogItem(this.value, this.label);
final V value;
final String label;
}
class MultiSelectDialog<V> extends StatefulWidget {
MultiSelectDialog({Key key, this.items, this.initialSelectedValues}) : super(key: key);
final List<MultiSelectDialogItem<V>> items;
final Set<V> initialSelectedValues;
#override
State<StatefulWidget> createState() => _MultiSelectDialogState<V>();
}
class _MultiSelectDialogState<V> extends State<MultiSelectDialog<V>> {
final _selectedValues = Set<V>();
void initState() {
super.initState();
if (widget.initialSelectedValues != null) {
_selectedValues.addAll(widget.initialSelectedValues);
}
}
void _onItemCheckedChange(V itemValue, bool checked) {
setState(() {
if (checked) {
_selectedValues.add(itemValue);
} else {
_selectedValues.remove(itemValue);
}
});
}
void _onCancelTap() {
Navigator.pop(context);
}
void _onSubmitTap() {
Navigator.pop(context, _selectedValues);
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Select animals'),
contentPadding: EdgeInsets.only(top: 12.0),
content: SingleChildScrollView(
child: ListTileTheme(
contentPadding: EdgeInsets.fromLTRB(14.0, 0.0, 24.0, 0.0),
child: ListBody(
children: widget.items.map(_buildItem).toList(),
),
),
),
actions: <Widget>[
FlatButton(
child: Text('CANCEL'),
onPressed: _onCancelTap,
),
FlatButton(
child: Text('OK'),
onPressed: _onSubmitTap,
)
],
);
}
Widget _buildItem(MultiSelectDialogItem<V> item) {
final checked = _selectedValues.contains(item.value);
return CheckboxListTile(
value: checked,
title: Text(item.label),
controlAffinity: ListTileControlAffinity.leading,
onChanged: (checked) => _onItemCheckedChange(item.value, checked),
);
}
}
You can use it like this:
void _showMultiSelect(BuildContext context) async {
final items = <MultiSelectDialogItem<int>>[
MultiSelectDialogItem(1, 'Dog'),
MultiSelectDialogItem(2, 'Cat'),
MultiSelectDialogItem(3, 'Mouse'),
];
final selectedValues = await showDialog<Set<int>>(
context: context,
builder: (BuildContext context) {
return MultiSelectDialog(
items: items,
initialSelectedValues: [1, 3].toSet(),
);
},
);
print(selectedValues);
}
Is this what you want?
In case you need a short and ready to use code, follow this article
import 'package:flutter/material.dart';
import 'package:multiple_selection_dialogue_app/widgets/multi_select_dialog.dart';
/// A demo page that displays an [ElevatedButton]
class DemoPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
/// Stores the selected flavours
List<String> flavours = [];
return ElevatedButton(
child: Text('Flavours'),
onPressed: () async {
flavours = await showDialog<List<String>>(
context: context,
builder: (_) => MultiSelectDialog(
question: Text('Select Your Flavours'),
answers: [
'Chocolate',
'Caramel',
'Vanilla',
'Peanut Butter'
])) ??
[];
print(flavours);
// Logic to save selected flavours in the database
});
}
}
import 'package:flutter/material.dart';
/// A Custom Dialog that displays a single question & list of answers.
class MultiSelectDialog extends StatelessWidget {
/// List to display the answer.
final List<String> answers;
/// Widget to display the question.
final Widget question;
/// List to hold the selected answer
/// i.e. ['a'] or ['a','b'] or ['a','b','c'] etc.
final List<String> selectedItems = [];
/// Map that holds selected option with a boolean value
/// i.e. { 'a' : false}.
static Map<String, bool> mappedItem;
MultiSelectDialog({this.answers, this.question});
/// Function that converts the list answer to a map.
Map<String, bool> initMap() {
return mappedItem = Map.fromIterable(answers,
key: (k) => k.toString(),
value: (v) {
if (v != true && v != false)
return false;
else
return v as bool;
});
}
#override
Widget build(BuildContext context) {
if (mappedItem == null) {
initMap();
}
return SimpleDialog(
title: question,
children: [
...mappedItem.keys.map((String key) {
return StatefulBuilder(
builder: (_, StateSetter setState) => CheckboxListTile(
title: Text(key), // Displays the option
value: mappedItem[key], // Displays checked or unchecked value
controlAffinity: ListTileControlAffinity.platform,
onChanged: (value) => setState(() => mappedItem[key] = value)),
);
}).toList(),
Align(
alignment: Alignment.center,
child: ElevatedButton(
style: ButtonStyle(visualDensity: VisualDensity.comfortable),
child: Text('Submit'),
onPressed: () {
// Clear the list
selectedItems.clear();
// Traverse each map entry
mappedItem.forEach((key, value) {
if (value == true) {
selectedItems.add(key);
}
});
// Close the Dialog & return selectedItems
Navigator.pop(context, selectedItems);
}))
],
);
}
}
import 'package:flutter/material.dart';
import 'package:multiple_selection_dialogue_app/pages/demo_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: DemoPage(),
),
),
);
}
}