2

In a Flutter app I'm developing, I have a class hierarchy of model objects of a certain base class. To take a typical example, the abstract base class could be Vehicle. Then I have a (pretty much) parallel hierarchy of widget classes that show info about a particular subclass of Vehicle: Car objects are shown with CarWidget, Trucks with TruckWidget, etc. These widgets all inherit from the ABC VehicleWidget. This is because all of the widgets have some part in common, which is the part that corresponds to the base class Vehicle, but each subclass of Vehicle needs to show some specialized info as well. (If you don't like inheritance with Flutter widgets, then replace "inheritance" with "composition", what follows still holds.)

My app's global state holds a List<Vehicle>, and for each vehicle in that list it needs to show the corresponding widget. In order to do this, the runtime type of the vehicle must be checked. The natural object-oriented approach that comes to mind is to use virtual methods: declare an abstract method makeWidget() in the Vehicle class, and then make each subclass select the appropriate widget subclass.

However, I don't like mixing UI with model. I thought of using extensions to define the makeWidget() method in a separate file, but extensions in Dart can't define virtual methods, they're essentially syntactic sugar for a regular function call (i.e. with static type checking). What I ended up doing is switching on the runtime type explicitly:

abstract class VehicleWidget extends StatelessWidget {
  static VehicleWidget of(Vehicle vehicle) {
    switch (vehicle.runtimeType) {
      case Car:
      // ...  
      case Truck:
      // ...
    }
  }
 
  // ...
}
// whenever I need to create the appropriate widget for a vehicle
VehicleWidget.of(vehicle)

Now the "UI logic" of selecting the appropriate widget is with UI code and separate from model code.

However, this has a big disadvantage. The compiler won't be able to tell me if I've forgotten to implement the appropriate widget (and specify it as a case in the switch) when I create a new subclass of Vehicle. Even worse, if there are multiple such parallel widget hierarchies (for example, VehicleCard and VehicleDetails), I need to mantain one switch statement for each of those.

My question is: which of the two approaches is better (for which scenario)? Is there any advantage to separating the selection of the appropriate widget class from the model hierarchy, as I have done?

3 Answers 3

2

However, I don't like mixing UI with model.

Why not? I mean, what is the rational reason behind this decision? I blame Robert Martin :). You're basically saying it would solve your problem, but you're not going to do it.

It is ok to mix UI with Business, in this case. How to display this stuff is closely linked with its internal representation and semantics. It makes absolutely no sense to artificially separate something from an object that basically needs everything from that object to work.

Note, you don't need to include details of the UI in the objects. You can even create a small, specialized widget set for this. You don't need to specify colors, or how it is positioned, etc., which is the scenario certain people are trying to scare others with.

Note 2: I did create Android apps previously (not flutter) where objects could present themselves. I needed to write a small UI framework for this, because Fragments were too heavy weight, stateful and of the wrong scope.

On a minor note, don't call it makeWidget(), call it display() for example. "The Vehicle displays itself" makes complete sense to me.

0

Another way to tackle this is to factor the switch logic into an interface/set of classes and a dictionary lookup. The key is the type of the vehicle and the class instance is the value. Voila--no more switch.

As you point out, the compiler no longer tells you when you've forgotten to implement an instance for a particular type of vehicle. However, it's easy to write an automated test that does this for you.

This is one example of the more general concept (you mention) of preferring composition over inheritance. Here is a question that addresses why one might want to do this.

2
  • What would be the advantage of using dictionary lookup instead of a switch? The latter can also be tested, right?
    – Anakhand
    Jan 20, 2023 at 7:59
  • @Anakhand Added a reference to an SO question with the "why." Jan 21, 2023 at 9:16
0

A language-specific solution, but a nice solution nonetheless: it seems that Dart 3 is introducing pattern matching and other features, so I could declare Vehicle as a sealed class for exhaustiveness checking and use pattern matching similar to before:

static VehicleWidget of(Vehicle vehicle) {
  switch (vehicle.runtimeType) {
    Car() => ...
    Truck() => ...
    ...
  }
}

By making Vehicle a sealed class, I'm telling the compiler that the hierarchy of subclasses of Vehicle is fixed and so it can check when I forget to handle a subclass of Vehicle in one of these patterns.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.