Flutter Reflection Demystified: Unleash the Power of Reflectable

Want to peek under the hood of your Flutter code at runtime? Traditional reflection lets programs analyze themselves, but Flutter (built on Dart) doesn’t offer this due to mobile app limitations.

Enter Reflectable: This Flutter package brings reflection-like power through a neat trick: code generation. It creates custom classes and methods specifically for your project, minimizing the impact on app size and performance.

How does it work? You define a Reflectable subclass with the features you need, like calling methods or reading annotations. This subclass acts as your personal code inspector, letting you analyze specific classes marked for reflection.

Setting Up Reflectable in Flutter

Add the Reflectable package to your pubspec.yaml file and import it where needed. This lets you leverage reflection’s power in your Dart code.

dependencies:
  reflectable: ^4.0.5

After including the Reflectable package, use a Reflector instance to annotate the classes you want to inspect at runtime, specifying the desired reflection capabilities (like calling methods or reading annotations). Remember, this generated code is specific to your project and shouldn’t be shipped in the final app.

@reflector
class AnotherExampleClass {
  // Class definition goes here
}

Code Generation with Reflectable

The magic happens with code generation! Reflectable analyzes your annotated classes and creates custom code that bridges your static Dart code with the dynamic reflection abilities. This avoids the usual performance drawbacks of reflection. Build automation tools like build_runner handle this process based on your project setup (we’ll see how in the next section!).

To illustrate, here’s how you might set up your build.yaml to include the necessary builders for the Reflectable package:

targets:
  $default:
    builders:
      reflectable:
        generate_for:
          - lib/*.dart

Generating Code for Reflection Support

Reflectable’s secret sauce: Code generation! This process analyzes your annotated classes at compile time, not runtime, to create custom code that unlocks reflection abilities. This avoids performance hits often associated with reflection. The generated code mirrors your annotated classes, allowing you to access information, call methods, and even create instances dynamically – all within your defined limitations. To trigger this magic, simply run pub run build_runner build, which scans your project for annotations and generates the corresponding Dart files.

// Example of a command to generate code
// Run this in the terminal at the root of your Flutter project
pub run build_runner build

The output of this command is a set of .reflectable.dart files, one for each Dart file in your project that contains annotated classes. These generated files must be imported into your project to enable reflection.

The Build Process and Generated Code

Integrating Reflectable involves three key steps:

  1. Annotate Your Classes: Tell Reflectable which classes you want to inspect by adding annotations with the desired reflection capabilities (like calling methods or reading annotations).
  2. Generate the Magic Code: Run a build command (pub run build_runner build) to trigger Reflectable’s code generation. This process analyzes your project and creates custom code specifically for your annotated classes.
  3. Import the Generated Code: Include the generated files in your project to unlock reflection abilities for your annotated classes.

Remember, these generated files are unique to your project and shouldn’t be shipped in the final app.

// Example of importing generated code in your main Dart file
import 'main.reflectable.dart';

Implementing Reflection Logic

Creating Instances and Invoking Methods

With the generated code in place, you can leverage reflection in your Flutter app. This lets you dynamically create class instances and call methods at runtime. The magic happens through InstanceMirror objects, which reflect the state of an instance. You can use these mirrors to interact with the instance’s methods and fields.

To create an instance dynamically, use the newInstance method provided by the class mirror. This method lets you specify the constructor (named or unnamed) and any required arguments.

// Assuming `ExampleClass` is annotated and has a default constructor
var classMirror = reflector.reflectType(ExampleClass) as ClassMirror;
var instance = classMirror.newInstance("", []);

When it comes to invoking methods, you can use the invoke method on an InstanceMirror. This method requires the method name as a string and an array of arguments to pass to the method. The result is returned if the method is successfully invoked; otherwise, an error is thrown.

// Assuming `ExampleClass` has a method called `add`
var instanceMirror = reflector.reflect(instance);
var result = instanceMirror.invoke('add', [5]);

Accessing Fields and Annotations

Reflection isn’t just about calling methods. Reflectable lets you delve deeper! You can use getField and setField methods on InstanceMirror objects to access and even modify the fields of your reflected instances.

But that’s not all! Reflectable also allows you to read annotations at runtime. These annotations can provide additional information about your classes and fields, unlocking further customization possibilities.

// Get the value of the field `value`
var fieldValue = instanceMirror.getField('value').reflectee;

// Set the value of the field `value`
instanceMirror.setField('value', 42);

Annotations are another critical aspect of reflection. They provide metadata about classes, methods, and fields that can be used to inform reflection logic. To access annotations, you can use the annotations property on mirrors. This allows you to obtain data that can dynamically drive your application’s behavior.

// Assuming `ExampleClass` has annotations
var classAnnotations = classMirror.annotations;

Conclusion

Flutter Reflectable empowers you to add reflection capabilities to your Flutter apps. Code generation ensures this happens efficiently, without sacrificing performance. By mastering code generation, reflection logic, and advanced techniques, you can unlock dynamic features that make your apps more flexible and adaptable.

Remember, with great power comes responsibility. Use reflection judiciously to avoid performance issues in mobile and web apps. By carefully considering your needs and using Reflectable strategically, you can reap the benefits of reflection while maintaining a smooth user experience.

Wanna Level up Your Flutter game? Then check out our ebook The Complete Guide to Flutter Developement where we teach you how to build production grade cross platform apps from scratch.Do check it out to completely Master Flutter framework from basic to advanced level.

Leave a comment