Comparing Methods for Projecting Fields from Classes in Dart

Ryoichi Izumita
7 min readJun 13, 2024

--

Photo by Andrey Strizhkov on Unsplash

When developing a Flutter app, projecting specific fields from a class to create a new data structure is essential in various situations.

In this article, we will explore five approaches for projection in Dart, comparing their characteristics, advantages, and disadvantages.

• Defining a new class

• Using record types

• Utilizing extension types

• Using mixin

• Using Map structures

We will provide criteria for choosing the appropriate approach for specific development scenarios.

Defining a New Class

This approach involves defining a new class that contains only the necessary fields from an existing class. In this method, you explicitly define a new class that holds the fields being projected.

Advantages

Type Safety: Ensuring type safety by defining a new class explicitly allows for compile-time error detection.

Ability to Implement Additional Features: You can add custom methods or logic to the new class, defining behaviors related to the fields.

Strong Encapsulation: By defining a class that only holds the minimum necessary information, encapsulation is enhanced.

Disadvantages

Redundancy: Defining a new class for each necessary field can lead to redundant code.

Instantiation Cost: Instantiating new classes may increase memory usage.

Selection Criteria

Defining a new class is suitable in the following scenarios:

Type Safety: When you want to prevent errors using explicit types.

Implementing Additional Features: When you need to add custom behaviors to the new data structure.

Encapsulation: When you need to strictly manage fields and exclude unnecessary data.

Example

Below is an example of projecting the name and email fields from a Person class.

// Original class
class Person {
final String name;
final int age;
final String email;

Person(this.name, this.age, this.email);
}

// Projected class
class ContactInfo {
final String name;
final String email;

// Factory constructor
factory ContactInfo.fromPerson(Person person) {
return ContactInfo._internal(person.name, person.email);
}

// Private constructor
ContactInfo._internal(this.name, this.email);
}

void main() {
Person person = Person('John Doe', 30, 'john.doe@example.com');
ContactInfo contactInfo = ContactInfo.fromPerson(person);

print('Name: ${contactInfo.name}, Email: ${contactInfo.email}');
}

In this example, the ContactInfo class is defined to hold name and email from the Person class, performing the projection within the factory constructor ContactInfo.fromPerson. This improves code readability and maintainability by encapsulating projection logic within the class.

Using Record Types

Record types are lightweight data structures that group multiple fields with concise syntax. They are simpler and more performant than classes, serving as convenient lightweight data carriers. Using record types for projection allows easy grouping of necessary data.

Advantages

Simple and Lightweight: Handle multiple fields with simple syntax and simpler definition compared to classes.

High Performance: Being lightweight, they are advantageous in terms of memory usage and performance.

Ease of Use: Simplifies the code and makes field extraction easy.

Disadvantages

Limited Functionality Extension: It is not impossible but challenging to have methods, limiting use to data carriers.

Selection Criteria

Using record types is appropriate in the following scenarios:

Need for Lightweight Data Structures: When high performance is required with minimal necessary fields.

Simple Data Processing: When the purpose is to hold and transfer simple data without complex logic or methods.

Rapid Prototyping: When you need to quickly write code without complex class definitions.

Example

Here is an example of projecting the name and email fields from a Person class using a record type.

// Original class
class Person {
final String name;
final int age;
final String email;

Person(this.name, this.age, this.email);
}

void main() {
Person person = Person('John Doe', 30, 'john.doe@example.com');

// Projection using record type
final contactInfo = (name: person.name, email: person.email);

print('Name: ${contactInfo.name}, Email: ${contactInfo.email}');
}

In this example, the name and email from the Person class are projected as a record type. Using record types simplifies the grouping of necessary fields, making the code concise and readable.

Utilizing Extension Types

Extension types in Dart allow the creation of wrappers for specific classes or types. This feature lets you extend existing class instances with additional properties and methods without adding extra state.

Advantages

Avoid Redundant Class Definitions: No need for new class definitions, keeping code concise.

Add Behaviors: Ability to add functionality.

Simple Generation: Can generate new types without data migration.

Clear Type Names: Unlike records with potentially ambiguous signatures, extension types have clear type names.

Disadvantages

Consistency Issues: As extension types can access non-projection-related data internally, adding unrelated functionality can compromise integrity.

Selection Criteria

Using extension types is suitable in these cases:

Simplistic Type Definition: When you want to define simple types without new class definitions.

Adding Behaviors: When you need to add behaviors along with data carriers.

Clear Naming: When it is necessary to clarify the intended meaning through type names.

Example

Here is an example of defining an extension type to access the name and email fields from a Person class.

// Original class
class Person {
final String name;
final int age;
final String email;

Person(this.name, this.age, this.email);
}

// Defining the extension type
extension type ContactInfo(Person _person) {
String get name => _person.name;
String get email => _person.email;
}

void main() {
Person person = Person('John Doe', 30, 'john.doe@example.com');
ContactInfo contactInfo = ContactInfo(person);

// Accessing fields via extension type
print('Name: ${contactInfo.name}, Email: ${contactInfo.email}');
}

In this example, the ContactInfo extension type is defined to provide name and email properties from the Person class. This approach allows easy access to necessary information without modifying the existing class.

