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...

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.

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 U.S. Embassy in Ethiopia in collaboration with GDG Addis (as an implementing partner) and CAWEE (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

  • 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.

Learn more about the program at https://techcamp.gdgaddis.dev

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.

$ 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;

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.

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.

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

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.

$ flutter pub get

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

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;
}

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.

Products screen
Cart screen
Profile screen

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

Introduction to JSON annotations

Installing and Using json_serializable and json_annotation packages

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

# pubspec.yaml
dependencies:
  json_annotation: ^4.0.0

dev_dependencies:
  build_runner:
  json_serializable 4.0.2

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:

import 'package:json_annotation/json_annotation.dart';

part 'my_model.g.dart';

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

Write the following code

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);
}

To generate the code type the following command

$ flutter pub run build_runner build

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:

analyzer:
  exclude:
    - "**/*.g.dart"

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.');
    }
  }
}

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.

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));
    }
  }
}

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

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;
}

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

  • Introduction to Freezed

  • Introduction to JSON annotation

  • Creating Products model

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

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:

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());

Performing a POST request:

response = await dio.post("/test", data: {"id": 12, "name": "wendu"});

Performing multiple concurrent requests:

response = await Future.wait([dio.post("/info"), dio.get("/token")]);

Downloading a file:

response = await dio.download("https://www.google.com/", "./xx.html");

Dio APIs

Creating an instance and set default configs.

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

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);

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

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

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:

# pubspec.yaml
dependencies:
  freezed_annotation:

dev_dependencies:
  build_runner:
  freezed:

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:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'my_demo.freezed.dart';

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

Write the following code

import 'package:freezed_annotation/freezed_annotation.dart';

part 'my_demo.freezed.dart';

@freezed
class Person with _$Person {
  factory Person({ String name, int age }) = _Person;
}

To generate the code type the following command

$ flutter pub run build_runner build

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:

analyzer:
  exclude:
    - "**/*.freezed.dart"

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 , incorporate dependency injection by means of the Inherited Widget model.

Adding Products Grid

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

Adding Products Tile

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

Creating Bloc Provider for ProductsCubit

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

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

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.

Mapping Products Cubit states to UI

Now let's edit ProductScreen class in product_screen.dart

Unit Testing

Testing the product model

create product_test.dart file in test/product/model/ with 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]),
    );
  }
}
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),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
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(),
      ),
    );
  }
}
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'),
                ),
              )
          ],
        );
      },
    );
  }
}
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 {}
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(),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
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());
    });
  });
}
flutter_bloc
provider
Setting up Dependency Injection
Creating Bloc Provider for Products Cubit
Mapping Products Cubit states to UI

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()));
}

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'),
          ),
        ),
      ],
    );
  }
}