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.
i'm using these dependencies to run
- Injectable --> Dependencies Injection
- Freeze --> Model Mapper
- Provider --> State Management
The Goal, What the apps will looks like
as a sample i create todolist app, like this
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
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]);
},),
),
)
],
),
),
),
);
}
}
as you can see
Widget build(BuildContext context, MainScreenViewModel viewModel, Widget? child)
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();
}
}
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;
}
}
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...
...
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
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>();
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),
);
}
}
now pay attention to the state implementation of BaseViewState
, see this code
T? viewModel = getIt<T>();
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>
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() {}
}
Pay attention to here
abstract class BaseViewModel extends ChangeNotifier
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),
);
}
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 ?
nah, up until here.. now we have this diagram.
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.
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
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>();
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)