Using Mixin

Mixin in Dart allow sharing common structures and functionalities across multiple classes. By using mixin, you can distribute specific fields and methods among various classes, thus improving code reusability and facilitating the sharing of specific functionalities.

Advantages

High Code Reusability: Common structures and functionalities can be shared across multiple classes, reducing redundant code.

Separation of Concerns: Each mixin focuses on a specific functionality, promoting separation of concerns.

Flexibility: You can add necessary features to existing classes without altering the class hierarchy.

Disadvantages

Broad Impact: Changes to a mixin can affect multiple classes, necessitating careful management of those changes.

Name Collisions: Multiple mixins defining the same field or method can lead to naming conflicts.

Increased Complexity: Excessive use of mixins can make the code harder to understand and maintain.

Selection Criteria

Mixins are suitable in the following scenarios:

Common Projection Across Multiple Classes: When you need to share the same fields or methods across multiple classes.

Maximizing Code Reusability: When you aim to reuse specific functionalities across classes to minimize redundancy.

Emphasizing Separation of Concerns: When functional code separation is important.

Example

Below is an example of defining a mixin to hold the name and email fields from a Person class.

// Original class
class Person with ContactInfoMixin {
final String name;
final int age;
final String email;

Person(this.name, this.age, this.email);
}

// Defining the mixin
mixin ContactInfoMixin {
String get name;
String get email;

String get contactInfo => 'Name: $name, Email: $email';
}

// Class using the ContactInfoMixin
class Student extends Person {
Student(String name, int age, String email) : super(name, age, email);
}

void main() {
ContactInfoMixin contactInfo = Student('John Doe', 20, 'john.doe@student.com');

// Accessing fields via mixin
print(contactInfo.contactInfo); // Output: Name: John Doe, Email: john.doe@student.com
}

In this example, the ContactInfoMixin is applied to the Person class and inherited by the Student class, allowing access to name and email fields. Using mixins enables the reuse of common functionalities across multiple classes.

Using Map Structures

By using Dart’s Map structures, you can manage data flexibly with key-value pairs. Map structures are convenient for dynamically storing and retrieving data based on keys. Projecting specific fields from a class and managing them flexibly suits situations where data dynamically changes.

Advantages

Flexibility: Dynamically add, remove, and modify fields without relying on fixed class fields.

Concise Code: No need for field definitions, directly using Map structures simplifies the code.

General Use: Applicable to different data structures, allowing flexible management based on use cases.

Disadvantages

Low Type Safety: Maps generally do not guarantee type safety, necessitating runtime checks for the presence of specific keys or values.

Unpredictability: Dynamic operations make it hard to predict which fields exist beforehand.

Performance Degradation: Dynamic manipulation of large amounts of data can lead to lower performance compared to fixed-type classes.

Selection Criteria

Using Map structures is appropriate in the following scenarios:

Dynamic Data Management: When frequent addition, deletion, and modification of fields are necessary.

Emphasizing Flexibility: When you want to handle flexible data structures without being bound by specific fields.

Simple Data Operations: When you need quick data extraction and updates.

Example

Below is an example of projecting the name and email fields from a Person class and managing them using a Map structure.

// Original class
class Person {
final String name;
final int age;
final String email;

Person(this.name, this.age, this.email);
}

void main() {
Person person = Person('John Doe', 30, 'john.doe@example.com');

// Projection using Map structure
Map<String, String> contactInfo = {
'name': person.name,
'email': person.email,
};

print('Name: ${contactInfo['name']}, Email: ${contactInfo['email']}');
}

In this example, the name and email fields from the Person class are extracted and managed as a Map structure. Using a Map structure allows dynamic manipulation of key-value pairs, providing flexible data management.

Conclusion

We have explored various methods for projecting fields from classes in Dart. Each method has its unique advantages and disadvantages, and it is essential to choose the optimal approach based on specific scenarios.

Defining a New Class is effective for situations emphasizing type safety and encapsulation, where adding custom methods or behaviors is necessary.

Record Types are suitable for lightweight and simple data structures, ideal for prototyping and high-performance requirements.

Extension Types are useful when you want to add functionalities to existing classes without modifications, allowing flexible addition of new fields or methods.

Mixins are beneficial for sharing common functionalities across multiple classes and promoting separation of concerns.

Map Structures are optimal for dynamic data management, offering quick and flexible data operations.

Selection Guidelines

Project Scale and Type Safety: For large-scale projects or where type safety is crucial, consider new classes, records, or extension types.

Performance and Simplicity: For high-performance needs or maintaining simple definitions, records or Map structures are suitable.

Expanding Existing Classes: For adding new functionalities to existing classes, consider extension types or mixins.

Flexibility and Dynamic Management: For scenarios requiring dynamic data management, Map structures are optimal.

Final Remarks

This article introduced five different methods for projecting fields from classes in Dart, detailing their characteristics and applicable scenarios. Choosing the best method can improve code quality and development efficiency. We hope this guide supports effective design decisions in your projects.

--

--

Ryoichi Izumita
Ryoichi Izumita

Written by Ryoichi Izumita

iOS / Flutter / Objective-C / Swift / Dart

No responses yet