从StatefulWidget外部控制状态

时间:2017-09-05 14:23:31

标签: flutter

我试图理解在Widgets State之外控制StatefulWidget状态的最佳实践。

我定义了以下界面。

abstract class StartupView {
  Stream<String> get onAppSelected;

  set showActivity(bool activity);
  set message(String message);
}

我想创建一个实现此接口的StatefulWidget StartupPage。我希望Widget能够做到以下几点:

  1. 按下按钮时,它将通过onAppSelected流发送事件。控制器会监听此操作并执行一些操作(数据库调用,服务请求等)。

  2. 控制人员可以致电showActivityset message让视图显示进度并显示消息。

  3. 由于Stateful Widget不会将其作为属性公开,因此我不知道访问和修改State属性的最佳方法。

    我希望使用它的方式如下:

    Widget createStartupPage() {
        var page = new StartupPage();
        page.onAppSelected.listen((app) {
          page.showActivity = true;
          //Do some work
          page.showActivity = false;
        });
      }
    

    我已经考虑通过传递我希望它在createState()中返回的状态来实例化Widget,但这感觉不对。

    我们采用这种方法的一些背景:我们目前有一个Dart Web应用程序。对于视图 - 控制器分离,可测试性和对Flutter的前瞻性思考,我们决定为应用程序中的每个视图创建一个接口。这将允许WebComponent或Flutter Widget实现此接口并使所有控制器逻辑保持不变。

9 个答案:

答案 0 :(得分:31)

有多种与其他有状态小部件进行交互的方式。

1。 ancestorStateOfType

第一个也是最直接的方法是通过context.ancestorStateOfType方法。

通常包裹在Stateful子类的静态方法中,如下所示:

class MyState extends StatefulWidget {
  static of(BuildContext context, {bool root = false}) => root
      ? context.rootAncestorStateOfType(const TypeMatcher<_MyStateState>())
      : context.ancestorStateOfType(const TypeMatcher<_MyStateState>());

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

class _MyStateState extends State<MyState> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

例如Navigator的工作方式。

专业人士:

  • 最简单的解决方案

骗局:

  • 试图访问State属性或手动调用setState
  • 需要公开State子类

要访问变量时不要使用此方法。当该变量更改时,您的窗口小部件可能不会重新加载。

2。 Listenable,Stream和/或InheritedWidget

有时您可能想要访问某些属性,而不是方法。问题是,只要该值随时间变化,您很可能希望您的小部件进行更新。

在这种情况下,飞镖提供StreamSink。颤动会在其顶部添加InheritedWidgetListenable,例如ValueNotifier。它们都做相对相同的事情:在与StreamBuilder / context.inheritFromWidgetOfExactType / AnimatedBuilder结合时订阅值更改事件。

当您希望State公开一些属性时,这是首选解决方案。我不会介绍所有可能性,但这是一个使用InheritedWidget的小例子:

首先,我们有一个InheritedWidget公开了count

class Count extends InheritedWidget {
  static of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(Count);

  final int count;

  Count({Key key, @required Widget child, @required this.count})
      : assert(count != null),
        super(key: key, child: child);

  @override
  bool updateShouldNotify(Count oldWidget) {
    return this.count != oldWidget.count;
  }
}

然后我们的State实例化了此InheritedWidget

class _MyStateState extends State<MyState> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Count(
      count: count,
      child: Scaffold(
        body: CountBody(),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
        ),
      ),
    );
  }
}

最后,我们有CountBody来获取暴露的count

class CountBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(Count.of(context).count.toString()),
    );
  }
}

优点:

  • ancestorStateOfType的表现更好
  • Stream选项仅是dart(可与Web一起使用),并且已与语言(await forasync*之类的关键字)紧密集成
  • 值更改时自动重新加载子项

缺点:

  • 更多样板
  • 流可能很复杂

3。通知

您可以从窗口小部件发送State,而不是直接在Notification上调用方法。并使State订阅这些通知。

Notification的示例为:

class MyNotification extends Notification {
  final String title;

  const MyNotification({this.title});
}

要发送通知,只需在通知实例上调用dispatch(context),它就会冒泡。

MyNotification(title: "Foo")..dispatch(context)

任何给定的小部件都可以使用NotificationListener<T>收听其子级发送的通知:

class _MyStateState extends State<MyState> {
  @override
  Widget build(BuildContext context) {
    return NotificationListener<MyNotification>(
      onNotification: onTitlePush,
      child: Container(),
    );
  }

  void onTitlePush(MyNotification notification) {
    print("New item ${notification.title}");
  }
}

示例为Scrollable,它可以调度ScrollNotification,包括开始/结束/过度滚动。然后由Scrollbar用来了解滚动信息,而无需访问ScrollController

