One primary criterion for building a fully scalable and maintainable application is the reusability of code. Complex numbers of code repetitions would result in many potential bugs. When there needs to be a change in the package offering a particular service to the app, a painfully long process would be required to completely swap out implementations in the app. That itself is a nightmare for developers and can be the launching pad to failure for a product/company.
This article will introduce you to Services in Flutter, along with their key benefits, and show example code. This article assumes you have a Flutter development environment setup and have been building apps with Flutter. If not, you can check out this guide from Flutter on getting started building Flutter apps.
Introduction
Services are classes that offer a specific functionality. A Service is a class that uses methods that enable it to provide a specialized feature, emphasizing specialized. A Service class offers just one distinct input to the app. Examples include:
- ApiService
- LocalStorageService
- ConnectivityService
- ThemeService
- MediaService etc.
Notice how each of the services listed out there offers one distinct feature. They keep the codebase as clean as possible and reduce the number of repeated codes on the codebase.
Services abstract functionalities got from a third-party source and reduced the dependence of the entire app on the goodwill of the package maintainer. For an app that depends directly on the packages, if the package maintainers abandon the package and there are no updates, it would break the app. When a decision is made later on to swap the package for another, there would be a lot involved in the process as developers would have to search through the codebase for places the package is used and then manually swap them. This process is painfully long and would cost development time and resources that the app could have better used.
The app does not depend on the 3rd party package but depends on the Service class. Hence, when there is a need for a swap, the only thing that would need to be changed would be functions from the packages the Service depends on.
Getting started
Let's use the MediaService to fetch an image from the user's phone and displays it on the screen. The first thing is to create a new project.
flutter create intro_to_service
Next, import stacked and image_picker packages, which Flutter would use in the project in the dependencies section of the pubspec
YAML.
dependencies:
image_picker: ^0.8.4+3
stacked: ^2.2.7
In the dev dependencies section, import the build_runner and stacked generator, which would be responsible for generating files from the annotations used in the app.
dev_dependencies:
build_runner: ^2.1.4
stacked_generator: ^0.5.5
Next, head over to your main.dart
file. Clear out the default counter app code, create a new material app, and pass an HomeView
to the home parameter (The HomeView
will be created later).
import 'package:flutter/material.dart';
import 'package:intro_to_services/app/app.locator.dart';
import 'views/home_view/home_view.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Material App',
home: HomeView(),
);
}
}
We would be making use of the image_picker package and picking the image from the gallery.
Create a folder named services; inside this folder, create a new file titled media_service.dart
. This is where the code for setting up the Service would be stored.
Next, create the method to get the image; we provide it with a parameter fromGallery
to indicate if we would be using the Camera or taking the image from the gallery. The image_picker
package gives us access to both.
This method would call the pickImage
function the package gives us access to and use the fromGallery
parameter to determine if we would get the image from the gallery or use the Camera.
import 'dart:io';
import 'package:image_picker/image_picker.dart';
class MediaService {
final ImagePicker _picker = ImagePicker();
Future<File?> getImage({required bool fromGallery}) async {
final XFile? image = await _picker.pickImage(
source: fromGallery ? ImageSource.gallery : ImageSource.camera,
);
final File? file = File(image!.path);
return file;
}
}
The getImage
method then returns the file that was selected to be used inside the application. That wraps up the MediaService we would be using.
Next, set up the locator file and register this Service that any class can use within the codebase. Create a new folder and name it app
. In this folder, create a file named app.dart
. This file would hold the setup for registering our services and other dependencies across the app.
Inside the app.dart
file, create a class named AppSetup
and annotate it with the @StackedApp
annotation. This annotation takes in a few parameters, among which are the routes and dependencies. The various services to be used within the app would be registered within the dependencies block of the StackedApp
annotation.
import 'package:intro_to_services/services/media_service.dart';
import 'package:stacked/stacked_annotations.dart';
@StackedApp(
dependencies: [
LazySingleton(classType: MediaService),
],
)
class AppSetup {}
Inside the block, we register the Service as a LazySingleton
, meaning it won't be initialized until it is used in the application. The classtype is the name of the class, which is MediaService.
Next, run the command to generate the locator file from the StackedApp
annotation using the stacked generator.
flutter pub run build_runner build --delete-conflicting-outputs
There is a function in this generated file, the setupLocator()
function, which sets up the environment and registers the Service. We call this function in the main block in the main.dart
file.
void main() {
WidgetsFlutterBinding.ensureInitialized();
setupLocator();
runApp(MyApp());
}
With this, we have successfully created the Service and registered it in the locator file, making the Service available for use in any part of the codebase.
Next, set up the homeView
and its ViewModel
. Create a new folder in the lib directory titled views. Inside this folder, create the folder which would hold the homeView and homeViewModel files. Name this folder home_view. Create the two files and name them home_view.dart
and home_viewmodel.dart
.
In the homeViewModel
file, create a class named HomeViewModel
which extends the BaseViewModel
from the stacked package. In this class, we first declare a variable that would access the MediaService
through the locator. The locator gives us access to the entire Service and its methods. Next, we declare a private nullable image of File type. We then link it to a getter for access by outside classes.
import 'dart:io';
import 'package:intro_to_services/app/app.locator.dart';
import 'package:intro_to_services/services/media_service.dart';
import 'package:stacked/stacked.dart';
class HomeViewModel extends BaseViewModel {
final mediaService = locator<MediaService>();
File? _image;
File? get imageFromGallery => _image;
}
Next, set up the method to make the call to the Service and fetch the image. The call to the getImage
function of the Service returns a file that we pass to the _image variable we created earlier. We then call notifyListeners
provided by the BaseViewModel
class from the stacked package. The notifyListerners()
call would inform the views bound to this ViewModel
that there has been a state change and that the views should perform a rebuild.
Future<void> getImageFromGallery() async {
_image = await mediaService.getImage(fromGallery: true);
notifyListeners();
}
Lastly, setting up the view itself. Create a stateless widget named HomeView
in the home_view.dart file. This widget returns the viewModelBuilder
widget from stacked, which binds the view to the ViewModel
. We pass in the ViewModel
to the viewModelBuilder
function parameter and a Scaffold to the builder parameter.
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'home_viewmodel.dart';
class HomeView extends StatelessWidget {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ViewModelBuilder<HomeViewModel>.reactive(
viewModelBuilder: () => HomeViewModel(),
builder: (context, viewModel, child) {
return Scaffold();
},
);
}
}
For the UI itself, we create a TextButton and pass the function to get the image from the ViewModel
in its onPressed Function; this would get the image from the gallery and makes it available for use in the view through the getter we declared in the ViewModel
class.
TextButton(
onPressed: () {
viewModel.getImageFromGallery();
},
child: const Text('Fetch Image'),
)
Lastly, if the image is not null, we want to display the image in the view if the user has selected an image. Using the image.file
from Flutter, we display the selected image from the gallery to the user in the UI.
if (viewModel.imageFromGallery != null)
Image.file(
viewModel.imageFromGallery!,
height: 30,
width: 30,
),
Here is the complete code for the home_view.dart view.
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'home_viewmodel.dart';
class HomeView extends StatelessWidget {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ViewModelBuilder<HomeViewModel>.reactive(
viewModelBuilder: () => HomeViewModel(),
builder: (context, viewModel, child) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (viewModel.imageFromGallery != null)
Image.file(
viewModel.imageFromGallery!,
height: 100,
width: 100,
),
TextButton(
onPressed: () {
viewModel.getImageFromGallery();
},
child: const Text('Fetch Image'),
)
],
),
),
);
});
}
}
Check out the complete code for the sample app here. Don't forget to drop a star on the repo.
Conclusion
Hurray, you have successfully learned how to create a service, declare it and use it anywhere within your application.
Services reduce the number of codes that would be reused within the application making the codebase cleaner and better organized. Services also protect us from the pain of manually swapping out implementations when there is a change in the third-party package being used within the codebase.
Services offer benefits that speed up the development time and effectively use available resources. Not using them yet? Try them out, and you will see the impact it would make on your codebase.
If you have any questions, don't hesitate to reach out to me on Twitter: @Blazebrain or LinkedIn: @Blazebrain.
Cheers!
Top comments (0)