Animating between provider stream changes - flutter

I have a pageview in flutter with 5 pages, each with its own scaffold. All states are managed through providers of streams or values. I have a stream that has it's own built in method which passes InternetConnected.connected or disconnected. When the internet connection is lost, I want to load a separate UI in the specific page which shows internet connection lost instead of the widgets that were previously present in the scaffold.
How I'm doing it now (pseudo code):
class ConnectionScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final connection = Provider.of<InternetStatus>(context);
detectConnection () {
if (connection.InternetConnection == InternetConnection.connected)
return Container(); // Returns UI when internet on
else
return Container(); // Returns disconnected UI
}
return Scaffold(body: detectConnection());
}
Two Questions:
I want to animate the transition between the two states ie. Connected and Disconnected with the disconnection screen flowing down from the top of the display and vice versa. What is the correct way to do that with provider state management? Right now it just rebuilds instantaneously, which is not very 'pretty'.
Since Provider.of<> does not allow granular rebuilds in case the stream value changes, how would I make it so that other properties 'provided' by provider are handled better? I know about Consumer and Selector, but those are rebuilding the UI too...
Thanks in advance

Animation
An AnimatedSwitcher widget (included in SDK, not related to Provider) might suffice to animate between your two widgets showing connected / disconnected states. (AnimatedContainer might work as well if you're just switching a color or something else in the constructor argument list of Container.)
A key is needed for the child of AnimatedSwitcher when the children are of the same class, but differ internally. If they're completely different Types, Flutter knows to animate between the two, but not if they're the same Type. (Has to do with how Flutter analyzes the widget tree looking for needed rebuilds.)
Rebuild Only Affected Widgets
In the example below, the YellowWidget isn't being rebuilt, and neither is its parent. Only the Consumer<InternetStatus> widget is being rebuilt when changing from Connected to Disconnected statuses in the example.
I'm not an expert on Provider and I find it easy to make mistakes in knowing which Provider / Consumer / Selector / watcher to use to avoid unnecessary rebuilds. You might be interested in other State Management solutions like Get, or RxDart+GetIt, etc. if Provider doesn't click for you.
Note: an extra Builder widget is used as parent to the child of ChangeNotifierProvider, to make everything underneath a child. This allows InheritedWidget to function as intended (the base upon which Provider is built). Otherwise, the child of ChangeNotifierProvider would actually share its context and be its sibling, not descendant.
i.e. They'd both get the context shown here:
class ProviderGranularPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
This is also a tricky nuance of Flutter. If you wrap your entire MaterialApp or MyApp widget in Provider, this extra Builder is obviously unnecessary.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class InternetStatus extends ChangeNotifier {
bool connected = true;
void setConnect(bool _connected) {
connected = _connected;
notifyListeners();
}
}
/// Granular rebuilds using Provider package
class ProviderGranularPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<InternetStatus>(
create: (_) => InternetStatus(),
child: Builder(
builder: (context) {
print('Page (re)built');
return SafeArea(
child: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 3,
child: Consumer<InternetStatus>(
builder: (context, inetStatus, notUsed) {
print('status (re)built');
return AnimatedSwitcher(
duration: Duration(seconds: 1),
child: Container(
key: getStatusKey(context),
alignment: Alignment.center,
color: getStatusColor(inetStatus),
child: getStatusText(inetStatus.connected)
),
);
},
),
),
Expanded(
flex: 3,
child: YellowWidget(),
),
Expanded(
flex: 1,
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Connect'),
onPressed: () => setConnected(context, true),
),
RaisedButton(
child: Text('Disconnect'),
onPressed: () => setConnected(context, false),
)
],
),
),
)
],
),
),
);
},
),
);
}
/// Show other ways to access Provider State, using context & Provider.of
Key getStatusKey(BuildContext context) {
return ValueKey(context.watch<InternetStatus>().connected);
}
void setConnected(BuildContext context, bool connected) {
Provider.of<InternetStatus>(context, listen: false).setConnect(connected);
}
Color getStatusColor(InternetStatus status) {
return status.connected ? Colors.blue : Colors.red;
}
Widget getStatusText(bool connected) {
String _text = connected ? 'Connected' : 'Disconnected';
return Text(_text, style: TextStyle(fontSize: 25));
}
}
class YellowWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Yellow was (re)built');
return Container(
color: Colors.yellow,
child: Center(
child: Text('This should not rebuild'),
),
);
}
}

Related

Are states not passed to BlocBuilder widgets that are children of a BlocConsumer widget?

