Flutter: How to change state of sibling widget widget? - flutter

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

Related

How to sync stream variable in BlocState with the TextFormField

I am not able to figure out the right architecture of getting this done using firestore streams and flutter_bloc.
My goal :
I have a logged in user and I want to store the user details in the collection with the document id same as logged in user id and want to listen to changes, Throughout the program.
Meanwhile i would want to even store user details (if doesn't exist) and update user details.
My approach:
I am having a usermodel in UserState and i am listening to userModel via state using BlocBuilder.
Problem:
As i am using BlocBuilder my setState isn't working and TextFormField isn't working as it says beginBatchEdit on inactive InputConnection
Code:
UserCubit.dart
class UserCubit extends Cubit<UserState> {
final _firestore = FirebaseFirestore.instance;
final User? _currentUser = FirebaseAuth.instance.currentUser;
UserCubit() : super(UserInitialState()) {
emit(UserMainLoadingState());
_firestore --> listening to stream and updating state
.collection("sample")
.doc(_currentUser?.uid)
.snapshots()
.listen((event) {
event.exists
? emit(UserExists(sample: SampleModel.fromjson(event.data()!, event.id)))
: {emit(UserNotExists())};
});
}
Future<void> addSampleUser({required SampleModel sample}) async {
emit(UserSideLoadingState());
_firestore
.collection('sample')
.doc(_currentUser?.uid)
.set(sample.toJson())
.then((value) => emit(UserSavedUpdatedState()));
}
}
UserState.dart
abstract class UserState extends Equatable {
final SampleModel? sampleModel;
const UserState({this.sampleModel});
#override
List<Object?> get props => [sampleModel];
}
class UserExists extends UserState {
const UserExists({required SampleModel sample}) : super(sampleModel: sample);
}
Widget.dart (Save/Update User Details)
class _MyWidgetState extends State<MyWidget> {
// #override
var fullNameKey;
TextEditingController? fullNameController;
bool _formChecked = false;
TextEditingController? phoneNumberController;
Avatar? selectedAvatar;
#override
void initState() {
fullNameController = TextEditingController();
fullNameKey = GlobalKey<FormState>();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: BlocConsumer<UserCubit, UserState>(listener: (context, state) {
if (state is UserSavedUpdatedState) {
print("Saved/Updated User");
context.goNamed(Routes.profileMain);
}
}, builder: (context, state) {
// Here i am trying to get details from the state
selectedAvatar = state.sampleModel?.avatar == "boy"
? Avatar.boy
: state.sampleModel?.avatar == "boy"
? Avatar.girl
: null;
fullNameController!.text = state.sampleModel?.name ?? "";
if (state is UserMainLoadingState) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Column(
children: [
Row(
children: [
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.girl; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.girl)),
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.boy; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.boy))
],
),
Form(
key: fullNameKey,
child: TextFormField(
// BlocBuilder even freezes TextFormField
autovalidateMode: _formChecked
? AutovalidateMode.always
: AutovalidateMode.disabled,
validator: (value) {
if (value == null ||
fullNameController?.text.trim() == "") {
return "Name cannot be empty";
}
if (value.length < 3) {
return "Username must be greater than 3 characters";
}
return null;
},
controller: fullNameController,
decoration: const InputDecoration(
labelText: "Full Name",
),
)).marginDown(),
FilledButton(
onPressed: () {
setState(() {
_formChecked = true;
});
if (fullNameKey.currentState!.validate() &&
selectedAvatar != null) {
SampleModel sample = SampleModel(
name: fullNameController!.text,
avatar: selectedAvatar == Avatar.boy ? "boy" : "girl");
BlocProvider.of<UserCubit>(context)
.addSampleUser(sample: sample);
}
},
child: const Text("Submit"),
)
],
));
}),
);
}
}
As soon as the submit button is clicked it erases the entire text and validator gets activated. Avatar selection doesn't work as well.
What is the best way to achieve the desired function using streams, flutter_bloc, Suggestions would be greatly appreciated
as far as I can see you pre-select the avatar based on the emitted state. However, I do not see that you return the selection via an event/function to the bloc/cubit. So this is needed in order to send the updated avatar with the next emit.
From what I can see, I would also possibly exchange the abstract class state with a class state implementing Equatable and the simply always copyWith the state for any updates. This way you always have the same UserState - no need for if and else if for state selection, however, the data of the state changes based on the situation. I think for a user bloc/cubit this makes the lifecycle a bit easier
UPDATE:
IconButton(
onPressed: () {
setState(() {
context.read<UserCubit>.updateUser(selectedAvatar: Avatar.boy);
selectedAvatar = Avatar.boy; -- > possibly no longer needed if returned from Cubit
});
},
icon: const Icon(Icons.boy))
As for the state management, a state can look like this:
class TaskListState extends Equatable {
const TaskListState({
this.status = DataTransStatus.initial,
this.taskList = const [],
this.filter,
this.error,
this.editThisTaskId,
});
final DataTransStatus status;
final List<TaskListViewmodel> taskList;
final TaskListFilter? filter;
final String? error;
final String? editThisTaskId;
TaskListState copyWith({
DataTransStatus Function()? status,
List<TaskListViewmodel> Function()? taskList,
TaskListFilter Function()? filter,
String Function()? error,
String? Function()? editThisTaskId,
}) {
return TaskListState(
status: status != null ? status() : this.status,
taskList: taskList != null ? taskList() : this.taskList,
filter: filter != null ? filter() : this.filter,
error: error != null ? error() : this.error,
editThisTaskId: editThisTaskId != null
? editThisTaskId() : this.editThisTaskId,
);
}
#override
List<Object?> get props => [
status,
taskList,
filter,
error,
editThisTaskId,
];
}
which you use - in this case with a Stream - like this:
await emit.forEach<dynamic>(
_propertiesRepository.streamTasks(event.propertyId),
onData: (tasks) {
return state.copyWith(
status: () => DataTransStatus.success,
taskList: () => List.from(
tasks.map((t) => TaskListViewmodel.fromDomain(t))),
);
},
onError: (_, __) {
return state.copyWith(
status: () => DataTransStatus.failure,
);
},
);

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),
);
},
))
],
),
);
}
}

