Simple but Scalable State Management for Flutter

fatal flaw.

Are you here now? Then let’s move on to the features

Combination of states:

class User { /* .. */ }
class SearchUsersScreen extends ViewModel {
  late final search = state('');
  late final users = state(<User>[]);

  /// `computed` позволяет комбинировать значение из `state`ов 
  /// и других `computed`ов
  late final lowercaseSearch = computed((watch) {
    return watch(search).toLowerCase();
  });
  
  late final filteredUsers = computed((watch) {
    return watch(users).where((user) {
      final name = user.fullName.toLowerCase();
      return name.contains(watch(lowercaseSearch));
    }).toList();
  })

  /// `computedFactory` - это computed, который еще и параметр умеет принимать
  late final userById = computedFactory((watch, int id) {
    return watch(users).singleWhere((user) => user.id == id);
  });
}

“Everyone can do it synchronously,” you will say, but here I will show it:

import "dart:async";

// ...
class SearchUsersScreen extends ViewModel {
  Timer? timer;
  
  late final search = state('')
    ..listen((previous, current) {
      timer?.cancel();
      timer = Timer(
        Duration(seconds: 1),
        () => refresh(),
      );
    });

  // AsyncState встроен в библиотеку. 
  late final users = state<AsyncState<List<User>>>(const Loading());
  
  Future<void> refresh()  async {
    users
      ..value = Loading()
      ..value = await Result.guard(
        () => ApiClient.fetchUsers(query: search.value)
      );
  }

  // ...
}

What about previously used computed‘s? How to use them userswhich has now become AsyncState?

And like this:

late final filteredUsers = computed<AsyncValue<List<User>>>((watch) {
  return watch(users).mapValue((users) => users.where((user) {
    final name = user.fullName.toLowerCase();
    return name.contains(watch(lowercaseSearch));
  }).toList());
})

The widget will look like this:

Widget build(BuildContext context) {
  return Observer(
    builder: (context, watch) {
      final users = watch(vm.filteredUsers);
      return switch(users) {
          Loading() => CircularProgressIndicator(),
          Data(value: final users) => ListView(/* .. */),
          Failure(:final error) => Text("Error: "),
      };
    }
  );
}

Because AsyncState – This sealed union, we can exhaustively go through all possible options. More about Pattern Matching – Here.

How to scale?

ViewModel easy to combine using compositions(en):

class UsersViewModel {
  SearchUsersViewModel(this.projectId);
  
  final Observable<int> projectId;
  
  late final _users = state(<User>[]);

  late final filteredUsers = computed((watch) {
    final projectId = watch(this.projectId);
    return watch(users)
      .where((user) => user.projects.contains(projectId))
      .toList();
  });
}

class TaskTrackerScreenViewModel {
  late final searchUsersVm = SearchUsersViewModel(this.selectedProjectId);

  // Изменение projectId спровоцирует моментальное изменение filteredUsers
  late final selectedProjectId = state(32);
}

Conclusion

My first article on Habr (and in principle). Thank you for reading. I will be glad to receive any feedback – both on the article and on the library.

The library API is quite stable, but I plan to release 1.0.0 only after 100% test coverage.

Github

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *