Flutter 提供程序嵌套 Objects

Flutter Provider Nested Objects

我正在使用 Provider Package 来管理我的 Flutter 应用程序中的状态。当我开始嵌套 objects 时,我 运行 遇到了问题。

一个非常简单的例子:ParentA有child类型B,有child类型C,有child类型D。在child D、我要管理一个颜色属性。下面的代码示例:

import 'package:flutter/material.dart';

class A with ChangeNotifier
{
    A() {_b = B();}

    B _b;
    B get b => _b;

    set b(B value)
    {
        _b = value;
        notifyListeners();
    }
}

class B with ChangeNotifier
{
    B() {_c = C();}

    C _c;
    C get c => _c;

    set c(C value)
    {
        _c = value;
        notifyListeners();
    }
}

class C with ChangeNotifier
{
    C() {_d = D();}

    D _d;
    D get d => _d;

    set d(D value)
    {
        _d = value;
        notifyListeners();
    }
}

class D with ChangeNotifier
{
    int                 _ColorIndex = 0;
    final List<Color>   _ColorList = [
        Colors.black,
        Colors.blue,
        Colors.green,
        Colors.purpleAccent
    ];

    D()
    {
        _color = Colors.red;
    }

    void ChangeColor()
    {
        if(_ColorIndex < _ColorList.length - 1)
        {
            _ColorIndex++;
        }
        else
        {
            _ColorIndex = 0;
        }

        color = _ColorList[_ColorIndex];
    }

    Color _color;

    Color get color => _color;

    set color(Color value)
    {
        _color = value;
        notifyListeners();
    }
}

现在我的 main.dart(管理我的 Placeholder() 小部件)包含以下内容:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_example/NestedObjects.dart';

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

class MyApp extends StatelessWidget
{
    @override
    Widget build(BuildContext context)
    {
        return MaterialApp(
            home: ChangeNotifierProvider<A>(
                builder: (context) => A(),
                child: MyHomePage()
            ),
        );
    }
}

class MyHomePage extends StatefulWidget
{

    @override
    State createState()
    {
        return _MyHomePageState();
    }
}

class _MyHomePageState extends State<MyHomePage>
{
    @override
    Widget build(BuildContext context)
    {
        A   a = Provider.of<A>(context);
        B   b = a.b;
        C   c = b.c;
        D   d = c.d;

        return Scaffold(
            body: Center(
                child: Column(
                    children: <Widget>[
                        Text(
                            'Current selected Color',
                        ),
                        Placeholder(color: d.color,),
                    ],
                ),
            ),
            floatingActionButton: FloatingActionButton(
                onPressed: () => ButtonPressed(context),
                tooltip: 'Increment',
                child: Icon(Icons.arrow_forward),
            ),
        );
    }

    void ButtonPressed(BuildContext aContext)
    {
        A   a = Provider.of<A>(context);
        B   b = a.b;
        C   c = b.c;
        D   d = c.d;

        d.ChangeColor();
    }
}

上面显示Placeholder Widget的颜色属性是由ClassD的颜色属性 (A -> B -> C -> D.color)。上面的代码非常简单,但它确实显示了我遇到的问题。

回到重点:我如何将child D的颜色属性分配给一个小部件,以便在更新 child D 的 属性 时,它也会自动更新小部件(使用 notifyListeners(),而不是 setState() ).

我用过无状态有状态Provider.ofConsumer,所有这些都给了我相同的结果。重申一下,objects 不能解耦,它必须具有 parent-child 关系。


编辑

更复杂的示例:

import 'dart:ui';

enum Manufacturer
{
    Airbus, Boeing, Embraer;
}

class Fleet
{
    List<Aircraft> Aircrafts;
}

class Aircraft
{
    Manufacturer        AircraftManufacturer;
    double              EmptyWeight;
    double              Length;
    List<Seat>          Seats;
    Map<int,CrewMember> CrewMembers;
}

class CrewMember
{
    String Name;
    String Surname;
}

class Seat
{
    int     Row;
    Color   SeatColor;
}

以上代码是真实世界示例的简化版本。就像你想象的那样,兔子洞会越陷越深。所以,我所说的 AD 示例的意思是试图简化情况的卷积。

例如,您想要在小部件中显示 and/or 更改船员姓名。在应用程序本身中,您通常会 select 来自 FleetAircraft(通过 List 索引传递给小部件),然后 select 一个 CrewMemberAircraft(通过 Map 键传递)然后 display/change CrewMemberName

最后,您的小部件将能够通过使用传入的 Aircrafts 索引和 CrewMembers 键来查看您所指的船员姓名。

我绝对愿意接受更好的架构和设计。

编辑:更新问题的答案,原文如下

不清楚 ABCD 在您的原始问题中代表什么。结果是 models.

我目前的想法是,用 MultiProvider/ProxyProvider 包装您的应用程序以提供 服务 ,而不是模型。

不确定您是如何加载数据的(如果有的话),但我假设有一项服务可以异步获取您的车队。如果您的数据由 parts/models 通过不同的服务加载(而不是一次全部加载),您可以将它们添加到 MultiProvider 并在需要加载更多数据时将它们注入适当的小部件。