when I am calling the provider in the inistate return data got null

My question is.......When the app Started(initial start) circular progress indicator appears & didn't got any data.....My app has 3 tabs(using tabbar), but when I swicth between my tabs & when I came to the Home tab now I got the data & ui render according to the data....
I want when the app start get data & needs render the data....
Ui inistate
class Home extends StatefulWidget {
const Home({ Key? key }) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _page = 1;
void initState() {
var postProvider = Provider.of<PostProvider>(context, listen: false);
postProvider.initStreamsPost();
postProvider.fetchAllData(_page);
}
#override
Widget build(BuildContext context) {
return Consumer<PostProvider>(builder: (context, postsModel, child) {
print("builder length... ${postsModel.allData!.length}"); //when the first time app opens what I got print in terminal => builder length... 0
if (postsModel.allData != null && postsModel.allData!.length > 0) {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: postsModel.allData?.length,
itemBuilder: (BuildContext context, int index) {
return PostCard(
);
});
}
return Center(child: CircularProgressIndicator());
}
}
}
changeNotifier class
class PostProvider extends ChangeNotifier {
int totalPages = 0;
int? get totalRecords => _postsFetcher!.totalPosts;
PostApi? _postApi;
PostsModel? _postsFetcher;
List<Post>? get allData =>
_postsFetcher?.posts != null ? _postsFetcher?.posts : [];
PostProvider() {
initStreamsPost();
}
initStreamsPost() async {
_postApi = await new PostApi();
_postsFetcher = await new PostsModel();
}
fetchAllData(pageNumber) async {
if ((totalPages == 0) || pageNumber <= totalPages) {
PostsModel itemModel = await _postApi!.getPost2(pageNumber);
print("have data");
if (_postsFetcher!.posts == null) {
totalPages = ((itemModel.totalPosts! - 1) / 4).ceil();
_postsFetcher = itemModel;
notifyListeners();
} else {
_postsFetcher?.posts?.addAll(itemModel.posts!);
_postsFetcher = _postsFetcher;
notifyListeners();
}
}
}
}
While using Provider in initState, prefer using it this way:
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
var postProvider = Provider.of<PostProvider>(context, listen: false);
postProvider.initStreamsPost();
postProvider.fetchAllData(_page);
});
}

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;
}
}

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