Only this pageAll pages
Powered by GitBook
1 of 36

Mobile Development Reference-TCE-C01

Loading...

Introduction

Loading...

First Steps

Loading...

Loading...

Loading...

Loading...

Routing

Loading...

Loading...

Loading...

Loading...

Products Feature

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Testing

Loading...

Loading...

Project Description

What are we building?

The project that we are going to build for this tech camp is a simple shopping app with the following features:

  • Get my profile

  • Get all products

  • Filter products by category

  • Add products to the cart

  • Increment or decrement products on the cart

The purpose of the project and its limited features is to introduce you basics of coding a standard app in a way that is scalable and easy to manage in the future.

These are what the screens of the application we are going to build would look like

That being said here's a couple of things to expect to learn with this project:

  • Bloc (Cubit) state management

  • Local state caching using hydrated cubit

  • Using DIO for Rest-API requests

  • Freezed for sealed class generation

JSON annotation for model generation

  • Unit testing with mockito

  • Widget testing

  • Auto Route for route management

  • Dependency injection using GetIt

  • Products screen
    Cart screen
    Profile screen

    Creating the first screen

    In order to create the first page create products_screen.dart under src/product/view/product with the following basic screen.

    import 'package:flutter/material.dart';
    
    class ProductsScreen extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Text("Product Screen"),
          ),
        );
      }
    }
    

    That's enough for now let's go ahead and create the router setup.

    Setting up auto route

    For this project, we are going to use auto_route for routing between screens as named routes. So to begin first add it on your "pubspec.yaml"

      dependencies: 
        auto_route: ^0.6.9
        
      dev_dependencies:
        auto_route_generator: ^0.6.10
        build_runner: ^1.11.1

    So auto route uses code generation if you're new to code generation in dart check out this article.

    After that let's go ahead and create our first page and create our router.

    Setting up Workspace

    Adding linters, Adding Dependency

    Adding dependencies

    On the main project folder go to pubspec.yaml

    dependencies:
      flutter:
        sdk: flutter
      bloc: ^6.1.1

    pubspec.yaml is used to maintain dependencies on the flutter project.

    Once you added the package run the following command to install the package in your project.

    Once install the package make sure to restart the project. Because the flutter hot-reload can't access the package unless it restarts.

    $ flutter pub get

    Setting up the app

    Alright so let's begin assuming you created the project and can currently run the hello world app, first thing to do is remove all the boilerplate and keep only the main method.

    void main() async {
      runApp(Application());
    }
    

    After this, you need to create the "Application" widget. In order to do that create a new folder under src and create a new file called "application.dart"

    import 'package:flutter/material.dart';
    
    class Application extends StatelessWidget {
      const Application({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
            title: 'Tech camp Flutter',
            debugShowCheckedModeBanner: false,
            home: Scaffold()
        );
      }
    }
    

    So all of this is straightforward. The next step is to define our routes.

    Creating Project

    On your terminal

    Navigate to your desired directory and use the command below to create a new project.

    $ flutter create techcamp_flutter_shopping_app

    All your essential files will be created in a folder named "techcamp_flutter_shopping_app"

    Open your project on your favorite IDE and let's make sure it works perfectly.

    Project structure

    So where do I put my files?

    Before heading off to explain the project details let's take a step back and discuss about the project structure we are going to follow.

    So basically folders are going to be grouped based on the feature under src as you can see from the image above.

    Apart from the feature, there is an application folder for putting the main app or the root widget, there is also a di folder for dependency injection and finally the router folder for putting all our page routes.

    Let's take a closer look at the product feature for instance.

    So each of the folders represent a layer of the feature to be coded. If the names don't make sense we will cover them later don't worry.

    $ flutter run

    Adding router to Material App

    So the last step is adding an instance of the router class we just generated to the material app to manage its routes.

     MaterialApp(
            title: 'Tech camp Flutter',
            debugShowCheckedModeBanner: false,
            //TODO: Add this line
            builder: ExtendedNavigator.builder(router: Router()),
     ),

    While doing this step flutter may cause an error that the router class is imported from two libraries. So in order to overcome that change your imports like this.

    import 'package:flutter/material.dart' hide Router;

    Welcome

    Welcome to Tech-camp Ethiopia.

    Introduction

    A tech boot camp is exactly what it sounds like – an encompassing program where we learn to either cultivate new tech skills or improve our current strengths. In a way, boot camps are like extended workshops. It is an intensive coding training course that prepares people with little or no prior experience in software development to work as junior developers.

    "Learning to write programs stretches your mind, and helps you think better, creates a way of thinking about things that I think is helpful in all domains.” Bill Gates

    The Tech Camp Ethiopia 2020 will be a series of 6 Bootcamp programs that encourage all of the youths – makers, engineers, do-gooders, executives, computer scientists, inventors, innovators – are making things that are not just nice to have, but that people need.

    Tech Camp Ethiopia creates connections, sparks, innovations, and empowers young tech-savvy society to solve real-world pressing challenges through technology.

    This program is organized by the in collaboration with (as an implementing partner) and (a financial administrator). It is one part of #USInvestsInEthiopians programs.

    Goal

    The goal of Tech Camp Ethiopia is to guide youths where to get started in the trending tech world and raise their technical, business, and other soft skills through one-to-one learning, hands-on training, and workshops in record time. It aims to pave the way for the participants how to convert ideas to prototype and then prototype into business by letting them enjoy their new skills with project-based practices.

    Objectives

    • Involve as many underrepresented communities as possible by implementing DIVERSITY & INCLUSION principles.

    • Let the participants find the best teams to work together and probably to form a startup.

    • Elevate the business and digital marketing skills of the youth

    Learn more about the program at

    Give the youths the habit of how to get the job done through hands-on practices and project-based exercises.
  • Create a fun way of learning experience through different gamification and engaging activities.

  • Create networking opportunities with tech companies who may potentially find skill power from the participants.

  • U.S. Embassy in Ethiopia
    GDG Addis
    CAWEE
    https://techcamp.gdgaddis.dev

    Creating models

    It is hard to think of a mobile app that doesn’t need to communicate with a web server or easily store structured data at some point. When making network-connected apps, the chances are that it needs to consume some good old JSON, sooner or later.

    In case you're not familiar with the term data class, it's simply a class with value equality, copyWith method, its fields are immutable and it usually easily supports serialization. Also, if you're familiar with Kotlin, you know that you can heavily cut down on the boilerplate by defining fields directly in the constructor like this:

    KotlinDataClass.kt
    data class User(val name: String, val age: Int)

    instead of this

    DartDataClass.dart
    @immutable
    class User {
      final String name;
      final int age;
    
      User(this.name, this.age);
    
      User copyWith({
        String name,
        int age,
      }) {
        return User(
          name ?? this.name,
          age ?? this.age,
        );
      }
    
      @override
      bool operator ==(Object o) {
        if (identical(this, o)) return true;
    
        return o is User && o.name == name && o.age == age;
      }
    
      @override
      int get hashCode => name.hashCode ^ age.hashCode;
    }

    instead of coding boilerplate freezed does all this with a few keystrokes

    In this section we will be Working on Creating the models of the Products features and the following will be covered.

    freezed_classes.dart
    import 'package:meta/meta.dart';
    
    part 'freezed_classes.freezed.dart';
    
    @immutable
    abstract class User with _$User {
      const factory User(String name, int age) = _User;
    }
    Introduction to Freezed
    Introduction to JSON annotation
    Creating Products model

    Implementing Get Products Functionality

    Get Products

    Import

    import 'package:dio/dio.dart';
    import 'package:techamp_flutter_shopping_app/app.dart';

    Create an abstract class of ProductRepository

    abstract class ProductRepository {
      Future<List<Product>> getAll();
    }

    Implement the abstract class

    class ProductRepositoryImpl implements ProductRepository {
      final Dio _dio;
    
      const ProductRepositoryImpl(this._dio) : assert(_dio != null);
    
      @override
      Future<List<Product>> getAll() async {
        try {
          final response = await _dio.get('products');
          final List productsJson = response.data;
          return productsJson.map((json) => Product.fromJson(json)).toList();
        } on DioError catch (_) {
          throw const AppError('Network error');
        } on dynamic catch (_) {
          throw const AppError('Something went wrong.');
        }
      }
    }

    Setting up router

    After that let's create the configuration needed for the router. First, create the "router.dart" file under src/router/ with the following code.

    import 'package:auto_route/auto_route.dart';
    import 'package:auto_route/auto_route_annotations.dart';
    import 'package:flutter/cupertino.dart';
    // Import your products_screen.dart 
    export 'router.gr.dart';
    
    @MaterialAutoRouter(routes: [
      MaterialRoute(page: ProductsScreen, initial: true),
    ])
    class $Router {}
    
    

    After this run, the command "pub run build_runner build" on the terminal of the project to generate the router source code. After running it you should see a "router.gr.dart" file next to it where the generated source code is written.

    What the above code basically does is register a new route for "ProductsScreen" and make it the initial route.

    Introduction to freezed

    Code generation for immutable classes that has a simple syntax/API without compromising on the features.

    While there are many code-generators available to help you deal with immutable objects, they usually come with a trade-off. Either they have a simple syntax but lack features, or they have very advanced features but with complex syntax.

    Installing and Using Freezed package

    Add this to your package's pubspec.yaml file:

    You can then install packages from the command line:

    $ dart pub get

    Alternatively, your editor might support dart pub get. Check the docs for your editor to learn more.

    Example

    Now you can use it in your Dart code by importing it like this:

    Note that the line partmy_demo.freezed.dartwill give you a warning, because the file is not created yet

    Write the following code

    To generate the code type the following command

    Ignore lint warnings on generated files

    It is likely that the code generated by Freezed will cause your linter to report warnings.

    The solution to this problem is to tell the linter to ignore generated files, by modifying your analysis_options.yaml:

    # pubspec.yaml
    dependencies:
      freezed_annotation:
    
    dev_dependencies:
      build_runner:
      freezed:
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'my_demo.freezed.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'my_demo.freezed.dart';
    
    @freezed
    class Person with _$Person {
      factory Person({ String name, int age }) = _Person;
    }
    $ flutter pub run build_runner build
    analyzer:
      exclude:
        - "**/*.freezed.dart"

    Creating Products Cubit

    Now let's create the "products_cubit.dart" file under src/product/cubit/products/ with the following code.

    import 'package:bloc/bloc.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    import 'package:techamp_flutter_shopping_app/app.dart';
    
    part 'products_cubit.freezed.dart';
    part 'products_state.dart';
    
    class ProductsCubit extends Cubit<ProductsState> {
      final ProductRepository _productRepository;
    
      ProductsCubit(this._productRepository)
          : assert(_productRepository != null),
            super(const InitialProductsState());
    
      Future<void> getAll() {
        emit(const ProductsLoadingState());
        return _getAllProducts();
      }
    
      Future<void> refresh() async {
        emit(const ProductsRefreshingState());
        return _getAllProducts();
      }
    
      Future<void> _getAllProducts() async {
        try {
          final products = await _productRepository.getAll();
          emit(ProductsLoadedState(products: products));
        } on AppError catch (error) {
          emit(ProductsErrorState(error: error.message));
        }
      }
    }

    Introduction to JSON annotations

    Installing and Using json_serializable and json_annotation packages

    Add this to your package's pubspec.yaml file:

    You can then install packages from the command line:

    $ dart pub get

    Alternatively, your editor might support dart pub get. Check the docs for your editor to learn more.

    Example

    Now you can use it in your Dart code by importing it like this:

    Note that the line partmy_demo.g.dartwill give you a warning, because the file is not created yet

    Write the following code

    To generate the code type the following command

    Ignore lint warnings on generated files

    It is likely that the code generated by Json_Serializer will cause your linter to report warnings.

    The solution to this problem is to tell the linter to ignore generated files, by modifying your analysis_options.yaml:

    # pubspec.yaml
    dependencies:
      json_annotation: ^4.0.0
    
    dev_dependencies:
      build_runner:
      json_serializable 4.0.2
    import 'package:json_annotation/json_annotation.dart';
    
    part 'my_model.g.dart';
    import 'package:json_annotation/json_annotation.dart';
    
    part 'my_model.g.dart';
    
    @freezed
    class Person with _$Person {
      const factory Person({
        @required String firstName,
        @required String lastName,
      }) = _Person;
    
      factory Person.fromJson(Map<String, dynamic> json) =>
          _$PersonFromJson(json);
    }
    $ flutter pub run build_runner build
    analyzer:
      exclude:
        - "**/*.g.dart"

    Creating Products States

    First, create the "products_state.dart" file under src/product/cubit/products/ with the following code.

    part of 'products_cubit.dart';
    
    @freezed
    abstract class ProductsState with _$ProductsState {
      const factory ProductsState.error({@required String error}) =
          ProductsErrorState;
    
      const factory ProductsState.refreshing() = ProductsRefreshingState;
    
      const factory ProductsState.initial() = InitialProductsState;
    
      const factory ProductsState.loaded({
        @required List<Product> products,
      }) = ProductsLoadedState;
    
      const factory ProductsState.loading() = ProductsLoadingState;
    }

    Creating products model

    After installing and experimenting on the freezed package, now let's create producs model.

    First, create the "product.dart" file under src/product/model/product/ with the following code.

    import 'package:flutter/foundation.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    import 'package:techamp_flutter_shopping_app/app.dart';
    
    part 'product.freezed.dart';
    part 'product.g.dart';
    
    @freezed
    abstract class Product with _$Product {
      const factory Product({
        @required int id,
        @required String title,
        @required String image,
        @required double price,
        @required String description,
        @required @CategoryConverter() ProductCategory category,
      }) = _Product;
    
      factory Product.fromJson(Map<String, dynamic> json) =>
          _$ProductFromJson(json);
    }

    After this run, the command "pub run build_runner build" on the terminal of the project to generate the product source code. After running it you should see "product.freezed.dart" and "product.g.dart" files next to it where the generated source code is written.

    Setting up Dio

    dio

    A powerful Http client for Dart, which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, Timeout etc.

    Add dependency

    # pubspec.yaml
    dependencies:
      dio: ^3.0.10

    Creating products repository

    A repository is nothing flutter specific. It's a design pattern with many implementations in many languages and for our case we are going to be using DIO. The Repository pattern implements separation of concerns by abstracting the data persistence logic in your applications. Design patterns are used as a solution to recurring problems in your applications, and the Repository pattern is one of the most widely used design patterns.

    Dio is a networking library developed by Flutter. It is powerful Http client for Dart, which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, ConnectionTimeout etc. Things that dio supports can be done with normal http library which we get in flutter sdk too but its not that easy to learn or understand so dio can be better.

    • Setting up Dio

    Making your first HTTP request with DIO

    Add Products Error Widget

    Inside the product_screen.dart file under src/product/view/products/ add the following code:

    class _ProductsErrorWidget extends StatelessWidget {
      final String error;
    
      const _ProductsErrorWidget({Key key, @required this.error})
          : assert(error != null),
            super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              error,
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.headline6,
            ),
            const SizedBox(height: 16),
            Align(
              child: RaisedButton(
                onPressed: context.read<ProductsCubit>().refresh,
                child: const Text('Retry'),
              ),
            ),
          ],
        );
      }
    }

    Adding Products Tile

    Inside the product_screen.dart file under src/product/view/products/ add the following code:

    class _ProductItem extends StatelessWidget {
      final Product product;
    
      const _ProductItem({Key key, @required this.product})
          : assert(product != null),
            super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTap: () => context.navigator.push(
            Routes.productScreen,
            arguments: ProductScreenArguments(product: product),
          ),
          child: Card(
            margin: EdgeInsets.zero,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Expanded(
                  child: Container(
                    color: Colors.white,
                    padding: const EdgeInsets.all(8),
                    child: Image.network(product.image),
                  ),
                ),
                Container(
                  height: 48,
                  color: Colors.grey[200],
                  padding: const EdgeInsets.all(4),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Expanded(
                        child: Padding(
                          padding: const EdgeInsets.only(left: 8),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                product.title,
                                overflow: TextOverflow.ellipsis,
                                style: const TextStyle(fontWeight: FontWeight.w600),
                              ),
                              const SizedBox(height: 4),
                              Text('${product.price} ETB'),
                            ],
                          ),
                        ),
                      ),
                      IconButton(
                        onPressed: () =>
                            context.read<CartCubit>().addProduct(product),
                        icon: const Icon(Icons.add_shopping_cart_rounded),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

    Adding Products Grid

    Inside the product_screen.dart file under src/product/view/products/ add the following code:

    class _ProductsGridView extends StatelessWidget {
      final List<Product> products;
    
      const _ProductsGridView({Key key, @required this.products})
          : assert(products != null),
            super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            mainAxisSpacing: 8,
            crossAxisSpacing: 8,
            childAspectRatio: 1 / 1.2,
          ),
          padding: const EdgeInsets.all(8),
          itemCount: products.length,
          physics: const BouncingScrollPhysics(),
          itemBuilder: (_, index) => _ProductItem(product: products[index]),
        );
      }
    }

    Making your first http request with DIO

    Import the DIO package

    import 'package:dio/dio.dart';

    Making your first http request with DIO

    var dio = Dio();
    Response response = await dio.get('https://google.com');
    print(response.data);

    The above example will make a request to http://google.com and print the result.

    Additional Examples

    Performing a GET request:

    Performing a POST request:

    Performing multiple concurrent requests:

    Downloading a file:

    Dio APIs

    Creating an instance and set default configs.

    You can create an instance of Dio with an optional BaseOptions object:

    import 'package:dio/dio.dart';
    
    Response response;
    Dio dio = new Dio();
    
    response = await dio.get("/test?id=12&name=wendu");
    print(response.data.toString());
    
    // Optionally the request above could also be done as
    response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
    print(response.data.toString());
    response = await dio.post("/test", data: {"id": 12, "name": "wendu"});
    response = await Future.wait([dio.post("/info"), dio.get("/token")]);
    response = await dio.download("https://www.google.com/", "./xx.html");
    Dio dio = new Dio(); // with default Options
    
    // Set default configs
    dio.options.baseUrl = "https://www.xx.com/api";
    dio.options.connectTimeout = 5000; //5s
    dio.options.receiveTimeout = 3000;
    
    // or new Dio with a BaseOptions instance.
    BaseOptions options = new BaseOptions(
        baseUrl: "https://www.xx.com/api",
        connectTimeout: 5000,
        receiveTimeout: 3000,
    );
    Dio dio = new Dio(options);

    Setting up Dependency injection

    Installing get_it package

    Add this to your package's pubspec.yaml file:

    dependencies:
      get_it: ^6.0.0

    Now let's set up the dependency injection

    First, create the "di.dart" file under src/di/ with the following code.

    import 'package:dio/dio.dart';
    import 'package:get_it/get_it.dart';
    import 'package:techamp_flutter_shopping_app/app.dart';
    
    GetIt getIt = GetIt.instance;
    
    void registerDependencies() {
      _registerConfigurations();
      _registerProduct();
      _registerProfile();
      _registerCart();
    }
    
    void _registerCart() {
      getIt.registerFactory<CartCubit>(() => CartCubit());
    }
    
    void _registerConfigurations() {
      getIt.registerSingleton(
        Dio(
          BaseOptions(
            baseUrl: 'https://fakestoreapi.com/',
            connectTimeout: 20000,
            receiveTimeout: 30000,
            sendTimeout: 30000,
          ),
        ),
      );
    }
    
    void _registerProduct() {
      getIt
        ..registerFactory<ProductRepository>(() => ProductRepositoryImpl(getIt()))
        ..registerFactory(() => ProductsCubit(getIt()));
    }
    
    void _registerProfile() {
      getIt
        ..registerFactory<ProfileRepository>(() => ProfileRepositoryImpl(getIt()))
        ..registerFactory(() => ProfileCubit(getIt()));
    }

    Creating products cubit

    Cubit, a simple solution for app state management in Flutter

    In flutter, state management is like a life support system where the whole App depends on, there are different state management techniques like Redux, MobX, bloc, flutter bloc, provider, and cubit. Actually Cubit is a cocktail of Flutter Bloc and Provider where we use some features of both techniques and we are going to learn how to use and implement Cubit, So before we go for its implementation firstly let’s have some cursory knowledge of it.

    Cubit is a lightweight state management solution. It is a subset of the bloc package that does not rely on events and instead uses methods to emit new states. Every cubit requires an initial state which will be the state of the cubit before emit has been called. The current state of a cubit can be accessed via the state getter.

    Creating Products States
    Creating Products Cubit
    Implementing Get Products Functionality

    Implementing Products Screen UI

    In this section we are going to be setting up our UI.

    • Adding Products Grid

    • Adding Products Tile

    Add Products Error Widget

    Widget Testing

    Widget testing is otherwise called component testing. As its name proposes, it is utilized for testing a single widget, and the objective of this test is to check whether the widget works and looks true to form.

    Widget testing proves to be useful when we are trying the particular widgets. On the off chance that the Text widget doesn’t contain the text, at that point, Widget testing throws an error expressing that the Text widget doesn’t have a text inside it. We likewise don’t host to install any third-party dependencies for widget testing in Flutter.

    Implementation:

    Steps to Implement Widget Testing

    Step 1: Add the dependencies

    Add the flutter_test dependency to pubspec — yaml file.

    Step 2: Create a file "product_screen_test.dart" in the directory test/product/view/products/ with the following code.

    dev_dependencies:
      flutter_test:
        sdk: flutter
    import 'dart:io';
    
    import 'package:flutter/material.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:mockito/mockito.dart';
    import 'package:techamp_flutter_shopping_app/app.dart';
    
    void main() {
      group('ProductsScreen', () {
        ProductsCubit productsCubit;
    
        setUp(() {
          productsCubit = _MockProductsCubit();
          getIt.registerFactory(() => productsCubit);
        });
    
        tearDown(() {
          productsCubit?.close();
          getIt.reset();
        });
    
        testWidgets('should show progress indicator on initial view',
            (tester) async {
          when(productsCubit.state).thenReturn(const InitialProductsState());
          await tester.pumpWidget(
            const MaterialApp(
              home: ProductsScreen(),
            ),
          );
    
          expect(find.byType(CircularProgressIndicator), findsOneWidget);
        });
    
        testWidgets('should show products when loaded successfully',
            (tester) async {
          const products = [
            Product(
              id: 1,
              title: 'Fjallraven',
              image: 'https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg',
              price: 109.95,
              description: 'Your perfect pack for everyday use.',
              category: MenClothingProductCategory(),
            ),
            Product(
              id: 2,
              title: 'Mens Casual Premium T-Shirts',
              image:
                  'https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg',
              price: 22.5,
              description: 'Slim-fitting style.',
              category: MenClothingProductCategory(),
            )
          ];
          when(productsCubit.state)
              .thenReturn(const ProductsLoadedState(products: products));
    
          // Fixes Image.network network exception
          HttpOverrides.global = null;
    
          await tester.pumpWidget(
            const MaterialApp(
              home: ProductsScreen(),
            ),
          );
    
          expect(find.text(products.first.title), findsOneWidget);
          expect(find.text('${products.first.price} ETB'), findsOneWidget);
    
          expect(find.text(products.last.title), findsOneWidget);
          expect(find.text('${products.last.price} ETB'), findsOneWidget);
        });
      });
    }
    
    class _MockProductsCubit extends Mock implements ProductsCubit {}
    

    Consuming the products cubit

    In order to use the blocs in your Flutter UI, you need to be able to access the blocs through-out your app. For example, you want to be able to do something like this in a widget. Provider is a term you'll see a lot in the Flutter world. In Flutter, it's used to describe a class that is used to inject other objects throughout your widget tree or scope, even when you're much further down the widget tree than where the objects are created.

    Dependency Injection (DI) is a technique used to reduce tight coupling between classes thus achieving greater re-usability of your code. Instead of your class instantiating/creating the dependent classes/objects, the dependent objects are injected or supplied to your class; thus maintaining an external library of sorts to create and manage object creation in your project.

    Some of the state management libraries such as flutter_bloc, provider incorporate dependency injection by means of the Inherited Widget model.

    • Setting up Dependency Injection

    Creating Bloc Provider for Products Cubit
    Mapping Products Cubit states to UI

    Unit Testing

    Testing the product model

    create product_test.dart file in test/product/model/ with the following code:

    import 'package:flutter_test/flutter_test.dart';
    import 'package:techamp_flutter_shopping_app/app.dart';
    
    void main() {
      group('Product', () {
        test('fromJson should return Product with correct values', () {
          final json = {
            'id': 1,
            'price': 109.95,
            'title': 'Fjallraven',
            'description': 'description',
            'category': 'men clothing',
            'image': 'image',
          };
    
          final actual = Product.fromJson(json);
    
          expect(actual.id, 1);
          expect(actual.price, 109.95);
          expect(actual.image, 'image');
          expect(actual.description, 'description');
          expect(actual.category, const MenClothingProductCategory());
        });
      });
    }

    Creating Bloc Provider for ProductsCubit

    Inside the product_screen.dart file we created earlier put the following code:

    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:techamp_flutter_shopping_app/app.dart';
    
    class ProductsScreen extends StatelessWidget {
      const ProductsScreen({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return BlocProvider(
          create: (_) => getIt<ProductsCubit>()..getAll(),
          child: Scaffold(
            appBar: AppBar(
              leading: IconButton(
                icon: const Icon(Icons.person, color: Colors.white),
                onPressed: () => context.navigator.push(Routes.profileScreen),
              ),
            ),
            floatingActionButton: const _CartButton(),
            body: Container(),
          ),
        );
      }
    }

    The _CartButton is a stateless widget placed inside product_screen.dart that builds a clickable widget we will use to navigate to the cartScreen

    class _CartButton extends StatelessWidget {
      const _CartButton({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return BlocBuilder<CartCubit, CartState>(
          builder: (_, state) {
            final count = state.carts.totalProductsQuantity;
            return Stack(
              overflow: Overflow.visible,
              children: [
                FloatingActionButton(
                  onPressed: () => context.navigator.push(Routes.cartScreen),
                  child: const Icon(Icons.shopping_cart),
                ),
                if (count > 0)
                  Positioned(
                    top: -4,
                    right: -2,
                    child: CircleAvatar(
                      radius: 12,
                      backgroundColor: Colors.white,
                      child: Text('$count'),
                    ),
                  )
              ],
            );
          },
        );
      }
    }

    Mapping Products Cubit states to UI

    Now let's edit ProductScreen class in product_screen.dart

    class ProductsScreen extends StatelessWidget {
      const ProductsScreen({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return BlocProvider(
          create: (_) => getIt<ProductsCubit>()..getAll(),
          child: Scaffold(
            appBar: AppBar(
              leading: IconButton(
                icon: const Icon(Icons.person, color: Colors.white),
                onPressed: () => context.navigator.push(Routes.profileScreen),
              ),
            ),
            floatingActionButton: const _CartButton(),
            body: SafeArea(
              child: Builder(
                builder: (context) => RefreshIndicator(
                  onRefresh: context.read<ProductsCubit>().refresh,
                  child: BlocBuilder<ProductsCubit, ProductsState>(
                    buildWhen: (previous, current) => previous.maybeWhen(
                      loaded: (products) => false,
                      orElse: () => true,
                    ),
                    builder: (context, state) => state.maybeWhen(
                      error: (error) => _ProductsErrorWidget(error: error),
                      loaded: (products) => _ProductsGridView(products: products),
                      orElse: () => const Center(
                        child: CircularProgressIndicator(),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
        );
      }
    }