DEV Community

njoee
njoee

Posted on • Updated on

MVVM With Flutter (Part1){MVVM, Provider, Injectable, Freezed}

Hai!,

Here i'd like to record my code base line for flutter using MVVM pattern,

The Idea and the pattern

so the idea is i'd like to bind view and viewmodel one on one and making the viewmodel as our boiler plate to orchestrate all the logic related to its view like bellow.

Image description

i'm using these dependencies to run

The Goal, What the apps will looks like

as a sample i create todolist app, like this

Image description!

Now The Dev, lets deep into the code

now, in order to create the apps as above, we need 3 things.

  • 1 Screen BIND to 1 View Model,
  • 1 Repositories Available for any view model Image description

1. Here is the MainScreen..

class MainScreen extends BaseView<MainScreenViewModel> {
  @override
  Widget build(
      BuildContext context, MainScreenViewModel viewModel, Widget? child) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          showModalBottomSheet(
              context: context,
              isScrollControlled: true,
              builder: (context) => SingleChildScrollView(
                    child: Container(
                        padding: EdgeInsets.only(
                            bottom: MediaQuery.of(context).viewInsets.bottom),
                        child: AddTaskScreen(onAddTaskClicked: viewModel.onAddTaskClicked,)),
                  ));
        },
        child: Icon(Icons.add),
      ),
      body: SafeArea(
        child: Container(
          color: Colors.lightBlue,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Container(
                padding: EdgeInsets.symmetric(horizontal: 30),
                margin: EdgeInsets.only(top: 50, left: 0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Container(
                      child: Container(
                        child: CircleAvatar(
                          child: Icon(
                            Icons.list_rounded,
                            color: Colors.lightBlue,
                            size: 75,
                          ),
                          radius: 50,
                          backgroundColor: Colors.white,
                        ),
                      ),
                    ),
                    Container(
                      child: Text(
                        "What To Do!",
                        style: TextStyle(
                            fontSize: 30, fontWeight: FontWeight.w300),
                      ),
                    ),
                  ],
                ),
              ),
              Expanded(
                child: Container(
                  padding: EdgeInsets.symmetric(horizontal: 20),
                  decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(30),
                          topRight: Radius.circular(30))),
                  margin: EdgeInsets.only(top: 25),
                  child: ListView.builder(
                    itemCount: viewModel.taskDataCount,
                    itemBuilder: (context, index) {
                      return ListViewTaskItem(data: viewModel.taskData[index]);
                  },),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

as you can see

Widget build(BuildContext context, MainScreenViewModel viewModel, Widget? child)
Enter fullscreen mode Exit fullscreen mode

now having access to the viewModel, meaning you can access all the state within that viewModel.

and now here is the MainScreenViewModel

2. Here is the MainScreenViewModel..

@injectable
class MainScreenViewModel extends BaseViewModel {
  List<TaskModel>? _tasksData;

  final ITaskRepository _taskRepository = getIt<TaskRepositoryImpl>();

  List<TaskModel> get taskData => _tasksData ?? [];  
  int get taskDataCount => _tasksData?.length ?? 0;

  @override
  void init() async{
    _tasksData = await _taskRepository.getAllTask();
    notifyListeners();
  }

  Future<void> onAddTaskClicked(String textValue) async {
    final data = TaskModel(description: textValue,title: textValue, isDone: false);
    _tasksData?.add(data);
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

on that code above everytime whenever the init are invoke the viewModel will fetch the taskData from repositories _taskRepository.getAllTask();

3. and here is the task repositories looks like..

@singleton 
class TaskRepositoryImpl implements ITaskRepository{
  final String baseUrl = "run.mocky.io";

  @override
  Future<List<TaskModel>?> getAllTask() async{
    var url = Uri.https(baseUrl, "/v3/6bb86bda-08f1-4d7d-99c6-a975bc1201e0");
    final response = await networking.get(url);
    final responseBody = GetTaskDTO.fromJson(jsonDecode(utf8.decode(response.bodyBytes)) as Map<String,dynamic>);
    return responseBody.tasks;
  }
}
Enter fullscreen mode Exit fullscreen mode

the repositories will responsible to makesure the data availability of task. and whenever the getAllTask() are invoked, repositories will fetch the data from the backendServies...

so now we have this flow :
Image description

...

When Is The ViewModel Are Injected?

ok up until here.. it was easy to understand right?. now we are moving to when exactly the viewModel are injected into view? how to make sure the viewModel are available when its needed.

First Lets take a look at the MainScreenViewModel

pay attention to the declaration.

@injectable
class MainScreenViewModel extends BaseViewModel
Enter fullscreen mode Exit fullscreen mode

we put @injectable to the MainScreenViewModel telling our Dependencies Injection DI to make sure to create the MainScreenViewModel whenever it was fetch by getIt..
for example like

final viewModel = getIt<MainScreenViewModel>();
Enter fullscreen mode Exit fullscreen mode

now getIt will create the object MainScreenViewModel and provide it to viewModel.

Ok, then where is the real one?

Back to our code structure, so where exactly the viewModel being injected to view? now you can open the BaseView class as bellow :

final RouteObserver<ModalRoute<void>> routeObserver =
    RouteObserver<ModalRoute<void>>();

abstract class BaseView<T extends BaseViewModel> extends StatefulWidget {
  BaseView({Key? key}) : super(key: key);

  Widget build(BuildContext context, T viewModel, Widget? child);

  @override
  State<BaseView> createState() => BaseViewState<T>();
}

class BaseViewState<T extends BaseViewModel> extends State<BaseView<T>>
    with RouteAware {
  T? viewModel = getIt<T>();

  @mustCallSuper
  @override
  void initState() {
    super.initState();
    viewModel?.init();
  }

  @mustCallSuper
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    // subscribe for the change of route
    routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
  }

  /// Called when the top route has been popped off, and the current route
  /// shows up.
  @mustCallSuper
  @override
  void didPopNext() {

    viewModel?.routingDidPopNext();
  }

  /// Called when the current route has been pushed.
  @mustCallSuper
  @override
  void didPush() {

    viewModel?.routingDidPush();
  }

  /// Called when the current route has been popped off.
  @mustCallSuper
  @override
  void didPop() {

    viewModel?.routingDidPop();
  }

  /// Called when a new route has been pushed, and the current route is no
  /// longer visible.
  @mustCallSuper
  @override
  void didPushNext() {

    viewModel?.routingDidPushNext();
  }

  @mustCallSuper
  @override
  void dispose() {
    routeObserver.unsubscribe(this);

    viewModel?.dispose();
    viewModel = null;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider.value(
      value: viewModel,
      child: Consumer<T>(builder: widget.build),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

now pay attention to the state implementation of BaseViewState, see this code

T? viewModel = getIt<T>();
Enter fullscreen mode Exit fullscreen mode

this section is when the viewModel are injected to the view. so whenever you create a Screen extending to BaseView and you pump the ViewModel, then the ViewModel will become available for that view.

now, what kind of view model can be injected to view.

see this code :

class BaseView<T extends BaseViewModel>
Enter fullscreen mode Exit fullscreen mode

this code saying that the Type of view model can be injected to view is only a view model that extend to BaseViewModel

here is the BaseViewModel looks like.

abstract class BaseViewModel extends ChangeNotifier{
  BaseViewModel();

  /// This method is executed exactly once for each State object Flutter's
  ///  framework creates.
  void init() {}

  ///  This method is executed whenever the Widget's Stateful State gets
  /// disposed. It might happen a few times, always matching the amount 
  /// of times `init` is called.
  void dispose();

  /// Called when the top route has been popped off, and the current route
  /// shows up.
  void routingDidPopNext() {}

  /// Called when the current route has been pushed.
  void routingDidPush() {}

  /// Called when the current route has been popped off.
  void routingDidPop() {}

  /// Called when a new route has been pushed, and the current route is no
  /// longer visible.
  void routingDidPushNext() {}
}
Enter fullscreen mode Exit fullscreen mode

Pay attention to here

abstract class BaseViewModel extends ChangeNotifier
Enter fullscreen mode Exit fullscreen mode

this saying that all the viewModel we create that extending BaseViewModel is a ChangeNotifier type. this make our View Model State able to be subscribed by other object, and any changes on our viewModel we can notify those changes to the viewModel subscriber.

So Where the provider scope are declared?

now get back to the BaseView class, and you'll find this section code.

@override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider.value(
      value: viewModel,
      child: Consumer<T>(builder: widget.build),
    );
  }
Enter fullscreen mode Exit fullscreen mode

whenever the view Extend to BaseView build the widget, the BaseView will first declare the provider first, so all the widget here..
child: Consumer<T>(builder: widget.build) will have access to the viewModel state.

Where Are We Now? how about the repositories?

remember this ?
Image description
nah, up until here.. now we have this diagram.
Image description

so now how about the repositories?

remember we would like to make the repositories as a singleton object, so whoever use the repositories, the object will remain single in our thread but available to many view models like this.
Image description
by the diagram above, it saying.. no matter how many screen are open with its own viewModel. and all those view model are using the Repositories(A) or Repositories(B) the Repositories itself remain only one object in our memory. thats why we have to told to our DI to make our repository as @singleton...Here is the link explain more about the singleton

##Back to our app,
as you can see previously on the TaskRepositoryImpl
the declaration is like this

@singleton 
class TaskRepositoryImpl implements ITaskRepository
Enter fullscreen mode Exit fullscreen mode

this should make our TaskRepository as a singleton object, so in our case.. whenever MainScreenViewModel require TaskRepositoryImpl as the code bellow :

final ITaskRepository _taskRepository = getIt<TaskRepositoryImpl>();
Enter fullscreen mode Exit fullscreen mode

our DI will return the existing object of the repository in the memory..

#Fine, Now how to run the code?
well, talking about the Injectable Package all those repositories and viewmodel with
@injectable and @singleton will actually generating implementation file before the flutter itself compiling.. so before we run our application, whenever you create new class with injectable annotation. you have to run this command
fvm flutter pub run build_runner build --delete-conflicting-outputs
you have to ask the build runner to create the implementation file.

Well, What is next?

oke, we haven't talk about how the repositories can communicate with the rest API?..
lets discuss about it MVVM With Flutter (Part2)

until then, you can enjoy the code Here..

Thanks Guys, hope this help for you to start the project with flutter.
P.S This are my codebaseline whenever i start a new project feel free to clone it.

Next :
MVVM With Flutter part 2
How To Use Flutter with Freeze
How To Use Flutter with Injectable

Top comments (0)