0

I'm trying to achieve a scroll section containing a row which contains a list view and a details view. The goal is to have one scroll view that scroll both the list and the details view. From an UX perspective, both sections should always be seen, such as if one section is much larger than the other, the smaller section will always be seen.

I'm under the impression that this is the use case for NestedScrollView so ultimately what I'm after is something using it. I know it's possible to sync multiple controllers together programmatically, but I'm hoping for something simpler.

Like the following (this is just an example, my screen does not look anything like this):

enter image description here

The code below is where I got to. It throws an error that scrollController is used in multiple places. Another problem is that scrolling the left side till the end does not scroll the right side till the end.

Here is a full repro:

https://github.com/cedvdb/flutter_repros/tree/double_scroll

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',
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) => [
          SliverAppBar(
            title: const Text('example'),
          )
        ],
        body: Row(
          children: [
            SizedBox(
              width: 500,
              child: ListView.builder(
                itemCount: 30,
                itemBuilder: (context, index) => ListTile(
                  title: Text('tile $index'),
                ),
              ),
            ),
            const SizedBox(
              width: 20,
            ),
            Expanded(
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    Container(height: 500, color: Colors.yellow),
                    Container(height: 500, color: Colors.orange),
                    Container(height: 500, color: Colors.blue),
                    Container(height: 500, color: Colors.yellow),
                  ],
                ),
              ),
            ),
            const SizedBox(
              width: 20,
            ),
          ],
        ),
      ),
    );
  }
}

Ced
  • 15,847
  • 14
  • 87
  • 146
  • Why do you want to make two scrolls instead of one for both? – SrPanda May 27 '22 at 07:55
  • I want to make one for both. If you put the snippet in dart pad that's the result. I updated the question to make it clearer – Ced May 27 '22 at 08:27
  • Have you taken a look at https://pub.dev/packages/linked_scroll_controller, th only issue is that if scrollable widget A is larger than B, b will overscroll. But maybe the answer lies there, synchronizing the scrollables – croxx5f May 29 '22 at 23:15
  • are you already try this? https://stackoverflow.com/questions/60579335/flutter-listview-scroll-parent-listview-when-child-listview-reach-bottom-c – anggadaz May 31 '22 at 08:18
  • @croxx5f yes I tried it, it does not seem to be working right with overscroll (it's bouncing back) – Ced Jun 03 '22 at 08:17

4 Answers4

2

Try this one, it works for me.

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',
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) => [
          SliverAppBar(
            title: const Text('example'),
          )
        ],
        body: SingleChildScrollView (
          child: Row(
            crossAxisAlignment : CrossAxisAlignment.start,
          children: [
            SizedBox(
              width: 500,
              child: ListView.builder(
                itemCount: 30,
                shrinkWrap: true,
                itemBuilder: (context, index) => ListTile(
                  title: Text('tile $index'),
                ),
              ),
            ),
            const SizedBox(
              width: 20,
            ),
            Expanded(
//               child: SingleChildScrollView(
                child: Column(
                  children: [
                    Container(height: 500, color: Colors.yellow),
                    Container(height: 500, color: Colors.orange),
                    Container(height: 500, color: Colors.blue),
                    Container(height: 500, color: Colors.yellow),
                  ],
//                 ),
              ),
            ),
            const SizedBox(
              width: 20,
            ),
          ],
        ),
          ),
      ),
    );
  }
}
  • Your answer could be improved by adding more information on what the code does and how it helps the OP. – Tyler2P Jun 07 '22 at 19:04
1

So basically when you are using one scrollview with two scrollable widgets flutter can't decide how to draw the scrollbar. so if you disable scrollbar then your code will stop throwing error(if you are okay with no scrollbar it will scroll but no scrollbars will be displayed).

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',
      //this line added here!
      scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), 
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) => [
          SliverAppBar(
            title: const Text('example'),
          )
        ],
        body: Row(
          children: [
            SizedBox(
              width: 500,
              child: ListView.builder(
                itemCount: 30,
                itemBuilder: (context, index) => ListTile(
                  title: Text('tile $index'),
                ),
              ),
            ),
            const SizedBox(
              width: 20,
            ),
            Expanded(
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    Container(height: 500, color: Colors.yellow),
                    Container(height: 500, color: Colors.orange),
                    Container(height: 500, color: Colors.blue),
                    Container(height: 500, color: Colors.yellow),
                  ],
                ),
              ),
            ),
            const SizedBox(
              width: 20,
            ),
          ],
        ),
      ),
    );
  }
}
Ruchit
  • 2,137
  • 1
  • 9
  • 24
  • not showing the two inner scrolls is a good idea, but there is still a problem that scrolling the smaller element till the end won't scroll the other element till the end – Ced Jun 03 '22 at 07:52
0

What i understood is that there are two scrolls and items from one point to items in the other and you need a method to move the second one on a event (scroll) but by the example having different size lines i asume it's some kind of cursor in a array thing; if that is what you need you only have to implement the exact movement you want, the example just scrolls to the place of the line you click

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

void main() => runApp(const App());

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Page_Home(),
      ),
    );
  }
}

class List_Element {
  String title;
  String content;
  final GlobalKey _key;

  List_Element(
    this.title,
    this.content,
  ) : this._key = GlobalKey();

  GlobalKey get_key() => _key;
}

class Page_Home extends StatefulWidget {
  const Page_Home({Key? key}) : super(key: key);

  @override
  _Page_Home createState() => _Page_Home();
}

class _Page_Home extends State<Page_Home> {
  late List<List_Element> elements;
  final scroll_view = ScrollController();

  double _get_heigth(key) {
    final size = key.currentContext!.size;
    if (size != null) {
      return size.height;
    } else {
      return 0;
    }
  }

  Widget build_list(BuildContext ctx) {
    return SingleChildScrollView(
      child: Column(
        children: List<Widget>.generate(elements.length, (i) {
          return InkWell(
              onTap: () {
                double pos = 0;
                for (int e = 0; e < i; e++) {
                  pos += _get_heigth(elements[e].get_key());
                }
                scroll_view.animateTo(
                  pos,
                  duration: const Duration(seconds: 1),
                  curve: Curves.easeIn,
                );
                print("Jump $i to $pos");
              },
              child: Container(
                margin: EdgeInsets.only(top: 10),
                color: Colors.blueGrey,
                width: double.infinity,
                height: 30,
                child: Center(child: Text("Line ${elements[i].title}")),
              ));
        }),
      ),
    );
  }

  Widget build_element_view(BuildContext ctx) {
    return SingleChildScrollView(
      controller: scroll_view,
      child: Column(
        children: List<Widget>.generate(elements.length, (i) {
          return Container(
            margin: EdgeInsets.only(top: 10),
            color: Colors.blueGrey,
            width: double.infinity,
            child: Text(elements[i].content),
            key: elements[i].get_key(),
          );
        }),
      ),
    );
  }

  @override
  void dispose() {
    scroll_view.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext ctx) {
    Size vsize = MediaQuery.of(ctx).size;
    this.elements = List<List_Element>.generate(
        30,
        (i) => List_Element(
              i.toString(),
              i.toString() * Random().nextInt(300),
            ));

    return ConstrainedBox(
      constraints: BoxConstraints.tight(vsize),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          SizedBox(
            width: vsize.width * 0.4,
            height: vsize.height,
            child: this.build_list(ctx),
          ),
          SizedBox(
            width: vsize.width * 0.5,
            height: vsize.height,
            child: this.build_element_view(ctx),
          ),
        ],
      ),
    );
  }
}

SrPanda
  • 854
  • 1
  • 5
  • 9
  • This is a valuable answer so I think it should stay, however that's not what I'm after. The example code I provided "works", but throws an error. I think the simplest way to understand is to run it (it can be copy pasted into dartpad) – Ced May 27 '22 at 12:39
  • I ran your code, but i never used `NestedScrollView` so i tried to use it and found it too complicated in the same way that `Row` is annoying, from that i would guess tat is a conflict in the `BuildContext` because they are using the gesture implementation in someway diferente (i base this because in the flutter examples use builder childs instead of letting the child overflow) but im not sure of any of that so i made something i know it works, better than nothing i thought – SrPanda May 27 '22 at 13:13
  • Yeah but your answers has two separate scroll sections, I only need one :p And yes NestedScrollView is indeed complicated – Ced May 27 '22 at 13:45
  • FYI I added a bounty to this question – Ced May 29 '22 at 20:44
0

I could not get the behavior I wanted (and I'm not even sure it makes sens anymore) so I made 2 scroll in the row:

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: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ScrollController left = ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) => [
          SliverAppBar.medium(
            title: const Text('example'),
          ),
        ],
        body: LayoutBuilder(builder: (context, constraints) {
          return Row(
            children: [
              Container(
                color: Colors.greenAccent,
                height: constraints.maxHeight,
                width: 400,
                child: MyListView(controller: left),
              ),
              Expanded(
                child: SingleChildScrollView(
                  child: Column(
                    children: [
                      Container(height: 500, color: Colors.yellow),
                      Container(height: 500, color: Colors.orange),
                      Container(height: 500, color: Colors.blue),
                      Container(height: 500, color: Colors.yellow),
                    ],
                  ),
                ),
              ),
            ],
          );
        }),
      ),
    );
  }
}

class MyListView extends StatelessWidget {
  final ScrollController? controller;
  const MyListView({Key? key, this.controller}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      controller: controller,
      slivers: [
        SliverFixedExtentList(
          itemExtent: 60,
          delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                    title: Text('tile $index'),
                  ),
              childCount: 30),
        ),
        const SliverToBoxAdapter(
          child: Padding(
            padding: EdgeInsets.all(8.0),
            child: Text('secon list'),
          ),
        ),
        SliverFixedExtentList(
          itemExtent: 60,
          delegate: SliverChildBuilderDelegate(
            (context, index) => ListTile(
              title: Text('tile $index'),
            ),
            childCount: 30,
          ),
        ),
      ],
    );
  }
}

Ced
  • 15,847
  • 14
  • 87
  • 146