The Dissolution Principle

Written By Advay Sanketi
| 7 min read

In Flutter, the way we manage state can feel like a delicate dance. Just as a skilled dancer interprets music into movement, a well-built Flutter app flows seamlessly, transforming data into visual elements. But how does this work? The answer lies in a principle called "Dissolution" - Breaking down complex features into Flutter's fundamental building blocks.

Key Concept: UI = f(data)(state)
This formula represents our entire journey: how we transform raw data through states into something visible on screen.

The Space-Time Journey of Your Code

Let's begin with a simple dart class:

// Step 1: Define a Dart class
class GreetingModel with ChangeNotifier {
String _name;

GreetingModel(this._name);

String get name => _name;

void updateName(String newName) {
_name = newName;
notifyListeners(); // Trigger UI update
}
}

Right now, this information exists only on my device. But with a bit of luck, it will travel through time and space to your device, and appear on your screen.

The fact that this works is a marvel of technology.

Deep inside your Flutter app, there are pieces of code that know how to display a widget or manage state. These pieces of code can vary between different state management solutions, and even between different versions of the same library. However, because these concepts have been given agreed-upon names (like ChangeNotifier for state management), I can refer to them without worrying about how they really work on your device. I can't directly access their internal logic, but I know which information I can pass to them (such as a Provider or Riverpod).

Thanks to Flutter's abstraction layers, I can be reasonably sure my greeting will appear as I intended.

Abstraction Layers: Each layer handles specific responsibilities:

  • Framework layer (Flutter core)
  • State management layer
  • Application layer (your code)

Tags like ChangeNotifier and Provider let us refer to the built-in Flutter concepts. However, names don't have to refer to something built-in. For example, I'm using a custom model like GreetingModel to manage my greeting's state. I didn't come up with that name myself—it comes from the principles of state management in Flutter. I've included it in my app, which lets me use any of the state management patterns it defines.

So why do we like giving names to things?


I wrote GreetingModel, and my editor recognized that name. So did your Flutter app. If you've done some Flutter development, you probably recognized it too, and maybe even guessed what would appear on the screen by reading the code. In that sense, names help us start with a bit of a shared understanding.

Fundamentally, Flutter executes relatively basic kinds of instructions—like rendering widgets, managing state, or responding to user input. Merely showing a greeting on your screen could involve running hundreds of thousands of such instructions.

If you saw all the instructions your Flutter app ran to display a greeting, you could hardly guess what they're doing. It would seem incomprehensible! You'd need to "zoom out" to see what's going on. To describe a complex system, or to instruct a complex system what to do, it helps to separate its behavior into layers that build on each other's concepts. This way, people working on the Flutter framework can focus on how to render widgets. Then people working on state management can focus on how to manage data. And that lets developers like me focus on picking just the right data for my "greetings.". This is called abstraction.

We like names because they let us forget what's behind them. Let's understand with an example:

// The complex version that flutter understands
ChangeNotifierProvider(
create: (context) => GreetingModel('Advay'),
child: GreetingWidget(),
);

From your Flutter app's perspective, this is a provider that supplies a GreetingModel with a name. But from my perspective, it's a greeting for Advay. Although my greeting happens to be managed by a state management solution, most of the time I want to think about it this way instead:

// What we want to write
Greeting(person: Advay); // Much clearer and easier!

Giving this concept a name provides me with some newfound flexibility. I can now display multiple Greetings without copying and pasting their code. I can pass different data to them. If I wanted to change how all greetings look and behave, I could do it in a single place. Turning Greeting into its own concept lets me adjust "which greetings to display" separately from "what a greeting is."

However, I have also introduced a problem.

Now that I've given this concept a name, the "language" in my mind is different from the "language" that Flutter speaks. Flutter knows about ChangeNotifier and Provider, but it has never heard of a Greeting—that's my own concept. If I wanted Flutter to understand what I mean, I'd have to "translate" this piece of code to only use the concepts that Flutter already knows.

I'd need to turn this:

Greeting(person: Advay);

into this:

ChangeNotifierProvider(
create: (context) => GreetingModel('Advay'),
child: GreetingWidget(),
);

