Filter in Flutter (Dart) - flutter

I want to search list item.
for example, Item name is "Iphone 7 Plus"
when i type iphone plus it shows empty Result but when i type iphone 7 it gives me that item. Can anyone help me how i get result on iphone plus
I am using this method:
List _getSuggestions(String query) {
List matches = [];
matches.addAll(searchItemList);
matches.retainWhere((s) =>
s.ItemName!.toLowerCase().contains(query.toLowerCase()) ||
s.ItemCode!.toLowerCase().contains(query.toLowerCase()));
return matches;
}

You have to split your query string to do want you want.
Check this code :
void main(List<String> args) {
final data = 'iPhone 7 plus';
var search = 'iphone plus 7';
var match = true;
for (var element in search.split(' ')) {
match = match && data.toLowerCase().contains(element.toLowerCase());
}
print('match = $match');
}

The logic will be
List<Item> _getSuggestions(String query) {
matches.clear();
matches = searchItemList.where((e) {
return e.code.toLowerCase().contains(query.toLowerCase()) ||
e.name.toLowerCase().contains(query.toLowerCase());
}).toList();
return matches;
}
And show all item on empty list
onChanged: (value) {
final resultSet = _getSuggestions(value);
matches = resultSet.isEmpty ? searchItemList : resultSet;
setState(() {});
},
Play with the widget.
class TestA extends StatefulWidget {
const TestA({Key? key}) : super(key: key);
#override
State<TestA> createState() => _TestAState();
}
class Item {
final String name;
final String code;
Item({
required this.name,
required this.code,
});
}
class _TestAState extends State<TestA> {
final List<Item> searchItemList =
List.generate(44, (index) => Item(name: "$index", code: "code $index"));
List<Item> matches = [];
List<Item> _getSuggestions(String query) {
matches.clear();
matches = searchItemList.where((e) {
return e.code.toLowerCase().contains(query.toLowerCase()) ||
e.name.toLowerCase().contains(query.toLowerCase());
}).toList();
return matches;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
TextField(
onChanged: (value) {
final resultSet = _getSuggestions(value);
matches = resultSet.isEmpty ? searchItemList : resultSet;
setState(() {});
},
),
Expanded(
child: ListView.builder(
itemCount: matches.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(matches[index].name),
);
},
))
],
),
);
}
}

Related

How to solve range error in growable list?

