Flutter - ListView внутри TabBarView теряет свою позицию прокрутки

У меня очень простое приложение Flutter с TabBarView с двумя представлениями (Tab 1 и Tab 2), один из них (Tab 1) имеет ListView со многими простыми текстовыми виджетами, проблема в том, что после прокрутки элементов ListView вкладки 1, если я перейду от вкладки 1 до вкладки 2 и, наконец, я перейду от Tab 2 к вкладке 1, предыдущая позиция прокрутки в ListView на вкладке 1 потеряется.

Вот код:

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  TabController controller;

  @override
  void initState() {
    super.initState();
    controller = new TabController(
      length: 2,
      vsync: this,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    var tabs = <Tab>[
      new Tab(icon: new Icon(Icons.home), text: 'Tab 1'),
      new Tab(icon: new Icon(Icons.account_box), text: 'Tab 2')
    ];

    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new TabBarView(controller: controller, children: <Widget>[
        new ListView(children: <Widget>[
          new Column(children: <Widget>[
            new Text('Data 1'),
            new Text('Data 2'),
            new Text('Data 3'),
            new Text('Data 4'),
            new Text('Data 5'),
            new Text('Data 6'),
            new Text('Data 7'),
            new Text('Data 8'),
            new Text('Data 9'),
            new Text('Data 10'),
            new Text('Data 11'),
            new Text('Data 12'),
            new Text('Data 13'),
            new Text('Data 14'),
            new Text('Data 15'),
            new Text('Data 16'),
            new Text('Data 17'),
            new Text('Data 18'),
            new Text('Data 19'),
            new Text('Data 20'),
            new Text('Data 21'),
            new Text('Data 22'),
            new Text('Data 23'),
            new Text('Data 24'),
            new Text('Data 25'),
            new Text('Data 26'),
            new Text('Data 27'),
            new Text('Data 28'),
            new Text('Data 29'),
            new Text('Data 30'),
            new Text('Data 31'),
            new Text('Data 32'),
            new Text('Data 33'),
            new Text('Data 34'),
            new Text('Data 35'),
            new Text('Data 36'),
            new Text('Data 37'),
            new Text('Data 38'),
            new Text('Data 39'),
            new Text('Data 40'),
            new Text('Data 41'),
            new Text('Data 42'),
            new Text('Data 43'),
            new Text('Data 44'),
            new Text('Data 45'),
            new Text('Data 46'),
            new Text('Data 47'),
            new Text('Data 48'),
          ])
        ]),
        new Center(child: new Text('Tab 2'))
      ]),
      bottomNavigationBar: new Material(
        color: Colors.deepOrange,
        child: new TabBar(controller: controller, tabs: tabs),
      ),
    );
  }
}

Я даже отделил TabBarView детей (Tab 1 и Tab 2) в других классах, и я заметил, что

@override
  Widget build(BuildContext context) {
  ...
}

метод каждого дочернего элемента (вкладка 1 и вкладка 2) выполняется каждый раз, когда я нажимаю на него вкладку контейнера.

Мои вопросы:

1. Как сохранить прокрутку списка ListView, даже если я перейду от вкладки к вкладке?

2.- Есть ли способ выполнить

@override
Widget build(BuildContext context) {

}

метод только один раз, если я отделяю дочерние элементы TabBarView (вкладки 1 и вкладку 2) к другим классам? Я имею в виду, если мне нужно будет получать данные, когда создаются вкладки 1 и 2, я не хочу делать это каждый раз, когда вы вставляете Tab. Это было бы дорого.

3.- В целом, есть ли способ предотвратить переустановку вида табуляции (включая переменные, данные и т.д.) Каждый раз, когда я перебираю эту вкладку?

Заранее спасибо.

Ответ 1

1. Как сохранить прокрутку списка ListView, даже если я перейду от вкладки к вкладке?

Хорошо, это было не так просто, как я думал, но я думаю, что мне удалось это сделать.

