Loading data on init with BLoC - flutter
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
Related
Filter in Flutter (Dart)
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), ); }, )) ], ), ); } }
Flutter + syncfusion charts: Is it possible to have a dataSource that is not a List? (type 'BanditData' is not a subtype of type 'List<BanditData>')
I am trying to display a bar chart in my app with the syncfusion library. It contains 6 bars where the height is defined by a score and the name is a player name. I have the following methods: getBanditBarData() which gets the data from a database in creates a list of BanditData-objects (BanditData class shown below), and barChart() which creates a List of ChartSeries that I can return in the series parameter of my SfCartesianChart. My problem is that the item of the dataSource: item-line in my barChart()-method gives the following exception: _TypeError (type 'BanditData' is not a subtype of type 'List<BanditData>') I've tried nesting an additional List around each BanditData object in the list, and even removing the for-loop of the method. Both changes result in similar errors somewhere in the same method. Future<List<BanditData>> getBanditBarData() async { var scores = await database.totalScore(); List<BanditData> banditData = []; for (var score in scores) { BanditData bandit = BanditData(score['name'], "", score['score']); banditData.add(bandit); } return banditData; } List<ChartSeries> barChart(data) { var barList = <ChartSeries>[]; for (var item in data) { barList.add(BarSeries<BanditData, String>( dataSource: item, xValueMapper: (BanditData b, _) => removeBanditSuffix(b.name), yValueMapper: (BanditData b, _) => b.score, animationDuration: 2000)); } return barList; } The BanditData-class is very simple and looks like this: class BanditData { BanditData(this.name, this.date, this.score); final String name; final String date; final int score; } The setup shown above works when I render my line chart. The methods are very similar: Future<List<List<BanditData>>> getBanditLineData() async { var dates = await database.getDistinctDatesList(); var scores = await database.createScoreDataStruct(); List<List<BanditData>> banditData = []; for (var i = 0; i < scores.length; i++) { List<BanditData> temp = []; var intList = scores[i]['scores']; for (var j = 0; j < scores[i]['scores'].length; j++) { BanditData bandit = BanditData(scores[i]['name'], dates[j], intList[j]); temp.add(bandit); } banditData.add(temp); } return banditData; } List<ChartSeries> lineChart(data) { var lineList = <ChartSeries>[]; for (var item in data) { lineList.add(LineSeries<BanditData, String>( dataSource: item, xValueMapper: (BanditData b, _) => b.date, yValueMapper: (BanditData b, _) => b.score, enableTooltip: true, name: removeBanditSuffix(item[1].name), width: 3.0, animationDuration: 2000, )); } return lineList; } If necessary, here is some more code showing how I build the chart. The above methods is placed inside MyStatsPageState, but figured it would be better to split it up for readability. Ideally, I should be able to replace series: lineChart(lineData) with series: barChart(barData): import 'database.dart'; import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; class MyStatsPage extends StatefulWidget { const MyStatsPage({Key? key}) : super(key: key); #override MyStatsPageState createState() { return MyStatsPageState(); } } class MyStatsPageState extends State<MyStatsPage> { late Future<List<List<BanditData>>> _banditLineData; late Future<List<BanditData>> _banditBarData; final database = Database(); bool displayLineChart = true; #override void initState() { _banditLineData = getBanditLineData(); _banditBarData = getBanditBarData(); super.initState(); getBanditBarData(); } #override Widget build(BuildContext context) { const appTitle = "Stats"; return Scaffold( appBar: AppBar( title: const Text( appTitle, style: TextStyle(fontSize: 25, fontWeight: FontWeight.w700), )), body: FutureBuilder( future: Future.wait([_banditLineData, _banditBarData]), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator(), ); } else { if (snapshot.hasError) { return ErrorWidget(Exception( 'Error occured when fetching data from database')); } else if (!snapshot.hasData) { return const Center(child: Text('No data found.')); } else { final lineData = snapshot.data![0]; final barData = snapshot.data![1]; return Padding( padding: const EdgeInsets.all(5.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded( child: SfCartesianChart( primaryXAxis: CategoryAxis(), enableAxisAnimation: true, series: lineChart(lineData), )), ], )); } } })); }
We have checked the code snippet attached in the query and found that it is a sample-level issue, it occurs due to you have passed the BanditData instead of List. Because dataSource property always supports the list value only. To resolve this, convert the barData to nested lists, or assign a value to the BarSeries dataSource like below. Code snippet: List<ChartSeries> barChart(data) { var barList = <ChartSeries>[]; for (var item in data) { barList.add(BarSeries<BanditData, String>( dataSource: [item], // Other required properties )); } return barList; }
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 bloc - Event only gets called 2 times inside the bloc logic
I'm having problems to send the event in the bloc. The problem I'm having is that when I send the first event the Bloc is getting called and after I recall it again with a diferrent tab and it works, but when I change to another tab the bloc doesn't get called even if the add event is sending. I have checked with the debbuger and when I call it after the 2 first it doesn't get executed the bloc. Example where I send the event: class ListOpportunitiesList extends StatefulWidget { final String title; final bool showTopIcons, showFilterMarvTypes; final OpportunitiesListTypeEnum opportunitiesListTypeEnum; const ListOpportunitiesList({ Key key, #required this.title, #required this.showTopIcons, #required this.showFilterMarvTypes, #required this.opportunitiesListTypeEnum, }) : super(key: key); #override _ListOpportunitiesListState createState() => _ListOpportunitiesListState(); } class _ListOpportunitiesListState extends State<ListOpportunitiesList> { InvestmentMarvTypeEnum _investmentMarvTypeEnum; InvestmentStatusEnum _investmentStatusEnum; InvestmentOpportunityBloc investmentOpportunityBloc; #override void initState() { super.initState(); investmentOpportunityBloc = getIt.getItInstance<InvestmentOpportunityBloc>(); this.getOpportunities(); } // Calling the event works only the first 2 times void getOpportunities() { investmentOpportunityBloc.add( InvestmentOpportunityLoadEvent( opportunitiesListTypeEnum: this.widget.opportunitiesListTypeEnum, investmentMarvTypeEnum: this._investmentMarvTypeEnum, investmentStatusEnum: this._investmentStatusEnum, ), ); } #override void dispose() { super.dispose(); this.investmentOpportunityBloc?.close(); } #override Widget build(BuildContext context) { final double height = MediaQuery.of(context).size.height; final double topMargin = this.widget.showFilterMarvTypes ? height * UIConstants.marginTopPercentageWithFilter : height * UIConstants.marginTopPercentage; return BlocProvider<InvestmentOpportunityBloc>( create: (context) => this.investmentOpportunityBloc, child: ListView( padding: EdgeInsets.zero, children: [ CustomAppBar( title: '${this.widget.title}', showLogged: this.widget.showTopIcons, ), Visibility( visible: this.widget.opportunitiesListTypeEnum == OpportunitiesListTypeEnum.all, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: InvestmentMarvTypeEnum.values .map( (e) => SelectableRoundedRectangleButton( onPressed: () { setState(() { this._investmentMarvTypeEnum = this._investmentMarvTypeEnum == e ? null : e; }); this.getOpportunities(); }, selected: e == this._investmentMarvTypeEnum, text: '${InvestmentOpportunityUtility.getMarvTypeString(e)}', ), ) .toList(), ), ), ), BlocBuilder<InvestmentOpportunityBloc, InvestmentOpportunityState>( builder: (context, state) { if (state is InvestmentOpportunityLoaded) { if (state.investmentOpportunityModel.isNotEmpty) { return Column( children: [ for (InvestmentOpportunityModel opportunity in state.investmentOpportunityModel) InvestmentOpportunityCard( investmentOpportunityModel: opportunity, ), Visibility( visible: !state.hasReachedMax, child: ListBottomLoadingWidget(), ), ], ); } else { return AppEmptyWidget( investmentMarvTypeEnum: this._investmentMarvTypeEnum, topMargin: topMargin, uiEmptyTypeEnum: UIEmptyTypeEnum.investment_opportunity, ); } } else if (state is InvestmentOpportunityError) { return AppErrorWidget( topMargin: topMargin, apiNetworkException: state.apiError, retryButton: () => this.getOpportunities(), ); } else { return AppLoadingWidget( topMargin: topMargin, ); } }, ), ], ), ); } } Here is the bloc logic: class InvestmentOpportunityBloc extends Bloc<InvestmentOpportunityEvent, InvestmentOpportunityState> { final GetInvestmentOpportunities getInvestmentOpportunities; final GetInvestedInvestmentOpportunities getInvestedInvestmentOpportunities; final GetFavorites getFavorites; List<InvestmentOpportunityModel> listOpportunity; InvestmentOpportunityBloc({ #required this.getInvestmentOpportunities, #required this.getInvestedInvestmentOpportunities, #required this.getFavorites, }) : super(InvestmentOpportunityInitial()); #override Stream<InvestmentOpportunityState> mapEventToState( InvestmentOpportunityEvent event, ) async* { final currentState = state; if (event is InvestmentOpportunityLoadEvent || event is InvestmentOpportunityChangeTab) { bool modifyType = false; if (currentState is InvestmentOpportunityLoaded) { switch ((event as InvestmentOpportunityLoadEvent).opportunitiesListTypeEnum) { case OpportunitiesListTypeEnum.invested: { modifyType = (event as InvestmentOpportunityLoadEvent).investmentStatusEnum != currentState .investmentsOpportunitiesInvestedDTO.investmentStatusEnum; } break; case OpportunitiesListTypeEnum.all: { modifyType = (event as InvestmentOpportunityLoadEvent).investmentMarvTypeEnum != currentState.investmentOpportunityDTO.investmentMarvTypeEnum; } break; } if (!modifyType && this._hasReachedMax(currentState)) { return; } } InvestmentsOpportunitiesInvestedDTO investmentsOpportunitiesInvestedDTO; InvestmentOpportunityDTO investmentOpportunityDTO; Either<ApiNetworkException, List<InvestmentOpportunityModel>> opportunityEither; switch ((event as InvestmentOpportunityLoadEvent).opportunitiesListTypeEnum) { case OpportunitiesListTypeEnum.invested: { if (currentState is InvestmentOpportunityLoaded && !modifyType) { currentState.investmentsOpportunitiesInvestedDTO.pageDTO .incrementPage(); investmentsOpportunitiesInvestedDTO = currentState.investmentsOpportunitiesInvestedDTO; } else { yield InvestmentOpportunityInitial(); investmentsOpportunitiesInvestedDTO = InvestmentsOpportunitiesInvestedDTO( pageDTO: PageDTO(), investmentStatusEnum: (event as InvestmentOpportunityLoadEvent).investmentStatusEnum, ); } opportunityEither = await this.getInvestedInvestmentOpportunities( investmentsOpportunitiesInvestedDTO); } break; case OpportunitiesListTypeEnum.all: { if (currentState is InvestmentOpportunityLoaded && !modifyType) { currentState.investmentOpportunityDTO.pageDTO.incrementPage(); investmentOpportunityDTO = currentState.investmentOpportunityDTO; } else { yield InvestmentOpportunityInitial(); investmentOpportunityDTO = InvestmentOpportunityDTO( pageDTO: PageDTO(), investmentMarvTypeEnum: (event as InvestmentOpportunityLoadEvent).investmentMarvTypeEnum, ); } opportunityEither = await this.getInvestmentOpportunities(investmentOpportunityDTO); } break; } yield opportunityEither.fold( (error) => InvestmentOpportunityError(error), (opportunities) { if (currentState is InvestmentOpportunityLoaded && opportunities.isEmpty) { return currentState.copyWith(hasReachedMax: true); } this.listOpportunity = currentState is InvestmentOpportunityLoaded && !modifyType ? currentState.investmentOpportunityModel + opportunities : opportunities; return InvestmentOpportunityLoaded( investmentOpportunityModel: this.listOpportunity, investmentOpportunityDTO: investmentOpportunityDTO, investmentsOpportunitiesInvestedDTO: investmentsOpportunitiesInvestedDTO, hasReachedMax: BlocUtility.hasReachedMax( opportunities.length, investmentsOpportunitiesInvestedDTO != null ? investmentsOpportunitiesInvestedDTO.pageDTO.take : investmentOpportunityDTO.pageDTO.take, ), ); }, ); } else if (event is InvestmentOpportunityUpdateStatusOfInvestment && currentState is InvestmentOpportunityLoaded) { currentState.investmentOpportunityModel[currentState .investmentOpportunityModel .indexOf(event.investmentOpportunityModel)] = event.investmentOpportunityModel; yield currentState.copyWith( investmentOpportunityModel: currentState.investmentOpportunityModel, ); } } bool _hasReachedMax(InvestmentOpportunityState state) { return state is InvestmentOpportunityLoaded && state.hasReachedMax; } }
Why adding data in Flutter Sink is not working?
Purpose is very simple. After getting data it is filterable by specific set of Strings. So I am initially filtering with 'all' which means showing all data and when clicking any choice chips then filtering based on that specific strings. Everything working fine except not showing all data after loading it from api call. Even if I Hot Reload again its showing the full list data. So basically adding string data in Sink is not working. I think I have done some silly mistake but couldn't figure it out. Need suggestions. BLOC Class final Application _application; ProductListScreenBloc(this._application); int totalPages = 1; final _productList = BehaviorSubject<List<Product>>(); Observable<List<Product>> _filteredProductList = Observable.empty(); final _filterName = BehaviorSubject<String>(); Stream<List<Product>> get productList => _productList.stream; Stream<List<Product>> get filteredProductList => _filteredProductList; Sink<String> get filterName => _filterName; void loadBrandWiseProductList( String categorySlug, String brandSlug, int pageNo) { if (totalPages >= pageNo) { //for pagination StreamSubscription subscription = _application.productListRepository .getBrandWiseProductList(categorySlug, brandSlug, pageNo) .listen((ProductListResponse response) { if (_productList.value == null) { totalPages = response.totalPage; _productList.add(response.productList); filterName.add('all'); _filteredProductList = Observable.combineLatest2( _filterName, _productList, applyModelFilter) .asBroadcastStream(); } }); } } List<Product> applyModelFilter( String filter, List<Product> products, ) { if (filter == 'all') { return products; } else { return products .where((seriesSLug) => seriesSLug.series.slug == filter) .toList(); } } UI Widget Class class _AllSeriesModelListScreenState extends State<AllSeriesModelListScreen> { AllSeriesModelListScreenArguments allSeriesModelListScreenArguments; ProductListScreenBloc bloc; int _selectedSeriesChipValue = -1; int _pageNo = 1; #override void initState() { super.initState(); } #override void dispose() { super.dispose(); bloc.dispose(); } #override Widget build(BuildContext context) { RouteSettings settings = ModalRoute.of(context).settings; allSeriesModelListScreenArguments = settings.arguments; _init(); return Scaffold( body: CustomScrollView( slivers: <Widget>[ StreamBuilder( stream: bloc.filteredProductList, builder: (context, snapshot) { if (snapshot.hasData) { List<Product> productList = snapshot.data; return SliverPadding( padding: EdgeInsets.symmetric( vertical: 8.0, horizontal: 10.0, ), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 0.0, mainAxisSpacing: 8.0, ), delegate: SliverChildListDelegate( buildModelGridList(productList), ), ), ); } else { return SliverList( delegate: SliverChildListDelegate([ PaddingWithTitle( title: 'No Model Available', ), ]), ); } }) ], ), ); } void _init() { if (null == bloc) { bloc = ProductListScreenBloc( AppProvider.getApplication(context), ); bloc.loadBrandWiseProductList( allSeriesModelListScreenArguments.categorySlug, allSeriesModelListScreenArguments.brandSlug, _pageNo); } } }
I believe you have missed something in these 2 lines. final _filterName = BehaviorSubject<String>(); Sink<String> get filterName => _filterName; You are not exposing the sink. BehaviorSubject is just a StreamController with default value and cache for last value. So as every Stream controller it has 2 props - sink and stream. to push data you need to access the sink. To do that you need to type StreamSink<String> get filterName => _filterName.sink; Plus why you do not have a seed value in the behavior subject? It is required to have that "default" value final _filterName = BehaviorSubject<String>(seedValue: '');
Just had to change the code into this void loadBrandWiseProductList( String categorySlug, String brandSlug, int pageNo) { if (totalPages >= pageNo) { //for pagination StreamSubscription subscription = _application.productListRepository .getBrandWiseProductList(categorySlug, brandSlug, pageNo) .listen((ProductListResponse response) { if (_productList.value == null) { totalPages = response.totalPage; _productList.add(response.productList); } _filteredProductList = Observable.combineLatest2( _filterName, _productList, applyModelFilter) .asBroadcastStream(); }); } }