I am new to Flutter Bloc and must be missing how State changes are processed by the UI widgets. At the top level I have a BlocConsumer and under that I have nested BlocBuilder widgets with buildWhen methods to indicate when and how the Bloc widget should be rebuilt. Based on print statements,it looks like the Bloc state is consumed in the top level BlocConsumer widget and never makes it down to the lower level BlocBuilder widgets.
The code below should
Display circular progress bar on startup - this works ok
Call a bunch of APIs - This is happening
In the meantime display the initial screen with default text values in various widgets - this happens
As API returns and Bloc passes states on the stream, the appropriate UI widget should be rebuilt replacing default text with the data in the stream object. -- this doesn't happen.
Code snippets:
RaspDataStates issued by Bloc (Just showing for reference. Not showing all subclasses of RaspDataState):
#immutable
abstract class RaspDataState {}
class RaspInitialState extends RaspDataState {
#override
String toString() => "RaspInitialState";
}
class RaspForecastModels extends RaspDataState {
final List<String> modelNames;
final String selectedModelName;
RaspForecastModels(this.modelNames, this.selectedModelName);
}
...
Bloc just to show how initialized. Code all seems to work fine and isn't shown.
class RaspDataBloc extends Bloc<RaspDataEvent, RaspDataState> {
RaspDataBloc({required this.repository}) : super(RaspInitialState());
#override
RaspDataState get initialState => RaspInitialState();
...
Now to the UI widget.
class SoaringForecast extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<RaspDataBloc>(
create: (BuildContext context) =>
RaspDataBloc(repository: RepositoryProvider.of<Repository>(context)),
child: RaspScreen(repositoryContext: context),
);
}
}
class RaspScreen extends StatefulWidget {
final BuildContext repositoryContext;
RaspScreen({Key? key, required this.repositoryContext}) : super(key: key);
#override
_RaspScreenState createState() => _RaspScreenState();
}
class _RaspScreenState extends State<RaspScreen>
with SingleTickerProviderStateMixin, AfterLayoutMixin<RaspScreen> {
// Executed only when class created
#override
void initState() {
super.initState();
_firstLayoutComplete = false;
print('Calling series of APIs');
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
_mapController = MapController();
}
#override
void afterFirstLayout(BuildContext context) {
_firstLayoutComplete = true;
print(
"First layout complete. mapcontroller is set ${_mapController != null}");
_setMapLatLngBounds();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
drawer: AppDrawer.getDrawer(context),
appBar: AppBar(
title: Text('RASP'),
actions: <Widget>[
IconButton(icon: Icon(Icons.list), onPressed: null),
],
),
body: BlocConsumer<RaspDataBloc, RaspDataState>(
listener: (context, state) {
print('In forecastLayout State: $state'); << Can see all streamed states here
if (state is RaspDataLoadErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green,
content: Text(state.error),
),
);
}
}, builder: (context, state) {
print('state is $state'); << Only see last streamed state here
if (state is RaspInitialState || state is RaspDataLoadErrorState) {
print('returning CircularProgressIndicator');
return Center(
child: CircularProgressIndicator(),
);
}
print('creating main screen'); << Only see this when all streams complete
return Padding(
padding: EdgeInsets.all(8.0),
child:
Column(mainAxisAlignment: MainAxisAlignment.start, children: [
getForecastModelsAndDates(),
getForecastTypes(),
displayForecastTimes(),
returnMap()
]));
}));
}
Widget getForecastModelsAndDates() {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: forecastModelDropDownList(), // ForecastModelsWidget()
),
Expanded(
flex: 7,
child: Padding(
padding: EdgeInsets.only(left: 16.0),
child: forecastDatesDropDownList(),
)),
],
);
}
// Display GFS, NAM, ....
Widget forecastModelDropDownList() {
return BlocBuilder<RaspDataBloc, RaspDataState>(
buildWhen: (previous, current) {
return current is RaspInitialState || current is RaspForecastModels;
}, builder: (context, state) {
if (state is RaspInitialState || !(state is RaspForecastModels)) {
return Text("Getting Forecast Models");
}
var raspForecastModels = state;
print('Creating dropdown for models');
return DropdownButton<String>(
value: (raspForecastModels.selectedModelName),
isExpanded: true,
iconSize: 24,
elevation: 16,
onChanged: (String? newValue) {
BlocProvider.of<RaspDataBloc>(context)
.add(SelectedRaspModel(newValue!));
},
items: raspForecastModels.modelNames
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value.toUpperCase()),
);
}).toList(),
);
});
}
... more BlocBuilder child widgets similar to the one above
The print statements in the console are:
Calling series of APIs
state is RaspInitialState
returning CircularProgressIndicator
First layout complete. mapcontroller is set true
... (First of bunch of API output displays - all successful)
state is RaspInitialState << Not sure why this occurs again
returning CircularProgressIndicator
... (More API output displays - all successful)
streamed RaspForecastModels
In forecastLayout State: Instance of 'RaspForecastModels' << Doesn't cause widget to be rebuild
streamed RaspForecastDates << Other states being produced by Bloc
In forecastLayout State: Instance of 'RaspForecastDates'
streamed RaspForecasts
In forecastLayout State: Instance of 'RaspForecasts'
In forecastLayout State: Instance of 'RaspForecastTime'
streamed RaspMapLatLngBounds
In forecastLayout State: Instance of 'RaspMapLatLngBounds'
state is Instance of 'RaspMapLatLngBounds'
creating main screen
Any words of wisdom on the errors of my way would be appreciated.
I added this earlier as a comment but then found Stackoverflow didn't initially show my comment (I needed to click on show more). So here it is in better readable form.
Problem solved. I needed to move the line:
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
from the initState() method to afterFirstLayout().
All blocbuilders then executed and the UI was built appropriately . And to answer my title question, the bloc states are broadcast and can be picked up by different BlocBuilders.

