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'),
);
}
}
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;
}
// ...
}
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);
}
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),
);
Horda Commands:
context.sendEntity(
name: 'CounterEntity',
id: 'counter-1',
cmd: IncrementCounter(by: 1),
);
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);
}
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++;
});
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;
}
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');
// ...
}
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(),
);
}
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'),
);
}
Horda Backend State:
@override
Widget build(BuildContext context) {
final counterValue = context
.query<CounterQuery>()
.counter((q) => q.counterValue);
return TextButton(
onPressed: _incrementCounter,
child: Text('$counterValue'),
);
}
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:
- Horda Website - Learn more about the stateful serverless platform
- Quick Start - Build your first real-time counter app
- Example Projects in GitHub - Explore Counter and Twitter example apps
Top comments (0)