I received the error of `A ValueNotifier was used after being disposed.
Step to reproduce the error:
I navigate to menu.dart from homePage.dart.
Then, I go back from menu.dart to homePage.dart.
I navigate again to menu.dart. The error happened.
Error message
FlutterError (A `ValueNotifier<bool>` was used after being disposed.
Once you have called `dispose()` on a ValueNotifier<bool>, it can no longer be used.)
clearNotifier.dart
import 'package:flutter/material.dart';
ValueNotifier<bool> cancelListen =ValueNotifier(false);
homePage.dart
import 'package:project/pages/MenuFrame.dart';
...
IconButton(
icon: Image.asset('assets/image.png'),
iconSize: 50,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChangeNotifierProvider<ValueNotifier>(
create: (_) => cancelListen,
child: MenuFrame(
userId: widget.userId
)
),
// MaterialPageRoute(
// builder: (BuildContext context) => MenuFrame(
// userId: widget.userId,
// ),
),
)
.then(
);
},
)
menu.dart
import 'package:project/class/clearNotifier.dart';
class MenuFrame extends StatefulWidget {
const MenuFrame({Key key, this.userId}) : super(key: key);
final String userId;
#override
_MenuFrame createState() => _MenuFrameState();
}
#override
void dispose() {
cancelListen?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: cancelListen,
builder: (BuildContext context, bool toClearListen,Widget child){
....
});
}
How can I rebuild the ValueNotifier once it has been disposed?
When you navigate from menu.dart back to homePage.dart, it call dispose function in menu.dart and your variable cancelListen was disposed. Therefore, when you navigate again to menu.dart, it will throw an error as you see.
Suggestion:
Do not pass variable cancelListen like that. You should create another ValueNotifier variable, I would temporary call it _cancelNotifier. You will pass the current value to homePage.dart:
MenuFrame(
userId: widget.userId,
value: cancelListen.value,
)
...............
late ValueNotifier<bool> _cancelNotifier;
initState() {
_cancelNotifier = ValueNotifier<bool>(widget.value);
}
Related
I am learning flutter + bloc and start with a demo.
Start of project I create starting app by delay 3 second and next to home page like this:
StartCubit
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'start_state.dart';
class StartCubit extends Cubit<StartState> {
StartCubit() : super(StartInitial());
void startRunning() {
loadData();
}
void loadData() async {
emit(StartDoing(0));
await Future.delayed(Duration(seconds: 1));
emit(StartDoing(1));
await Future.delayed(Duration(seconds: 1));
emit(StartDoing(2));
await Future.delayed(Duration(seconds: 1));
emit(StartDoing(3));
await Future.delayed(Duration(seconds: 1));
emit(StartDone());
}
}
And this is code in start page:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:money_lover/home_page/home_page.dart';
import 'package:money_lover/start/bloc/start_cubit.dart';
class StartPage extends StatelessWidget {
const StartPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => StartCubit(),
child: StartView(),
);
}
}
class StartView extends StatefulWidget {
const StartView({Key? key}) : super(key: key);
#override
State<StartView> createState() => _StartViewState();
}
class _StartViewState extends State<StartView> {
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: BlocBuilder<StartCubit, StartState>(
builder: (context, state) {
if (state is StartInitial) {
context.read<StartCubit>().startRunning();
} else if (state is StartDone) {
Future.delayed(Duration.zero, () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
});
}
return Text('Starting $state');
},
)));
}
}
If I don't call future delay with zero time It will show error before next screen.
And I don't need print stateDone when go to next screen, have any way to code more correctly ?
I tried add delay addPostFrameCallback in this link Error: Unhandled Exception: 'package:flutter/src/widgets/navigator.dart': Failed assertion: line 2845 pos 18: '!navigator._debugLocked': is not true
it is ok but I think maybe I code not correctly way.
You should be using a listener to handle the navigation.
In your case a blocconsumer will do
BlocConsumer exposes a builder and listener in order react to new states.
BlocConsumer<StartCubit,StartState>(
listener: (context, state) {
if (state is StartDone) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
}
},
builder: (context, state) {
if (state is StartInitial) {
// Instead of running this here, you can run it in your build if you want init state like behavior.
context.read<StartCubit>().startRunning();
}
return Text('Starting $state');
}
)
Running in build method
class _StartViewState extends State<StartView> {
#override
Widget build(BuildContext context) {
context.read<StartCubit>().startRunning();
return Scaffold(body: Center(child: BlocBuilder<StartCubit, StartState>(
I've already checked the previous answer but can't help me.
I'm checking that user has internet or not if not then I'm showing pic of no internet. Now I've a button of Retry where user can click on it to check if he has internet back.
Now here I'm facing error when i click on Retry button
Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
E/flutter (25542): Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
My Updated splash screen
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
Future<bool> isLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString("token");
print("Token obtained: $token");
return token != null;
}
#override
void initState() {
_init();
super.initState();
}
Future<void> _init() async {
bool isConnected = await NetworkHelper.checkInternet();
if (!isConnected) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => CheckConnection(
onRetry: _init,
)),
);
}
} else {
await Future.delayed(const Duration(seconds: 3), () async {
final isTokenValid = await isLoggedIn();
if (isTokenValid) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => BottomNav()),
);
}
} else {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Signup()),
);
}
}
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: FlutterLogo(size: MediaQuery.of(context).size.height),
),
);
}
}
Here is my image showing if there is no internet.
class CheckConnection extends StatelessWidget {
final VoidCallback onRetry;
const CheckConnection({Key? key, required this.onRetry}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/image/no_internet.jpg"),
const SizedBox(height: 20),
ElevatedButton(
onPressed: onRetry,
child: const Text("Retry"),
),
],
),
);
}
}
You have problems in your code. You are trying to access the context after what's called asynchronous suspension, i.e., you are accessing the context after some Future has done its work, while at that instance, the context might be in an invalid state, maybe because the Element tree has changed during this "asyncronois suspension". I also assume you are getting a warning about this but you are ignoring it.
The solution to this problem is, as the error suggests, use the getter mounted which is available inside the State class, after your Future does its work and before you access the context:
await Future.delayed(...);
if(mounted){
// you can use the context
}
else{
// you can't use the context
}
In your case, you need to check if the context is still valid by checking mounted variable before using it after a Future returns:
if(mounted) // you should put this check in all places where you use context after an asyncronous suspension
{
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => BottomNav()),
);
}
else{
// the context is not valid
// you should handle this case according to your app needs
}
Also see this answer.
I have two classes MainScreen and SearchScreen which uses nested function with a boolean value
For example, here in MainScreen, I have a function showMap, its return value is obtained from SearchScreen
Future showMap(bool checkValue) async {
try {
//do something
}
//somewhere in the widgets
#override
Widget build(BuildContext context) {
super.build(context);
//
GestureDetector(
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchScreen(
showMapFunction: showMap)));)
}
}
NoW in my SearchScreen I have
class SearchScreen extends StatefulWidget {
final Function showMapFunction;
const SearchScreen({
this.showMapFunction
}); //,this.String});
#override
_SearchScreenState createState() => _SearchScreenState();
}
///
#override
#mustCallSuper
Widget build(BuildContext context) {
GestureDetector(
onTap: () async {
Navigator.pop(
//send back data
// context,
widget.showMapFunction(true));
},
child: Icon(Icons.arrow_back)),
}
This works fine, when I navigate back to MainScreen the function showMap is called, Is there any other way to do this perhaps with provider package or sth? This causes a lot of rebuilds to my widget tree.
What you can do is to await the result of the navigator, like the one used in the Flutter Cookbook.
void _navigateToSearch(BuildContext context) async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchScreen())),
);
}
And then, when you pop the Search Screen, do the following
GestureDetector(
onTap: () async => Navigator.pop(true),
child: Icon(Icons.arrow_back),
),
I also suggest using WillPopScope.
The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}
In my splash screen, I am calling a method from my model class (getting some data from API and putting them into a list). It works and generates the list. But I can't send this list to my listview widget. Here is the relevant code;
SplashScreen part which is calling model class method (getFixtures)
class _SplashScreenState extends State<SplashScreen> {
List<Fixture> fixtureList = List<Fixture>();
FixtureData callFixtureData = FixtureData();
#override
void initState() {
super.initState();
SplashScreen.fixtureData = getFixture();
Timer(
Duration(seconds: 5),
() => Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) => WelcomeScreen(),
),
),
);
}
Future<List<Fixture>> getFixture() async {
fixtureList = await callFixtureData.getFixtures();
return fixtureList;
}
Model class part; I see the list is not null and correct
class FixtureData extends ChangeNotifier {
List<Fixture> fixtures = List<Fixture>();
List<Fixture> bets = List<Fixture>();
//to show in FAB at HomeScreen
int get betCount {
return bets.length;
}
Future<List<Fixture>> getFixtures() async {
fixtures = await FootballApi.getFixtureData();
print(fixtures[0].homeTeam);
notifyListeners();
return fixtures;
}
ListView part. I'm calling the list with final fixture = fixtureData.fixtures[index]; But nothing happens. What am I missing?
class FixtureList extends StatelessWidget {
final dateFormat = DateFormat('MMMd');
final timeFormat = DateFormat('Hm');
#override
Widget build(BuildContext context) {
return Consumer<FixtureData>(
builder: (context, fixtureData, child) {
return ListView.builder(
itemBuilder: (context, index) {
final fixture = fixtureData.fixtures[index];
return FixtureTile(
date: dateFormat.format(fixture.dateTime),
time: timeFormat.format(fixture.dateTime),
homeTeam: fixture.homeTeam,
awayTeam: fixture.awayTeam,
homeOdds: fixture.homeOdds,
drawOdds: fixture.drawOdds,
awayOdds: fixture.awayOdds,
isHomeSelected: fixture.homeSelected,
isDrawSelected: fixture.drawSelected,
isAwaySelected: fixture.awaySelected,
homeCallBack: () => fixtureData.updateSelection(fixture, 'home'),
drawCallBack: () => fixtureData.updateSelection(fixture, 'draw'),
awayCallBack: () => fixtureData.updateSelection(fixture, 'away'),
);
},
itemCount: fixtureData.fixtures.length,
);
},
);
}
}
you should read Provider documentation or sample first , this is so wrong , you've never set value to FixtureData.fixtures but in ListView you want to use this !!
class _SplashScreenState extends State<SplashScreen> {
List<Fixture> fixtureList = List<Fixture>();
//FixtureData callFixtureData = FixtureData();
#override
void initState() {
super.initState();
// SplashScreen.fixtureData = getFixture();
Provider.of<FixtureData>(context, listen: false).getFixtures();
Timer(
Duration(seconds: 5),
() => Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) => WelcomeScreen(),
),
),
);
}
// Future<List<Fixture>> getFixture() async {
// fixtureList = await callFixtureData.getFixtures();
// return fixtureList;
// }
after that you can use your Consumer ...
also make sure ChangeNotifierProvider is an ancestor to this SplashScreen Widget , like :
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => FixtureData()),
],
child: SplashScreen(),
),