0 purchases
beholder flutter
Simple state management for Flutter.
This package is built to work with:
beholder_form - elegant form validation
beholder_provider - package:provider integration
Getting Started #
Define a ViewModel
class CounterViewModel extends ViewModel {}
copied to clipboard
Define state and a method to update it:
class CounterViewModel extends ViewModel {
late final counter = state(0);
void increment() => counter.value++;
}
copied to clipboard
Watch value with Observer - it will rebuild the widget when the value changes:
final vm = CounterViewModel();
// ...
Widget build(BuildContext context) {
return Observer(
builder: (context, watch) => OutlinedButton(
onPressed: vm.increment,
child: Text("${watch(vm.counter)}")
),
);
}
copied to clipboard
ViewModel #
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:
class SearchUsersScreenVm extends ViewModel {
late final search = state("");
late final users = state(Loading<List<User>>()); // *
SearchUsersScreenVm() {
search.listen((_, current) => refresh());
}
Future<void> refresh() async {
users.value = Loading();
try {
final List<User> result = Api.fetchUsers(search: search.value);
users.value = Data(result);
} catch (error) {
users.value = Failure(error);
}
}
}
copied to clipboard
*Data, Failure and Loading - are helper classes. Read more about them here
Dispose #
Every class extending ViewModel has dispose method.
Call it once you don't need ViewModel to release resources:
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final vm = SearchUsersScreenVm();
@override
Widget build(BuildContext context) {
// ...
}
@override
void dispose() {
vm.dispose();
super.dispose();
}
}
copied to clipboard
Observables #
state #
state is a core concept in beholder.
It tracks changes to its value and notifies every observer depending on it.
Updating value #
late final counter = state(0);
void increment() {
counter.value = counter.value + 1;
// or
counter.update((current) => current + 1);
}
copied to clipboard
Listening to value changes #
counter.listen((previous, current) {
// Do something with `current`
});
copied to clipboard
computed #
Use computed to derive from state:
class User {
final String name;
User(this.name);
}
class UserProfileVm extends ViewModel {
late final user = state<User?>(null);
late final username = computed((watch) => watch(user)?.name ?? 'Guest');
}
copied to clipboard
computedFactory #
Need a parametrized computed? Use computedFactory:
class UserListVm extends ViewModel {
late final users = state(<User>[]);
late final usernameByIndex = computedFactory((watch, int index) {
return watch(users)[index];
});
}
copied to clipboard
Usage:
final vm = UserListVm();
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) => Observer(
builder: (context, watch) {
final username = watch(vm.usernameByIndex(index));
return Text(username);
}
)
);
}
copied to clipboard
Observable as stream #
Every Observable could be converted to a stream.
class SearchScreenVm extends ViewModel {
SearchScreenVm(this.githubApi) {
final subscription = search.asStream().listen((value) {
print("Search query changed to $value");
});
disposers.add(subscription.cancel);
}
late final search = state('');
}
copied to clipboard
Utils #
AsyncValue #
AsyncValue is a default type for handling async data in asyncStates.
It has three subtypes:
Data - the future is completed successfully
Loading - the future is not completed yet
Failure - the future 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:
Widget build(BuildContext context) {
return Observer(
builder: (context, watch) {
final posts = watch(vm.posts);
if (posts case Loading(previousResult: Data(value: var posts))) {
return Stack(
children: [
ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) => Text(posts[index].title),
),
const CircularProgressIndicator(),
]
);
}
// ...
}
);
}
copied to clipboard
Why late? #
late allows to call instance method in field initializer.
The following:
class CounterViewModel extends ViewModel {
late final counter = state(0);
}
copied to clipboard
is a shorter (but not the same!*) version for:
class CounterViewModel extends ViewModel {
final ObservableState<int> counter;
CounterViewModel(): counter = ObservableState(0) {
disposers.add(counter.dispose);
}
}
copied to clipboard
*late fields are initialized lazily - when they are first accessed.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.