how to reuse previous state fields in flutter bloc pattern? - flutter

have the following. using sliding_up_panel that has body with messages and with a different view in panel content that pops up on click action from bottom bar.
#override
Stream<AppState> mapEventToState(
AppEvent event,
) async* {
if (event is LoadChat) {
List<Msg> msgs = await Api.getMessages();
yield LoadedChat(messages: msgs);
} else if (event is OrderPanelOpen) {
yield OpenedPanelState();
} else if (event is OrderPanelClose) {
yield ClosedPanelState();
}
}
Goal is to hide the appBar when panel is opened. appBar is present in AppLayout which is parent holding the SlidingUpPanel widget itself in a Scaffold.
class _AppLayoutState extends State<AppLayout> {
#override
Widget build(BuildContext context) {
var bloc = BlocProvider.of<AppBloc>(context);
return Container(
child: Scaffold(
appBar: widget.showAppBar
? AppBar(...)
: null,
bottomNavigationBar: BottomAppBar(...),
body: SlidingUpPanel(...),
),
);
}
}
following is the action that adds panel events to bloc
IconButton(
icon: Icon(Icons.description),
onPressed: () {
if (widget.pannelCtrl.isPanelClosed()) {
widget.pannelCtrl.open();
bloc.add(OrderPanelOpen());
} else {
widget.pannelCtrl.close();
bloc.add(OrderPanelClose());
}
})
problem here is SlidingUpPanel has a body that needs to show messages regardless of panel open or close. If panel open and close events are mapped to states with bloc, these open and close events has to be separate states but messages from current state has to be passed to new state by either as constructor params to new state or other ways. is that right approach to achieve this or is there anything else cleaner that I'm missing here.
class ClosedPanelState implements LoadedChat {
final messagesArg;
ClosedPanelState({this.messagesArg});
#override
Widget get currentView => Chat(messages: this.messagesArg);
#override
List<Object> get props => [];
#override
bool get showAppBar => true;
#override
String get title => 'Order Food';
#override
List<Msg> get messages => messages;
}

According to your question's title there's a thing you can do.
For your bloc class you can create a stack-like variable (you can use your own implementation of a LIFO stack or use a List, as you wish). Your bloc class must be Singleton in order to save the Events.
static final List<AppEvent> _events = new List<AppEvent>();
And on your _mapEventToState implementation add the events called to the variable:
#override
Stream<AppState> mapEventToState(
AppEvent event,
) async* {
_events.add(event); // New Line
if (event is LoadChat) {
List<Msg> msgs = await Api.getMessages();
yield LoadedChat(messages: msgs);
} else if (event is OrderPanelOpen) {
yield OpenedPanelState();
} else if (event is OrderPanelClose) {
yield ClosedPanelState();
}
}
Later you write a method for calling the last Event on the Stack/List.
dispatchPreviousState() {
this.add(_events.removeLast());
}
This worked for me to get the latest Event called and dispatching it again.

You can use reversible_bloc to add this behaviour to your bloc / cubit.
Ex.:
class MyReversibleCubit extends Cubit<int> with ReversibleBlocMixin {
MyReversibleCubit(int initialState) : super(initialState);
void changeValue(int newValue) => emit(newValue);
}
Then, when you need to rollback, use revert()
final myReversibleCubit = context.read<MyReversibleCubit>();
myReversibleCubit.changeValue(2);
myReversibleCubit.changeValue(4);
// Current state is 4
myReversibleCubit.revert();
// Current state back to 2

Related

Cubit - listener does not catching the first state transition

