DEV Community

Blazebrain
Blazebrain

Posted on • Updated on

Using Services in Flutter

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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(),
      );
     }
    }
Enter fullscreen mode Exit fullscreen mode

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;
     }
    }
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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());
    }
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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();
     }
Enter fullscreen mode Exit fullscreen mode

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();
       },
      );
     }
    }
Enter fullscreen mode Exit fullscreen mode

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'),
     )
Enter fullscreen mode Exit fullscreen mode

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,
       ),
Enter fullscreen mode Exit fullscreen mode

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'),
             )
            ],
           ),
          ),
         );
        });
     }
    }
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (0)