looking up a deactivated widget's ancestor is unsafe => using Riverpod => using "Navigator.of(context).pushReplacementNamed('/page');"

Viewed 442

The scenario which I did and caused this Error: 1. after Login page created when I used hot-reload button 2. when I pressed Login-button and state of the page changed.

Recently I decided to use riverpod package in my flutter application, so I used hooks_riverpod: ^1.0.0-dev.7 with flutter_hooks: ^0.18.0 But when I create my LoginScreen with the help of Riverpod and Hooks I faced with problems which I provided my log in below.

LoginScreen:

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lambda/configs/sizes/index.dart';
import 'package:lambda/configs/strings.dart';
import 'package:lambda/core/validator/src/mobile_number_validator.dart';
import 'package:lambda/presentation/state_notifiers/auth/index.dart';
import 'package:lambda/presentation/utils/input_formatter/index.dart';
import 'package:lambda/presentation/widgets/alert_message/alert_messge.dart';
import 'package:lambda/presentation/widgets/background/background.dart';
import 'package:lambda/presentation/widgets/progress/progress.dart';
import 'package:lambda/presentation/widgets/spacer/spacer.dart';
import 'package:lambda/routes.dart';

class LoginScreen extends HookConsumerWidget with MobileNumberValidator {
  LoginScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context,WidgetRef ref) {
    ref.listen<AuthState>(authStateNotifierProvider, (state) {
      state.maybeWhen(
          orElse: () {},
          otpSent: (mobileNumber) {
            AppNavigator.replaceWith<String>(
                NavigationPaths.verifyLogin, mobileNumber);
          },
          error: (message) {
            AlertMessage(context).warning(message);
          });
    });
    final phoneFieldController = useTextEditingController();

    return NormalBackground(
      child: Scaffold(
        body: Padding(
          padding: EdgeInsets.symmetric(
              horizontal: LayoutSizes(context).responsive(60)),
          child: Column(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text(
                Strings.pleaseEnterYourMobileNumberForLoginToTheLambda,
                style: Theme.of(context).textTheme.caption,
                textAlign: TextAlign.center,
              ),
              VSpacer(LayoutSizes(context).marginXXL),
              TextFormField(
                controller: phoneFieldController,
                style: Theme.of(context).textTheme.caption,
                textAlign: TextAlign.center,
                keyboardType: TextInputType.number,
                inputFormatters: [PersianNumberFormatter()],
                decoration: const InputDecoration(
                  hintText: Strings.mobileNumberHint,
                ),
              ),
              VSpacer(LayoutSizes(context).marginL),
              ref.watch(authStateNotifierProvider).maybeMap(
                orElse: () {
                  return ElevatedButton(
                    onPressed: () {
                      if (isValidIRMobileNumber(phoneFieldController.text)) {
                        ref
                            .read(authStateNotifierProvider.notifier)
                            .sendOtp(phoneFieldController.text);
                      } else {
                        AlertMessage(context).warning(
                            Strings.isInvalidInput(Strings.mobileNumber));
                      }
                    },
                    style: ButtonStyle(
                      fixedSize: MaterialStateProperty.all(
                        Size(double.maxFinite,
                            LayoutSizes(context).buttonHeightL),
                      ),
                    ),
                    child: const Text(Strings.next),
                  );
                },
                loading: (_) {
                  return const CircularProgress();
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

AuthStateProviderNotifier:

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lambda/configs/strings.dart';
import 'package:lambda/core/extensions/strings.dart';
import 'package:lambda/data/repositories/auth/authentication_repository.dart';
import 'package:lambda/services/http/index.dart';
import 'package:lambda/services/logger/logger.dart';

import 'auth_state.dart';

final authStateNotifierProvider =
    StateNotifierProvider<AuthStateNotifier, AuthState>((ref) {
  final authRepository = ref.read(authRepositoryProvider);
  return AuthStateNotifier(authRepository);
});

class AuthStateNotifier extends StateNotifier<AuthState> {
  final AuthenticationRepository _repository;

  AuthStateNotifier(this._repository) : super(const AuthState.initial());

  Future<void> sendOtp(String mobileNumber) async {
    try {
      state = const AuthState.loading();
      await _repository.sendValidationCode(
          mobileNumber: mobileNumber.convertToEnNum());
      state = AuthState.otpSent(mobileNumber: mobileNumber);
    } catch (e, s) {
      _handleError(e, s);
    }
  }

  Future<void> verifyOtp(String mobileNumber, String code) async {
    try {
      state = const AuthState.loading();
      await _repository.login(
          mobileNumber: mobileNumber.convertToEnNum(),
          verificationCode: code.convertToEnNum());
      state = const AuthState.authenticated();
    } catch (e, s) {
      _handleError(e, s);
    }
  }

  void _handleError(Object e, StackTrace s) {
    Logger().info('error : $e stack: $s');
    if (e is NetworkExceptionX) {
      state = AuthState.error(
          errorMessage: e.messageForUser ?? Strings.someErrorHappened);
    } else {
      state = const AuthState.error(errorMessage: Strings.someErrorHappened);
    }
  }
}

Run:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building LoginScreen(dirty, dependencies: [_LocalizationsScope-[GlobalKey#aacaf], UncontrolledProviderScope, _InheritedTheme], state: _ConsumerState#cf20e, useTextEditingController: TextEditingController#f5c6d(TextEditingValue(text: ┤├, selection: TextSelection(baseOffset: -1, extentOffset: -1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1)))):
Looking up a deactivated widget's ancestor is unsafe.

At this point the state of the widget's element tree is no longer stable.

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

The relevant error-causing widget was: 
  LoginScreen file:///Users/taleb/FlutterProjects/lambda/lib/routes.dart:40:36
When the exception was thrown, this was the stack: 
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:3944:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:3958:6)
#2      Element.findAncestorWidgetOfExactType (package:flutter/src/widgets/framework.dart:3996:12)
#3      debugCheckHasMediaQuery.<anonymous closure> (package:flutter/src/widgets/debug.dart:218:50)
#4      debugCheckHasMediaQuery (package:flutter/src/widgets/debug.dart:234:4)
...
====================================================================================================

======== Exception caught by widgets library =======================================================
The following assertion was thrown building LoginScreen(dirty, dependencies: [_LocalizationsScope-[GlobalKey#aacaf], UncontrolledProviderScope, _InheritedTheme], state: _ConsumerState#cf20e, useTextEditingController: TextEditingController#f5c6d(TextEditingValue(text: ┤├, selection: TextSelection(baseOffset: -1, extentOffset: -1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1)))):
Looking up a deactivated widget's ancestor is unsafe.

At this point the state of the widget's element tree is no longer stable.

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

The relevant error-causing widget was: 
  LoginScreen file:///Users/taleb/FlutterProjects/lambda/lib/routes.dart:40:36
When the exception was thrown, this was the stack: 
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:3944:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:3958:6)
#2      Element.findAncestorWidgetOfExactType (package:flutter/src/widgets/framework.dart:3996:12)
#3      debugCheckHasMediaQuery.<anonymous closure> (package:flutter/src/widgets/debug.dart:218:50)
#4      debugCheckHasMediaQuery (package:flutter/src/widgets/debug.dart:234:4)
...
====================================================================================================

These errors happened when I used TextField in HookConsumerWidget class. I am using HookConsumerWidget instead of StatefullWidget. I also tried to use StatefullConsumerWidget but the problem was not > solved.(ConsumerStatefulWidget+riverPod). My question is how can we use Textfield in HookConsumerWidget + Riverpod ????

If you want to run it by yourself, I Provided a sample code of this error on my Github: smaple_hook_riverpod

2 Answers

I think "Navigator.of (context).pushReplacementNamed" is not the right solution for "Flutter Navigator + Riverpod".

A better solution is an entire navigation stack (which is a List of Page's) to create from a list of immutable objects. The problem of navigation is than reduced to manipulation an immutable collection.

  • final navigationStackProvider = Provider<>((ref) => [Obj1, Obj2, Obj3]);
  • [Obj1, Obj2, Obj3] je následně synchronizován s navigation stack, odpovídající [Screen1, Screen2, Screen3]...

I prepared example that implements a simple login logic, where some pages are not available without login.

Example is here: riverpod_navigator_example

The cause of this issue is that you're calling Navigator on Widget build. You may want to consider moving ref.listen<AuthState> on initState. Then wrap the Navigator with a SchedulerBinding to wait for the rendering state to finish before navigating.

SchedulerBinding.instance.addPostFrameCallback((_) {
  AppNavigator.replaceWith<String>(NavigationPaths.verifyLogin, mobileNumber);
});
Related