How dispose Global key from provider with flutter web?

Want to build drawer with flutter web. But got
Duplicate GlobalKey detected in widget tree.
instance is moved to the new location. The key was:
[LabeledGlobalKey#c9754]
GlobalKey reparenting is:
MainScreen(dependencies: [MediaQuery], state: _MainScreenState#dc897)
A GlobalKey can only be specified on one widget at a time in the widget tree.
import 'package:flutter/material.dart';
class MenuController with ChangeNotifier {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
GlobalKey<ScaffoldState> get scaffoldKey => _scaffoldKey;
void controlMenu() {
if (!_scaffoldKey.currentState!.isDrawerOpen) {
_scaffoldKey.currentState!.openDrawer();
}
}
// void disposeKey() {
// _scaffoldKey.currentState.();
// }
}
class _MainScreenState extends State<MainScreen> {
#override
void initState() {
print('init CALLED- GAME---');
super.initState();
}
#override
void dispose() {
print('DISPOSE CALLED- GAME---');
context.read<MenuController>().scaffoldKey.currentState!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: context.read<MenuController>().scaffoldKey,
drawer: SideMenu(),
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// We want this side menu only for large screen
if (Responsive.isDesktop(context))
Expanded(
// default flex = 1
// and it takes 1/6 part of the screen
child: SideMenu(),
),
Expanded(
// It takes 5/6 part of the screen
flex: 5,
child: DashboardScreen(),
),
],
),
),
);
}}
then want to reuse key from another widget
class ProductsScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
key: context.read<MenuController>().scaffoldKey,
drawer: SideMenu(),
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// We want this side menu only for large screen
if (Responsive.isDesktop(context))
Expanded(
// default flex = 1
// and it takes 1/6 part of the screen
child: SideMenu(),
),
Expanded(
// It takes 5/6 part of the screen
flex: 5,
child: ProductsListScreen(),
),
],
),
),
);
}
}
and got bug
To navigate inside SideMenu widget use
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ProductsScreen(),
),
);
You do not need to manually dispose of a GlobalKey. The main requirement is that they cannot be inserted into the widget tree twice. This is not the case with other keys (LocalKeys):
// this is allowed
Row(
children: [
SizedBox(key: Key('hello')),
Container(key: Key('hello')),
],
)
// this is not
final key = GlobalKey<ScaffoldState>();
Row(
children: [
Scaffold(key: key),
Scaffold(key: key),
],
)
A common reason why this is violated is animations. While the animation between one page and another page is playing, both pages are in the widget tree, and if they have the same GlobalKey, an error will be thrown.
Calling globalKey.currentState!.dispose() actually disposes the State of the associated widget. You should not call this yourself.
Instead, provide a new GlobalKey to the second subtree or remove the old one before navigating to the new page.

Navigate part of screen from drawer

let's say I have an app with the following setup:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
I would like to know how can I navigate only the MainLoginScreen widget from the MainMenu with any .push() method.
(I found a way to navigate from a context inside the mainloginscreen,by wrapping it with a MaterialApp widget, but what if I want to use the MainMenu widget instead, which has another context)
There is a general agreement that a 'screen' is a topmost widget in the route. An instance of 'screen' is what you pass to Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()). So if it is under Scaffold, it is not a screen. That said, here are the options:
1. If you want to use navigation with 'back' button
Use different screens. To avoid code duplication, create MenuAndContentScreen class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
Then for each screen create a pair of a screen and a nested widget:
class MainLoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2. If you do not need navigation with 'back' button
You may use IndexedStack widget. It can contain multiple widgets with only one visible at a time.
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
The first way is generally preferred as it is declrative which is a major idea in Flutter. When you have the entire widget tree statically declared, less things can go wrong and need to be tracked. Once you feel it, it really is a pleasure. And if you want to avoid back navigation, use replacement as ahmetakil has suggested in a comment: Navigator.of(context).pushReplacement(...)
The second way is mostly used when MainMenu needs to hold some state that needs to be preserved between views so we choose to have one screen with interchangeable content.
3. Using a nested Navigator widget
As you specifically asked about a nested Navigator widget, you may use it instead of IndexedStack:
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
This should do the trick, however it is a bad practice because:
MainMenu knows that a particular Navigator exists and it should interact with it. It is better to either abstract this knowledge with a callback as in (2) or do not use a specific navigator as in (1). Flutter is really about passing information down the tree and not up.
At some point you would like to highlight the active item in MainMenu, but it is hard for MainMenu to know which widget is currently in the Navigator. This would add yet another non-down interaction.
For such interaction there is BLoC pattern
In Flutter, BLoC stands for Business Logic Component. In its simpliest form it is a plain object that is created in the parent widget and then passed down to MainMenu and Navigator, these widgets may then send events through it and listen on it.
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
#override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
#override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
#override
void dispose() {
_subscription.cancel();
}
This has advantages:
MainMenu does not know who will receive the event, if anybody.
Any number of listeners may listen on such events.
However, there is still a fundamental flaw with Navigator. It can be navigated without MainMenu knowledge using 'back' button or by its internal widgets. So there is no single variable that knows which page is showing now. To highlight the active menu item, you would query the Navigator's stack which eliminates the benefits of BLoC.
For all these reasons I still suggest one of the first two solutions.