When I am calling getUserById() function everything works fine and the growable string works fine but when I use the same variable inside a text widget then it shows RangeError.
The problem is occuring only when I try to use a growable list of string named as dataAsString inside the build.
Here is the Code with Error. Go to the scaffold where I have commented the line of error
import 'package:Healthwise/helpers/user.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../helpers/backEnd.dart';
import '../helpers/frontEnd.dart';
class ResultPage extends StatefulWidget {
const ResultPage({Key? key}) : super(key: key);
#override
State<ResultPage> createState() => _ResultPageState();
}
class _ResultPageState extends State<ResultPage> {
// String docId = '';
String objectToString = '';
String e = '';
//This variable is creating the problem...
// I have tried this by using a final instead of var, but nothing worked.
var dataAsString = <String>[];
#override
void initState() {
super.initState();
// getUsers();
getUserById();
}
getUserById() {
final String id = itemName;
userRef.doc(id).get().then((DocumentSnapshot doc) {
// final x = doc.data();
// docId= doc.id;
objectToString = doc.data().toString();
String temp = '';
// print(doc.data());
// print(doc.id);
int i = 1;
// int j = 0;
bool end = false;
//We are just parsing the object into string.
while (objectToString[i] != '}') {
if (objectToString[i - 1] == ' ' && objectToString[i - 2] == ':') {
while (objectToString[i] != ',' && end != true) {
// print(z[i]);
temp += objectToString[i];
if (objectToString[i + 1] != '}') {
i++;
} else {
end = true;
}
}
//Here I add all the strings to list...
// This line works fine.
dataAsString.add(temp);
temp = '';
// j++;
print("The code below this line prints everything perfectly");
print(dataAsString.length);
print(dataAsString[0]);
}
i++;
}
// print(dataAsString[0]);
// print(dataAsString[1]);
// print("+++++++++++");
// print(dataAsString[2]);
// for (var k in dataAsString) {
// print(k);
// }
// print(dataAsString);
// setState(() {});
});
}
// getUsers() {
// userRef.get().then((QuerySnapshot snapshot) {
// snapshot.docs.forEach((DocumentSnapshot doc) {
// print(doc.data());
// print(doc.id);
// print(doc.exists);
// });
// });
// }
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: primary_color, title: Text("Apple")),
body: Container(
child: Column(children: [
ListTile(
//The problem arises here and the app do not crash if I run the same code with just a //string in place of dataAsString[0]
title: Text(dataAsString[0]),
),
ListTile(
title: Text(dataAsString[1]),
),
ListTile(
title: Text(dataAsString[2]),
),
]),
),
);
}
}
You cant have data on dataAsString because you are using future(.then) on fetching data. You can do value check like
Text( dataAsString.isNotEmpty? dataAsString[0]: "0 is empty"),
Text( dataAsString.length>2? dataAsString[1]: "1 index is empty"),
Text( dataAsString.length>3? dataAsString[2]: "3rd item is empty"),
It's cause your dataAsString ins't ready on first Flutter rendered frame. So, you need to wait for the getUserById finish to access the dataAsString values.
You can use a builder to check if the data is ready in a readable way.
Builder(
builder: (context) {
if (dataAsString.length >= 2) {
return Column(children: [
ListTile(
title: Text(dataAsString[0]),
),
ListTile(
title: Text(dataAsString[1]),
),
ListTile(
title: Text(dataAsString[2]),
),
]);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
Warning: it'll work cause you're calling setState when you finish the async task.

Unexpected end of input (at character 1)

I have json file with the following data in it:
{
"item1": "value1",
"item2": "value2",
"item3": "value3"
}
I also have Items() class in a seperate file which has the method getItems() method which loads the json file:
class Items {
Future<Map<String, dynamic>> getItems() async {
String jsonData =
await rootBundle.loadString('assets/items.json');
Map<String, dynamic> data = jsonDecode(jsonData);
return data;
}
}
I also have a scaffold to show the items with ListView.builder. I first use setState to assign the returned value of Items().getItems() to the field items. I then use the value of the field inside the LisView.builder
class ItemList extends StatefulWidget {
const ItemList({Key? key}) : super(key: key);
#override
_ItemListState createState() => _ItemListState();
}
class _ItemListState extends State<ItemList> {
late String items = '';
setItems() async {
final item = await Items().getItems();
setState(() {
items = jsonEncode(item);
});
}
#override
initState() {
setItems();
}
#override
Widget build(BuildContext context) {
Map<String, dynamic> data = jsonDecode(items);
debugPrint(data.toString());
debugPrint(items);
return Scaffold(
body: ListView.builder(
itemCount: data.keys.length,
itemBuilder: (c, index) {
return ListTile(
title: Text("key " + data.keys.toList()[index]),
subtitle: Text("value " + data.values.toList()[index]),
);
},
),
);
}
}
I am able to show the list of items on the Scaffold() but i still get the error: Unexpected end of input (at character 1)
When i click on the error it highlights on the jsonDecode(items). So something goes wrong there but i don't know what.
Try to simplify your code like this
class _ItemListState extends State<ItemList> {
Map<String, dynamic> data = {};
setItems() async {
await Items().getItems().then((value) => setState(() => data = value));
}
#override
initState() {
setItems();
}
#override
Widget build(BuildContext context) {
debugPrint(data);
//rest of code ...
}
and check if you still get the error.

Flutter: How to change state of sibling widget widget?

I have already tried the solution here. My code already depended on a class passed on a parent class.
import 'package:flutter/cupertino.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../widgets/cupertino_modal_button_row.dart';
import '../widgets/simple_widgets.dart';
import '../models/match.dart';
class AddScoreSection extends StatefulWidget {
AddScoreSection({
this.set,
this.partnerIds,
Key key,
}) : super(key: key);
final MatchSet set;
final List<String> partnerIds;
#override
_AddScoresState createState() => _AddScoresState();
}
class _AddScoresState extends State<AddScoreSection> {
String _partnerText;
#override
Widget build(BuildContext context) {
final set = widget.set;
final _hostId = FirebaseAuth.instance.currentUser.uid;
String _partnerId = set.hostTeam != null
? set.hostTeam.firstWhere(
(element) => element != FirebaseAuth.instance.currentUser.uid)
: null;
Future<String> _partnerName(String partnerId) async {
if (partnerId == null) {
return null;
}
final userData = await FirebaseFirestore.instance
.collection('users')
.doc(partnerId)
.get();
return userData['name']['full_name'];
}
print(widget.set.visitingGames);
return CupertinoFormSection(
header: const Text('Set'),
children: [
CupertinoModalButtonRow(
builder: (context) {
return CupertinoActionSheet(
title: _partnerText == null
? const Text('Select a Partner')
: _partnerText,
actions: widget.partnerIds
.map((partner) => CupertinoActionSheetAction(
child: FutureBuilder<String>(
future: _partnerName(partner),
builder: (ctx, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return SimpleWidget.loading10;
}
return Text(snapshot.data);
},
),
onPressed: () async {
final partnerTemp = await _partnerName(partner);
_partnerId = partner;
setState(() {
_partnerText = partnerTemp;
});
set.addToHostTeam = [_partnerId, _hostId];
set.addToVisitTeam = widget.partnerIds
.where((element) =>
widget.set.hostTeam.contains(element))
.toList();
print(set.hostTeam);
Navigator.of(context).pop();
},
))
.toList());
},
buttonChild:
_partnerText == null ? 'Select your Partner' : _partnerText,
prefix: 'Your Partner'),
ScoreEntryRow(
setsData: widget.set,
prefix: 'Team Score',
),
ScoreEntryRow(
prefix: 'Opponent Score',
setsData: widget.set,
isHostMode: false,
),
],
);
}
}
class ScoreEntryRow extends StatefulWidget {
ScoreEntryRow({
Key key,
#required this.setsData,
#required this.prefix,
this.isHostMode: true,
}) : super(key: key);
final MatchSet setsData;
final String prefix;
final bool isHostMode;
#override
_ScoreEntryRowState createState() => _ScoreEntryRowState();
}
class _ScoreEntryRowState extends State<ScoreEntryRow> {
#override
Widget build(BuildContext context) {
final ValueNotifier _games = widget.isHostMode
? ValueNotifier<int>(widget.setsData.hostGames)
: ValueNotifier<int>(widget.setsData.visitingGames);
List<int> scoreList = List.generate(9, (index) => index + 1);
return CupertinoModalButtonRow(
builder: (context) {
return SizedBox(
height: 300,
child: ListView.builder(
itemCount: scoreList.length,
itemBuilder: (context, i) {
return CupertinoButton(
child: Text(scoreList[i].toString()),
onPressed: () {
setState(() {
if (widget.isHostMode) {
widget.setsData.setHostGames = scoreList[i];
widget.setsData.setVisitGames = 9 - scoreList[i];
return;
}
widget.setsData.setVisitGames = scoreList[i];
widget.setsData.setHostGames = 9 - scoreList[i];
});
});
}),
);
},
buttonChild: widget.isHostMode
? widget.setsData.hostGames == null
? '0'
: widget.setsData.hostGames.toString()
: widget.setsData.visitingGames == null
? '0'
: widget.setsData.visitingGames.toString(),
prefix: widget.prefix,
);
}
}
The code for the custom class is:
class MatchSet {
MatchSet(this.setData);
final Map<String, dynamic> setData;
List<String> hostTeam;
List<String> visitingTeam;
int hostGames;
int visitingGames;
set addToHostTeam(List<String> value) {
if (setData.values.every((element) => element != null))
hostTeam = setData['host_team'];
hostTeam = value;
}
set addToVisitTeam(List<String> value) {
if (setData.values.every((element) => element != null))
visitingTeam = setData['visiting_team'];
visitingTeam = value;
}
set setHostGames(int value) {
if (setData.values.every((element) => element != null))
hostGames = setData['host_games'];
hostGames = value;
}
set setVisitGames(int value) {
if (setData.values.every((element) => element != null))
visitingGames = setData['visiting_games'];
visitingGames = value;
}
Map<List<String>, int> get matchResults {
return {
hostTeam: hostGames,
visitingTeam: visitingGames,
};
}
List<String> get winningTeam {
if (matchResults.values.every((element) => element != null)) {
return null;
}
return matchResults[hostTeam] > matchResults[visitingTeam]
? hostTeam
: visitingTeam;
}
String result(String userId) {
if (matchResults.values.every((element) => element != null)) {
return null;
}
if (winningTeam.contains(userId)) {
return 'W';
}
return 'L';
}
bool get isUpdated {
return hostGames != null && visitingGames != null;
}
}
The state does not change for the sibling as you can see in the video. I have also trying the whole button in a ValueListenableBuilder, nothing changes.
What am I doing wrong? By all my knowledge the sibling row widget should rebuild, but it is not rebuilding.
You can use multiple approaches
the solution that you point out is a callback function and it works fine ... you use a function that when it is called it would go back to its parent widget and do some stuff there(in your case setState)
but It does not seem that you are using this it.
the other one is using StateManagement. you can use GetX , Provider, RiverPod,... .
in this one, you can only rebuild the widget that you want but in the first solution, you have to rebuild the whole parent widget.
both solutions are good the second one is more professional and more efficient but i think the first one is a must-know too

Flutter Provider: How to notify a model that a change happened on a model it contains?

I'm starting to learn Flutter/Dart by building a simple Todo app using Provider, and I've run into a state management issue. To be clear, the code I've written works, but it seems... wrong. I can't find any examples that resemble my case enough for me to understand what the correct way to approach the issue is.
This is what the app looks like
It's a grocery list divided by sections ("Frozen", "Fruits and Veggies"). Every section has multiple items, and displays a "x of y completed" progress indicator. Every item "completes" when it is pressed.
TheGroceryItemModel looks like this:
class GroceryItemModel extends ChangeNotifier {
final String name;
bool _completed = false;
GroceryItemModel(this.name);
bool get completed => _completed;
void complete() {
_completed = true;
notifyListeners();
}
}
And I use it in the GroceryItem widget like so:
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
GroceryItem(this.model);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryItemModel>(builder: (context, groceryItem, child) {
return ListTile(
title: Text(groceryItem.name),
leading: groceryItem.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked)
onTap: () => groceryItem.complete();
})
);
}
}
The next step I want is to include multiple items in a section, which tracks completeness based on how many items are completed.
The GroceryListSectionModel looks like this:
class GroceryListSectionModel extends ChangeNotifier {
final String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
// THIS RIGHT HERE IS WHERE IT GETS WEIRD
items.forEach((item) {
item.addListener(notifyListeners);
});
// END WEIRD
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
}
And I use it in the GroceryListSection widget like so:
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
final ValueChanged<bool> onChanged;
GroceryListSection(this.model, this.onChanged);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.map((groceryItemModel) =>
GroceryItem(groceryItemModel)).toList()
)
);
}
)
);
}
}
The Problems:
It seems weird to have a ChangeNotifierProvider and a Consumer in both Widgets. None of the examples I've seen do that.
It's definitely wrong to have the GroceryListSectionModel listening to changes on all the GroceryItemModels for changes to propagate back up the tree. I don't see how that can scale right.
Any suggestions? Thanks!
this ist not a nested Provider, but i think in your example it is the better way..
only one ChangeNotifierProvider per section ("Frozen", "Fruits and Veggies") is defined
the complete() function from a ItemModel is in the GroceryListSectionModel() and with the parameter from the current List Index
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
// final ValueChanged<bool> onChanged;
GroceryListSection(this.model);
#override
Widget build(BuildContext context) {
return new ChangeNotifierProvider<GroceryListSectionModel>(
create: (context) => GroceryListSectionModel(model.name, model.items),
child: new Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.asMap().map((i, groceryItemModel) => MapEntry(i, GroceryItem(groceryItemModel, i))).values.toList()
)
);
}
)
);
}
}
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
final int index;
GroceryItem(this.model, this.index);
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(model.name),
leading: model.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked),
onTap: () => Provider.of<GroceryListSectionModel>(context, listen: false).complete(index),
);
}
}
class GroceryListSectionModel extends ChangeNotifier {
String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
// complete Void with index from List items
void complete(int index) {
this.items[index].completed = true;
notifyListeners();
}
}
// normal Model without ChangeNotifier
class GroceryItemModel {
final String name;
bool completed = false;
GroceryItemModel({this.name, completed}) {
this.completed = completed == null ? false : completed;
}
}

