I am trying to increase counter when app call init and using provider to change the state of counter but getting this error i don't understand why i have made the listen to false but no solved kindly help
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:providerpractice/counter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int count = 0;
Timer? timer;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), (timer) {
var pro = Provider.of<Counter>(context, listen: false);
pro.addCounter();
});
}
#override
Widget build(BuildContext context) {
print("build" + count.toString());
return Scaffold(
appBar: AppBar(
title: Text("Provider State Management"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<Counter>(builder: ((context, value, child) {
return Text(
value.counter.toString(),
style: TextStyle(fontSize: 30),
);
}))
],
),
),
);
}
}
If I am not mistaken, you can't use "context" in the initState method, rather move it out of there put it in a didChangeDependancy method.
I am sure someone can elaborate more on this, but the widget has no context until after the initState.
I think the error you're getting is a result of the fact that you do not have any logic to dispose of your active work. Try implementing something in onDispose().
You could also elaborate more on the error you're getting. There is no mention of the error type or statement in your question.
Related
Get the working code sample here
I have an RxList of addOnProducts which contains product and selected attributes.
I am trying to implement the simple multiSelectable grid View, but on clicking the checkBox the selected attribute changes but it is not reflected back to the ui,
If i refresh it will be updated.
I tried Obx()=> (); widget , It is still not updating
My ProductController
class ProductsController extends GetxController {
late Worker worker;
static ProductsController instance = Get.find();
RxList<ProductModel> products = RxList<ProductModel>([]);
RxList<CheckProduct> addOnProducts = <CheckProduct>[].obs;
String collection = "products";
#override
void onReady() {
super.onReady();
products.bindStream(getAllProducts());
worker = once(products, (List<ProductModel> value) {
fillAddOnProducts(value);
}, condition: () => products.isNotEmpty);
}
Stream<List<ProductModel>> getAllProducts() => FirebaseFirestore.instance
.collection(collection)
.snapshots()
.map((query) => query.docs
.map((item) => ProductModel.fromMap(item.data(), item.id))
.toList());
void fillAddOnProducts(List<ProductModel> products) => {
products.forEach((element) {
addOnProducts.add(CheckProduct(product: element, selected: false));
})
};
}
class CheckProduct {
ProductModel product;
bool selected;
CheckProduct(
{required ProductModel this.product, required bool this.selected});
}
My Grid View
class AddOns extends StatelessWidget {
const AddOns({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [],
title: Text("Select Addons"),
),
body: Obx(() => GridView.count(
crossAxisCount: 2,
children: productsController.addOnProducts
.map((element) => ProductWidget(product: element))
.toList(),
)));
}
}
class ProductWidget extends StatelessWidget {
final CheckProduct product;
const ProductWidget({Key? key, required this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(10),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
child: Checkbox(
value: product.selected,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
product.selected.toString());
product.selected = value!;
print("value of product selected after is: " +
product.selected.toString());
},
),
),
],
));
}
}
Therefore in the console it is :
I/flutter (20067): value of the value is : true
I/flutter (20067): value of product selected before is: false
I/flutter (20067): value of product selected after is: true
But the checkBox is not updating, it updates only when i refresh, How to overCome this? Adding Obx() to the parent isn't helping..
Find the github link to code below here which has just the question and and the problem faced..
After going through your code. I've implemented the following that will change state without hot reload:
In your main dart you do not need to put your product controller here as you are not using it
main.dart
import 'package:flutter/material.dart';
import 'grid.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GridSelect(),
);
}
}
Next, I have changed your grid class to generate a list of product widget as the size of the addProduct list length. In my opinion this is a better way to write GridView counts children. Remove obx from your gridview and change your stateful widget to stateless as you are using Getx. It will manage your state even in a stateless widget. Add your product controller here as you will access addProduct list from the controller class.
grid.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:test_project/controllers/productController.dart';
import 'package:test_project/productWidget.dart';
class GridSelect extends StatelessWidget {
final _controller = Get.put(ProductController());
GridSelect({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.count(
crossAxisCount: 2,
children: List.generate(_controller.addOnProducts.length, (index) => ProductWidget(index: index))
),
);
}
}
In your product controller class, remove the instance as it is not important. That is the only change here:
ProductController.dart
import 'package:get/get.dart';
import 'package:test_project/models/productModel.dart';
class ProductController extends GetxController {
RxList<CheckProduct> addOnProducts = <CheckProduct>[].obs;
#override
void onReady() {
super.onReady();
addOnProducts.add(CheckProduct(product: ProductModel('productOne', 20)));
addOnProducts.add(CheckProduct(product: ProductModel('productTwo', 25)));
addOnProducts.add(CheckProduct(product: ProductModel('productThree', 30)));
addOnProducts.add(CheckProduct(product: ProductModel('productFour', 40)));
}
}
class CheckProduct {
ProductModel product;
RxBool selected = false.obs;
CheckProduct({
required this.product,
});
}
Lastly, your productWidget class needs a required value index. So, the widget knows which index in gridview the user is clicking and use Obx() here in checkbox as you have an observable value selected here. Remember to always use Obx() when you have an obs value. This will update the widget whenever an obs value changes. Here, if you notice we are using Get.find() instead of Put as Get.put is already inside the scope so all you need to do is find the controller that you will use. You can find or put multiple controllers and update values as much as you want.
productWidget.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:test_project/controllers/productController.dart';
class ProductWidget extends StatelessWidget {
final ProductController _controller = Get.find();
final int index;
ProductWidget({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(20),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
child: Obx(()=>Checkbox(
value: _controller.addOnProducts[index].selected.value,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
_controller.addOnProducts[index].selected.toString());
_controller.addOnProducts[index].selected.value = value!;
print("value of product selected after is: " +
_controller.addOnProducts[index].selected.toString());
},
)),
)
],
),
);
}
}
Go through GetX documentation for proper use of GetX. Even though I have 2 apps in Playstore with GetX, I still go through documentation from time to time. They have a clear documentation on how to manage state.
In ProductWidget adding an additional Obx() solved my problem
class ProductWidget extends StatelessWidget {
final CheckProduct product;
const ProductWidget({Key? key, required this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(10),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
// Even the child needs Obx() ; The parent's Obx() is not reflected here
child: Obx(()=>(Checkbox(
value: product.selected,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
product.selected.toString());
product.selected = value!;
print("value of product selected after is: " +
product.selected.toString());
},
),))
),
],
));
}
I have 3 page (all statefull widgets) :
Home page
Weather page
Setting page
The things is when i'm going from home page to weather page with a "Navigator.pushNamed" and going from the weather page to home page with a "Navigator.pop", the next time i'm trying to go to the weather page from the home page, initState method is called again...
How i can manage to make it call only the first time and not been called every time i push into the weather page ?
Here my app.dart code :
import 'package:exomind/src/core/views/home_view.dart';
import 'package:exomind/src/features/weather/presentation/views/weather_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import '../injection_container.dart';
import 'core/styles/colors.dart';
import 'features/settings/presentation/bloc/settings_bloc.dart';
import 'features/settings/presentation/views/settings_view.dart';
import 'features/weather/presentation/bloc/weather_bloc.dart';
/// The Widget that configures your application.
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
// Glue the SettingsController to the MaterialApp.
//
// The AnimatedBuilder Widget listens to the SettingsController for changes.
// Whenever the user updates their settings, the MaterialApp is rebuilt.
return MultiBlocProvider(
providers: [
BlocProvider<WeatherBloc>(
create: (_) => serviceLocator<WeatherBloc>()),
BlocProvider<SettingsBloc>(
create: (_) => serviceLocator<SettingsBloc>()
..add(
const SettingsLoaded(),
)),
],
child:
BlocBuilder<SettingsBloc, SettingsState>(builder: (context, state) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// Providing a restorationScopeId allows the Navigator built by the
// MaterialApp to restore the navigation stack when a user leaves and
// returns to the app after it has been killed while running in the
// background.
restorationScopeId: 'app',
// Provide the generated AppLocalizations to the MaterialApp. This
// allows descendant Widgets to display the correct translations
// depending on the user's locale.
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', ''), // English, no country code
],
// Use AppLocalizations to configure the correct application title
// depending on the user's locale.
//
// The appTitle is defined in .arb files found in the localization
// directory.
onGenerateTitle: (BuildContext context) =>
AppLocalizations.of(context)!.appTitle,
// Define a light and dark color theme. Then, read the user's
// preferred ThemeMode (light, dark, or system default) from the
// SettingsController to display the correct theme.
theme:
ThemeData(fontFamily: 'Circular', primaryColor: kPrimaryColor),
darkTheme: ThemeData.dark(),
themeMode: state.themeMode,
// Define a function to handle named routes in order to support
// Flutter web url navigation and deep linking.
onGenerateRoute: (RouteSettings routeSettings) {
return MaterialPageRoute<void>(
settings: routeSettings,
builder: (BuildContext context) {
switch (routeSettings.name) {
case SettingsView.routeName:
return const SettingsView();
case WeatherView.routeName:
return const WeatherView();
case HomeView.routeName:
return const HomeView();
default:
return const HomeView();
}
},
);
},
);
}));
}
}
Here my home_view.dart code :
import 'package:flutter/material.dart';
import '../../features/weather/presentation/views/weather_view.dart';
class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key);
static const routeName = '/home';
#override
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView>
with SingleTickerProviderStateMixin {
late AnimationController rotationController;
#override
void initState() {
rotationController =
AnimationController(duration: const Duration(seconds: 1), vsync: this)
..repeat();
super.initState();
}
#override
void dispose() {
rotationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final double height = MediaQuery.of(context).size.height;
final double width = MediaQuery.of(context).size.width;
return Scaffold(
body: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: (height / 2),
child: RotationTransition(
turns: Tween(begin: 0.0, end: 1.0).animate(rotationController),
child: IconButton(
icon: const Icon(Icons.wb_sunny),
color: Colors.yellow,
iconSize: (width * 0.2),
onPressed: () {
Navigator.of(context).pushNamed(WeatherView.routeName);
},
),
),
)
],
),
);
}
}
Here my weather_view.dart code :
import 'dart:async';
import 'package:exomind/src/features/weather/presentation/bloc/weather_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/percent_indicator.dart';
class WeatherView extends StatefulWidget {
const WeatherView({Key? key}) : super(key: key);
static const routeName = '/weather';
#override
State<WeatherView> createState() => _WeatherViewState();
}
class _WeatherViewState extends State<WeatherView>
with SingleTickerProviderStateMixin {
#override
void initState() {
print("initcalled")
super.initState();
}
#override
void dispose() {
rotationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;
return Scaffold();
}
}
Any help and explanation would be appreciate :)
I can't think of a "clean" way of not executing the initState in _WeatherViewState. Are you trying to avoid the same city added to the WeatherBloc more than once? If so, I'd check for the existence of 'city' in the WeatherBloc before adding.
In your onGenerateRoute you call the WeatherView constructor each time:
case WeatherView.routeName:
return const WeatherView();
This in turn will call initState. What you need to do is create the WeatherView page widget once and use it in the onGenerateRoute:
final _weatherView = const WeatherView();
In your onGenerateRoute:
case WeatherView.routeName:
return _weatherView;
As #RoslanAmir said there is no way to prevent initstate of been called each time we push into a statefulwidget.
So to prevent my event of being added into my bloc each time we push into the stateful widget i add a bool variable to each state to know if the event should be added or not again.
For those who want a precise answer don't hesitate.
Just add a parameter to the Weather page: a boolean that specifies if the rebuild is true or false. (If true, it will call the initState())
This code works fine.
class WeatherView extends StatefulWidget {
final bool rebuild;
static const routeName = '/weather';
WeatherView({
Key? key,
required this.rebuild,
}) : super(key: key);
#override
State<WeatherView> createState() => _WeatherViewState();
}
and the WeatherViewState's initState() will be:
#override
void initState() {
if (widget.rebuild) {
print("initcalled");
super.initState();
} else {
print("Not called");
}
}
So, in your app.dart you should now route to the page by doing
case WeatherView.routeName:
return const WeatherView(rebuild: true); //Choose if rebuild or not by true and false
I want to keep child widget state using GlobalKey after parent's state is changed. There is a workaround by using Opacity in order to solve the problem, but I wonder why GlobalKey doesn't work as expected in this scenario.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Retrieve Text Input',
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
final _key = GlobalKey();
bool _showTimer = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextButton(
onPressed: () => setState(() {
_showTimer = !_showTimer;
}),
child: Text('show/hide')),
_showTimer ? TimerWidget(key: _key) : Container()
],
),
));
}
}
class TimerWidget extends StatefulWidget {
const TimerWidget({Key key}) : super(key: key);
#override
_TimerWidgetState createState() => _TimerWidgetState();
}
const int TIME_REMINDING_SECONDS = 480;
class _TimerWidgetState extends State<TimerWidget> {
Timer _timer;
int _start = TIME_REMINDING_SECONDS;
#override
Widget build(BuildContext context) {
return Text(
'${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
}
#override
initState() {
super.initState();
_startTimer();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
_startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
} else {
_start = _start - 1;
}
},
),
);
}
}
You will see the timer restarts to initial value every times the parent's state is changed. I tried with the solutions here but didn't work.
as an option you can skip GlobalKey and simple use Offstage widget
Offstage(offstage: !_showTimer, child: TimerWidget()),
another answer mentioned Visibility with maintainState parameter.
This is pointless because it uses Offstage under the hood.
By Every time in the previous code every time the state changes it creates a new instance of timer so GlobalKey won't take effect there since its new instance.
Global keys uniquely identify elements. Global keys provide access to
other objects that are associated with those elements, such as
BuildContext. For StatefulWidgets, global keys also provide access to
State.
https://api.flutter.dev/flutter/widgets/GlobalKey-class.html
By the Above statement, the global key is used to access the state within the widgget.
So in your case when TimerWidget() switches it's disposed of its state and not gonna preserve that's why its timer getting reset every time you change state.
--- Update ---
Instead of _showTimer ? TimerWidget(key: _key) : Container()
Use below code:
Visibility(
visible: _showTimer,
maintainState: true,
child: page
)
Here, maintain state is keeping the state of the widget.
Update
The following code moves the scope of a globally unique key so that it will maintain its state while the app lives. When adding this key to an Offset widget, you can show/hide the timer while retaining its state. Without this step, the timer widget would continue to reset as the timer widget is removed and re-added to the rendering tree. I also added the late modifier to the state class _timer variable.
Removing the timer widget from the tree will normally call the dispose method; so one alternative is to use Offstage which is designed to temporarily remove widgets based on state. This seems to be precisely what you are attempting to do. However, the Visibility widget does this same behavior without having to maintain a Global Key (but your focus seemed to be on wanting to leverage a key). Note the other widgets discussed in Visibility notes may provide other alternatives.
Some important considerations:
Animations continue to run when using Offstage widget.
From the docs (on the Offstage widget):
A widget that lays the child out as if it was in the tree, but without
painting anything, without making the child available for hit testing,
and without taking any room in the parent.
Offstage children are still active: they can receive focus and have
keyboard input directed to them.
Animations continue to run in offstage children, and therefore use
battery and CPU time, regardless of whether the animations end up
being visible.
Offstage can be used to measure the dimensions of a widget without
bringing it on screen (yet). To hide a widget from view while it is
not needed, prefer removing the widget from the tree entirely rather
than keeping it alive in an Offstage subtree.
From the docs (on the Visibility widget):
By default, the visible property controls whether the child is
included in the subtree or not; when it is not visible, the
replacement child (typically a zero-sized box) is included instead.
A variety of flags can be used to tweak exactly how the child is
hidden. (Changing the flags dynamically is discouraged, as it can
cause the child subtree to be rebuilt, with any state in the subtree
being discarded. Typically, only the visible flag is changed
dynamically.)
These widgets provide some of the facets of this one:
Opacity, which can stop its child from being painted. Offstage, which can stop its child from being laid out or painted.
TickerMode, which can stop its child from being animated. ExcludeSemantics, which can hide the child from accessibility tools. IgnorePointer, which can disable touch interactions with
the child. Using this widget is not necessary to hide children. The
simplest way to hide a child is just to not include it, or, if a
child must be given (e.g. because the parent is a StatelessWidget)
then to use SizedBox.shrink instead of the child that would
otherwise be included.
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
//create a key that will persist in app scope
var timerKey = GlobalKey();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Retrieve Text Input',
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
bool _showTimer = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextButton(
onPressed: () => {
setState(() {
_showTimer = !_showTimer;
})
},
child: Text('show/hide')),
//reuse the current timer logic to show/hide the time
Offstage(
offstage: _showTimer,
child: TimerWidget(
key: (timerKey),
),
)
],
),
));
}
}
class TimerWidget extends StatefulWidget {
const TimerWidget({Key? key}) : super(key: key);
#override
_TimerWidgetState createState() => _TimerWidgetState();
}
const int TIME_REMINDING_SECONDS = 480;
class _TimerWidgetState extends State<TimerWidget> {
late Timer _timer;
int _start = TIME_REMINDING_SECONDS;
#override
Widget build(BuildContext context) {
return Text(
'${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
}
#override
initState() {
super.initState();
_startTimer();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
_startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
} else {
_start = _start - 1;
}
},
),
);
}
}
Nota Bene
Visibility does not require a key at all.
Visibility(
visible: _showTimer,
maintainState: true,
child: TimerWidget(),
),
Original
Review my related question here. You will want to ensure that a Unique Key is available to the parent widget before you start to use the child. My example is pretty in-depth; let me know if you have follow-up issues.
I cannot use StreamGroup or StreamZip for some reason
here's the code (that is complete useless, but I don't want to anger the admins)
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp() : super(key: const Key('MyApp'));
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatefulWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const _duration = Duration(seconds: 3);
static const _initialCx = 3.0;
final _pageStream = Stream<int>.periodic(_duration, (int i) => i);
ScrollController _scrollController;
StreamController<double> _throttle;
#override
void initState() {
_scrollController = ScrollController();
_throttle = StreamController();
StreamGroup<num>.merge([_pageStream, _throttle.stream]); // <= this
StreamZip([_pageStream, _throttle.stream]); // or this
/// i NEED TO IMPLEMENT A LISTENER HERE... THE CODE IS USELESS OTHERWISE
super.initState();
}
#override
void dispose() {
_scrollController?.dispose();
_throttle?.close();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
body: Stack(children: <Widget>[
ListView.builder(
controller: _scrollController,
itemBuilder: (BuildContext context, int index) => const Center(
child: FlutterLogo(size: 160),
),
),
Align(
alignment: Alignment.bottomCenter,
child: StreamBuilder<double>(
builder: (context, throttle) => Slider(
min: 1,
max: 5,
onChanged: (double value) => _throttle.sink.add(value),
value: throttle.data ?? _initialCx,
),
),
),
]),
);
}
here are the errors respectively for
StreamZip
The method 'StreamZip' isn't defined for the type '_MyHomePageState'.
Try correcting the name to the name of an existing method, or defining
a method named 'StreamZip'.
SteamGroup
The name 'StreamGroup' isn't a class. Try correcting the name to match
an existing class.
I admittedly don't have much experience with those classes, so the solution might be trivial,
can you please explain me how do I access those?
Thank you
It's not obvious due to the way that the Flutter API documentation merges everything together, but StreamZip and StreamGroup come from package:async, not from dart:async. You'll need to replace import 'dart:async'; with import 'package:async/async.dart'; and add an async dependency in your pubspec.yaml file.
(Even though package:async is from the Dart developers, it's separate from dart:async because:
The classes and functions in package:async aren't considered to be core parts of the Dart SDK.
package:async can be implemented in pure Dart code.
A separate package can be updated separately from the Dart SDK, allowing for more frequent updates.)
I am trying to use the BLoC pattern to manage data from an API and show them in my widget. I am able to fetch data from API and process it and show it, but I am using a bottom navigation bar and when I change tab and go to my previous tab, it returns this error:
Unhandled Exception: Bad state: Cannot add new events after calling close.
I know it is because I am closing the stream and then trying to add to it, but I do not know how to fix it because not disposing of the publish subject will result in a memory leak.
I know maybe this question is almost the same as this question.
But I have implemented it and it doesn't work in my case, so I make questions with a different code and hope someone can help me in solving my case. I hope you understand, Thanks.
Here is my BLoC code:
import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';
import '../models/meals_list.dart';
class MealsBloc {
final _repository = Repository();
final _mealsFetcher = PublishSubject<MealsList>();
Observable<MealsList> get allMeals => _mealsFetcher.stream;
fetchAllMeals(String mealsType) async {
MealsList mealsList = await _repository.fetchAllMeals(mealsType);
_mealsFetcher.sink.add(mealsList);
}
dispose() {
_mealsFetcher.close();
}
}
final bloc = MealsBloc();
Here is my UI code:
import 'package:flutter/material.dart';
import '../models/meals_list.dart';
import '../blocs/meals_list_bloc.dart';
import '../hero/hero_animation.dart';
import 'package:dicoding_submission/src/app.dart';
import 'detail_screen.dart';
class DesertScreen extends StatefulWidget {
#override
DesertState createState() => new DesertState();
}
class DesertState extends State<DesertScreen> {
#override
void initState() {
super.initState();
bloc.fetchAllMeals('Dessert');
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: getListDesert()
);
}
getListDesert() {
return Container(
color: Color.fromRGBO(58, 66, 86, 1.0),
child: Center(
child: StreamBuilder(
stream: bloc.allMeals,
builder: (context, AsyncSnapshot<MealsList> snapshot) {
if (snapshot.hasData) {
return _showListDessert(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white)
));
},
),
),
);
}
Widget _showListDessert(AsyncSnapshot<MealsList> snapshot) => GridView.builder(
itemCount: snapshot == null ? 0 : snapshot.data.meals.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child: Card(
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5))),
margin: EdgeInsets.all(10),
child: GridTile(
child: PhotoHero(
tag: snapshot.data.meals[index].strMeal,
onTap: () {
showSnackBar(context, snapshot.data.meals[index].strMeal);
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 777),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
DetailScreen(
idMeal: snapshot.data.meals[index].idMeal),
));
},
photo: snapshot.data.meals[index].strMealThumb,
),
footer: Container(
color: Colors.white70,
padding: EdgeInsets.all(5.0),
child: Text(
snapshot.data.meals[index].strMeal,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.deepOrange),
),
),
),
),
);
},
);
}
If you need the full source code, this is the repo with branch submission-3
bloc.dispose(); is the problem.
Since the bloc is initialised outside your UI code, there is no need to dispose them.
Why are you instantiating your bloc on the bloc class?
You must add your bloc instance somewhere in your widget tree, making use of a InheritedWidget with some Provider logic. Then in your widgets down the tree you would take that instance and access its streams. That is why this whole process it is called 'lifting up the state'.
That way, your bloc will always be alive when you need it, and the dispose would still be called sometime.
A bloc provider for example:
import 'package:flutter/material.dart';
abstract class BlocBase {
void dispose();
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
final T bloc;
final Widget child;
#override
State<StatefulWidget> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider = context
.ancestorInheritedElementForWidgetOfExactType(type)
?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>> {
#override
Widget build(BuildContext context) {
return new _BlocProviderInherited(
child: widget.child,
bloc: widget.bloc
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc
}) : super(key: key, child: child);
final T bloc;
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
It makes use of a combination of InheritedWidget (to be available easily down the widget tree) and StatefulWidget (so it can be disposable).
Now you must add the provider of some bloc somewhere into your widget tree, that is up to you, I personally like to add it between the routes of my screens.
In the rout of my MaterialApp widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
onGenerateRoute: _routes,
);
}
Route _routes(RouteSettings settings) {
if (settings.isInitialRoute)
return MaterialPageRoute(
builder: (context) {
final mealsbloc = MealsBloc();
mealsbloc.fetchAllMeals('Dessert');
final homePage = DesertScreen();
return BlocProvider<DesertScreen>(
bloc: mealsbloc,
child: homePage,
);
}
);
}
}
With the help of routes, the bloc was created 'above' our homePage. Here I can call wherever initialization methods on the bloc I want, like .fetchAllMeals('Dessert'), without the need to use a StatefulWidget and call it on initState.
Now obviously for this to work your blocs must implements the BlocBase class
class MealsBloc implements BlocBase {
final _repository = Repository();
final _mealsFetcher = PublishSubject<MealsList>();
Observable<MealsList> get allMeals => _mealsFetcher.stream;
fetchAllMeals(String mealsType) async {
MealsList mealsList = await _repository.fetchAllMeals(mealsType);
_mealsFetcher.sink.add(mealsList);
}
#override
dispose() {
_mealsFetcher.close();
}
}
Notice the override on dispose(), from now on, your blocs will dispose themselves, just make sure to close everything on this method.
A simple project with this approach here.
To end this, on the build method of your DesertScreen widget, get the available instance of the bloc like this:
var bloc = BlocProvider.of<MealsBloc>(context);
A simple project using this approach here.
For answers that resolve my problem, you can follow the following link: This
I hope you enjoy it!!