digicradle.dev

Comparing Flutter Widgets to React Components

In this article, we'll take a look at Flutter Widgets from a React Developers perspective. You might be interested in this comparison if you work with React and want to try out Flutter.

StatlessWidget

StatelessWidgets as PureComponents. As long as the input is the same, the output will remain unchanged.

The build method of a stateless widget is typically only called in three situations: the first time the widget is inserted in the tree, when the widget's parent changes its configuration, and when an InheritedWidget it depends on changes.

from Flutter Docs

class SimpleHeading extends StatelessWidget {
  final String title;

  const SimpleHeading({Key? key, required this.title}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Text(title);
  }
}

/// used like:
SimpleHeading(title: "Hello world");

To create something similar to a StatelessWidget in React, we must create a Component that uses no state and wrap the Function Component into a React.memo call or extend React.PureComponent in the case of Class Components. We must do this because StatelessWidgets shallow-check their props to decide if they should rerender or not.

interface SimpleHeadingProps {
  title: string
}

const SimpleHeading = React.memo<SimpleHeadingProps>((props) => {
  return (
    <h2>{props.title}</h2>
  );
});

/// used like:
<SimpleHeading title="Hello world"/>

One thing to keep in mind for Flutter widgets, in general, is they don't have a default "children" property. The way we define our child or children API is entirely up to us.

The common practice is to define a property "children" for widgets that support a list of children and "child" for widgets that only work with one.

/// A widget that accepts a single child
Container(
  child: SimpleHeading(title: "Hello world")
);

/// A widget that accepts multiple children
Column(
  children: [
    SimpleHeading(title: "Hello world"),
    SimpleHeading(title: "This is another title"),
  ]
);

A more practical example of a StatelessWidget that accepts children and some additional props would be a Panel. The Panel has a fixed style for the header and only allows the heading text to be changed. For the content area, the Panel will accept a single child. This way, we can decide if we want to place a Row, Column, or just a piece of text there.

class SimplePanel extends StatelessWidget {
  final String title;
  final Widget child;

  const SimplePanel({Key? key, required this.title, required this.child})
      : super(key: key);

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Heading
        Text(title),
        // Content
        child
      ],
    );
  }
}

Which could be used to display content in a row, column or something else:

SimplePanel(
  title: "User details",
  child: Row(children: [Text("Username:"), Text("John")]));

SimplePanel(
  title: "Balance sheet",
  child: Column(children: [Text("Item 1"), Text("Item 2")]));

StatefulWidget

Both React and Flutter have similar state management implementations. A change of State will be passed down to children via props or made available if needed to descendants via Context. Flutter gives us access to the State functionality with the StatefulWidget.

We'll build a component that fetches a user avatar from somewhere and displays it. The property that we get from the parent is the user id. The State that we must keep in our widget is the actual image that corresponds to that user id.

class UserProfile extends StatefulWidget {
  final String userId;

  const UserProfile({Key? key, required this.userId}) : super(key: key);

  
  _UserProfileState createState() => _UserProfileState();
}

The StatefulWidget defines the Props that the widget needs. The State will contain the entire sub-hierarchy of content.

In Flutter, the State is an entire Component, not just the data. State has lifecycle methods, a build method like the StatelessWidget, and returns a hierarchy of other Widgets.

class _UserProfileState extends State<UserProfile> {
  late String avatarUrl;
  bool initialised = false;

  
  Widget build(BuildContext context) {
    if (!initialised) {
      /// a loading screen maybe
      return Container();
    }
    return Image.network(avatarUrl!);
  }
}

The _ in "_UserProfileState" makes the State class private and only accessible to the entities defined in the current file.

So far, we are not changing the avatarUrl in any way in our State. For this widget to be functional, it should:

  • Fetch the avatarUrl of the user when mounted and store it in the State.
  • Fetch the user's avatarUrl again if the userId prop is changed and store it in the State.

It sounds like we need lifecycle methods. The equivalent to a componentDidMount is initState. The only difference is that initState runs before the first render. Since Flutter is not serverside rendered (yet?), this distinction is not too important.

class _UserProfileState extends State<UserProfile> {
  late String avatarUrl;
  bool initialised = false;

  
  void initState() {
    updateAvatar();
    super.initState();
  }

  updateAvatar() {
    fetchUserAvatarURL(widget.userId).then((response) {
      /// mounted flag is already provided by Flutter
      if (mounted) {
        setState(() {
          initialised = true;
          avatarUrl = response;
        });
      }
    });
  }

  
  Widget build(BuildContext context) {
    if (!initialised) {
      /// a loading screen maybe
      return Container();
    }
    return Image.network(avatarUrl!);
  }
}

Flutter executes the State's build method and decides if it needs to rerender the UI when we call the setState method available in the State class.

Notice the mounted variable in the if statement that wraps the setState call. If you've worked with React a bit, you probably encountered the warning about settings state when the Component is no longer mounted. In React, we have to implement the flag ourselves. With Flutter, we get this out of the box.

With these two widget types, we should be able to build entire applications. Like in React, we can build State Management, Themeing and other systems on top of these two essential building blocks.

InheritedWidget

The React Context and Flutter's InheritedWidget provide very similar functionality for the same problems. It is a bit harder to draw a 1:1 comparison between this widget and React Context as the API is quite different.

For example, the Themeing data from the officially supported Flutter material widgets is exposed via the Context and accessed like so:

class SimpleHeading extends StatelessWidget {
  final String title;

  const SimpleHeading({Key? key, required this.title}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
        child: Text(
      title,
      style: Theme.of(context).primaryTextTheme.headline2,
    ));
  }
}

The of(context) notation is equivalent with a useContext hook. We know the ContextType/InheritedWidget and call an API to get the current value in both cases.

// A hreading that uses the theme context
const SimpleHeading: FC = (props) => {
  const themeContext = useContext(AppThemeContext); // defined app wide
  return <h2 style={{ color: themeContext.primaryColor }}>{props.children}</h2>;
};

We'll explore how to work with Inherited Widgets in a later article. If you're interested in this kind of content, follow me on Twitter @agilius.

Vlad Nicula's avatar

Vlad Nicula

Software architect, passionate about low code tooling, flutter, typescript, react and clean code.

twitterlinkedin