Skip to content

praja/jetpack

Repository files navigation

Jetpack for Flutter

A set of abstractions, utilities inspired from Android Jetpack 🚀 to help manage state in flutter applications.

Features

LiveData

State holder and change notifier, that also allows to read current value.

If you are fully onto Streams and reactive programming, you might not need this. But if you want to write imperative code to update state, this should help.

EventQueue

For pushing ephemeral state to the UI and clearing off after being handled. Useful for triggering toasts / popups from within ViewModel

ViewModel

Business logic container that exposes state, event methods to the UI and communicates with the rest of the application

Installation

$ flutter pub add jetpack

Usage

Create your ViewModel and expose state using LiveData

import 'package:jetpack/jetpack.dart';

class CounterViewModel extends ViewModel {
  final MutableLiveData<int> _counter = MutableLiveData(0);

  LiveData<int> get counter => _counter;

  void increment() {
    _counter.value++;
  }
}

You can access your CounterViewModel anywhere using BuildContext as described below

@override
Widget build(BuildContext context) {
  final CounterViewModel viewModel = context.viewModel();
}

And you can consume LiveData using LiveDataBuilder

LiveDataBuilder<int>(
  liveData: viewModel.counter,
  builder: (BuildContext buildContext, int count) =>
  Text('$count'),
  )
)

And you can pass UI events to ViewModel by just invoking the method on it

FloatingActionButton(
  onPressed: viewModel.increment,
  //...
)

Setup (One time in a flutter project)

Create a ViewModelFactory for your app
class MyAppViewModelFactory extends ViewModelFactory {
  const MyAppViewModelFactory();

  @override
  T create<T extends ViewModel>() {
    if (T == HomeViewModel) {
      return HomeViewModel() as T;
    }
    throw Exception("Unknown ViewModel type");
  }
}

If you use dependency injection frameworks like get_it, your factory could like this

class MyAppViewModelFactory extends ViewModelFactory {
  const MyAppViewModelFactory();

  @override
  T create<T extends ViewModel>() {
    return GetIt.I.get<T>();
  }
}
Provide your ViewModelFactory at the root of your App
void main() {
  const MyAppViewModelFactory viewModelFactory = MyAppViewModelFactory();
  runApp(const MyApp(viewModelFactory: viewModelFactory));
}

class MyApp extends StatelessWidget {
  final ViewModelFactory viewModelFactory;
  const MyApp({super.key, required this.viewModelFactory});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ViewModelFactoryProvider(
        viewModelFactory: viewModelFactory,
        child: MaterialApp(
          title: 'Flutter App',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            ),
          home: const HomePage(title: 'Home Page'),
          ),
        );
  }
}
Create a base widget Page to wrap all page contents with a `ViewModelScope`
abstract class Page extends StatelessWidget {
  const Page({super.key});

  Widget buildContent(BuildContext context);

  @override
  Widget build(BuildContext context) {
    return ViewModelScope(builder: buildContent);
  }
}

If you have a base class already for all pages, then wrap the content using ViewModelScope as above

Why another State Management Library?

These are proven patterns in Android Ecosystem for more than 5 years. They are still intact even after the adoption of a completely new UI framework - Jetpack Compose. These abstractions have been resilient to change because of low coupling and flexibility.

Existing solutions in flutter like bloc, provider etc. limit the logic holders to only emit one stream of state by default, and require extra boiler plate to "select" the pieces of states that the UI would want to react to.

class MyLogicHolder: LogicHolder<StateModel>

Sometimes, we want to expose multiple different state streams that are related but change/emit at a different frequency. Exposing them right from the ViewModel without any boilerplate overhead of writing Selectors etc. is very convenient without any cost.

class MyViewModel: ViewModel {
  final MutableLiveData<int> _counter = MutableLiveData(0);
  final MutableLiveData<boolean> _isModified = MutableLiveData(false);

  LiveData<int> get counter => _counter;
  LiveData<int> get isModified => _isModified;

  void increment() {
    _counter.value++;
    _isModified.value = true;
  }
}

This allows us to organize and propagate state the way it is consumed in the UI and minimize unnecessary rebuilding of widgets

You can expose state to the UI using Futures and Streams as well. Your choice.

class ProductViewModel: ViewModel {
  //

  Future<ProductDetails> productDetails;
  Stream<bool> isAddedToCart;
  ValueNotifier<bool> isLoading;
}

And use FutureBuilder, StreamBuilder and ValueListenableBuilder to listen and update the UI.

And there is no need of creating extra models for communicating UI events to ViewModel. Just call the methods directly.

ElevatedButton(
  onPressed: viewModel.increment
  //...
)