lulupointu / vrouter

MIT License
202 stars 39 forks source link

Change URL without refreshing the page #208

Closed iliyami closed 1 year ago

iliyami commented 1 year ago

Hi @lulupointu First of all, thanks for this strong package, I can't even compare it with go_router... It's a pity that the go_router went more popular than this package, so thanks for your efforts.

I have a question, I want to change the url without refreshing the page. How can I achieve that?

For example I have this in my url: post/1536 Then I want to change it to post/someID when user start scrolling the posts. If I use context.vRouter.to or toNamed with new path params, potentially it'll be start refreshing and I want to avoid this.

lulupointu commented 1 year ago

Thanks! 😊

I wouldn't expect this to refresh the page. Can you post a small reproducible example?

iliyami commented 1 year ago

Thanks! 😊

I wouldn't expect this to refresh the page. Can you post a small reproducible example?

Sorry for the delay. This is an example:

import 'package:flutter/material.dart';
import 'package:vrouter/vrouter.dart';

void main() {
  runApp(
    VRouter(
      debugShowCheckedModeBanner: false,
      routes: [
        VNester(
          path: '/',
          widgetBuilder: (child) =>
              MyScaffold(child), // Child is the widget from nestedRoutes
          nestedRoutes: [
            VWidget.builder(
                path: 'page/:id',
                builder: (context, state) => HomeScreen(
                    index: int.parse(state.pathParameters['id']!)),
              ),
            VWidget(path: 'settings', widget: const SettingsScreen()),
          ],
        ),
      ],
    ),
  );
}

class MyScaffold extends StatelessWidget {
  final Widget child;

  const MyScaffold(this.child, {super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: context.vRouter.url.contains('settings') ? 1 : 0,
        onTap: (value) =>
            context.vRouter.to((value == 0) ? '/page/0' : '/settings'),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(
              icon: Icon(Icons.settings), label: 'Settings'),
        ],
      ),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key, required this.index});

  final int index;

  String get title => (index == 0) ? 'followers' : 'following';

  String get buttonText => 'Go to ${(index == 0) ? 'followers' : 'following'}';

  String get to => '/page/${(index == 0) ? 1 : 0}';

  @override
  State<StatefulWidget> createState() => _HomeScreen();
}

class _HomeScreen extends State<HomeScreen> with TickerProviderStateMixin {

  @override
  Widget build(BuildContext context) {
    final controller = TabController(
                  length: 2,
                  vsync: this,
                );
    return Material(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const SizedBox(height: 50),
          Container(
            color: Colors.black,
            child: SizedBox(
              height: 55,
              child: TabBar(
                controller: controller,
                isScrollable: true,
                indicatorPadding: const EdgeInsets.all(4),
                labelPadding: const EdgeInsets.all(0),
                tabs: List<Widget>.generate(2, (int index) {
                  if (index == 0) {
                    return SizedBox(
                      width: MediaQuery.of(context).size.width / 2,
                      child: const Tab(text: "followers"),
                    );
                  }
                  return SizedBox(
                    width: MediaQuery.of(context).size.width / 2,
                    child: const Tab(text: "following"),
                  );
                }),
              ),
            ),
          ),
          Expanded(
            child: TabBarView(
                controller: controller,
                children: List.generate(2, (index) {
                  if (index == 0) {
                  return FollowersFollowing(
                    index: index,
                  );
                } else {
                  return FollowersFollowing(
                    index: index,
                  );
                }
              }),
            ),
          ),
        ],
      ),
    );
  }
}

class SettingsScreen extends StatelessWidget {
  const SettingsScreen({super.key});

  String get title => 'Settings';

  String get buttonText => 'Go to Home';

  String get to => '/page/0';

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(title),
            const SizedBox(height: 50),
            ElevatedButton(
              onPressed: () => context.vRouter.to(to),
              child: Text(buttonText),
            ),
          ],
        ),
      ),
    );
  }
}

class FollowersFollowing extends StatelessWidget {
  const FollowersFollowing({super.key, required this.index});
  final int index;
  String get to => '/page/${(index == 0) ? 1 : 0}';

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const SizedBox(height: 50),
            Text((index == 0) ? 'Followers page' : 'Following page'),
          ],
        ),
      ),
    );
  }
}