using Provider to avoid rebuild all widget tree in flutter

as my understand, one of Provider benefits is to avoid rebuild the widget tree, by calling build function, but when i try it practically with this simple example:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:providerexamplelast/counterWidget.dart';
void main() => runApp(ChangeNotifierProvider<Provider1>(
create: (_) => Provider1(),
child: MaterialApp(
home: Counter(),
),
));
int n =0;
class Counter extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("${n++}");
var counter = Provider.of<Provider1>(context);
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: ()=> counter.counter(),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("1"),
SizedBox(height: 5,),
countText(),
SizedBox(height: 5,),
Text("3"),
SizedBox(height: 5,),
Text("4"),
],
),
),
);
}
}
Widget countText(){
return Builder(
builder: (context){
var count = Provider.of<Provider1>(context);
return Text("${count.c}");
},
);
}
by using this part :
print("${n++}");
i noticed that (build) function is recall whenever i press the button and call (counter) function from provider?
so the question here it is just (Stateless) widget, how it rebuild again? and then why i need to use Provider if it is not solve this problem?
Edit: I heard about this way:
var counter = Provider.of<Provider1>(context, listen: false);
so is it solve this problem? and how?
var counter = Provider.of<Provider1>(context, listen: false);
The Provider is one of state management mechanism which flutter have, under the hood, Provider keeps track of the changes which are done inside the widget. It doesn't matter to Provider whether it's a Stateless widget or Stateful widget, It's gonna rebuild widget if anything gets change.
listen: false tells the Provider not to rebuild widget even if data gets modified.

flutter_bloc subwidget closes the bloc of the parent widget

I am using the flutter_bloc package.
I have a page with a tabbar.
The page creates a bloc and provides it to the widgets, which are used as tab content.
class _MyScreenState extends State<MyScreen> {
MyBloc _myBloc;
...
Widget _buildTabBar() {
...
var children = [
_buildFirstTab()
_buildSecondTab(),
];
return WillPopScope(
onWillPop: _onWillPop,
child: DefaultTabController(
length: children.length,
child: Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(75.0),
child: AppBar(
bottom: TabBar(isScrollable: true, tabs: tabs),
)),
body: TabBarView(
children: children,
),
),
),
);
}
..
}
Widget _buildSecondTab() {
if (_myBloc == null) {
_myBloc = MyBloc(<..initialdata..>);
}
_myBloc.add(LoadServerDataEvent());
return BlocProvider<MyBloc>(create: (_) => _myBloc, child: SecondTab());
}
The widget on the second tab gets the bloc from the BlocProvider and uses it.
class _SecondTabState extends State<SecondTab> {
MyBloc _myBloc;
#override
void initState() {
_myBloc = BlocProvider.of<MyBloc>(context);
super.initState();
}
My problem is that the block is automatically closed when I switch between the tabs.
This is kind of strange, because the block is still used on the page.
Well, you are doing a couple of things wrong:
if you want your bloc to be accessible in both first and second tab you should wrap the whole tab bar with it,
that's very odd that you keep an instance of a bloc in _MyScreenState and do some magic with lazy init singeltone if (_myBloc == null) _myBloc = MyBloc(),
The _myBloc.add(LoadServerDataEvent()); in most cases should be triggered in initState method of the _SecondTabState .
That's very unusal that a bloc gets initial data from the UI. If you want to build your architecture using blocs this initial data should come from another bloc.
But if you want to provide an existing bloc to the second tab and prevent it from being closed you should use:
BlocProvider.value(
value: _myBloc,
child: SecondTab(),
);
Instead:
BlocProvider<MyBloc>(create: (_) => _myBloc, child: SecondTab());