Loading data on init with BLoC

New to Flutter with BLoC. Building off of a search template, looking to have data (items) load on app load instead of on state change.
The method getCrystals() returns the correct data when the search intent .isEmpty but how can it be done on app load?
crystal_repo.dart
abstract class CrystalRepo {
Future<BuiltList<Crystal>> getCrystals();
Future<BuiltList<Crystal>> searchCrystal({
#required String query,
int startIndex: 0,
});
}
crystal_repo_impl.dart
class CrystalRepoImpl implements CrystalRepo {
static const _timeoutInMilliseconds = 120000; // 2 minutes
final Map<String, Tuple2<int, CrystalResponse>> _cached = {};
///
final CrystalApi _api;
final Mappers _mappers;
CrystalRepoImpl(this._api, this._mappers);
#override
Future<BuiltList<Crystal>> searchCrystal({
String query,
int startIndex = 0,
}) async {
assert(query != null);
final crystalsResponse = await _api.searchCrystal(
query: query,
startIndex: startIndex,
);
final crystal = crystalsResponse.map(_mappers.crystalResponseToDomain);
return BuiltList<Crystal>.of(crystal);
}
#override
Future<BuiltList<Crystal>> getCrystals() async {
final crystalsResponse = await _api.getCrystals();
final crystal = crystalsResponse.map(_mappers.crystalResponseToDomain);
return BuiltList<Crystal>.of(crystal);
}
}
search_bloc.dart
class SearchBloc implements BaseBloc {
/// Input [Function]s
final void Function(String) changeQuery;
final void Function() loadNextPage;
final void Function() retryNextPage;
final void Function() retryFirstPage;
final void Function(String) toggleFavorited;
/// Ouput [Stream]s
final ValueStream<SearchPageState> state$;
final ValueStream<int> favoriteCount$;
/// Subscribe to this stream to show message like snackbar, toast, ...
final Stream<SearchPageMessage> message$;
/// Clean up resource
final void Function() _dispose;
SearchBloc._(
this.changeQuery,
this.loadNextPage,
this.state$,
this._dispose,
this.retryNextPage,
this.retryFirstPage,
this.toggleFavorited,
this.message$,
this.favoriteCount$,
);
#override
void dispose() => _dispose();
factory SearchBloc(final CrystalRepo crystalRepo, final FavoritedCrystalsRepo favCrystalsRepo,){
assert(crystalRepo != null);
assert(favCrystalsRepo != null);
/// Stream controllers, receive input intents
final queryController = PublishSubject<String>();
final loadNextPageController = PublishSubject<void>();
final retryNextPageController = PublishSubject<void>();
final retryFirstPageController = PublishSubject<void>();
final toggleFavoritedController = PublishSubject<String>();
final controllers = [
queryController,
loadNextPageController,
retryNextPageController,
retryFirstPageController,
toggleFavoritedController,
];
/// Debounce query stream
final searchString$ = queryController
.debounceTime(const Duration(milliseconds: 300))
.distinct()
.map((s) => s.trim());
/// Search intent
final searchIntent$ = searchString$.mergeWith([
retryFirstPageController.withLatestFrom(
searchString$,
(_, String query) => query,
)
]).map((s) => SearchIntent.searchIntent(search: s));
/// Forward declare to [loadNextPageIntent] can access latest state via [DistinctValueConnectableStream.value] getter
DistinctValueConnectableStream<SearchPageState> state$;
/// Load next page intent
final loadAndRetryNextPageIntent$ = Rx.merge(
[
loadNextPageController.map((_) => state$.value).where((currentState) {
/// Can load next page?
return currentState.crystals.isNotEmpty &&
currentState.loadFirstPageError == null &&
currentState.loadNextPageError == null;
}),
retryNextPageController.map((_) => state$.value).where((currentState) {
/// Can retry?
return currentState.loadFirstPageError != null ||
currentState.loadNextPageError != null;
})
],
).withLatestFrom(searchString$, (currentState, String query) =>
Tuple2(currentState.crystals.length, query),
).map(
(tuple2) => SearchIntent.loadNextPageIntent(
search: tuple2.item2,
startIndex: tuple2.item1,
),
);
/// State stream
state$ = Rx.combineLatest2(
Rx.merge([searchIntent$, loadAndRetryNextPageIntent$]) // All intent
.doOnData((intent) => print('[INTENT] $intent'))
.switchMap((intent) => _processIntent$(intent, crystalRepo))
.doOnData((change) => print('[CHANGE] $change'))
.scan((state, action, _) => action.reduce(state),
SearchPageState.initial(),
),
favCrystalsRepo.favoritedIds$,
(SearchPageState state, BuiltSet<String> ids) => state.rebuild(
(b) => b.crystals.map(
(crystal) => crystal.rebuild((b) => b.isFavorited = ids.contains(b.id)),
),
),
).publishValueSeededDistinct(seedValue: SearchPageState.initial());
final message$ = _getMessage$(toggleFavoritedController, favCrystalsRepo, state$);
final favoriteCount = favCrystalsRepo.favoritedIds$
.map((ids) => ids.length)
.publishValueSeededDistinct(seedValue: 0);
return SearchBloc._(
queryController.add,
() => loadNextPageController.add(null),
state$,
DisposeBag([
...controllers,
message$.listen((message) => print('[MESSAGE] $message')),
favoriteCount.listen((count) => print('[FAV_COUNT] $count')),
state$.listen((state) => print('[STATE] $state')),
state$.connect(),
message$.connect(),
favoriteCount.connect(),
]).dispose,
() => retryNextPageController.add(null),
() => retryFirstPageController.add(null),
toggleFavoritedController.add,
message$,
favoriteCount,
);
}
}
/// Process [intent], convert [intent] to [Stream] of [PartialStateChange]s
Stream<PartialStateChange> _processIntent$(
SearchIntent intent,
CrystalRepo crystalRepo,
) {
perform<RESULT, PARTIAL_CHANGE>(
Stream<RESULT> streamFactory(),
PARTIAL_CHANGE map(RESULT a),
PARTIAL_CHANGE loading,
PARTIAL_CHANGE onError(dynamic e),
) {
return Rx.defer(streamFactory)
.map(map)
.startWith(loading)
.doOnError((e, s) => print(s))
.onErrorReturnWith(onError);
}
searchIntentToPartialChange$(SearchInternalIntent intent) =>
perform<BuiltList<Crystal>, PartialStateChange>(
() {
if (intent.search.isEmpty) {
return Stream.fromFuture(crystalRepo.getCrystals());
}
return Stream.fromFuture(crystalRepo.searchCrystal(query: intent.search));
},
(list) {
final crystalItems = list.map((crystal) => CrystalItem.fromDomain(crystal)).toList();
return PartialStateChange.firstPageLoaded(crystals: crystalItems, textQuery: intent.search,);
},
PartialStateChange.firstPageLoading(),
(e) {
return PartialStateChange.firstPageError(error: e,textQuery: intent.search,);
},
);
loadNextPageIntentToPartialChange$(LoadNextPageIntent intent) =>
perform<BuiltList<Crystal>, PartialStateChange>();
return intent.join(
searchIntentToPartialChange$,
loadNextPageIntentToPartialChange$,
);
}
search_state.dart
abstract class SearchPageState implements Built<SearchPageState, SearchPageStateBuilder> {
String get resultText;
BuiltList<CrystalItem> get crystals;
bool get isFirstPageLoading;
#nullable
Object get loadFirstPageError;
bool get isNextPageLoading;
#nullable
Object get loadNextPageError;
SearchPageState._();
factory SearchPageState([updates(SearchPageStateBuilder b)]) = _$SearchPageState;
factory SearchPageState.initial() {
return SearchPageState((b) => b
..resultText = ''
..crystals = ListBuilder<CrystalItem>()
..isFirstPageLoading = false
..loadFirstPageError = null
..isNextPageLoading = false
..loadNextPageError = null);
}
}
class PartialStateChange extends Union6Impl<
LoadingFirstPage,
LoadFirstPageError,
FirstPageLoaded,
LoadingNextPage,
NextPageLoaded,
LoadNextPageError> {
static const Sextet<LoadingFirstPage, LoadFirstPageError, FirstPageLoaded,
LoadingNextPage, NextPageLoaded, LoadNextPageError> _factory =
Sextet<LoadingFirstPage, LoadFirstPageError, FirstPageLoaded,
LoadingNextPage, NextPageLoaded, LoadNextPageError>();
PartialStateChange._(
Union6<LoadingFirstPage, LoadFirstPageError, FirstPageLoaded,
LoadingNextPage, NextPageLoaded, LoadNextPageError>
union)
: super(union);
factory PartialStateChange.firstPageLoading() {
return PartialStateChange._(
_factory.first(
const LoadingFirstPage()
)
);
}
factory PartialStateChange.firstPageError({
#required Object error,
#required String textQuery,
}) {
return PartialStateChange._(
_factory.second(
LoadFirstPageError(
error: error,
textQuery: textQuery,
),
),
);
}
factory PartialStateChange.firstPageLoaded({
#required List<CrystalItem> crystals,
#required String textQuery,
}) {
return PartialStateChange._(
_factory.third(
FirstPageLoaded(
crystals: crystals,
textQuery: textQuery,
),
)
);
}
factory PartialStateChange.nextPageLoading() {
return PartialStateChange._(
_factory.fourth(
const LoadingNextPage()
)
);
}
factory PartialStateChange.nextPageLoaded({
#required List<CrystalItem> crystals,
#required String textQuery,
}) {
return PartialStateChange._(
_factory.fifth(
NextPageLoaded(
textQuery: textQuery,
crystals: crystals,
),
),
);
}
factory PartialStateChange.nextPageError({
#required Object error,
#required String textQuery,
}) {
return PartialStateChange._(
_factory.sixth(
LoadNextPageError(
textQuery: textQuery,
error: error,
),
),
);
}
/// Pure function, produce new state from previous state [state] and partial state change [partialChange]
SearchPageState reduce(SearchPageState state) {
return join<SearchPageState>(
(LoadingFirstPage change) {
return state.rebuild((b) => b..isFirstPageLoading = true);
},
(LoadFirstPageError change) {
return state.rebuild((b) => b
..resultText = "Search for '${change.textQuery}', error occurred"
..isFirstPageLoading = false
..loadFirstPageError = change.error
..isNextPageLoading = false
..loadNextPageError = null
..crystals = ListBuilder<CrystalItem>());
},
(FirstPageLoaded change) {
return state.rebuild((b) => b
//..resultText = "Search for ${change.textQuery}, have ${change.crystals.length} crystals"
..resultText = ""
..crystals = ListBuilder<CrystalItem>(change.crystals)
..isFirstPageLoading = false
..isNextPageLoading = false
..loadFirstPageError = null
..loadNextPageError = null);
},
(LoadingNextPage change) {
return state.rebuild((b) => b..isNextPageLoading = true);
},
(NextPageLoaded change) {
return state.rebuild((b) {
var newListBuilder = b.crystals..addAll(change.crystals);
return b
..crystals = newListBuilder
..resultText =
"Search for '${change.textQuery}', have ${newListBuilder.length} crystals"
..isNextPageLoading = false
..loadNextPageError = null;
});
},
(LoadNextPageError change) {
return state.rebuild((b) => b
..resultText =
"Search for '${change.textQuery}', have ${state.crystals.length} crystals"
..isNextPageLoading = false
..loadNextPageError = change.error);
},
);
}
#override
String toString() => join<String>(_toString, _toString, _toString, _toString, _toString, _toString);
}
search_page.dart
class SearchListViewWidget extends StatelessWidget {
final SearchPageState state;
const SearchListViewWidget({Key key, #required this.state})
: assert(state != null),
super(key: key);
#override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<SearchBloc>(context);
if (state.loadFirstPageError != null) {}
// LOOKING TO HAVE items LOADED ON APP LOAD //
final BuiltList<CrystalItem> items = state.crystals;
if (items.isEmpty) {
debugPrint('items.isEmpty');
}
return ListView.builder(
itemCount: items.length + 1,
padding: const EdgeInsets.all(0),
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
debugPrint('itemBuilder');
if (index < items.length) {
final item = items[index];
return SearchCrystalItemWidget(
crystal: item,
key: Key(item.id),
);
}
if (state.loadNextPageError != null) {
final Object error = state.loadNextPageError;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
error is HttpException
? error.message
: 'An error occurred $error',
textAlign: TextAlign.center,
maxLines: 2,
style:
Theme.of(context).textTheme.body1.copyWith(fontSize: 15),
),
SizedBox(height: 8),
RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
onPressed: bloc.retryNextPage,
padding: const EdgeInsets.all(16.0),
child: Text(
'Retry',
style: Theme.of(context).textTheme.body1.copyWith(fontSize: 16),
),
elevation: 4.0,
),
],
),
);
}
return Container();
},
);
}
}
Ended up solving this by passing in an empty query in the app.dart
home: Consumer2<FavoritedCrystalsRepo, CrystalRepo>(
builder: (BuildContext context, FavoritedCrystalsRepo sharedPref, CrystalRepo crystalRepo) {
final searchBloc = SearchBloc(crystalRepo, sharedPref);
// Do the first search to get first result on init
searchBloc.changeQuery('');
return BlocProvider<SearchBloc>(
child: SearchPage(),
initBloc: () => searchBloc,
);
},
An alternative would be to convert your search page into a StatefulWidget and then calling searchBloc.changeQuery(''); inside of initState