16

For navigation, I built a simple factory class that generates a ListTile that pushes a route to the Navigator:

static Widget simpleNavRow(String text, BuildContext context, String route) {
  return Column(
    children: <Widget>[
      ListTile(
        title: Text(text),
        onTap: () {
          Navigator.pushNamed(context, route);
        },
      ),
      Divider(),
    ],
  );
}

However, I soon realized that it would be convenient to support pushing widgets as well (or instantiate from their class if possible). I couldn't figure out how to make the "route" argument accept either a String or a Widget, so I created a class that initializes with one of those two types. This code works, but is there a better way to achieve this?

class NavTo {
  String route;
  Widget widget;

  NavTo.route(this.route);
  NavTo.widget(this.widget);

  push(BuildContext context) {
    if (route != null) {
      Navigator.pushNamed(context, route);
    }
    if (widget != null) {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return widget;
      }));
    }
  }
}

class ListHelper {
  static final padding = EdgeInsets.all(12.0);

  static Widget simpleNavRow(String text, BuildContext context, NavTo navTo) {
    return Column(
      children: <Widget>[
        ListTile(
          title: Text(text),
          onTap: () {
            navTo.push(context);
          },
        ),
        Divider(),
      ],
    );
  }
}

// usage:
// ListHelper.simpleNavRow('MyWidget', context, NavTo.widget(MyWidget()))
GoldenJoe
  • 7,874
  • 7
  • 53
  • 92
  • 1
    what about `MaterialApp#onGenerateRoute` ? that way you can use `pushNamed` and still have the freedom for returning any `Route` you want – pskink Nov 10 '18 at 09:33
  • @pskink In this specific implementation, I am setting up a playground within my project to test out different widgets as I build them. Each component gets its own screen, but it isn't important enough for it to be listed in my routes index. That's why I just want to push a dumb widget. – GoldenJoe Nov 10 '18 at 21:29

4 Answers4

15

Since you are expecting one of multiple types, what about having dynamic and then in the push method of NavTo, you could check the type:

class NavTo {
  dynamic route;

  push(BuildContext context) {
    if (route is String) {
       ...
    } else if (route is Widget) {
       ...
    }
  }
}
graphicbeacon
  • 326
  • 1
  • 6
  • 1
    To extend my answer, you are looking for a union type. Some research brought me across this [solution](https://pub.dartlang.org/packages/union_type) by Tobe and [this article](https://theburningmonk.com/2014/06/dart-emulating-fs-discriminated-union-i-e-an-algebraic-data-type/). Hope these help inform how you proceed. – graphicbeacon Nov 10 '18 at 19:03
  • Thanks. Wasn't sure what it's called. Since it isn't supported by the language, I'll probably just stick to the current solution. A whole library just for this is overkill. – GoldenJoe Nov 10 '18 at 21:20
3

I don’t believe the union type is available in Dart. I like your solution over the use of dynamic as it is strongly typed. You could use named parameters.

NavTo({this.route,this.widget})

But then you don’t have compile-type checking for one and only one parameter.

The only improvement I would make to your constructors is to add @required.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Chris Reynolds
  • 5,453
  • 1
  • 15
  • 12
1

Personnally i like to give Items a MaterialPageRoute params

static Widget simpleNavRow(String text, BuildContext context, MaterialPageRoute route) {
 return Column(
   children: <Widget>[
     ListTile(
      title: Text(text),
      onTap: () {
        Navigator.push(context, route);
      },
     ),
    Divider(),
  ],);
}

items stays dumb like this and i decide what they do in the parent. After you can create an item factory for each type you have that initialize the correct route like this :

class ItemExemple extends StatelessWidget {

final String text;
final MaterialPageRoute route;

ItemExemple(this.text, this.route);

factory ItemExemple.typeA(String text, BuildContext context) =>
  new ItemExemple(text, new MaterialPageRoute(builder: (context) => new   ItemA()));

factory ItemExemple.typeB(String text, BuildContext context) =>
  new ItemExemple(text, new MaterialPageRoute(builder: (context) => new ItemB()));

@override
Widget build(BuildContext context) {
  return Column(
    children: <Widget>[
      ListTile(
        title: Text(this.text),
        onTap: () {
          Navigator.push(context, route);
        },
      ),
      Divider(),
    ],);
}

}
mcfly
  • 774
  • 1
  • 8
  • 18
0

Why are you using one prop for different functions? There is no need to do everything in one function, you are violating the Single Responsibility Rule. You can create two methods for this.

class NavTo {
  String routeName;
  Widget widget;

  NavTo.routeName(this.routeName);
  NavTo.widget(this.widget);

  push(BuildContext context) {
    if (widget != null) {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return widget;
      }));
    }
  }

  pushNamed(BuildContext context) {
    if (routeName != null) {
      Navigator.pushNamed(context, route);
    }
  }
}