Skip to content

Instantly share code, notes, and snippets.

@danReynolds
Last active August 26, 2025 20:21
Show Gist options
  • Save danReynolds/2bd4e34b2ba48e44283e05b5599c55fe to your computer and use it in GitHub Desktop.
Save danReynolds/2bd4e34b2ba48e44283e05b5599c55fe to your computer and use it in GitHub Desktop.
Flutter view controller/model gist
class ExampleViewModel {
final String name;
final String description;
final bool editMode;
ExampleViewModel({ required this.name, required this.description, required this.editMode});
}
class ExampleViewController extends ViewController<ExampleViewModel> {
final _editMode = Computable(model.editMode);
final _name = Computable(model.name);
final _description = Computable(model.description);
ExampleViewController() : super(
ExampleViewModel(editMode: false, name: 'This is a title', description: 'This is a description'),
) {
forward(
Computable.compute3(
_name,
_description,
_editMode
(name, description, editMode) {
return ExampleViewModel(
name: name,
description: description,
editMode: editMode,
);
}
)
);
}
void editName(String updatedName) {
_name.add(updatedName);
}
void editDescription(String updatedDescription) {
_description.add(updatedDescription);
}
void _toggleEditMode() {
_editMode.add(!model.editMode);
}
}
class ExampleScreen extends StatelessWidget {
@override
build(context) {
return ViewControllerBuilder(
() => ExampleViewController(),
(context, controller) {
final ExampleViewModel(:name, :description, :isEditing) = controller.model;
return Column(
children: [
Input(
label: 'name',
initialValue: name,
onChange: (updatedName) {
controller.editName(updatedName);
}
),
Input(
label: 'description',
initialValue: description,
onChange: (updatedDescription) {
controller.editDescription(updatedDescription);
}
),
Button(
title: 'Toggle edit mode',
onTap: controller.toggleEditMode,
),
],
);
}
);
}
}
import 'dart:async';
import 'package:computables/computables.dart';
class ViewController<T> extends ForwardingComputable<T> {
final List<StreamSubscription> _subscriptions = [];
ViewController(super.initialValue) : super(dedupe: false);
/// Subscribes to the given computable for the duration of the view controller. Automatically unsubscribes
/// from it when the controller is disposed.
///
/// Note that this is an *asynchronous* subscription so if the subscription modifies the controller's model, it will
/// not be immediately reflected.
void subscribe<S>(Computable<S> computable, void Function(S value) listener) {
_subscriptions.add(computable.stream().listen(listener));
}
@override
dispose() {
super.dispose();
for (final subscription in _subscriptions) {
subscription.cancel();
}
}
T get model {
return get();
}
}
import 'package:computables_flutter/widgets/computable_builder.dart';
import 'package:flutter/material.dart';
typedef _ViewControllerBuilderFn<T extends ViewController> = Widget Function(
BuildContext context,
T controller,
);
class ViewControllerBuilder<T extends ViewController> extends StatefulWidget {
final T Function() factory;
final _ViewControllerBuilderFn<T> builder;
final bool autoDispose;
const ViewControllerBuilder(
this.factory, {
required this.builder,
this.autoDispose = true,
});
@override
ViewControllerBuilderState<T> createState() =>
ViewControllerBuilderState<T>();
static ViewControllerBuilder<T> of<T extends ViewController>(
T controller,
_ViewControllerBuilderFn<T> builder,
) {
return ViewControllerBuilder(
() => controller,
builder: builder,
autoDispose: false,
);
}
}
class ViewControllerBuilderState<T extends ViewController>
extends State<ViewControllerBuilder<T>> {
late final T controller;
@override
initState() {
super.initState();
controller = widget.factory();
}
@override
dispose() {
if (widget.autoDispose) {
controller.dispose();
}
super.dispose();
}
@override
build(context) {
return ComputableBuilder(
computable: controller,
builder: (context, _) {
return widget.builder(context, controller);
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment