emitting of state doesn't seem to work the second time , flutter_bloc - flutter

I am working with flutter_bloc and trying to understand how it works entirely.
I have Profile Screen. Where in the User should enter his details if previously not existed else should update the details.
Logic: If the user already exists then i fill up the textfields prior to loading , else the textfields are left blank
Problem: I have worked out of achieving the above mentioned goal , but everything seems to work only the first time , Once i save the profile and come back to the profile page it doesn't load the data and textfields are empty even if the user exists.
User_Cubit
class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserInitialState()) {
checkIfUserExists();
}
void checkIfUserExists() {
emit(UserLoadingState());
...
if (userExists) {
emit(UserExists(user));
} else {
emit(UserNotExists());
}
});
}
Profile Screen
class _MyProfileScreenState extends State<MyProfileScreen> {
TextEditingController? fullNameController;
TextEditingController? mailAddressController;
late UserCubit _userCubit;
#override
void initState() {
fullNameController = TextEditingController();
mailAddressController = TextEditingController();
_userCubit = UserCubit(); // initializing the cubit here
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: MultiBlocListener(
listeners: [
BlocListener<UserCubit, UserState>(
listener: (context, state) {
if (state is UserExists) {
appUser = state.user;
mailAddressController!.text = appUser!.email; // Loading the fields
fullNameController!.text = appUser!.fullName; // Loading the fields
}
},
)
],
child: BlocBuilder<UserCubit, UserState>(
builder: (context, state) {
if (state is UserLoadingState) {
return const Center(child: CircularProgressIndicator());
}
return Container(
TextFormField() // For fullName
TextFormField() // For EmailAddress
)
)
);
}
Why does this functionality work only the first time not the consecutive times. Thougth the UserCubit is intialized in initstate ?
Any further suggestions to improve this logic by not initializing the UserCubit on every page render would be also appreciated !!

Related

Bloc - Is it possible to yield states for a previous page in the navigation stack?

I have a BlocBuilder which handles building widgets depending on the yielded state for my dashboard page.
body: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
print(state);
if (state is DashboardInitial) {
return loadingList();
} else if (state is DashboardEmpty) {
return emptyList();
} else if (state is DashboardLoaded) {
return loadedList(context, state);
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => AddPage()));
},
I want to be able to navigate to the add page, fill in some textfields, and then dispatch an event to my dashboard bloc, with the idea being that upon navigating back to the dashboard, my list will be updated.
class AddPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
TextEditingController titleController = TextEditingController();
TextEditingController descriptionController = TextEditingController();
return Scaffold(
appBar: AppBar(title: Text('Add')),
body: Container(
padding: EdgeInsets.all(10),
child: Column(
children: [
TextField(
controller: titleController,
),
TextField(
controller: descriptionController,
),
RaisedButton(onPressed: () {
BlocProvider.of<DashboardBloc>(context)
.add(DashboardWorryAdded('title', 'description'));
}),
],
),
),
);
}
}
When following the code using breakpoints, i am able to see that my state is yielded in the 'mapeventtostate' function, however my dashboard is never rebuilt with the new values.
Here is the code for my Bloc, events, and states. My first thought would be that Equatable was detecting the same state being returned, but upon removing Equatable, my problem is still persists.
#override
Stream<DashboardState> mapEventToState(
DashboardEvent event,
) async* {
if (event is DashboardWorryAdded) {
yield* _mapDashboardWorryAddedToState(event);
} else if (event is DashboardLoading) {
yield* _mapDashboardLoadingToState(event);
} else if (event is AppStarted) {
yield* _mapAppStartedToState(event);
}
}
Stream<DashboardState> _mapAppStartedToState(AppStarted event) async* {
List<Worry> _wList = await repo.getAllWorries();
if (_wList.length != 0) {
yield DashboardLoaded(worryList: _wList);
} else {
yield DashboardEmpty();
}
}
Stream<DashboardState> _mapDashboardLoadingToState(
DashboardLoading event) async* {
List<Worry> _wList = await repo.getAllWorries();
if (_wList != 0) {
yield DashboardLoaded(worryList: _wList);
} else {
yield DashboardEmpty();
}
}
Stream<DashboardState> _mapDashboardWorryAddedToState(
DashboardWorryAdded event) async* {
await repo.addWorry(event.title, event.description);
List<Worry> worryList = List<Worry>();
worryList = await repo.getAllWorries();
yield DashboardLoaded(worryList: worryList);
}
}
#immutable
abstract class DashboardEvent {}
class DashboardLoading extends DashboardEvent {
DashboardLoading();
}
class DashboardWorryAdded extends DashboardEvent {
final String title, description;
DashboardWorryAdded(this.title, this.description);
}
class AppStarted extends DashboardEvent {
AppStarted();
}
#immutable
abstract class DashboardState {}
class DashboardInitial extends DashboardState {
DashboardInitial();
}
class DashboardLoaded extends DashboardState {
final List<Worry> worryList;
DashboardLoaded({this.worryList});
}
class DashboardEmpty extends DashboardState {
DashboardEmpty();
}
Instead of trying to mutate another page's state (a bit of a no-no where state management is concerned), take advantage of the fact that the push method of the navigator returns a future that completes when that page gets popped, and as a bonus, the value of the future will include the value that was given to the pop method in the other page. So you can now do something like this:
class DashboardBloc {
...
void showAddPage() async {
// Do this to retrieve the value passed to the add page's call to `pop`
final value = await Navigator.of(context).push(...);
// Do this if the add page doesn't return a value in `pop`
await Navigator.of(context).push(...);
// Either way, you can now refresh your state in response to
// the add page popping
emit(...);
}
}
Note: This works just as well for named routes too.

Widgets with future builder not removing widget after provider was updated with async

I have been learning flutter for 2-3 months now and I feel I have a reached a fundamental roadblock with understanding state management. This post will be long unfortunately so please bare with me and I hope I put the right detail.
Problem Definition
I have a list of widgets in a shopping cart,im at the point where I click minus and it only has 1 left the widget must be removed.No matter what I try I cant get that widget to be removed. If I click back button and go back into cart the Item will not appear anymore.
I have considered other methods, like disposing the widget(that didn't seem to work) and I was busy implementing Visibility Show/hide widgets in Flutter programmatically
but that doesn't feel like the right way.If my understanding of providers,changeNotifiers,async and future builders,is correct the below method should work and I think its fundamental to my flutter journey to understand why it doesn't work.
Overview:The idea was to use the minus button on CartItemWidget to call a method that updates Json stored on the local device, then repopulate the List cartProdList in ProductProvider which calls
notifyListeners() and then should propagate everywhere the provider is used. Now I have used this pattern successfully 5 times now, the only different this time is it will be removing a widget which I haven't done before. But this should work dynamically if the future is based of the same provider right ?
function call order
CartItemWidget.onPressed:()
calls >>>
ProductProvider.cartMinusOne(String id)
calls >>>
ProductProvider.Future<List<Product>> cartProducts()
well here goes the code.I also wouldn't mind comments on things I could be doing better in all areas.
CartWidget
class CartWidget extends StatefulWidget {
#override
_CartWidgetState createState() => _CartWidgetState();
}
class _CartWidgetState extends State<CartWidget> {
var providerOfProd;
ProductProvider cartProdProvider = new ProductProvider();
#override
void initState() {
_productsList = new ProductsList();
super.initState();
providerOfProd = Provider.of<ProductProvider>(context, listen: false).cartProducts();
}
#override
Widget build(BuildContext context) {
........
Column(children: <Widget>[
FutureBuilder(
future: providerOfProd,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Container(
width: 0,
height: 0,
);
case ConnectionState.done:
return ListView.separated(
..............
},
itemBuilder: (context, index) {
return CartItemWidget(
product: cartProdProvider.cartProdList.elementAt(index),
heroTag: 'cart',
quantity: cartProdProvider.cartProdList.elementAt(index).cartqty,
key: UniqueKey(),
);
},
);
.........
CartItemWidget
class CartItemWidget extends StatefulWidget {
CartItemWidget({Key key, this.product, this.heroTag, this.quantity = 1}) : super(key: key);
// ProductProvider cartProd = new ProductProvider();
String heroTag;
Product product;
int quantity;
#override
_CartItemWidgetState createState() => _CartItemWidgetState();
}
class _CartItemWidgetState extends State<CartItemWidget> {
#override
Widget build(BuildContext context) {
return Consumer<ProductProvider>(
builder: (context, productProv, _) => InkWell(
child: Container(
.............
child: Row(
children: <Widget>[
.............
IconButton(
onPressed: () {
setState(() {
productProv.cartMinusOne(widget.product.id);
widget.quantity = this.decrementQuantity(widget.quantity);
});
}
.............
ProductProvider
class ProductProvider with ChangeNotifier {
ProductProvider() {
cartProducts();
}
List<Product> cartProdList;
cartMinusOne(String id) async {
//Code to minus item,then return as a string to save as local jason
var test = jsonEncode(cartList);
saveLocalJson(test, 'cart.json');
cartProducts();
notifyListeners();
}
Future<List<Product>> cartProducts() async {
String jsonString = await JsonProvider().getProductJson();
String cartString = await getCartJson();
var filterProdList = (json.decode(jsonString) as List).map((i) => Product.fromJson(i)).toList();
//code to get match cart list to product list
cartProdList = filterProdList.where((element) => element.cartqty > 0).toList();
notifyListeners();
return cartProdList;
}
........................

flutter TabBarView sometimes not triggering on gesture swipe

I am using Flutter -Android studio. I have main screen with TabBarView control (2 pages). each page get data from sqfite database. both have same statefull widget class, but I pass parameter to look into database and display.
Issue : when I click tabbar header , data displayed is Ok. But when I swipe tabs, it sometimes work and sometimes does not work. as per below video. I have check and seems that Build event is not trigger every if Swipe was done.
Main widget with tabbarview
screen record
Note that 1st tab has only 1 record, while second has 6 records. tabClick works fine, but swipe sometime not work properly.
body: TabBarView(
controller: _tabController,
children: [
DisplayTransactions(
tmptransType: TransactionType.enIncome,
transacBloc: _transacBloc,
SelectedItemsCount: bRowSelectionCount,
onSelectionChanged: (count) {
setState(() {
bRowSelectionCount = count;
});
},
),
// Center(child: Text("Page 1")),
DisplayTransactions(
tmptransType: TransactionType.enExpense,
transacBloc: _transacBloc,
SelectedItemsCount: bRowSelectionCount,
onSelectionChanged: (count) {
setState(() {
bRowSelectionCount = count;
});
},
//Center(child: Text("Page 2")),
)
],
),
------ > Widget to display data into listview for each tabbar page
class DisplayTransactions extends StatefulWidget {
final TransactionType tmptransType;
final FinanceTransBlock transacBloc;
final Function onSelectionChanged;
int SelectedItemsCount;
/// -1 expense 1 income
DisplayTransactions(
{Key key,
#required this.tmptransType,
#required this.transacBloc,
this.SelectedItemsCount,
this.onSelectionChanged})
: super(key: key);
#override
_DisplayTransactionsState createState() => _DisplayTransactionsState();
}
class _DisplayTransactionsState extends State<DisplayTransactions> {
var isSelected = false;
var mycolor = Colors.white;
// int iselectionCount = 0;
#override
Widget build(BuildContext context) {
widget.transacBloc.transacType = widget.tmptransType;
debugPrint("----------------------------- ${widget.tmptransType}");
if (widget.SelectedItemsCount == 0) {// if no rows selected, then reload database based on trans type, eg expense, or income.. etc
widget.transacBloc.refresh();
}
return StreamBuilder<List<FinanceTransaction>>(
stream: widget.transacBloc.transacations,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<FinanceTransaction> list = snapshot.data;
// return buildTaskListWal(snapshot.data);
return Padding(
It looks like the app is taking some time to retrieve the new data. Try checking for null on the snapshot.data and display a circularProgressIndicator.
if (snapshot.data == null) {
return CirgularProgressIndicator();
} else {
//display data
}

Reload widget in flutter

I have an API that returns content and I put this content in a GridView.builder to allow pagination.
I have architected the page in such a way that I have a FutureBuilder on a stateless widget and when the snapshot is done I then pass the snapshot data to a stateful widget to build the grid.
It is all working fine, however I want now to implement a functionality that allows me to reload the widget by placing a reload icon when snapshot has error and on click reloading widget. How can I accomplish this?
The following is my FutureBuilder on my Stateless widget:
return new FutureBuilder<List<Things>>(
future: apiCall(),
builder: (context, snapshot) {
if (snapshots.hasError)
return //Reload Icon
switch (snapshots.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
return StatefulWidhet(things: snapshot.data);
default:
}
});
}
You'll need to lift the state up. The whole loading concept is abstracted by the FutureBuilder, but because you don't want to do one-time-loading, that's not the right abstraction layer for you. That means, you'll need to implement the "waiting for the future to complete and then build stuff" yourself in order to be able to trigger the loading repeatedly.
For example, you could put everything in a StatefulWidget and have isLoading, data and error properties and set these correctly.
Because this is probably a recurring task, you could even create a widget to handle that for you:
import 'package:flutter/material.dart';
class Reloader<T> extends StatefulWidget {
final Future<T> Function() loader;
final Widget Function(BuildContext context, T data) dataBuilder;
final Widget Function(BuildContext context, dynamic error) errorBuilder;
const Reloader({
Key key,
this.loader,
this.dataBuilder,
this.errorBuilder,
}) : super(key: key);
#override
State<StatefulWidget> createState() => ReloaderState<T>();
static of(BuildContext context) =>
context.ancestorStateOfType(TypeMatcher<ReloaderState>());
}
class ReloaderState<T> extends State<Reloader<T>> {
bool isLoading = false;
T data;
dynamic error;
#override
void initState() {
super.initState();
reload();
}
Future<void> reload() async {
setState(() {
isLoading = true;
data = null;
error = null;
});
try {
data = await widget.loader();
} catch (error) {
this.error = error;
} finally {
setState(() => isLoading = false);
}
}
#override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return (data != null)
? widget.dataBuilder(context, data)
: widget.errorBuilder(context, error);
}
}
Then, you can just do
Reloader(
loader: apiCall,
dataBuilder: (context, data) {
return DataWidget(things: data);
},
errorBuilder: (context, error) {
return ...
RaisedButton(
onPressed: () => Reloader.of(context).reload(),
child: Text(reload),
),
...;
},
)
Also, I wrote a package for that case which has some more features built-in and uses a controller-based architecture instead of searching the state through Reload.of(context): flutter_cached
With it, you could just do the following:
In a state, create a CacheController (although you don't need to cache things):
var controller = CacheController(
fetcher: apiCall,
saveToCache: () {},
loadFromCache: () {
throw 'There is no cache!';
},
),
Then, you could use that controller to build a CachedBuilder in the build method:
CachedBuilder(
controller: controller,
errorScreenBuilder: (context, error) => ...,
builder: (context, items) => ...,
...
),
When the reload button is pressed, you can simply call controller.fetch(). And you'll also get some cool things like pull-to-refresh on top.

Change state in one Widget from another widget

I'm programming a flutter application in which a user is presented with a PageView widget that allows him/her to navigate between 3 "pages" by swiping.
I'm following the setup used in https://medium.com/flutter-community/flutter-app-architecture-101-vanilla-scoped-model-bloc-7eff7b2baf7e, where I use a single class to load data into my model, which should reflect the corresponding state change (IsLoadingData/HasData).
I have a main page that holds all ViewPage widgets. The pages are constructed in the MainPageState object like this:
#override
void initState() {
_setBloc = SetBloc(widget._repository);
_notificationBloc = NotificationBloc(widget._repository);
leftWidget = NotificationPage(_notificationBloc);
middleWidget = SetPage(_setBloc);
currentPage = middleWidget;
super.initState();
}
If we go into the NotificationPage, then the first thing it does is attempt to load data:
NotificationPage(this._notificationBloc) {
_notificationBloc.loadNotificationData();
}
which should be reflected in the build function when a user directs the application to it:
//TODO: Consider if state management is correct
#override
Widget build(BuildContext context) {
return StreamBuilder<NotificationState>(
stream: _notificationBloc.notification.asBroadcastStream(),
//initialData might be problematic
initialData: NotificationLoadingState(),
builder: (context, snapshot) {
if (snapshot.data is NotificationLoadingState) {
return _buildLoading();
}
if (snapshot.data is NotificationDataState) {
NotificationDataState state = snapshot.data;
return buildBody(context, state.notification);
} else {
return Container();
}
},
);
}
What happens is that the screen will always hit "NotificationLoadingState" even when data has been loaded, which happens in the repository:
void loadNotificationData() {
_setStreamController.sink.add(NotificationState._notificationLoading());
_repository.getNotificationTime().then((notification) {
_setStreamController.sink
.add(NotificationState._notificationData(notification));
print(notification);
});
}
The notification is printed whilst on another page that is not the notification page.
What am i doing wrong?
//....
class _SomeState extends State<SomeWidget> {
//....
Stream<int> notificationStream;
//....
#override
void initState() {
//....
notificationStream = _notificationBloc.notification.asBroadcastStream()
super.initState();
}
//....
Widget build(BuildContext context) {
return StreamBuilder<NotificationState>(
stream: notificationStream,
//....
Save your Stream somewhere and stop initialising it every time.
I suspect that the build method is called multiple times and therefore you create a new stream (initState is called once).
Please try let me know if this helped.