DEV Community

Surhid Amatya
Surhid Amatya

Posted on

Implementing Object-Oriented Programming (OOP) in Flutter and Dart

If you are in Programming field and have done programming then in your travel you must have googled OOP and it's implemnetaiton in your respective language, right? Found one example in counter, other in login and similar other in other different ways. While exploring these examples did you ever felt disconnected and thought if only it answered a single query I had in my mind.

I had that thought if only it took a single thing and connected using different OOP principle. In this blog I am trying to solve same query of mine. While saying this to understand this blog you must have Basic understanding of OOP concepts (classes, inheritance, interfaces, and polymorphism) and familiarity with running Flutter apps.

When you build Flutter apps, you are already using OOP whether you realize it or not. Widgets are classes. State is an object. Your app is a forest of interacting objects quietly doing their jobs.

OOP is not about writing more classes. It is about modeling your app in a way that matches how the problem behaves in the real world. When done well, the code reads like a story instead of a puzzle.

Object Oriented Programming is built on four core pillars:

  1. Inheritance
  2. Encapsulation
  3. Polymorphism
  4. Abstraction

Today, we will break each one down and show how they show up naturally in Flutter and Dart code.

When people hear “OOP”, they think of service layers and interfaces. Flutter quietly teaches OOP every day through widgets and state.

Let's put on our "OOP Glasses" and look at the code you write every day.

Inheritance: Inheritance lets one class acquire the properties and methods of another class.

Every time you make an StatelessWidget:

class MyButton extends StatelessWidget {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

You are using Inheritance. What Happens When You Ctrl + Click StatelessWidget

If you Ctrl (or Cmd) + click on StatelessWidget, you don’t see magic. You see code.

Simplified, it looks like this:

abstract class StatelessWidget extends Widget {
  const StatelessWidget({Key? key}) : super(key: key);

  @protected
  Widget build(BuildContext context);

  @override
  StatelessElement createElement() => StatelessElement(this);
}
Enter fullscreen mode Exit fullscreen mode

Already, several things are happening. What Your Widget Inherits Automatically. When you extend StatelessWidget, Flutter asks you for exactly one thing:

Widget build(BuildContext context);
Enter fullscreen mode Exit fullscreen mode

That’s it. Now flutter handles:

  1. layout
  2. constraints
  3. repainting
  4. diffing
  5. scheduling
  6. performance

You will only describe what the UI should look like.

Encapsulation: The definition of encapsulation is "the action of enclosing something in or as if in a capsule". Removing access to parts of your code and making things private is exactly what Encapsulation is all about (often times, people refer to it as data hiding).

In a simple mean each object in your code should control its own state. State is the current "snapshot" of your object. The keys, the methods on your object, Boolean properties and so on. If you were to reset a Boolean or delete a key from the object, they're all changes to your state.

Limit what pieces of your code can access. Make more things inaccessible, if they aren't needed.

We’ve all written a basic counter:

class _CounterState extends State<CounterWidget> {
  // That little underscore (_) is doing heavy lifting
  int _count = 0; 