I'm using a Cubit in my app and I'm struggling to understand one behavior.
I have a list of products and when I open the product detail screen I want to have a "blank" screen with a loading indicator until receiving the data to populate the layout, but the loading indicator is not being triggered in the listener (only in this first call, when making a refresh in the screen it shows the loader).
I'm using a BlocConsumer and i'm making the request in the builder when catching the ApplicationInitialState (first state), in cubit I'm emitting the ApplicationLoadingState(), but this state transition is not being caught in the listener, only when the SuccessState is emitted the listener triggers and tries to remove the loader.
I know the listener does not catch the first State emitted but I was expecting it to catch the first state transition.
UI
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
Widget build(BuildContext context) {
_l10n = AppLocalizations.of(context);
return _buildConsumer();
}
_buildConsumer() {
return BlocConsumer<ProductCubit, ApplicationState>(
bloc: _productCubit,
builder: (context, state) {
if (state is ApplicationInitialState) {
_getProductDetail();
}
return Scaffold(
appBar: _buildAppbar(state),
body: _buildBodyState(state),
);
},
listener: (previous, current) async {
if (current is ApplicationLoadingState) {
_loadingIndicator.show(context);
} else {
_loadingIndicator.close(context);
}
},
);
}
Cubit
class ProductCubit extends Cubit<ApplicationState> with ErrorHandler {
final ProductUseCase _useCase;
ProductCubit({
required ProductUseCase useCase,
}) : _useCase = useCase,
super(const ApplicationInitialState());
void getProductDetail(String id) async {
try {
emit(const ApplicationLoadingState());
final Product = await _useCase.getProductDetail(id);
emit(CSDetailSuccessState(
detail: ProductDetailMapper.getDetail(Product),
));
} catch (exception) {
emit(getErrorState(exception));
}
}
}
ApplicationLoadingState
abstract class ApplicationState extends Equatable {
const ApplicationState();
#override
List<Object> get props => [];
}
class ApplicationLoadingState extends ApplicationState {
const ApplicationLoadingState();
}

Flutter: How to trigger a bloc event in the controller?