The question is here, How can I change the url path param silently when changing the tab? And happy new year!

lulupointu commented 1 year ago

To change the url you have to make it part of your routes:

VNester(
  path: 'page/:id',
  widgetBuilder: (child) => child, // Child is the widget from nestedRoutes
  nestedRoutes: [
    VWidget.builder(
      path: 'followers',
      buildTransition: (_, __, child) => child,
      builder: (context, state) => HomeScreen(
        index: int.parse(
          state.pathParameters['id']!,
        ),
        selectedTab: HomeTabItem.followers,
      ),
    ),
    VWidget.builder(
      path: 'following',
      buildTransition: (_, __, child) => child,
      builder: (context, state) => HomeScreen(
        index: int.parse(
          state.pathParameters['id']!,
        ),
        selectedTab: HomeTabItem.following,
      ),
    ),
  ],
)

To do it "silently" I guess you mean no animation so you would override buildTransition:

VWidget.builder(
  buildTransition: (_, __, child) => child,
  ...
)
Here is the entire code ```dart import 'package:flutter/material.dart'; import 'package:vrouter/vrouter.dart'; void main() { runApp( VRouter( initialUrl: '/page/2/followers', debugShowCheckedModeBanner: false, routes: [ VNester( path: '/', widgetBuilder: (child) => MyScaffold(child), // Child is the widget from nestedRoutes nestedRoutes: [ VNester( path: 'page/:id', widgetBuilder: (child) => child, // Child is the widget from nestedRoutes nestedRoutes: [ VWidget.builder( path: 'followers', buildTransition: (_, __, child) => child, builder: (context, state) => HomeScreen( index: int.parse( state.pathParameters['id']!, ), selectedTab: HomeTabItem.followers, ), ), VWidget.builder( path: 'following', buildTransition: (_, __, child) => child, builder: (context, state) => HomeScreen( index: int.parse( state.pathParameters['id']!, ), selectedTab: HomeTabItem.following, ), ), ], ), VWidget(path: 'settings', widget: const SettingsScreen()), ], ), ], ), ); } class MyScaffold extends StatelessWidget { final Widget child; const MyScaffold(this.child, {super.key}); @override Widget build(BuildContext context) { return Scaffold( body: child, bottomNavigationBar: BottomNavigationBar( currentIndex: context.vRouter.url.contains('settings') ? 1 : 0, onTap: (value) => context.vRouter.to((value == 0) ? '/page/0/followers' : '/settings'), items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem( icon: Icon(Icons.settings), label: 'Settings'), ], ), ); } } enum HomeTabItem { followers, following } class HomeScreen extends StatefulWidget { const HomeScreen({super.key, required this.index, required this. selectedTab}) ; final int index; final HomeTabItem selectedTab; String get title => (index == 0) ? 'followers' : 'following'; String get buttonText => 'Go to ${(index == 0) ? 'followers' : 'following'}'; String get to => '/page/${(index == 0) ? 1 : 0}'; @override State createState() => _HomeScreen(); } class _HomeScreen extends State with TickerProviderStateMixin { late final controller = TabController( length: 2, vsync: this, initialIndex: widget.selectedTab.index, ); @override void initState() { super.initState(); controller.addListener(() { if (controller.index != widget.selectedTab.index) { context.vRouter.to( '/page/${widget.index}/${(controller.index == 0) ? 'followers' : 'following'}', ); } }); } @override void didUpdateWidget(covariant HomeScreen oldWidget) { controller.index = widget.selectedTab.index; super.didUpdateWidget(oldWidget); } @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Material( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 50), Container( color: Colors.black, child: SizedBox( height: 55, child: TabBar( controller: controller, isScrollable: true, indicatorPadding: const EdgeInsets.all(4), labelPadding: const EdgeInsets.all(0), tabs: List.generate(2, (int index) { if (index == 0) { return SizedBox( width: MediaQuery.of(context).size.width / 2, child: const Tab(text: "followers"), ); } return SizedBox( width: MediaQuery.of(context).size.width / 2, child: const Tab(text: "following"), ); }), ), ), ), Expanded( child: TabBarView( controller: controller, children: List.generate(2, (index) { if (index == 0) { return FollowersFollowing( index: index, ); } else { return FollowersFollowing( index: index, ); } }), ), ), ], ), ); } } class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); String get title => 'Settings'; String get buttonText => 'Go to Home'; String get to => '/page/0/followers'; @override Widget build(BuildContext context) { return Material( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(title), const SizedBox(height: 50), ElevatedButton( onPressed: () => context.vRouter.to(to), child: Text(buttonText), ), ], ), ), ); } } class FollowersFollowing extends StatelessWidget { const FollowersFollowing({super.key, required this.index}); final int index; String get to => '/page/${(index == 0) ? 1 : 0}'; @override Widget build(BuildContext context) { return Material( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 50), Text((index == 0) ? 'Followers page' : 'Following page'), ], ), ), ); } } ```
iliyami commented 1 year ago

