beholder 0.4.1+1 copy "beholder: ^0.4.1+1" to clipboard
beholder: ^0.4.1+1 copied to clipboard

State Management inspired by MobX. Yet another MVVM implementation. Reinventing the wheel, but with a twist.

Simple state management for Flutter.

This package is built to work with:

Getting Started #

  1. Define a ViewModel

    class CounterViewModel extends ViewModel {}
    
  2. Define state and a method to update it:

    class CounterViewModel extends ViewModel {
      late final counter = state(0);
      void increment() => counter.value++;
    }
    
  3. 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)}")
        ),
      );
    }
    

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);
    }
  }
}

*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();
   }
}

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);
}

Listening to value changes #

counter.listen((previous, current) {
  // Do something with `current`
});

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');
}

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];
  });
}

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);
        }
     )
   );
}

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('');
}

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(),
            ]
         );
       }
       
       // ...
     }
  );
}

Why late? #

late allows to call instance method in field initializer. The following:

class CounterViewModel extends ViewModel {
  late final counter = state(0);
}

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);
  }
}

*late fields are initialized lazily - when they are first accessed.

0
likes
120
pub points
2%
popularity

Publisher

unverified uploader

State Management inspired by MobX. Yet another MVVM implementation. Reinventing the wheel, but with a twist.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

meta

More

Packages that depend on beholder