下面的示例功能齐全。为了简单起见,并且由于您以更新 name 为例,我只做了 属性 setter notifyListeners().

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

main() {
  runApp(
    MultiProvider(
      providers: [Provider.value(value: Service())],
      child: MyApp()
    )
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Consumer<Service>(
            builder: (context, service, _) {
              return FutureBuilder<Fleet>(
                future: service.getFleet(), // might want to memoize this future
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    final member = snapshot.data.aircrafts[0].crewMembers[1];
                    return ShowCrewWidget(member);
                  } else {
                    return CircularProgressIndicator();
                  }
                }
              );
            }
          ),
        ),
      ),
    );
  }
}

class ShowCrewWidget extends StatelessWidget {

  ShowCrewWidget(this._member);

  final CrewMember _member;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CrewMember>(
      create: (_) => _member,
      child: Consumer<CrewMember>(
        builder: (_, model, __) {
          return GestureDetector(
            onDoubleTap: () => model.name = 'Peter',
            child: Text(model.name)
          );
        },
      ),
    );
  }
}

enum Manufacturer {
    Airbus, Boeing, Embraer
}

class Fleet extends ChangeNotifier {
    List<Aircraft> aircrafts = [];
}

class Aircraft extends ChangeNotifier {
    Manufacturer        aircraftManufacturer;
    double              emptyWeight;
    double              length;
    List<Seat>          seats;
    Map<int,CrewMember> crewMembers;
}

class CrewMember extends ChangeNotifier {
  CrewMember(this._name);

  String _name;
  String surname;

  String get name => _name;
  set name(String value) {
    _name = value;
    notifyListeners();
  }

}

class Seat extends ChangeNotifier {
  int row;
  Color seatColor;
}

class Service {

  Future<Fleet> getFleet() {
    final c1 = CrewMember('Mary');
    final c2 = CrewMember('John');
    final a1 = Aircraft()..crewMembers = { 0: c1, 1: c2 };
    final f1 = Fleet()..aircrafts.add(a1);
    return Future.delayed(Duration(seconds: 2), () => f1);
  }

}

运行 应用程序,等待 2 秒以加载数据,您应该会看到 "John" 是该地图中 id=1 的船员。然后双击文本,它应该更新为 "Peter".

如您所见,我正在使用顶级服务注册 (Provider.value(value: Service())) 和本地级别的模型注册 (ChangeNotifierProvider<CrewMember>(create: ...))。

我觉得这个架构(模型数量合理)应该是可行的。

关于本地级别的提供者,我觉得它有点冗长,但可能有办法让它更短。此外,为具有 setters 的模型提供一些代码生成库来通知更改会很棒。

