tomgilder / routemaster

Easy-to-use Navigator 2.0 router for web, mobile and desktop. URL-based routing, simple navigation of tabs and nested routes.
https://pub.dev/packages/routemaster
MIT License
328 stars 61 forks source link

Navigating from Deeply Nested Template -> Another Template Stack from Open Drawer Has Null Error #206

Open mdrideout opened 2 years ago

mdrideout commented 2 years ago

Deep Nested Path Example: /products/:productId/details Another Template: /profile

Example Flow:

  1. From screen button: Routemaster.of(context).push('/products')
  2. From screen button: Routemaster.of(context).push('/products/:productId/details')
  3. From Drawer() overlay widget: Routemaster.of(context).push('/profile')

Attempt at Step 3 results in no page transition, however, the URL path does change to match the desired path. Opening the drawer and clicking the same route button again will result in the following errors. (Note: this does not occur with buttons in the normal screen widget, only in the Drawer class)

Web Error

======== Exception caught by gesture ===============================================================
The following TypeErrorImpl was thrown while handling a gesture:
Unexpected null value.

When the exception was thrown, this was the stack: 
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 251:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 528:63  nullCheck
packages/routemaster/src/route_data.dart 181:61                                   of
packages/rdgconnect/ui/navigation/main_drawer.dart 20:38                          navigateOrClose
packages/rdgconnect/ui/navigation/main_drawer.dart 104:21                         <fn>
packages/flutter/src/material/ink_well.dart 989:21                                [_handleTap]
packages/flutter/src/gestures/recognizer.dart 193:24                              invokeCallback
packages/flutter/src/gestures/tap.dart 608:48                                     handleTapUp
packages/flutter/src/gestures/tap.dart 296:5                                      [_checkUp]
packages/flutter/src/gestures/tap.dart 267:7                                      acceptGesture
packages/flutter/src/gestures/arena.dart 157:12                                   sweep
packages/flutter/src/gestures/binding.dart 444:20                                 handleEvent
packages/flutter/src/gestures/binding.dart 420:14                                 dispatchEvent
packages/flutter/src/rendering/binding.dart 278:11                                dispatchEvent
packages/flutter/src/gestures/binding.dart 374:7                                  [_handlePointerEventImmediately]
packages/flutter/src/gestures/binding.dart 338:5                                  handlePointerEvent
packages/flutter/src/gestures/binding.dart 296:7                                  [_flushPointerEventQueue]
packages/flutter/src/gestures/binding.dart 279:32                                 [_handlePointerDataPacket]
lib/_engine/engine/platform_dispatcher.dart 1018:13                               invoke1
lib/_engine/engine/platform_dispatcher.dart 182:5                                 invokeOnPointerDataPacket
lib/_engine/engine/pointer_binding.dart 130:39                                    [_onPointerData]
lib/_engine/engine/pointer_binding.dart 555:18                                    <fn>
lib/_engine/engine/pointer_binding.dart 508:21                                    <fn>
lib/_engine/engine/pointer_binding.dart 216:16                                    loggedHandler
Handler: "onTap"
Recognizer: TapGestureRecognizer#633f5
  debugOwner: GestureDetector
  state: ready
  won arena
  finalPosition: Offset(93.9, 310.9)
  finalLocalPosition: Offset(93.9, 23.9)
  button: 1
  sent tap down
====================================================================================================

iOS Error (provides more data re: routemaster)

======== Exception caught by gesture ===============================================================
The following _CastError was thrown while handling a gesture:
Null check operator used on a null value

When the exception was thrown, this was the stack: 
#0      RouteData.of (package:routemaster/src/route_data.dart:181:61)
#1      MainDrawer.build.navigateOrClose (package:rdgconnect/ui/navigation/main_drawer.dart:20:21)
#2      MainDrawer.build.<anonymous closure> (package:rdgconnect/ui/navigation/main_drawer.dart:107:21)
#3      _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:989:21)
#4      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:193:24)
#5      TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:608:11)
#6      BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:296:5)
#7      BaseTapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:267:7)
#8      GestureArenaManager.sweep (package:flutter/src/gestures/arena.dart:157:27)
#9      GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:444:20)
#10     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:420:22)
#11     RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:278:11)
#12     GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:374:7)
#13     GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:338:5)
#14     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:296:7)
#15     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:279:7)
#19     _invoke1 (dart:ui/hooks.dart:185:10)
#20     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:293:7)
#21     _dispatchPointerDataPacket (dart:ui/hooks.dart:98:31)
(elided 3 frames from dart:async)
Handler: "onTap"
Recognizer: TapGestureRecognizer#2db3d
  debugOwner: GestureDetector
  state: ready
  won arena
  finalPosition: Offset(117.5, 328.0)
  finalLocalPosition: Offset(117.5, 25.0)
  button: 1
  sent tap down
====================================================================================================

Required Solution

The user must first close the drawer, before calling the Routemaster push method. Note, this does not happen with transitioning between two "Root" paths (both starting with a '/' and having only the single path segment).

Example function that closes the drawer if we're already on the path, or first closes the drawer and then navigates.

    /// Close Drawer & Maybe Navigate
    void navigateOrClose(String path) {
      if (RouteData.of(context).path == path) {
        // We're already there, close the drawer
        Navigator.pop(context);
      } else {
        // Close the drawer, then navigate
        Navigator.pop(context);
        Routemaster.of(context).push(path);
      }
    }
mdrideout commented 2 years ago

Hey @tomgilder - curious if your gut reaction to this is that I have some sort of anti-pattern happening somewhere? (on 0.10.0-dev5)

Having implemented this solution to be able to navigate, I now get the following error with this solution:

[debug] Capture from onError 'package:routemaster/src/route_data.dart': Failed assertion: line 194 pos 12: 'routeData != null': Couldn't find RouteData for page
[error] Exception caught by widgets library

The offending call is to this RouteData which is in a widget on the screen (not the drawer in the example below).

  // Get this route's path
  String path = RouteData.of(context).path;

The navigation still works fine. But navigating away from the screen using the drawer is what triggers the above error.

Example drawer navigation feature

class MainDrawer extends ConsumerWidget {
  const MainDrawer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    // Vars
    User? _user = context.read(userStateProvider).user;
    String? _name = _user?.parsedIdToken.name;
    String? _email = _user?.email;
    UserType? _userType = _user?.userType;
    String? _userSupportUrl = _user?.accountData.supportUrl;
    String? _userSupportEmail = _user?.accountData.supportEmail;

    /// Close Drawer & Maybe Navigate
    void navigateOrClose(String path) {
      if (RouteData.of(context).path == path) {
        // We're already there, close the drawer
        Navigator.pop(context);
      } else {
        // Close the drawer, then navigate
        Navigator.pop(context);
        Routemaster.of(context).push(path);
      }
    }

    /// Render drawer
    return Drawer(
      child: Column(
mdrideout commented 2 years ago

OK so i have another solution for if you encounter the above Failed assertion error. I looked a bit more through the code and found the maybeOf variant to get RouteData, which allows it to return null if no routedata is found.

So I've changed the code as follows to eliminate the error.

String path = RouteData.of(context).path;

to

String? _path = RouteData.maybeOf(context)?.path;