How would I go about that?

The Translation Process

When we run Flutter code, we're actually creating a series of translations:

  1. High-level concepts (Your mental model)
  2. Widget definitions (Your Flutter code)
  3. Framework instructions (Flutter's internal representation)
  4. Rendered UI (What users see)

Consider this Greeting that I have defined:

// Step 1: Define what a Greeting means
class Greeting extends StatelessWidget {

Widget build(BuildContext context) {
final greetingModel = Provider.of<GreetingModel>(context);
// Transform data into UI
return Text('Hello, ${greetingModel.name}!');
}
}

Your Flutter app wouldn't know what a Greeting is—but now that I wrote a definition for that concept, I can apply this definition to "unpack" what I meant.

The Dissolution Process

Like a chain reaction, each widget in your app undergoes a transformation process. Under the hood, Flutter constructs a widget tree with properties corresponding to the widget's attributes. To get the final result, we need to plug the properties(data) into the code.

Here is a little function I wrote that does exactly that:

// The translation function
Widget translateForFlutter(Widget originalWidget) {
// Step 1: Handle custom widgets
// Greeting
if (originalWidget is Greeting) {
return Text('Hello, ${Greeting.name}!');
}
// ExpandableGreeting
else if (originalWidget is ExpandableGreeting) {
return translateForFlutter(ExpandableGreeting(originalWidget.person));
}
// Step 2: Handle Flutter widgets
else if (originalWidget is Column) {
return Column(
children: originalWidget.children.map(translateForFlutter).toList(),
);
}
// Step 3: Pass through unchanged widgets
return originalWidget;
}

Visualization of the Process

Your Code:
Greeting(person: Advay)
    ↓ [Translation]
Flutter Widget:
Text('Hello, Advay!')
    ↓ [Rendering]
Screen Output:
Hello, Advay!

The Dissolution Process in Action

Let's understand how a complex widget dissolves into its basic components with an example:

I'll define WelcomePage like this:

// Initial complex widget
class WelcomePage extends StatelessWidget {

Widget build(BuildContext context) {
return Column(
children: [
Text('Welcome'),
ExpandableGreeting(Advay), // Custom widget
ExpandableGreeting(HSP), // Custom widget
ExpandableGreeting(FOSS), // Custom widget
],
);
}
}

Now suppose that I try to define an ExpandableGreeting:

class ExpandableGreeting extends StatelessWidget {
final GreetingModel person;

ExpandableGreeting(this.person);


Widget build(BuildContext context) {
return Column(
children: [
Greeting(person: person),
],
);
}
}

The Dissolution Steps

  1. Initial State:
    Now let's say I start the process with the original widget:

    WelcomePage()

    First, imagine WelcomePage dissolving, leaving behind its output:

  2. First Dissolution:

    Column(
    children: [
    Text('Welcome'),
    ExpandableGreeting(Advay),
    ExpandableGreeting(HSP),
    ExpandableGreeting(FOSS),
    ],
    )

    Then imagine each ExpandableGreeting dissolving, leaving behind its output:

  3. Second dissolution:

    Column(
    children: [
    Text('Welcome'),
    Column(
    children: [
    Greeting(person: Advay),
    ],
    ),
    Column(
    children: [
    Greeting(person: HSP),
    ],
    ),
    Column(
    children: [
    Greeting(person: FOSS),
    ],
    ),
    ],
    );

    Then imagine each Greeting dissolving, leaving behind its output:

  4. Final Form:

    Column(
    children: [
    Text('Welcome'),
    Text('Hello, Advay!'),
    Text('Hello, HSP!'),
    Text('Hello, FOSS!'),
    ],
    )

    And now there is nothing left to "translate." All my concepts have dissolved.

Hands-on Exercise: See Dissolution in Action

Let's move beyond theory and see dissolution in action! I've created a simple interactive demo that will help you visualize exactly how Flutter widgets transform and dissolve.

Setting Up

To follow along (which I highly recommend!), you'll need:

Even if you're new to Flutter, this exercise will help make the concept of dissolution click. Here's what we're going to build:


  1. Create a new Flutter project and replace the contents of lib/main.dart with the code below:

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
title: 'Dissolution Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const DissolutionDemo(),
);
}
}

