DEV Community

Cover image for Flutter Stateful Widget on the Backend
Horda
Horda

Posted on

Flutter Stateful Widget on the Backend

If you're a Flutter developer who's ever tried to build a backend, you know how challenging the transition can be. You need to learn about databases, HTTP protocols, and often pick up an entirely new programming language. And if your app requires custom business logic with complex state management, things get even more complicated.

Perhaps you even worked with Firebase, so you know how cloud functions become difficult to work with, and maintaining state across serverless invocations feels like fighting against the platform.

This is where Horda comes in! Horda is a stateful serverless platform that lets you transition from writing frontend Flutter code to Dart backend with ease, using the same familiar patterns you already know.

Introducing Horda Entity - your stateful widget on the backend

As a Flutter developer you are most likely used to working with stateful widgets. When you think of a stateful widget, three key concepts come to mind:

  • Triggering state changes through user interactions or events
  • Updating the state based on those triggers
  • Rebuilding the widget to reflect state changes in the UI

Now imagine if this very familiar concept was used on the backend too. That's exactly what Horda does! Let's compare them:

Flutter StatefulWidget:

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

  @override
  State<CounterWidget> createState() =>
      _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: _incrementCounter,
      child: Text('$_counter'),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Horda Entity:

class CounterEntity extends Entity<CounterState> {
  Future<RemoteEvent> incrementCounter(
    IncrementCounter command,
    CounterState state,
    EntityContext context,
  ) async {
    return CounterIncremented(by: command.by);
  }

  // ...
}

@JsonSerializable()
class CounterState extends EntityState {
  int _counter;

  CounterState({this._counter = 0});

  void counterIncremented(CounterIncremented event) {
    _counter += event.by;
  }

 // ...
}
Enter fullscreen mode Exit fullscreen mode

Just like a Flutter StatefulWidget, a Horda Entity has state and can handle actions. But instead of responding to user taps and gestures, it responds to commands sent from your client app. And instead of calling setState(), it emits events that update its state. The structure should feel immediately familiar if you've worked with stateful widgets!

Triggering state changes with commands

Let's begin breaking down our Counter Entity by looking at how it triggers state changes! In the Counter Horda Entity, the incrementCounter method is a command handler:

Future<RemoteEvent> incrementCounter(
  IncrementCounter command,
  CounterState state,
  EntityContext context,
) async {
  return CounterIncremented(by: command.by);
}
Enter fullscreen mode Exit fullscreen mode

Command handlers work like this:

  • They receive a command object (like IncrementCounter) which contains the data needed to perform an action
  • They execute your business logic. In this case, it's simple, but this is where you'd add validation, calculations, or any custom logic
  • They return an event (like CounterIncremented) that describes what happened

Sending commands from the client to your backend entity is very similar to dispatching notifications in Flutter:

Flutter Notifications:

context.dispatchNotification(
  IncrementCounter(by: 1),
);
Enter fullscreen mode Exit fullscreen mode

Horda Commands:

context.sendEntity(
  name: 'CounterEntity',
  id: 'counter-1',
  cmd: IncrementCounter(by: 1),
);
Enter fullscreen mode Exit fullscreen mode

The key difference? Unlike a Flutter widget notification which is sent and handled locally within your app, the command is sent to the server and handled there.

That's all it takes! No HTTP endpoints to set up, no request/response handling. Just send the command and let Horda handle the rest.

Business logic and updating state

Now let's look at how state updates work. Remember in the command handler, we had access to the current state:

Future<RemoteEvent> incrementCounter(
  IncrementCounter command,
  CounterState state,  // Current state is passed in
  EntityContext context,
) async {
  // You can read state here to make decisions
  if (state.counter > 10) {
    throw StateError('Counter value is too big!');
  }

  return CounterIncremented(by: command.by);
}
Enter fullscreen mode Exit fullscreen mode

The current state is passed into every command handler, so you can read it to make decisions in your business logic, like checking if an operation is valid before proceeding.

But how does the state actually get updated?

Flutter State Updates:

You define state variables in your Dart class that extends State and update them with setState():

setState(() {
  _counter++;
});
Enter fullscreen mode Exit fullscreen mode

Horda State Updates:

You define state in your Dart class that extends EntityState and update it with event handlers:

void counterIncremented(CounterIncremented event) {
  _counter += event.by;
}
Enter fullscreen mode Exit fullscreen mode

When the CounterIncremented event is returned from the command handler, Horda automatically calls the counterIncremented method on your state. Inside this method, you update your state variables just like you would in a Flutter widget's setState() callback. The method name follows a simple convention: it matches the event name but in camelCase.

The Horda platform persists state and updates it every time you change it.

What about rebuilding the UI

Of course, when you think of a stateful widget, you think about rebuilding it to reflect state changes in the UI. With Flutter's StatefulWidget, it's straightforward. You call setState() and display the state value in your widget via build() method.

But now we're talking about backend state. You might be thinking: "Don't I need to write a ton of code to get this data into my UI?" With traditional backends or Firebase, you'd need to:

  • Create services to manage HTTP requests or set up change listeners
  • Wire up your widgets to these services
  • Handle loading states, errors, and data synchronization

With Horda, it's actually quite straightforward. You just query the data you need directly in your widget's build method!

First, define a query for your entity:

class CounterQuery extends EntityQuery {
  @override
  String get entityName => 'CounterEntity';

  final counterValue = EntityCounterView('counter');

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Then, set up the query in your widget's build method:

@override
Widget build(BuildContext context) {
  return context.entityQuery(
    entityId: 'counter-1',
    query: CounterQuery(),
    child: const CounterWidget(),
  );
}
Enter fullscreen mode Exit fullscreen mode

Finally, in any descendant widget, you can query the values directly to display them, and the widget will automatically rebuild when those values change. Notice how similar the code looks:

Flutter Local State:

@override
Widget build(BuildContext context) {
  return TextButton(
    onPressed: _incrementCounter,
    child: Text('$_counter'),
  );
}
Enter fullscreen mode Exit fullscreen mode

Horda Backend State:

@override
Widget build(BuildContext context) {
  final counterValue = context
      .query<CounterQuery>()
      .counter((q) => q.counterValue);

  return TextButton(
    onPressed: _incrementCounter,
    child: Text('$counterValue'),
  );
}
Enter fullscreen mode Exit fullscreen mode

That's the magic! The context.query() method fetches the current value from your backend entity, and whenever that value changes on the backend, your widget automatically rebuilds to reflect the new state. No manual refresh needed!

The system is also quite optimized. Your widget will only rebuild when the specific data it depends on changes. If you're querying multiple properties, changes to unrelated data won't trigger unnecessary rebuilds.

Try it out and get to know more!

Ready to try it out? Horda SDK includes a local development server that lets you run your backend code directly on your machine. Just generate the code with dart run build_runner build, then run dart bin/main.dart to start your local server. Connect your Flutter app to ws://localhost:8080/client and you're up and running!

When you feel like your project is ready for deployment you can create an account and a project on Horda Console at console.horda.dev and publish your package with dart pub tool to a special URL.

More info and details:

Top comments (0)