(你有 C# 背景吗?我修复了你的 类 以符合 Dart 语法。)

让我知道这是否适合你。


如果您想使用 Provider,则必须使用 Provider 构建依赖关系图。

(您可以选择构造函数注入,而不是 setter 注入)

这个有效:

main() {
  runApp(MultiProvider(
    providers: [
        ChangeNotifierProvider<D>(create: (_) => D()),
        ChangeNotifierProxyProvider<D, C>(
          create: (_) => C(),
          update: (_, d, c) => c..d=d
        ),
        ChangeNotifierProxyProvider<C, B>(
          create: (_) => B(),
          update: (_, c, b) => b..c=c
        ),
        ChangeNotifierProxyProvider<B, A>(
          create: (_) => A(),
          update: (_, b, a) => a..b=b
        ),
      ],
      child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(context) {
    return MaterialApp(
      title: 'My Flutter App',
      home: Scaffold(
          body: Center(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                      Text(
                          'Current selected Color',
                      ),
                      Consumer<D>(
                        builder: (context, d, _) => Placeholder(color: d.color)
                      ),
                  ],
              ),
          ),
          floatingActionButton: FloatingActionButton(
              onPressed: () => Provider.of<D>(context, listen: false).color = Colors.black,
              tooltip: 'Increment',
              child: Icon(Icons.arrow_forward),
          ),
      ),
    );
  }
}

此应用程序基于您的 ABCD 类.

您的示例不使用代理,因为它仅使用没有依赖项的 D。但是你可以看到 Provider 已经正确地连接了这个例子的依赖:

Consumer<A>(
  builder: (context, a, _) => Text(a.b.c.d.runtimeType.toString())
),

会打印出"D".

ChangeColor() 没有工作,因为它没有调用 notifyListeners()

在此之上无需使用有状态小部件。

正如我之前所说,您的设置似乎过于复杂。模型的每个实例 class 都是一个 ChangeNotifier,因此负责维护自身。这是一个架构问题,将导致扩展和维护问题。

几乎所有现存的软件架构都有一些共同点——将状态与控制器分开。数据应该只是数据。它不需要关心程序其余部分的操作。同时,控制器(bloc、view model、manager、service 或任何你想称呼它的东西)为程序的其余部分提供访问或修改数据的接口。通过这种方式,我们保持关注点分离并减少服务之间的交互点数量,从而大大减少依赖关系(这对保持程序简单和可维护性大有帮助)。

在这种情况下,不可变状态方法可能是一个很好的选择。在这种方法中,模型 classes 就是不可变的。如果你想改变模型中的某些东西,而不是更新一个字段,你可以换出整个模型 class 实例。这可能看起来很浪费,但它实际上通过设计在您的状态管理中创建了几个属性:

  1. 由于无法直接更改字段,模型的使用者被迫改用控制器中的更新端点。
  2. 每个模型 class 都成为独立的事实来源,程序其余部分的任何重构都不会影响,从而消除了过度耦合的副作用。
  3. 每个实例都代表您的程序存在的全新状态,因此通过适当的监听机制(此处通过提供程序实现)告诉程序根据状态变化进行更新非常简单。

这是一个示例,说明您的模型 classes 可能如何由不可变状态管理来表示:

main() {
  runApp(
    ChangeNotifierProvider(
      create: FleetManager(),
      child: MyApp(),
    ),
  );
}

...

class FleetManager extends ChangeNotifier {
  final _fleet = <String, Aircraft>{};
  Map<String, Aircraft> get fleet => Map.unmodifiable(_fleet);

  void updateAircraft(String id, Aircraft aircraft) {
    _fleet[id] = aircraft;
    notifyListeners();
  }

  void removeAircraft(String id) {
    _fleet.remove(id);
    notifyListeners();
  }
}

class Aircraft {
  Aircraft({
    this.aircraftManufacturer,
    this.emptyWeight,
    this.length,
    this.seats = const {},
    this.crewMembers = const {},
  });

  final String aircraftManufacturer;
  final double emptyWeight;
  final double length;
  final Map<int, Seat> seats;
  final Map<int, CrewMember> crewMembers;

  Aircraft copyWith({
    String aircraftManufacturer,
    double emptyWeight,
    double length,
    Map<int, Seat> seats,
    Map<int, CrewMember> crewMembers,
  }) => Aircraft(
    aircraftManufacturer: aircraftManufacturer ?? this.aircraftManufacturer,
    emptyWeight: emptyWeight ?? this.emptyWeight,
    length: length ?? this.length,
    seats: seats ?? this.seats,
    crewMembers: crewMembers ?? this.crewMembers,
  );

  Aircraft withSeat(int id, Seat seat) {
    return Aircraft.copyWith(seats: {
      ...this.seats,
      id: seat,
    });
  }

  Aircraft withCrewMember(int id, CrewMember crewMember) {
    return Aircraft.copyWith(seats: {
      ...this.crewMembers,
      id: crewMember,
    });
  }
}

class CrewMember {
  CrewMember({
    this.firstName,
    this.lastName,
  });

  final String firstName;
  final String lastName;

  CrewMember copyWith({
    String firstName,
    String lastName,
  }) => CrewMember(
    firstName: firstName ?? this.firstName,
    lastName: lastName ?? this.lastName,
  );
}

class Seat {
  Seat({
    this.row,
    this.seatColor,
  });

  final int row;
  final Color seatColor;

  Seat copyWith({
    String row,
    String seatColor,
  }) => Seat(
    row: row ?? this.row,
    seatColor: seatColor ?? this.seatColor,
  );
}

每当您想要添加、修改或移除机队中的飞机时,您都需要查看 FleetManager,而不是单个模型。例如,如果我有一名船员,我想更改他们的名字,我会这样做:

final oldCrewMember = oldAircraft.crewMembers[selectedCrewMemberId];
final newCrewMember = oldCrewMember.copyWith(firstName: 'Jane');
final newAircraft = oldAircraft.withCrewMember(selectedCrewMemberId, newCrewMember);
fleetManager.updateAircraft(aircraftId, newAircraft);

当然,它比 crewMember.firstName = 'Jane'; 更冗长,但请考虑此处发挥的架构优势。通过这种方法,我们没有庞大的相互依赖关系网络,任何地方的变化都可能对其他地方产生影响,其中一些可能是无意的。只有一种状态,所以只有一个地方可能发生变化。监听此更改的任何其他内容都必须经过 FleetManager,因此只需担心一个接口点 - 一个故障点而不是可能的数十个。有了所有这些体系结构的安全性和简单性,在代码中多一点冗长是值得的交易。

这是一个有点简单的例子,虽然肯定有改进的方法,但无论如何还是有一些包可以为我们处理这类事情。为了更稳健地执行不可变状态管理,我建议查看 flutter_bloc or redux 包。 redux 包本质上是 React 中 Redux 到 Flutter 的直接端口,因此如果您有 React 经验,您会感到宾至如归。 flutter_bloc 包对不可变状态采用了一种不太严格的方法,并且还结合了有限状态机模式,这进一步降低了围绕如何判断您的应用程序在任何给定时间所处状态的复杂性。

(另请注意,在此示例中,我将 Manufacturer 枚举更改为 Airline class 中的一个字符串字段。这是因为航空公司制造商太多了在这个世界上,跟上他们所有人将是一件苦差事,任何不由枚举代表的制造商都不能存储在舰队模型中。将它作为一个字符串只是你需要主动做的一件少事维护。)