In summary I want the "context.read().add(EnabledEvent())" to work from the controller itself or some other alternative I can use to trigger an event from the controller and update the state:
class SplashCtrl extends GetxController {
#override
void onReady() {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
Another thing I tried was this:
ExampleBloc testBloc = ExampleBloc();
testBloc.add(TestEvent());
in the controller, and it does seem to trigger the event, but the UI doesn't update with the state change. I seem to really need the Context for that to change. Any ideas?
MORE DETAILS AND CONTEXT:
I want to trigger bloc events not just in the page itself, but in the page's controller!
So I have pages in my flutter app with each view having a controller set up like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
SplashCtrl is the controller for this page. This splash view page is one of the first pages that loads it runs functions when it's added and ready (basically instantly), such as checking if the user is logged in and loading their data to the app, like this:
class SplashCtrl extends GetxController {
#override
void onReady() {
// run stuff here
}
I have been able to get away with creating lambda functions in the pages and triggering events based on what they do and toggle like this:
IconSwitchedButtonUi(
value: state.isExampleOn,
icon: Images.toggleIcon,
title: "Is Example enabled?",
onChanged: (value) {
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},
),
but now I need a bloc event to trigger and change the state a bit if a user has a certain value for one of their fields. How do I do that? How do I trigger an event from within the controller? The "context.read" doesn't work because the controller doesn't have context, or does it?
ExampleBloc testBloc = ExampleBloc(); testBloc.add(TestEvent());
This does not work because you were declaring totally new instance of bloc that wasnt connected by any BlocProvider to your widget tree, hence there is no way you could trigger this bloc events that would emit state your BlocBuilder could react to. Since there is no bloc provided to widget tree read() wont find this bloc.
In order for this work you need to:
Pass BuildContext that have some Bloc or Cubit provided by BlocProvider
void foo(BuildContext context) {
User user = loadUser();
if (user.isExampleEnabled) {
context.read<ExampleBloc>().add(EnabledEvent());
}
}
..or listen to bloc changes directly inside controller
class SplashCtrl extends GetxController {
SplashCtrl(MyBloc bloc) {
bloc.stream.listen((event) {
// React to bloc changes here
if (event is MyBlocEvent) {
// Do something
}
}
}
}
Remember that even in this approach bloc instance given to SplashCrtl constructor must be the same that is provided by BlocProvider to a widget tree in order for this to work.
I was able to resolve this by adding a late BuildContext to my controller:
class SplashCtrl extends GetxController {
late BuildContext context;
#override
void onReady() {
// run stuff here
}
After that, in the page itself, I passed the context like this:
class SplashPage extends GetView<SplashCtrl> {
#override
Widget build(BuildContext context) {
controller.context = context;
Get.lazyPut(() => SplashCtrl());
return Scaffold(...);
}
}
Then I was able to use the context within the controller itself!!
if (value) {
context.read<ExampleBloc>().add(EnabledEvent());
} else {
context.read<ExampleBloc>().add(DisabledEvent());
}
},

Detect if current Widget/State is visible in page route?

I want to add a Listener when the widget/state is visible and remove myself from the Listener when I leave the route.
But if the user clicks on the back button and returns to the current widget/state, I want to do the same thing again.
Currently initState, didChangeDependencies, didUpdateWidget and build are NOT called when the user clicks back from the next page, therefore I cannot detect when the user is returning and the widget was loaded from cache.
After much poking around the API, I've discovered that ModalRoute and RouteObserver is what I want.
https://api.flutter.dev/flutter/widgets/ModalRoute-class.html
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html
If I just want to check if the current route is active I can call isCurrent on ModalRoute.of(context):
void onNetworkData(String data) {
if (ModalRoute.of(context).isCurrent) {
setState(() => list = data);
}
}
If I want to listen to route load/unload, I just create it and serve it up the hood with Provider like this:
class HomePage extends StatelessWidget {
final spy = RouteObserver<ModalRoute<void>>();
build(BuildContext context) {
return Provider<RouteObserver<ModalRoute<void>>>.value(
value: spy,
child: MaterialApp(
navigatorObservers: [spy],
),
);
}
}
Then somewhere in another widget:
class _AboutPageState extends State<AboutPage> with RouteAware {
RouteObserver<ModalRoute<void>>? spy;
void didChangeDependencies() {
super.didChangeDependencies();
spy = context.read<RouteObserver<ModalRoute<void>>>();
spy.subscribe(this, ModelRoute.of(context)!);
}
void dispose() {
spy.unsubscribe(this);
super.dispose();
}
void didPush() => attachListeners();
void didPopNext() => attachListeners();
void didPop() => removeListeners();
void didPushNext() => removeListeners();
attachListeners() {
}
removeListeners() {
}
}

Bloc is either calling multiple times or not getting called at all

I have 2 screens
Screen A
Screen B
I go from screen A to Screen B via the below code
onTap: () {
Navigator.pushNamed(context, '/screen_b');
},
Screen B code
class ScreenB extends StatefulWidget {
#override
_ScreenBState createState() => _ScreenBState();
}
class _ScreenBState extends State<ScreenB> {
SampleBloc sampleBloc;
#override
void initState() {
super.initState();
sampleBloc = BlocProvider.of<SampleBloc>(context);
sampleBloc.stream.listen((state) {
// this is getting called multiple times.
}
}
#override
void dispose() {
super.dispose();
sampleBloc.clear(); // If i add this, no event is firing from second time i come to the page. initState() is being called i checked so sampleBloc is not null.
}
#override
Widget build(BuildContext context) {
....
onTap: () {
sampleBloc.add(CreateSampleEvent());
},
....
}
}
When i click tap to add CreateSampleEvent to the sampleBloc in Screen B, the 'sampleBloc.stream.listen' getting fired multiples times.
Sample outcome
Step 1. First do sampleBloc.add (tap) -> sampleBloc.stream.listen
fired one time
Step 2. Go back to Screen A and come back to
Screen B Second go sampleBloc.add (tap) -> In one case i saw
the firing takes place in a pair of 2 times, and in other times the
firing takes place in pair of 4 times.
class SampleBloc extends Bloc<SampleEvent, SampleState> {
final SampleRepository sampleRepository;
SampleBloc({this.sampleRespository}) : super(null);
#override
Stream<SampleState> mapEventToState(SampleEvent event) async* {
if (event is SampleEvent) {
yield* mapSampleEventToState();
}
}
Stream<SampleEvent> mapSampleEventToState() async* {
yield SampleInProgressState();
try {
String sampleId = await sampleRepository.createSample();
if (sampleId != null) {
yield SampleCompletedState(sampleId, uid);
} else {
yield SampleFailedState();
}
} catch (e) {
print('Error: $e');
yield SampleFailedState();
}
}
Any ideas what might be going wrong ?
Since you are creating a manual subscription (by fetching the Bloc and then listening to a stream) you'll also need to manually cancel that subscription when you're done with it, otherwise, the SampleBloc will just keep getting new subscriptions and yielding events to all of them.
For that, you can either:
Save the subscription and then cancel it in the dispose() method.
SampleBloc sampleBloc;
StreamSubscription _subscription;
#override
void initState() {
super.initState();
sampleBloc = BlocProvider.of<SampleBloc>(context);
_subscription = sampleBloc.stream.listen((state) {
// this is getting called multiple times.
}
}
#override
void dispose() {
_subscription.cancel();
super.dispose();
}
You can take advantage of the BlocListener from the package which is a widget that is disposed automatically, hence, removes the subscriptions it created.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: Container(),
)

Widget continuously being reloaded

Please check out this 36 seconds video for more clarity, cause it was getting too verbose explaning things : https://www.youtube.com/watch?v=W6WdQuLjrCs
My best guess
It's due to the provider.
App structure ->
Outer Page -> NoteList Page
The Outer Page code :
class OuterPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return OuterPageState();
}
}
class OuterPageState extends State<OuterPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _selectedTab = 0;
var noteList;
final _pageOptions = [
NoteList(),
AnotherPageScreen(),
];
#override
Widget build(BuildContext context) {
var noteProvider = Provider.of<NotesProvider>(context, listen: false);
var customFabButton;
if (_selectedTab == 0) {
customFabButton = FloatingActionButton(
// Password section
onPressed: () {
navigateToDetail(context, Note('', '', 2), 'Add Note');
},
child: Icon(Icons.add),
);
~~~ SNIP ~~~
The Notes Tab aka NoteList page code :
class NoteList extends StatefulWidget {
NoteList();
#override
NoteListState createState() => NoteListState();
}
class NoteListState extends State<NoteList> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
List<Note> noteList;
int count = 0;
#override
Widget build(BuildContext context) {
Provider.of<NotesProvider>(context).getNotes();
return Scaffold(
key: _scaffoldKey,
body: Provider.of<NotesProvider>(context).count > 0
? NoteListScreen(_scaffoldKey)
: CircularProgressIndicator());
}
}
For full code : check here : https://github.com/LuD1161/notes_app/tree/reusable_components
Update 1 - Possible solution is FutureBuilder
I know that there's a possible solution with FutureBuilder but I think even Provider is apt for this use case.
Moreover is it an anti-pattern here ?
Also, please don't suggest another package for the same thing, if possible try limiting the solution to Provider or base libraries.
Update 2 - Not possible with FutureBuilder
FutureBuilder can't be used here because there's a delete button in the list tile and hence when the note gets deleted the note list won't get updated.
The issue is coming because of the getNotes function you are calling from build method. You are calling notifyListeners from that function. It again re-builds the widget and calls the build method again and this cycle continues.
You either need to set false to the listen property of provider, but it will break your functionality. To fix this, you have to move the getNotes call from build function to initState like following:
#override
void initState() {
super.initState();
postInit(() {
Provider.of<NotesProvider>(context).getNotes();
});
}
Implement postInit (Reference: Flutter Issue 29515):
extension StateExtension<T extends StatefulWidget> on State<T> {
Stream waitForStateLoading() async* {
while (!mounted) {
yield false;
}
yield true;
}
Future<void> postInit(VoidCallback action) async {
await for (var isLoaded in waitForStateLoading()) {}
action();
}
}
Note: Instead of writing the postInit code, you can also use after_init package for same purpose.
Several other posts discussing similar kind of issues:
How to correctly fetch APIs using Provider in Flutter
Using provider in fetching data onLoad