Search code examples
flutterdartscrollnavbar

How to implement dynamic scrolling via a navbar in flutter for a single-page web app?


I'm trying to find a way to click on an element in my navbar and have the page auto-scroll down to that section. My one-page website consists of a SingleChildScrollView with the different sections (e.g. about, services, contact us,..) as children of that scroll view. This structure is written in my HomeScreen class. The NavBar is in a different dart file and gets generated. How can I make it so that these get linked to each other. GIF: https://i.sstatic.net/VkM2W.jpg

Homescreen class (gets initiated in main.dart)

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          children: [
            TopSection(),
            SizedBox(height: kDefaultPadding * 2),
            AboutSection(),
            ServiceSection(),
            TestimonialSection(),
            SizedBox(height: kDefaultPadding),
            ContactSection(),
            SizedBox(height: 50)
          ],
        ),
      ),
    );
  }
}

Part where you can click on element in nav bar

class _MenuState extends State<Menu> {
  int selectedIndex = 0;
  int hoverIndex = 0;
  List<String> menuItems = [
    "Home",
    "About",
    "Services",
    "Testimonials",
    "Contact"
  ];
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: kDefaultPadding * 2.5),
      constraints: BoxConstraints(maxWidth: 1110),
      height: 100,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(10),
          topRight: Radius.circular(10),
        ),
        boxShadow: [kDefaultShadow],
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: List.generate(
          menuItems.length,
          (index) => buildMenuItem(index),
        ),
      ),
    );
  }

  Widget buildMenuItem(int index) => InkWell(
        onTap: () {
          setState(() {
            selectedIndex = index;
          });
        },

Example of navbar with underneath the sections navbar


Solution

  • UPDATE:

    I solved the problem with a little change in your code and work for me :) I use ValueNotifier and ValueLisenableBuilder for transfer value & use ValueChanged for pass value to outside the menu widget. but better and recommended using one state manager for this work . for switch scroll can using GlobalKey and Scrollable.ensureVisible. enter image description here

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: HomeScreen(),
        );
      }
    }
    
    class HomeScreen extends StatefulWidget {
      @override
      State<HomeScreen> createState() => _HomeScreenState();
    }
    
    class _HomeScreenState extends State<HomeScreen> {
      late ValueNotifier<int> notifier;
      final List<GlobalKey> itemKeys = [
        GlobalKey(),
        GlobalKey(),
        GlobalKey(),
        GlobalKey(),
        GlobalKey()
      ];
    
      @override
      void initState() {
        notifier = ValueNotifier(0);
        super.initState();
      }
    
      @override
      void dispose() {
        notifier.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SingleChildScrollView(
              child: ValueListenableBuilder<int>(
            valueListenable: notifier,
            builder: (context, val, widget) {
              return Column(children: [
                Menu(
                  valueChanged: (int value) {
                    print("$value");
                    notifier.value = value;
                    notifier.notifyListeners();
                    Scrollable.ensureVisible(itemKeys[value].currentContext!,
                        duration: Duration(seconds: 1),
                        // duration for scrolling time
                        alignment: .5,
                        // 0 mean, scroll to the top, 0.5 mean, half
                        curve: Curves.easeInOutCubic);
                  },
                ),
                Item(title: "Home", key: itemKeys[0]),
                Item(title: "About", key: itemKeys[1]),
                Item(title: "Services", key: itemKeys[2]),
                Item(title: "Testimonials", key: itemKeys[3]),
                Item(title: "Contact", key: itemKeys[4]),
              ]);
            },
          )),
        );
      }
    }
    
    class Item extends StatelessWidget {
      const Item({Key? key, required this.title}) : super(key: key);
      final String title;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 400,
          width: double.infinity,
          color: Colors.deepPurpleAccent,
          child: Text("$title"),
        );
      }
    }
    
    class Menu extends StatefulWidget {
      Menu({Key? key, required this.valueChanged}) : super(key: key);
      ValueChanged<int> valueChanged;
    
      @override
      State<Menu> createState() => _MenuState();
    }
    
    class _MenuState extends State<Menu> {
      int selectedIndex = 0;
      int hoverIndex = 0;
      List<String> menuItems = [
        "Home",
        "About",
        "Services",
        "Testimonials",
        "Contact"
      ];
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: EdgeInsets.symmetric(horizontal: 70 * 2.5),
          constraints: BoxConstraints(maxWidth: 1110),
          height: 100,
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(10),
              topRight: Radius.circular(10),
            ),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: List.generate(
              menuItems.length,
              (index) => Container(
                color: selectedIndex == index ? Colors.red : Colors.green,
                width: 100,
                height: 100,
                child: InkWell(
                  child: Text("${menuItems[index]}"),
                  onTap: () {
                    setState(() {
                      selectedIndex = index;
                      widget.valueChanged((index));
                    });
                  },
                ),
              ),
            ),
          ),
        );
      }
    }