NOTE: Package is supported. I just haven't found any bugs since the last commit.
Simple state management for Flutter.
This package is built to work with:
- beholder_form - elegant form validation
- beholder_provider - package:provider integration
Define a
ViewModelclassCounterViewModelextendsViewModel {}
Define state and a method to update it:
classCounterViewModelextendsViewModel { latefinal counter =state(0); voidincrement() => counter.value++; }
Watch value with
Observer- it will rebuild the widget when the value changes:final vm =CounterViewModel(); // ...Widgetbuild(BuildContext context) { returnObserver( builder: (context, watch) =>OutlinedButton( onPressed: vm.increment, child:Text("${watch(vm.counter)}") ), ); }
ViewModel is used to group Observables.
Usually, you want to define ViewModel per piece of UI - it should represent UI state and related business rules.
If we need to develop a screen for searching users, its ViewModel might look like that:
classSearchUsersScreenVmextendsViewModel {
latefinal search =state("");
latefinal users =state(Loading<List<User>>()); // *SearchUsersScreenVm() {
search.listen((_, current) =>refresh());
}
Future<void> refresh() async {
users.value =Loading();
try {
finalList<User> result =Api.fetchUsers(search: search.value);
users.value =Data(result);
} catch (error) {
users.value =Failure(error);
}
}
}*Data, Failure and Loading - are helper classes. Read more about them here
Every class extending ViewModel has dispose method.
Call it once you don't need ViewModel to release resources:
classMyWidgetextendsStatefulWidget {
constMyWidget({super.key});
@overrideState<MyWidget> createState() =>_MyWidgetState();
}
class_MyWidgetStateextendsState<MyWidget> {
final vm =SearchUsersScreenVm();
@overrideWidgetbuild(BuildContext context) {
// ...
}
@overridevoiddispose() {
vm.dispose();
super.dispose();
}
}state is a core concept in beholder.
It tracks changes to its value and notifies every observer listening.
latefinal counter =state(0);
voidincrement() {
counter.value = counter.value +1;
// or
counter.update((current) => current +1);
}counter.listen((previous, current) {
// Do something with `current`
});Use computed to derive from state:
classUser {
finalString name;
User(this.name);
}
classUserProfileVmextendsViewModel {
latefinal user =state<User?>(null);
latefinal username =computed((watch) =>watch(user)?.name ??'Guest');
}Need a parametrized computed? Use computedFactory:
classUserListVmextendsViewModel {
latefinal users =state(<User>[]);
latefinal usernameByIndex =computedFactory((watch, int index) {
returnwatch(users)[index];
});
}Usage:
final vm =UserListVm();
Widgetbuild(BuildContext context) {
returnListView.builder(
itemBuilder: (context, index) =>Observer(
builder: (context, watch) {
final username =watch(vm.usernameByIndex(index));
returnText(username);
}
)
);
}Every Observable could be converted to a stream.
classSearchScreenVmextendsViewModel {
SearchScreenVm(this.githubApi) {
final subscription = search.asStream().listen((value) {
print("Search query changed to $value");
});
disposers.add(subscription.cancel);
}
latefinal search =state('');
}AsyncValue is a utility type to repesent result of an asynchronous operation.
It has three subtypes:
Data- operation is completed successfullyLoading- operation is not completed yetFailure- operation is completed with an error
It's a sealed class, so you can use switch to handle all cases.
Loading also has previousResult field, which is the last Data/Failure value.
It might be useful for showing old data while loading new one:
Widgetbuild(BuildContext context) {
returnObserver(
builder: (context, watch) {
final posts =watch(vm.posts);
if (posts caseLoading(previousResult:Data(value:var posts))) {
returnStack(
children: [
ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) =>Text(posts[index].title),
),
constCircularProgressIndicator(),
]
);
}
// ...
}
);
}late allows to call instance method in field initializer.
The following:
classCounterViewModelextendsViewModel {
latefinal counter =state(0);
}is a shorter (but not the same!*) version for:
classCounterViewModelextendsViewModel {
finalObservableState<int> counter;
CounterViewModel(): counter =ObservableState(0) {
disposers.add(counter.dispose);
}
}
*late fields are initialized lazily - when they are first accessed.
Accessing counterVm.counter for the first time after counterVm was disposed will result in an error.