Моя идея состоит в том, чтобы сохранить смещение списка в HomePageState, и когда мы прокручиваем список, мы просто получаем смещение от уведомителя и устанавливаем его через setter (пожалуйста, сделайте его более чистым и делитесь!).

Затем, когда мы перестраиваем listview, мы просто просим нашего главного виджета предоставить нам сохраненное смещение, а ScrollController мы инициализируем список с этим смещением.

Я также изменил ваш список, так как он имел один элемент столбца с 50 текстами, чтобы использовать 50 элементов с одним текстом каждый. Надеюсь, вы не возражаете :)

Код:

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

typedef double GetOffsetMethod();
typedef void SetOffsetMethod(double offset);

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  TabController controller;
  double listViewOffset=0.0;

  @override
  void initState() {
    super.initState();
    controller = new TabController(
      length: 2,
      vsync: this,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    var tabs = <Tab>[
      new Tab(icon: new Icon(Icons.home), text: 'Tab 1'),
      new Tab(icon: new Icon(Icons.account_box), text: 'Tab 2')
    ];

    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new TabBarView(
      controller: controller,
      children: <Widget>[
        new StatefulListView(
          getOffsetMethod: () => listViewOffset,
          setOffsetMethod: (offset) => this.listViewOffset = offset,
        ),
        new Center(child: new Text('Tab 2'))
      ]),
      bottomNavigationBar: new Material(
        color: Colors.deepOrange,
        child: new TabBar(controller: controller, tabs: tabs),
      ),
    );
  }
}

class StatefulListView extends StatefulWidget {
  StatefulListView({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key);

  final GetOffsetMethod getOffsetMethod;
  final SetOffsetMethod setOffsetMethod;

  @override
  _StatefulListViewState createState() => new _StatefulListViewState();
}

class _StatefulListViewState extends State<StatefulListView> {

  ScrollController scrollController;

  @override
  void initState() {
    super.initState();
    scrollController = new ScrollController(
      initialScrollOffset: widget.getOffsetMethod()
    );
  }

  @override
  Widget build(BuildContext context) {
    return new NotificationListener(
      child: new ListView.builder(
        controller: scrollController,
        itemCount: 50,
        itemBuilder: (BuildContext context, int index) {
          return new Text("Data "+index.toString());
        },
      ),
      onNotification: (notification) {
        if (notification is ScrollNotification) {
          widget.setOffsetMethod(notification.metrics.pixels);
        }
      },
    );
  }
}

Ответ 3

Чтобы быть более точным, вы можете использовать PageStorageKey с любым прокручиваемым представлением, чтобы сохранить положение прокрутки, например:

new ListView.builder(key: new PageStorageKey('myListView'), ...)

Ответ 4

Выход:

enter image description here


Код:

@override
Widget build(BuildContext context) {
  return DefaultTabController(
    length: 2,
    child: Scaffold(
      appBar: AppBar(
        title: Text("PageStorageKey"),
        bottom: TabBar(
          tabs: [
            Tab(icon: Icon(Icons.looks_one), text: "List1"),
            Tab(icon: Icon(Icons.looks_two), text: "List2"),
          ],
        ),
      ),
      body: TabBarView(
        children: [
          _buildList(key: "key1", string: "List1: "),
          _buildList(key: "key2", string: "List2: "),
        ],
      ),
    ),
  );
}

Widget _buildList({String key, String string}) {
  return ListView.builder(
    key: PageStorageKey(key),
    itemBuilder: (_, i) => ListTile(title: Text("${string} ${i}")),
  );
}

Ответ 5

Есть ли способ выполнить конструкцию Widget build (BuildContext context) только один раз...

Имхо, идея флаттера должна быть всегда готова к перестройке. Это должно быть дешево. Если у вас есть дорогие действия, вы можете использовать State для "кэширования" результатов. Например, вы можете выполнить сетевой запрос в initState и через setState rebuild при получении ответа. Для вкладок вы можете подготовить и сохранить данные в родительском виджете. Вы можете найти дополнительную информацию в учебнике флаттера об управлении состоянием