class DissolutionDemo extends StatefulWidget {
const DissolutionDemo({super.key});


State<DissolutionDemo> createState() => _DissolutionDemoState();
}

class _DissolutionDemoState extends State<DissolutionDemo> {
bool showDissolved = false;


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dissolution Demo'),
elevation: 2,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
borderRadius: BorderRadius.circular(8),
),
child: ElevatedButton(
onPressed: () {
setState(() {
showDissolved = !showDissolved;
});
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(showDissolved ? Icons.merge_type : Icons.call_split),
const SizedBox(width: 8),
Text(
showDissolved ? 'Show Original' : 'Show Dissolved',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Code Structure:',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
),
child: Text(
showDissolved
? '''
Column(
children: [
Container(
width: 100,
height: 100,
color: Colors.red,
child: Text('Box'),
),
Container(
width: 100,
height: 100,
color: Colors.blue,
child: Text('Box'),
),
],
)'''

: '''
Column(
children: [
CustomBox(color: Colors.red),
CustomBox(color: Colors.blue),
],
)'''
,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 14,
color: Colors.grey[850],
),
),
),
],
),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Visual Output:',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Center(
child: showDissolved ? dissolvedVersion() : originalVersion(),
),
],
),
),
],
),
),
),
);
}

Widget originalVersion() {
return Column(
children: const [
CustomBox(color: Colors.red),
CustomBox(color: Colors.blue),
],
);
}

Widget dissolvedVersion() {
return Column(
children: [
Container(
width: 100,
height: 100,
color: Colors.red,
child: const Center(child: Text('Box')),
),
const SizedBox(height: 8),
Container(
width: 100,
height: 100,
color: Colors.blue,
child: const Center(child: Text('Box')),
),
],
);
}
}

class CustomBox extends StatelessWidget {
final Color color;

const CustomBox({super.key, required this.color});


Widget build(BuildContext context) {
return Column(
children: [
Container(
width: 100,
height: 100,
color: color,
child: const Center(child: Text('Box')),
),
const SizedBox(height: 8),
],
);
}
}

  1. Run the app. You'll see:
  • A button to toggle between original and dissolved versions
  • The original version using our custom CustomBox widget
  • The dissolved version showing the basic Flutter widgets

    Notice that both code versions (original and dissolved) create the exact same widget, despite having different syntax. This proves they are functionally equivalent - demonstrating the concept of dissolution.


  1. Try to understand:
    • How does CustomBox dissolve into basic Flutter widgets?
    • Why is the dissolved version simpler for Flutter even though the code is more complex?
    • Open the Flutter Widget Inspector to see both widget trees

  1. Experiment:
    • Try adding more properties to CustomBox
    • Create your own custom widget and predict how it will dissolve
    • Change the dissolved version and see if you can match it with a custom widget

This exercise will help you understand how your custom widgets eventually break down into Flutter's basic building blocks.

Why This Matters

Understanding how widgets dissolve into their fundamental forms, you gain four key advantages:

  1. Cleaner Architecture: Build better abstractions by understanding how complex widgets break down into Flutter primitives.
  2. Smarter Debugging: Track down issues by following the dissolution chain instead of getting lost in widget trees.
  3. Better Performance: Optimize your widget hierarchy by minimizing unnecessary transformation steps.
  4. Intentional Design: Create widgets that balance power and simplicity, knowing they must eventually dissolve into basic Flutter building blocks.

Conclusion

As your code completes its journey through space and time, transforming from your abstract concepts into pixels on someone else's screen, consider: Where exactly does this transformation happen? On your device? On mine? Or somewhere in between?

The beauty of Flutter's dissolution principle is that it makes these questions both fascinating and, in a way, irrelevant. What matters is that your intentions, encoded in your widgets, faithfully materialize wherever they need to be.


Through the process of dissolution, your code transcends its original form, becoming something both less and more - less complex, yet more universal. And that's the real magic of Flutter development.