优点:

  • 凉爽的反应性API。我们不会直接在State上做任何事情。 State订阅了由其子级触发的事件
  • 一个以上的小部件可以订阅同一通知
  • 防止孩子访问不需要的State属性

缺点:

  • 可能不适合您的用例
  • 需要更多样板

答案 1 :(得分:14)

您可以使用静态方法公开状态的窗口小部件,一些颤动的示例以这种方式执行,我也开始使用它:

class StartupPage extends StatefulWidget {
  static _StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<_StartupPageState>());

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

class _StartupPageState extends State<StartupPage> {
  ...
}

然后,您可以通过调用StartupPage.of(context).doSomething();来访问该州。

这里需要注意的是,你需要在树的某个地方有一个BuildContext。

答案 2 :(得分:5)

我愿意:

class StartupPage extends StatefulWidget {
  StartupPageState state;

  @override
  StartupPageState createState() {
    this.state = new StartupPageState();

    return this.state;
  }
}

class DetectedAnimationState extends State<DetectedAnimation> {

startupPage.state之外

答案 3 :(得分:1)

您是否考虑过将状态提升到父窗口小部件?据我所知,这是一种常见的,虽然不如Redux理想的方式来管理React中的状态,而repository显示了如何将该概念应用于Flutter应用程序。

答案 4 :(得分:1)

在尝试解决类似问题时,我发现ancestorStateOfType()TypeMatcher已被弃用。相反,必须使用findAncestorStateOfType()。但是,根据文档,“调用此方法相对昂贵”。 findAncestorStateOfType()方法的文档可以在here中找到。

无论如何,要使用findAncestorStateOfType(),可以实现以下目的(这是使用findAncestorStateOfType()方法对正确答案的修改):

class StartupPage extends StatefulWidget {
  static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();

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

class _StartupPageState extends State<StartupPage> {
  ...
}

可以使用与正确答案中所述相同的方式来访问状态(使用StartupPage.of(context).yourFunction())。我想用新方法更新帖子。

答案 5 :(得分:0)

还有另一种常用的方法可以访问国家的属性/方法:

class StartupPage extends StatefulWidget {
  StartupPage({Key key}) : super(key: key);

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

// Make class public!
class StartupPageState extends State<StartupPage> {
  int someStateProperty;

  void someStateMethod() {}
}

// Somewhere where inside class where `StartupPage` will be used
final startupPageKey = GlobalKey<StartupPageState>();

// Somewhere where the `StartupPage` will be opened
final startupPage = StartupPage(key: startupPageKey);
Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);

// Somewhere where you need have access to state
startupPageKey.currentState.someStateProperty = 1;
startupPageKey.currentState.someStateMethod();

答案 6 :(得分:0)

您可以使用 eventify

该库提供了向发射器注册事件通知的机制 或发布者,并在发生事件时得到通知。

您可以执行以下操作:

// Import the library
import 'package:eventify/eventify.dart';
final EventEmitter emitter = new EventEmitter();

var controlNumber = 50;

List<Widget> buttonsGenerator() {
    final List<Widget> buttons = new List<Widget>();
    for (var i = 0; i < controlNumber; i++) {
        widgets.add(new MaterialButton(
                  // Generate 10 Buttons afterwards
                  onPressed: () {
                    controlNumber = 10;
                    emitter.emit("updateButtonsList", null, "");
                  },
                );
    }
}

class AState extends State<ofYourWidget> {
    @override
    Widget build(BuildContext context) {
        List<Widget> buttons_list = buttonsGenerator();
        emitter.on('updateButtonsList', null, (event, event_context) {
            setState(() {
                buttons_list = buttonsGenerator();
            });
        });
    }
    ...
}

我想不出事件驱动编程无法实现的任何事情。你是无限的!

“不能赋予自由-必须实现自由。” -埃尔伯特·哈伯德(Elbert Hubbard)

答案 7 :(得分:0)

对于可能需要此解决方案的其他人。以下是对@santi 回复的贡献:

class StartupPage extends StatefulWidget {
  final StartupPageState _state = StartupPageState();

  @override
  StartupPageState createState() {
     return _state;
  }
}

这确保仅创建状态的单个实例。 我决定将 _state 设为私有,这样您就不会将其所有属性暴露给外部类。如果您想使用其中的特定字段,我建议您使用封装来访问/修改它(使用 _state 变量)。

答案 8 :(得分:-1)

我没有像大多数答案所说的那样向所有人公开整个MailApp,而是建议公开一种与自己的State交互的方法。这样,与执行工作的State交互的Widget不会知道Widget是无状态的还是有状态的。很干净。

只需在Widget中创建一个方法,如下所示:

Widget