To change the url you have to make it part of your routes:

VNester(
  path: 'page/:id',
  widgetBuilder: (child) => child, // Child is the widget from nestedRoutes
  nestedRoutes: [
    VWidget.builder(
      path: 'followers',
      buildTransition: (_, __, child) => child,
      builder: (context, state) => HomeScreen(
        index: int.parse(
          state.pathParameters['id']!,
        ),
        selectedTab: HomeTabItem.followers,
      ),
    ),
    VWidget.builder(
      path: 'following',
      buildTransition: (_, __, child) => child,
      builder: (context, state) => HomeScreen(
        index: int.parse(
          state.pathParameters['id']!,
        ),
        selectedTab: HomeTabItem.following,
      ),
    ),
  ],
)

To do it "silently" I guess you mean no animation so you would override buildTransition:

VWidget.builder(
  buildTransition: (_, __, child) => child,
  ...
)

Here is the entire code Thanks for the reply and code.

By do it silently, I mean I want to change the url and avoid starting page from the scratch (Animations, API calls, rebuilds, and ...) I just want to change the url in the current page without affecting on anything except the url! Is it possible?

Imagine you have a listview.builder and the current URL is listview/item_1 and you want to change the URL based on the listview items as you scroll them down or up -> listview/item_2 listview/item_3 etc.

lulupointu commented 1 year ago

VRouter does not control the state of your app differently than any other widget: If a widget (or its key) at the top changes, it will rebuild its children.

What you might be missing is that VRouter uses the resolved path as a key, so if you use

VWidget.builder(
  path: '/items/:id',
  builder: (context, data) => MyList(
    currentItemId: int.parse(data.pathParameters['id']!),
  ),
)

to create a list, indeed the list will be "rebuilt" every time. What you have to do in this case is to fix the key, which you can do with the key parameter:

VWidget.builder(
  key: const ValueKey('items'),
  path: '/items/:id',
  builder: (context, data) => MyList(
    currentItemId: int.parse(data.pathParameters['id']!),
  ),
)
Here is a full example ```dart import 'package:flutter/material.dart'; import 'package:vrouter/vrouter.dart'; void main() { runApp( VRouter( initialUrl: '/items/1', debugShowCheckedModeBanner: false, routes: [ VWidget.builder( key: const ValueKey('items'), path: '/items/:id', builder: (context, data) => MyList( currentItemId: int.parse(data.pathParameters['id']!), ), ), ], ), ); } class MyList extends StatefulWidget { const MyList({Key? key, required this.currentItemId}) : super(key: key); final int currentItemId; @override State createState() => _MyListState(); } class _MyListState extends State { final controller = ScrollController(); final itemHeight = 400.0; @override void initState() { super.initState(); controller.addListener(() { final currentIndex = controller.offset ~/ itemHeight; if (currentIndex != widget.currentItemId) { VRouter.of(context).to('/items/$currentIndex'); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Item ${widget.currentItemId}'), ), body: ListView.builder( controller: controller, itemCount: 100, itemBuilder: (context, index) { return Container( height: itemHeight, color: index % 2 == 0 ? Colors.red : Colors.blue, child: Center( child: Text( 'Item $index', style: const TextStyle(color: Colors.white, fontSize: 24), ), ), ); }, ), ); } } ```
iliyami commented 1 year ago

I totally got it. Thanks for the explanation and the demo . It was very helpful🔥