Dart: Inheritance, Factory, Enums & Equality

How I Use the Above using a Shopping App Example

Soo Kim
5 min readJan 22, 2023

It took some time for me to become comfortable using classes and definitely struggled when I started studying programming a year and a half ago. So, I thought I’d share how I use classes, inheritance, enums and equality (in Dart). I’ll be using a shopping app as an example.

Abstract Class & Inheritance

Abstract classes cannot be instantiated — used to create objects. Instead, you create sub-classes that inherit that class through the keyword “extends” or “implements”. In Java, abstract classes do not contain constructors, but in Dart, you can.

My example — Product — is an abstract class because I want it to be the parent class of more specific categories of products (shoes, pants, t-shirts etc).

abstract class Product {
final String productUid;
final String productName;
final Gender gender;
final ProductType type;
final bool isOnSale;
final double originalPrice;
final double salePercentage;

const Product({
required this.productUid,
required this.productName,
required this.gender,
required this.type,
required this.isOnSale,
required this.originalPrice,
required this.salePercentage
});
}

The advantage of having a top-level class is that you can use the parent class to refer to all its subclasses. Let’s say that you make an order. The product could be Shoes, Pants, Hoodie, Bag etc…so instead, you refer to the abstract class Product.

class Order {
final List<Product> products;
/// and other fields

const Order({required this.products});
}

Extend

Think of this as your sub-class being an “extension” of the parent class. You can use super to refer to the parent class and use its objects. An easy example is that many Flutter widgets extend either StatelessWidget or StatefulWidget.

I would extend Product class for all my specific categories of products. If your parent-class has a constructor, you can use super.fieldName in the constructor, or invoke a super constructor.

class Shoes extends Product {
final ShoeType shoeType;
final int size;

const Shoes({
required super.productUid,
required super.productName,
required super.gender,
required super.type,
required super.originalPrice,
required super.salePercentage,
required this.size,
required this.shoeType,
}) : super (isOnSale: salePercentage > 0.0 ? true : false); // super constructor
}

class Pants extends Product {
final PantsType pantsType;
final ApparelSize size;

const Pants({required super.productUid,// ...etc});
}

Implement

I rarely implement a class, but in to explain, in Dart, if you implement a class, all fields of the parent class must be present in the sub-class. The parent class’ fields do not have to be instantiated and methods can have empty bodies. But the sub-class must have concrete implementations. For example, ChangeNotifier (which I use as mixin for my providers) implements Listenable class.

I personally have not used implement yet, as I prefer to use mixins instead.

Mixin

I prefer to use mixin because I can reuse a class’s code in multiple classes without needing to implement every single one. For example, if the shopping app allows a user to change his/her profile picture AND upload a photo on his/her review, this is what I would do:

import 'dart:typed_data';

class ImageService {
Future<Uint8List?> pickPhoto() async {
// some code that returns user's photo from gallery
}

Future<Uint8List?> takePicture() async {
// some code that opens user camera and returns the photo taken
}
}

class ReviewProvider with ImageService {
List<Uint8List> photos = [];

Future<void> pickPhotoFromGallery() async {
final Uint8List? _photo = await super.pickPhoto();
if (_photo == null) return;
this.photos.add(_photo);
}

Future<void> openCamera() async {
final Uint8List? _photo = await super.pickPhoto();
if (_photo == null) return;
this.photos.add(_photo);
}
}

class UserProvider with ImageService {

Future<void> pickPhotoFromGallery() async {
final Uint8List? _photo = await super.pickPhoto();
if (_photo == null) return;
/// some code that changes user's photo
}
}

Factory

I use the factory constructor for two primary reasons: (1) create an instance of the class from JSON, and (2) create a new instance when a field of an existing instance has to be changed. As for the latter reason, I do so because I prefer all my classes to be immutable, which makes my code less prone to errors.

typedef Json = Map<String, dynamic>;

class Shoes extends Product {
// same code as above...
factory Shoes.fromJson(Json json) => Shoes(
productName: json["productName"],
productUid: json["productUid"],
gender: Gender.values.singleWhere((Gender gender) => gender.name == json["gender"]),
size: int.parse(json["size"]),
type: ProductType.values.singleWhere((ProductType type) => type.name == json["productType"]),
shoeType: ShoeType.values.singleWhere((ShoeType type) => type.name == json["shoeType"]),
originalPrice: double.parse(json["price"]),
salePercentage: double.parse(json["salePercentage"]),
);
}
class User {
final String userUid;
final String userName;
final String email;

const User({required this.userUid, required this.userName, required this.email});

factory User.changeUserName({required User user, required String newName}) => User(
userUid: user.userUid, email: user.email, userName: newName
);
}

Sidenote: This is the code I use whenever I get a list as JSON that needs to be converted to a List of a class. There were too many places where I was re-writing basically the same code and decided to make a method to make my life easier. Instead of making the method static, you can also use it as a mixin.

class JsonMethod {
static List<T> convertJsonList<T>({required List<dynamic> jsonList, required T Function(Json) factory}) {
final List<Json> _jsonList = List<Json>.from(jsonList);
List<T> _list = [];
_jsonList.forEach((Json json) => _list.add(factory(json)));
return _list;
}
}

Enums

Enums are a great way to declare constant, customized types. In the abstract class Product, I had two enums — Gender and ProductType. Let’s look at ProductType. You can use enum.value to get the String representation of it (as I did in the factory constructor of Shoes). Also, the enum extension method was recently introduced, which allows you to define getters or methods to enumerations. Y

enum ProductType {
shoes, pants, shorts, shirt, hoodie
}

extension ProductTypeExt on ProductType {
String get koreanName {
switch (this) {
case ProductType.hoodie: return "후디";
case ProductType.pants: return "바지";
case ProductType.shoes: return "신발";
case ProductType.shirt: return "티셔츠";
case ProductType.shorts: return "반바지";
}
}
}

class Shoes extends Product {
final ProductType type; ///....other code
}

final Shoes _nikeShoes = Shoes(ProductType.shoes);
final String _typeInKorean = _nikeShoes.type.koreanName;

Equality

Even if all the variables in your classes have the same value, one does not “equal” the other. By overriding the == operator and hashCode (any two Objects that is deemed equal must have the same hashCode, although two Objects with the same hashCode isn’t necessarily equal), you can check whether one instance is the same as another.

class Shoes extends Product {
//...same code as above...

@override
bool operator == (other) {
if (other is! Shoes) return false;
if (this.productName != other.productName || this.type != other.type) return false; /// and so on...
return this.productUid == other.productUid && this.size == other.size;
}

@override
int get hashCode => this.productUid.hashCode + this.size;
}

Happy Coding!

--

--