为什么我的 StatefulWidget 会丢失其状态?

Thé*_*ion 6 flutter

我正在构建一个类似火种的卡片刷卡机制,我在一个Stack小部件中有一个卡片小部件列表。第一张卡片被包裹在一个Dismissible小部件中,在关闭时,关闭顶部的卡片并在底部添加一个新的卡片。

我的问题是,当第一张牌被解散而第二张牌成为第一张牌时,那张牌会暂时被处理并initState再次被调用

但是,正如 Linus Torvalds 曾经说过的,

“话费便宜,给我看代码”

所以我整理了一个例子:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Container(color: Colors.white, child: CardStack()),
    );
  }
}

class CardStack extends StatefulWidget {
  @override
  _CardStackState createState() => _CardStackState();
}

class _CardStackState extends State<CardStack> {
  final List<String> _postIds = ["1", "2", "3"];
  int _nextId = 4;

  _updatePosts(DismissDirection dir) {
    setState(() {
      _postIds.removeAt(0);
      _postIds.add((_nextId++).toString());
    });
  }

  List<Widget> _buildCards() {
    List<Widget> cards = [];
    for (int i = 0; i < _postIds.length; ++i) {
      if (i == 0) {
        cards.insert(
            0,
            Dismissible(
              key: UniqueKey(),
              onDismissed: _updatePosts,
              child: Card(key: ValueKey(_postIds[i]), postId: _postIds[i]),
            ));
      } else {
        cards.insert(0, Card(key: ValueKey(_postIds[i]), postId: _postIds[i]));
      }
    }
    return cards;
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: _buildCards(),
    );
  }
}

class Card extends StatefulWidget {
  Card({Key key, this.postId}) : super(key: key);

  final String postId;

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

class _CardState extends State<Card> {
  @override
  void initState() {
    super.initState();
    print("Init state card with id ${widget.postId}");
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 200,
      width: 200,
      color: Colors.blueAccent,
      child: Center(
        child: Text(widget.postId),
      ),
    );
  }
}

Run Code Online (Sandbox Code Playgroud)

如果我运行它记录

I/flutter (26740): Init state card with id 3
I/flutter (26740): Init state card with id 2
I/flutter (26740): Init state card with id 1
// swipe
I/flutter (26740): Init state card with id 4
I/flutter (26740): Init state card with id 2 <- unwanted rebuild
Run Code Online (Sandbox Code Playgroud)

我认为第一张卡片的重建是由于现在包裹在Dismissible小部件中,所以 flutter 不知道如何重用底层的 Card 小部件。

有没有办法防止这种不必要的重建?

Rém*_*let 7

问题是,您的位置StatefulWidget在小部件树内发生了变化。

您可能StatefulWidget有条件地包装在特定的小部件内,因此您的小部件树在以下之间变化:

SomeWidget
  MyStatefulWidget
Run Code Online (Sandbox Code Playgroud)

和:

SomeWidget
  AnotherWidget
    MyStatefulWidget
Run Code Online (Sandbox Code Playgroud)

默认情况下,Flutter 不会理解在 widget 树内MyStatefulWidget 移动的内容。

相反,它会认为前一个MyStatefulWidget已被删除,而新的与原始的完全无关。

这会导致dispose在前一个上被调用State,并initState在新上被调用State


为了防止这种情况,您有一些解决方案。

不要有条件地将您的小部件包装在另一个小部件中

如果可以的话,这是最好的解决方案:重构布局,使小部件树始终相同。

例如,如果您只想将 a 中的第一个小部件ListView关闭,则可以:

SomeWidget
  MyStatefulWidget
Run Code Online (Sandbox Code Playgroud)

做:

SomeWidget
  AnotherWidget
    MyStatefulWidget
Run Code Online (Sandbox Code Playgroud)

这样,树不会有条件地改变,但行为仍然是有条件的

使用全局密钥

不太理想,因为成本更高,但更灵活。

您可以用来GlobalKey告诉 Flutter 特定的小部件可以从一个位置移动到另一个位置。

因此,我们可以写:

ListView.builder(
  itemBuilder: (_, index) {
    final child = MyStatefulWidget();
    if (index == 0) {
      return Dismissible(key: ValueKey(index), onDismissed: ..., child: child);
    }
  }
)
Run Code Online (Sandbox Code Playgroud)

这样,当从 过渡SomeTree到时AnotherTree,小部件MyStatefulWidget将保留其状态。

稍微提醒一下,如果可能的话,请避免这样做。这是相对昂贵的。