Флаттер: Используйте AbsorbPointer без перестройки всего дерева виджетов

#flutter #dart #setstate

Вопрос:

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

Тем не менее, у меня также есть домашняя страница , завернутая в an AbsorbPointer , и я хочу установить absorbing = true , когда я нажимаю на дочерний виджет. Цель состоит в том, чтобы запретить пользователю щелкать по экрану, пока приложение выполняет некоторую асинхронную работу в фоновом режиме. Проблема сейчас в том, что если я вызову setState() домашнюю страницу, чтобы установить значение «поглощение» в значение true, она перестроит все дочерние виджеты.

Я мог бы передать некоторые параметры в дочерние виджеты, чтобы только тот, на который я нажал, имел циклический указатель, но даже тогда все остальные дочерние виджеты все равно будут перестроены.

Я предполагаю, что это сводится к тому факту, что я не могу вызвать setState() в родительском виджете без перестройки всех дочерних элементов, даже если параметр, который я передаю этому setState () ( absorbing ), не имеет ничего общего с этими дочерними элементами.

Есть ли обходной путь для этого?

Спасибо!

 // home_screen.dart

class HomeScreen extends StatefulWidget {
  static const String routeName = "homeScreen";
  final MyUser? user;

  const HomeScreen({Key? key, required this.user}) : super(key: key);

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

class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
  final MyDatabase _db = MyDatabase();
  MyUser? _me;

  int _currentPage = -1;
  bool _isLoading = false;

  ...

  @override
  Widget build(BuildContext context) {
    return AbsorbPointer(
      absorbing: _isLoading,
      child: Container(
        decoration: BoxDecoration(
          // color: Color(0xFF0d0717),
          image: DecorationImage(
            image: Image.asset(
              'assets/background.png',
            ).image,
            fit: BoxFit.cover,
          ),
        ),
        child: Scaffold(
          backgroundColor: Colors.transparent,
          appBar: ...,
          bottomNavigationBar: ...,
          body: SingleChildScrollView(
            child: Container(
              padding: const EdgeInsets.symmetric(
                vertical: 8.0,
                horizontal: 12.0,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  StreamBuilder<QuerySnapshot>(
                    stream: _db.getLiveChannels(),
                    builder: (_, snapshot) {
                      if (!snapshot.hasData) {
                        // print("Has no data");
                        return Center(
                          child: CircularProgressIndicator(),
                        );
                      }
                      _channels.addAll(List.generate(
                          snapshot.data!.docs.length,
                          (index) => Channel.fromSnapshot(
                              snapshot.data!.docs[index])));
                      return Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            children: [
                              Text(
                                'Now Playing',
                                style: TextStyle(
                                  fontSize: 24.0,
                                  fontWeight: FontWeight.w700,
                                ),
                              ),
                              SizedBox(width: 8.0),
                              LiveIndicator(),
                            ],
                          ),
                          SizedBox(height: 8.0),
                          Container(
                            height: 250,
                            child: PageView.builder(
                              physics: PageScrollPhysics(),
                              scrollDirection: Axis.horizontal,
                              controller: PageController(
                                viewportFraction: .9,
                              ),
                              itemCount: _channels.length,
                              itemBuilder: (context, index) {
                                Channel channel =
                                    _channels[_channels.length - 1 - index];
                                return ChildWidget(
                                  callback: _callback;
                                  loading: (_isLoading amp;amp; _currentPage == index),
                                  key: UniqueKey(),
                                );
                              },
                            ),
                          ),
                          ...,
                        ],
                      );
                    },
                  ),
                  ...,
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Future<void> _callback(params) async {
    if (_isLoading == false) {
      setState(() {
        _isLoading = true;
        _currentPage = index;
      });
    }
    someAsyncMethod().then((_) => setState(() {
          _isLoading = false;
          _currentPage = -1;
        }));
  }

}

 
 // child_widget.dart

class ChildWidget extends StatefulWidget {
  final Future<void> Function(params) callback;
  final bool loading;

  const ChildWidget({
    Key? key,
    required this.callback,
    required this.loading,
  }) : super(key: key);

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

class _ChildWidgetState extends State<ChildWidget> {
  late Future<void> Function(params) callback;

  late bool loading;

  @override
  void initState() {
    super.initState();
    callback = widget.callback;
    loading = widget.loading;
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      child: Column(
        children: [
          Expanded(
            child: CustomClickableWidget(
              onPressed: callback,
              child: Expanded(
                child: Container(
                  child: Stack(
                    children: [
                      ...,
                      if (loading) ...[
                        Container(
                          alignment: Alignment.center,
                          child: CircularProgressIndicator(),
                        ),
                      ],
                    ],
                  ),
                ),
              ),
            ),
          ),
          ...,
        ],
      ),
    );
  }
}

 

Скриншот

Ответ №1:

Функция setState запускает функцию Build (), поэтому весь код, присутствующий в функции Build (), будет выполнен снова. Я не совсем понимаю, почему это для вас проблема ?

С другой стороны, в вашем коде я вижу, что для вашего ребенка вы определили ключ: uniqueKey (). Когда функция сборки будет запущена после setState (), она создаст новый дочерний элемент без сохранения состояния предыдущего дочернего элемента. Вы не должны определять uniqueKey () в своей функции, а скорее как переменную экземпляра вашего состояния

 ChildWidget(callback: _callback;
            loading: (_isLoading amp;amp; _currentPage == index),
            key: UniqueKey(),
)
 

Вы должны определить свой ключ здесь

 class _ChildWidgetState extends State<ChildWidget> {
UniqueKey myKey = UniqueKey();
 

и вы функционируете

 ChildWidget(callback: _callback;
            loading: (_isLoading amp;amp; _currentPage == index),
            key: myKey,
)
 

Комментарии:

1. Спасибо за комментарий! Дело в том, что я хочу каким-то образом перестроить только дочерний элемент, на который я нажимаю, чтобы он показывал индикатор загрузки (как на скриншоте), но также изменить absorbing параметр указателя загрузки в родительском виджете. Не похоже, что есть способ, так что я думаю, что я ищу обходной путь. Моя цель состоит в том, чтобы 1) указать, что я нажал на конкретного ребенка, и 2) запретить пользователю нажимать/прокручивать приложение во время выполнения фоновой задачи.

2. Удалите создание uniqueKey в функции Build (). а также я вижу, как вы инициализируете переменную «Загрузка» в своем исходном состоянии childwidget. поэтому эта переменная определяется только один раз и не меняется во время перестройки вашего ребенка, проблема может возникнуть и оттуда

3. Теперь это сработало! Я не знаю, потому ли это, что я удалил уникальный ключ, как вы сказали, или потому, что раньше были некоторые проблемы с моим ожиданием/асинхронностью. Но после исправления этого я теперь мог вызвать setState() в дочернем устройстве, чтобы переключить его индикатор загрузки, и вызвать setState() в родительском, чтобы переключить его переменную _isLoading.