Flutter - RepaintBoundary вызывает сброс состояния StatefulWidget

У меня есть виджет предварительного просмотра, который загружает данные после нажатия пользователем. Это состояние (уже нажата или нет) не должно быть потеряно при прокрутке (предварительный просмотр находится в списке) или навигации по другому экрану. Прокрутка решается добавлением AutomaticKeepAliveClientMixin, который сохраняет состояние при прокрутке.

Теперь мне также нужно обернуть виджет предварительного просмотра (на самом деле более сложный виджет, который содержит предварительный просмотр) с помощью RepaintBoundary, чтобы иметь возможность сделать «снимок экрана» только этого виджета.

Прежде чем я оберну виджет с помощью RepaintBoundary, состояние сохраняется как при прокрутке, так и при переходе на другой экран. После добавления RepaintBoundary прокрутка по-прежнему работает, но для навигации состояние сбрасывается.

Как я могу обернуть виджет с отслеживанием состояния, который должен сохранять свое состояние, с помощью RepaintBoundary?

Код - это упрощенный пример моей реализации с той же проблемой.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {

  MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final title = 'Test';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: TestList(40),
      ),
    );
  }
}


class TestList extends StatefulWidget {

  final int numberOfItems;

  TestList(this.numberOfItems);

  @override
  _TestListState createState() => _TestListState();

}

class _TestListState extends State<TestList> {

  @override
  Widget build(BuildContext context) {
    print('_TestListState build.');
    return ListView.builder(
      itemCount: widget.numberOfItems,
      itemBuilder: (context, index) {

        return RepaintBoundary(
          key: GlobalKey(),
          child: Preview()
        );
      },
    );
  }
}


class Preview extends StatefulWidget {
  @override
  _PreviewState createState() => _PreviewState();
}

class _PreviewState extends State<Preview> with AutomaticKeepAliveClientMixin {

  bool loaded;

  @override
  void initState() {
    super.initState();
    print('_PreviewState initState.');

    loaded = false;
  }

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);

    print('_PreviewState build.');

    if(loaded) {
      return GestureDetector(
        onTap: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => NewScreen()),
          );
        },
        child: ListTile(
          title: Text('Loaded. Tap to navigate.'),
          leading: Icon(Icons.visibility),
        ),
      );
    } else {
      return GestureDetector(
        onTap: () {
          setState(() {
            loaded = true;
          });
        },
        child: ListTile(
          title: Text('Tap to load.'),
        ),
      );
    }
  }
}


class NewScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('New Screen')),
      body: Center(
        child: Text(
          'Navigate back and see if loaded state is gone.',
          style: TextStyle(fontSize: 14.0),
        ),
      ),
    );
  }
}

person user23423294    schedule 23.05.2019    source источник


Ответы (1)


Взгляните на RepaintBoundary.wrap, он назначает RepaintBoundary виджету ключ на основе его дочернего элемента или childIndex, поэтому состояние сохраняется:

class _TestListState extends State<TestList> {
  @override
  Widget build(BuildContext context) {
    print('_TestListState build.');
    return ListView.builder(
      itemCount: widget.numberOfItems,
      itemBuilder: (context, index) {
        return RepaintBoundary.wrap(
          Preview(),
          index,
        );
      },
    );
  }
}

https://api.flutter.dev/flutter/widgets/RepaintBoundary/RepaintBoundary.wrap.html

РЕДАКТИРОВАТЬ: Согласно приведенным ниже комментариям, похоже, что это решение нарушит возможность создания снимков экрана, поэтому вам придется хранить список дочерних виджетов в своем состоянии следующим образом:

class _TestListState extends State<TestList> {
  List<Widget> _children;

  @override
  void initState() {
    super.initState();
    _children = List.generate(
        widget.numberOfItems,
        (_) => RepaintBoundary(
              key: GlobalKey(),
              child: Preview(),
            ));
  }

  @override
  Widget build(BuildContext context) {
    print('_TestListState build.');
    return ListView(children: _children);
  }
}
person Jordan Davies    schedule 23.05.2019
comment
Похоже, что это точно решает описанную проблему состояния, однако теперь я не могу использовать созданный RepaintBoundary для создания снимка экрана, так как в RepaintBoundary.wrap создается только ValueKey, и создание снимка экрана виджета, похоже, работает только с GlobalKey. Любые идеи? - person user23423294; 23.05.2019
comment
Ага! хорошая точка зрения. Возможно, вам придется попробовать другое решение. Я обновил свой ответ. - person Jordan Davies; 23.05.2019
comment
Это работает для этого примера, но не может использоваться для моей фактической реализации, поскольку список потенциально очень длинный и дорогостоящий, и я не хочу хранить все элементы в памяти, что касается вашего первого решения, знаете ли вы, почему / как именно ключ, полученный из ребенок помогает поддерживать состояние? так как остальное - это обычный RepaintBoundary, возможно, я смогу сделать что-то подобное, но с GlobalKeys - person user23423294; 23.05.2019
comment
Также кажется, что initState _TestListState вызывается в каждом Navigator.push / pop в примере кода, который я действительно не понимаю, почему - person user23423294; 24.05.2019