  void increment() {
    setState(() {
      _count++;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

That underscore _ isn't just a naming convention; it’s a security guard. It locks _count inside this specific class. No other file in your project can reach in and mess with that number.

This is Encapsulation.

If you made _count public, some other random widget might accidentally reset it to zero. By hiding the data and only exposing a function (increment) to change it, you’ve made your code crash-proof.

Polymorphism: Polymorphism means "the condition of occurring in several different forms." That's exactly what the fourth and final pillar is concerned with – types in the same inheritance chains being able to do different things.

If you have used inheritance correctly you can now reliably use parents like their children. When two types share an inheritance chain, they can be used interchangeably with no errors or assertions in your code.

Have you noticed that Text, Container, Row, and Image all look completely different, but you can stick any of them into the child property of a Center widget?

That’s Polymorphism in action. Now, look at how this exact same logic runs your UI. You do this every time you use a Column.

// POLYMORPHISM IN FLUTTER:
// The Column expects a List<Widget>. 
// It doesn't care if they are texts, icons, or complex containers.
Column(
  children: [
    // Form 1: A Text Widget
    Text("Hello World"), 

    // Form 2: A Container Widget
    Container(height: 50, color: Colors.red),

    // Form 3: A Custom Button Widget
    MyCustomButton(), 
  ],
)
Enter fullscreen mode Exit fullscreen mode

Why this matters: The Column widget's code loops through that list and calls build() on every item.

It calls build() on Text -> You get letters.

It calls build() on Container -> You get a red box.

That is Polymorphism. One list, many forms, zero crashes.

Abstraction: To abstract something away means to hide away the implementation details inside something – sometimes a prototype, sometimes a function. So when you call the function you don't have to understand exactly what it is doing.

If you had to understand every single function in a big codebase you would never code anything. It would take months to finish reading through it all.

You can create a reusable, simple-to-understand, and easily modifiable codebase by abstracting away certain details.

Imagine you write a widget that is composed of an icon. You force it to be a specific Icon class.

class RigidCard extends StatelessWidget {
  // RIGID: This card allows ONLY an Icon.
  // It is composed of a concrete class.
  final Icon icon; 
  final String title;

  const RigidCard({required this.icon, required this.title});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          icon, // We know exactly what this is.
          Text(title),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this fails: If tomorrow you want to put an Image or a Lottie Animation inside that card, you can't. The composition is too specific. You'd have to rewrite the class.

Composition In Terms of Abstraction (Flexible)
Now, let's use Abstraction (Widget) to define the "slot" for the composition.

class FlexibleCard extends StatelessWidget {
  // ABSTRACT: This card allows ANY Widget.
  // It is composed of an abstraction.
  final Widget visual; 
  final String title;

  const FlexibleCard({required this.visual, required this.title});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          visual, // We don't know what this is! It's just "a Widget".
          Text(title),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, look at how the Composition changes based on the implementation:

// Usage 1: Composed with an Icon

FlexibleCard(
  visual: Icon(Icons.star), // Abstraction lets this fit
  title: "Favorites",
);
Enter fullscreen mode Exit fullscreen mode

// Usage 2: Composed with an Image

FlexibleCard(
  visual: Image.asset('assets/logo.png'), // Abstraction lets this fit too
  title: "Brand",
);
Enter fullscreen mode Exit fullscreen mode

// Usage 3: Composed with a Switch

FlexibleCard(
  visual: Switch(value: true, onChanged: (_) {}), // Even this fits!
  title: "Settings",
);
Enter fullscreen mode Exit fullscreen mode

Now the detail:
Composition: The FlexibleCard HAS-A visual element inside it. That is the structure.

Abstraction: The type of that element is Widget. Widget is the contract (the Abstraction). It says "I don't care if you are an image, text, or button, as long as you can paint yourself on the screen."

Result: Because you used Composition in terms of Abstraction (holding a generic Widget instead of a specific Icon), your FlexibleCard is now infinite. You never have to touch its code again, no matter what new design requirements come in.

This is the heart of Flutter: Build slots (Abstraction) and fill them with blocks (Composition).

Conclusion
You don't need to read a dusty textbook to understand Object-Oriented Programming.

  1. Inheritance is why you extend StatelessWidget.
  2. Encapsulation is why you use _ in your State variables.
  3. Polymorphism is why every widget has a build() method.
  4. Abstraction is why you nest widgets inside widgets.

You aren't just "building screens." You are orchestrating a conversation between objects. And the best part? You were already doing it.

Flutter didn’t invent a new paradigm. It applied object-oriented principles with restraint and taste.

If you align your services, state, and business logic with the same rules that Flutter uses internally, your app starts to feel coherent rather than stitched together.

You don’t need to learn OOP for Flutter. You need to realize that Flutter has been teaching you all along.

Top comments (0)