Блок флаттера: поддержание дочернего состояния при обновлении родительского виджета

#flutter #bloc

#флаттер #блок

Вопрос:

Прямо сейчас у меня есть приложение, которое создает список таймеров. Он использует два блока, один для отдельных таймеров и один для общего списка. Пример списка можно увидеть ниже: Примерный список

На изображении вы можете видеть, что индексы 0 и 2 не были запущены, в то время как индекс 1 запущен. Моя проблема в том, что всякий раз, когда я добавляю или удаляю элемент из списка, все таймеры сбрасываются.

Есть ли способ сохранить их состояние при перерисовке родительского виджета?

Вот код списка:

Здесь плитка элемента содержит блок для таймера:

   class HomePage extends StatefulWidget {
  static const TextStyle timerTextStyle = TextStyle(
    fontSize: 30,
    fontWeight: FontWeight.bold,
  );

  final Stream shouldTriggerChange;
  HomePage({@required this.shouldTriggerChange});
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  StreamSubscription streamSubscription;

  @override
  initState() {
    super.initState();
    streamSubscription = widget.shouldTriggerChange.listen((data) {
      createTimer(context, data);
    });
  }

  void createTimer(BuildContext context, timer_type timer) {
    BlocProvider.of<ListBloc>(context).add(Add(
        id: DateTime.now().toString(),
        duration: timer == timer_type.timer ? 100 : 0,
        timer: timer,
        timerTextStyle: HomePage.timerTextStyle));
  }

  @override
  didUpdateWidget(HomePage old) {
    super.didUpdateWidget(old);
    // in case the stream instance changed, subscribe to the new one
    if (widget.shouldTriggerChange != old.shouldTriggerChange) {
      streamSubscription.cancel();
      streamSubscription = widget.shouldTriggerChange
          .listen((data) => createTimer(context, data));
    }
  }

  @override
  dispose() {
    super.dispose();
    streamSubscription.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ListBloc, ListState>(
      builder: (context, state) {
        if (state is Failure) {
          return Center(
            child: Text('Oops something went wrong!'),
          );
        }
        if (state is Loaded) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              state.timers.isEmpty
                  ? Center(
                      child: Text('no content'),
                    )
                  : Expanded(
                      child: ListView.builder(
                        itemBuilder: (BuildContext context, int index) {
                          return ItemTile(
                            index: index,
                            timer: state.timers[index],
                            onDeletePressed: (id) {
                              BlocProvider.of<ListBloc>(context)
                                  .add(Delete(id: id));
                            },
                          );
                        },
                        itemCount: state.timers.length,
                      ),
                    ),
            ],
          );
        }
        return Center(
          child: CircularProgressIndicator(),
        );
      },
    );
  }
}
  

Первоначально я думал, что это ключевая проблема, потому что список удалялся неправильно, но я считаю, что я реализовал ключи так, как они показаны в примере, и у меня все еще есть проблема.

 class ItemTile extends StatelessWidget {
  final key = UniqueKey();
  final Function(String) onDeletePressed;
  final int index;
  final Timer timer;
  ItemTile({
    Key key,
    @required this.index,
    @required this.timer,
    @required this.onDeletePressed,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Row(
        children: [
          Text('${index.toString()}'),
          BlocProvider(
            create: (context) => TimerBloc(ticker: Ticker(), timer: timer),
            child: Container(
              height: (MediaQuery.of(context).size.height - 100) / 5,
              child: Row(
                children: [
                  Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      Padding(
                        padding: EdgeInsets.symmetric(vertical: 10.0),
                        child: Center(
                          child: BlocBuilder<TimerBloc, TimerState>(
                            builder: (context, state) {
                              final String minutesStr =
                                  ((state.duration / 60) % 60)
                                      .floor()
                                      .toString()
                                      .padLeft(2, '0');
                              final String secondsStr = (state.duration % 60)
                                  .floor()
                                  .toString()
                                  .padLeft(2, '0');
                              return Text(
                                '$minutesStr:$secondsStr',
                              );
                            },
                          ),
                        ),
                      ),
                      BlocBuilder<TimerBloc, TimerState>(
                        buildWhen: (previousState, currentState) =>
                            currentState.runtimeType !=
                            previousState.runtimeType,
                        builder: (context, state) => TimerActions(),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          timer.isDeleting
              ? CircularProgressIndicator()
              : IconButton(
                  icon: Icon(Icons.delete, color: Colors.red),
                  onPressed: () {
                    onDeletePressed(timer.id);
                  },
                ),
        ],
      ),
    );
  }
}
  

Вот модель таймера, которая передается в блоке списка:

 class Timer extends Equatable {
  final timer_type timer;
  final int duration;
  final String id;
  final bool isDeleting;
  const Timer({
    @required this.id,
    @required this.timer,
    @required this.duration,
    this.isDeleting = false,
  });
  Timer copyWith({timer_type timer, int duration, String id, bool isDeleting}) {
    return Timer(
      id: id ?? this.id,
      duration: duration ?? this.duration,
      timer: timer ?? this.timer,
      isDeleting: isDeleting ?? this.isDeleting,
    );
  }

  @override
  List<Object> get props => [id, duration, timer, isDeleting];
  @override
  String toString() =>
      'Item { id: $id, duration: $duration, timer type: $timer, isDeleting: $isDeleting }';
}
  

Любая помощь будет с благодарностью принята.

Спасибо!

Ответ №1:

Вы создаете новый UniqueKey каждый раз, когда список перестраивается, что приводит к удалению состояния

Поэтому, чтобы исправить это, вам нужно связать ключ с таймером, например, обернув таймер в класс, который содержит таймер и ключ, например, так:

 class KeyedTimer {
  final key = UniqueKey();
  Timer timer;
}
  

Итак, теперь у вас будет список объектов KeyedTimer вместо списка timer, и вы можете использовать ключ в itembuilder списка

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

1. Прежде всего, спасибо за ответ! Я обновил сообщение для ясности и включил весь код. Я пытался поместить уникальный ключ в файл ItemTile, и он все равно каждый раз перерисовывается. Нужно ли мне как-то передавать это родительскому или дочернему элементу? Я пробовал включать и отключать ключевое слово super в конструкторе listtile.

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

3. Как я уже сказал в ответе, вы должны связать ключ с таймером.

4. Поэтому поместите ключ в свою модель таймера, чтобы он не воссоздавался при каждой сборке. Однако я вижу, что у каждого таймера уже есть уникальный идентификатор, поэтому вместо добавления ключа в класс timer вы можете просто использовать ValueKey(timer.id

5. Да, я смог исправить эту проблему, но теперь она сбрасывает все таймеры ниже удаленного элемента.