Home Blog

How to add an underline on a TabBar

0

There is no such method where you can add an underline on a tabbar.

Here on StakeOverflow, https://stackoverflow.com/questions/52236509/flutter-tabbarview-underline-color/52239046#52239046, where @tomwyr explains how you can somewhat achieve the effect but it only applies if you have a tabbar with all tabs filling the width of the screen.

Here, I would like to show how can you add an underline on a tabbar by just wrapping your tabbar and a divider within a stack widget.

Stack(
  alignment: Alignment.bottomLeft,
  children: [
    TabBar(
      isScrollable: true,
      indicatorColor: Colors.black,
      indicatorSize: TabBarIndicatorSize.label,
      labelColor: theme.primaryColor,
      labelStyle: theme.textTheme.headline3!.copyWith(
        fontWeight: FontWeight.w800,
      ),
      unselectedLabelStyle: theme.textTheme.headline3!.copyWith(
        fontWeight: FontWeight.w600,
      ),
      tabs: [
        Tab(text: 'Posts'),
        Tab(text: 'Bookmarks'),
      ],
    ),
    Positioned(
      child: Divider(
        color: Color.fromRGBO(241, 241, 241, 1),
        height: 0,
        thickness: 1,
      ),
    )
  ],
),

State Management with Provider

0

What is state in Flutter

Before we get into ways to manage states in Flutter, it would be ideal if we understand what actually is a state in Flutter. A state is the information of an active widget that may change over the widget’s lifespan.

There are two types of state in Flutter, The ephemeral state, and the app state. The ephemeral state is when the information of the widget is only needed within the widget itself. The app state is when the information of the widget is needed in other parts of the widget tree. In other words, when an ephemeral state widget has its data changed, it will only affect the widget itself. Meanwhile, when there is a data modification of an app state widget, it has an app-wide effect.

Why is state important?

Imagine you have a fitness app, where you require users to log in upon opening the app. When a user submits their username and password, you will need to authenticate the information from the server, which may take some time. In that case, you would like to show a CircularProgressIndicator to let the user know you are processing. The question has to be asked, how does the app know when to output a CircularProgressIndicator? This is where the state comes in handy.

For the same circumstances, when a user submits a login request, the app set state into busy, and notify responsible components to do certain tasks such as showing the CircularProgressIndicator.

State management without Provider

Passing data around without Provider.

Allow me to explain the diagram above, now we are looking at a simple online store app, whereupon opening you will see HomeView. There will be a button where you can press to view the CatalogueView. On CatalogueView, there is ProductListView where products are displayed in a list form. Upon clicking a product, you will be redirected to SingleProductView, where the details of the product you tapped on are displayed. You can add products into your shopping cart through ProductListView and SingleProductView pages.

Here, the data (information about each product and its details such as id, price, dimension, … ) is stored at the root of the app. Imagine if the data is needed in SingleProductView, you will have to pass the data from Catalogue View all the way to Single Product View through constructor or Navigator NamedRoute arguments (more on that in link). Although this is fine for this simple app, you can already imagine how tedious and frustrating it will be when the codebase is getting larger and larger (for example, you want to display the price in ShoppingCartView, or the data at the root is needed in the leaf of a tall widget tree).

Why provider?

You may use setState to invoke rebuild of widgets, and passing arguments from parent widget to child widget in order to satisfy each state, but things will get tedious and unmanageable when the codebase is getting bigger, and especially when you have a broad widget tree.

This is where the Provider package can be helpful. With the use of Provider, you no longer need to worry about callbacks or InheritedWidgets. Fundamentally, these are the three classes to use Provider state management:

  • ChangeNotifier (Flutter SDK)
  • ChangeNotifierProvider
  • Consumer

In simple terms, ChangeNotifier notifies its listeners by calling notifyListeners(), updating them about the changes made in the class, as such, the listeners may make changes accordingly.

ChangeNotifierProvider is a class that provides ChangeNotifier instances to its descendants! A good practice would be putting it just above the widgets that need to access it.

Consumer listens or consumes when notifyListeners() (of the same type) is called. Good practice would be putting it lower in the tree as possible, so it only rebuilds widgets that need to be rebuilt.

State management with Provider

To implement the Provider package in your project, first, you need to ask yourself:

(1) What is the data / state that will affect different areas of the app upon changes?

(2) What is the data that you are passing through widgets that are only interested in the destination widget?

In the case of our simple shopping app, it is the list of products that has an app-wide effect. It is also being passed down through CatalogueViewjust to reach ProductListView, SingleProductView, and ProductListView.

Now that you found out your app needs to implement a better way to handle state management. For our shopping app, unsurprisingly, we are going to implement Provider.

This is how our app structure will be like after implementing Provider:

shopping app with Provider implementation.

For this tutorial, we will be implementing the basics of Provider.

To be specific, we will be using:

  • ChangeNotifier (Flutter SDK)
  • ChangeNotifierProvider (Provider SDK)
  • Consumer (Provider SDK)

Now, go to our GitHub Repo to clone the starter folder, and follow along with the tutorial:

Code walkthrough

Before we implement Provider, let us have an overview of the existing files and directories:

Directory overview

Under the hood, the code is divided into two main directories: (1) Core and (2) UI. All the logic handling components and models are stored under the Core directory, while the UI directory stores the UI-related components, such as widgets, and the app router for navigation.

  • DefaultScaffold is a custom Scaffold widget with a IconButton that brings user to ShoppingCartView.
  • HomeView is a screen with a button that brings user to CatalogueView. Also the products data is stored here.
  • CatalogueView is a screen with a custom ListView of products (ProductListView).
  • ProductListView is a custom ListView in which tapping each product will bring the user to that particular product’s SingleProductView.
  • SingleProductView is a screen showing the details of a single product. An “Add to Cart” button is present and add one item to ShoppingCartView when pressed.
  • ShoppingCartView is a screen displaying the products that ther user has added to cart. The logic is not yet implemented though, we will do that together.

Let’s see what we have got now shall we?

  1. Firstly, open the starter folder with your IDE.
  2. Enter flutter pub get in your terminal to get all dependencies you needed.
  3. Now, enter flutter run to run the project. Observe the pattern of the app.
Stater App Demo
  1. As you can see, the ShoppingCartView only has a placeholder test widget. Also, if you observe the code, products data are stored inside HomeView and are being passed down through contrustors, as illustrated in the flowchart before. Let’s have a closer look below:

home_view.dart:

HomeView

catelogue_view.dart:

CatalogueView

product_list_view.dart:

ProductListView

single_product_view.dart:

SingleProductView

Here we can see that when the user presses the ‘View Catalogue’ button in HomeView, the products data is passed down to CatalogueView through the constructor. The data is again passed down to ProductListView, which then passed down to SingleProductView.

So far, here are some taking points:

  1. CatalogueView does not need products data, it only acts as a ‘middleman’ to pass down the data to ProductListView.
  2. Might not seem like a problem for such a simple app, but we can see the potential problem when the codebase is growing bigger and bigger.
  3. Also, notice how the shopping cart cannot keep the ‘state’ of which product is being added to cart.

Time to implement Provider!

Step 1: Create a ChangeNotifier class

As mentioned above, products are app-wide data for the time being, which means a change of the data will affect not just the widget itself, but also other components throughout the app. To make sure a Change will Notify the widgets that concern, extends the data class with ChangeNotifier.

  1. Under core\models, create a new file called products.dart, and add the following code:
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:shopping_app/core/models/product.dart';

class Products extends ChangeNotifier {
  List<Product> _products = [
    Product(
      id: 'p1',
      name: 'Shoe',
      price: 30.0,
      imageUrl:
          'https://cdn.pixabay.com/photo/2013/07/12/18/20/shoes-153310_960_720.png',
      description:
          'This is a very comfortable shoe, can be worn by any person. Socks sold seperately',
    ),
    Product(
      id: 'p2',
      name: 'Jumper',
      price: 50.0,
      imageUrl:
          'https://cdn.pixabay.com/photo/2016/03/31/19/21/clothes-1294933_960_720.png',
      description:
          'This is a very stylish jumper, welp you can call it sweater if you prefer to...',
    ),
    Product(
      id: 'p3',
      name: 'Fridge',
      price: 1000.0,
      imageUrl:
          'https://cdn.pixabay.com/photo/2016/10/24/21/03/appliance-1767311_960_720.jpg',
      description:
          'This is a very energy-efficient fridge, perfect for your store-bought steaks',
    ),
    Product(
      id: 'p4',
      name: 'Spatula',
      price: 10.0,
      imageUrl:
          'https://cdn.pixabay.com/photo/2017/04/04/17/18/spatula-2202239_960_720.jpg',
      description:
          'This is a very comfortable shoe, can be worn by any person.',
    ),
    Product(
      id: 'p5',
      name: 'One Sock',
      price: 5.0,
      imageUrl:
          'https://cdn.pixabay.com/photo/2019/07/11/08/19/sock-4330279_960_720.jpg',
      description:
          'This is a comfy yet luxury sock, can be worn by any person. Pss... remember to buy a pair',
    ),
  ];


  List<Product> get products => UnmodifiableListView(_products);
 
  Product getProductById(String id) {
    return _products.firstWhere((product) => product.id == id);
  }

Now that the products data is in products.dart, head over to ui/viewshome_view.dart and delete _products.

Products class now extends ChangeNotifier, which allows it to nofitier its listeners whenever notifylisteners() is called.

Let’s modify the existing code of our files to complement Products.

Step 2: Wrap with ChangeNotifierProvider

To make use of Products, let’s wrap our app with ChangeNotifierProvider, and provides Products (which is a ChangeNotifier).

Firstly, add Provider into pubspec.yaml file, and then run flutter pub get to update the dependencies.

...
dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.0  // <--- add this line
...

In main.dart, import the provider package.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; // <--- import provider package
import 'package:shopping_app/core/models/products.dart'; // <--- import products
import 'package:shopping_app/ui/shared/theme.dart';
import 'package:shopping_app/ui/views/home_view.dart';
...

Still main.dart, wrap MaterialApp with ChangeNotifierProvider.

...
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(           // <--- wrap with ChangeNotifierProvider and
      create: (context) => Products(),       // <--- provide a ChangeNotifier - Products
      child: MaterialApp(
        title: 'Shopping App',
        theme: myTheme,
        home: HomeView(),
        debugShowCheckedModeBanner: false,
      ),
    );
  }
...

Congratuations! You have wrap your root widget with a ChangeNotifierProvider which provides Provider to its descendants.

Step 3: Listening to Changes

Everything is wholly prepared, now it’s time to see what Provider can do.

Head over to home_view.dart, remove the argument for CatalogueView.

...
          Container(
            margin: const EdgeInsets.symmetric(vertical: 20, horizontal: 50),
            child: ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => CatalogueView(),  // <--- remove CatalogueView arguments as we no longer need it.
                  ),
                );
              },
              child: const Text(
                'View Catalogue',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.w300),
              ),
              style: ElevatedButton.styleFrom(
                elevation: 5,
                padding: const EdgeInsets.all(20),
              ),
            ),
          ),
...

Go to catagloue_view.dart, remove these following lines.

class CatalogueView extends StatelessWidget {
  // final List<Product> _products; <--- remove this

  const CatalogueView({
    Key? key,
    //required products, <--- remove this too
  }) : // _products = products, <--- and this
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return DefaultScaffold(
      title: 'Catalogue',
      body: ProductListView(), // <--- and the arguments
    );
  }
}

After some cleanups, now we will see Provider in action. Go to product_list_view.dart, modify the codes as shown below.

class ProductListView extends StatelessWidget {
  //final List<Product> _products; <--- remove this please

  const ProductListView({
    Key? key,
    //required products, <--- and this
  }) : //_products = products, <--- yes, remove this
        super(key: key);

  @override
  Widget build(BuildContext context) {
    // add this line. MAGIC!!
    final List<Product> _products =
        Provider.of<Products>(context).products;

    return ListView.builder(
      itemBuilder: (context, index) {
        return GestureDetector(
          onTap: () => Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) =>
                  SingleProductView(product: _products[index]),
            ),
          ),
          child: Card(
            elevation: 8,
            margin: const EdgeInsets.all(20),
            child: Column(
              children: [
                Container(
                  padding: const EdgeInsets.all(8),
                  height: 100,
                  child: Image.network(_products[index].imageUrl),
                ),
                const SizedBox(height: 15),
                Text(
                  '${_products[index].name} : RM${_products[index].price}',
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.w400,
                  ),
                ),
                const SizedBox(height: 15),
              ],
            ),
          ),
        );
      },
      itemCount: _products.length,
    );
  }
}

Here, you can see we get products data from Products (the ChangeNotifier we injected, remember?).

Now let’s implement our shopping cart.

Go to shopping_cart.dart and edit the code to match the following.

First remember to include relevant imports

import 'package:provider/provider.dart';
import 'package:shopping_app/core/models/products.dart';
class ShoppingCartView extends StatelessWidget {
  const ShoppingCartView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Shopping Cart'),
      ),
      body: Consumer<Products>( // <--- We are using Consumer to listen to changes this time
        builder: (context, model, child) => model.shoppingCartProducts.isEmpty
            ? const Center(
                child: Text(
                  'Shopping Cart is Empty',
                  style: TextStyle(
                    fontSize: 30,
                    fontWeight: FontWeight.w400,
                  ),
                ),
              )
            : ListView.separated(
                itemBuilder: (context, index) => Card(
                  elevation: 5,
                  margin: const EdgeInsets.all(20),
                  child: Column(
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          IconButton(
                            onPressed: () => model.addToShoppingCart(
                                id: model.shoppingCartProducts[index].id,
                                count: -1),
                            icon: const Icon(Icons.exposure_minus_1),
                          ),
                          Container(
                            padding: const EdgeInsets.all(10),
                            height: 150,
                            width: 150,
                            child: Image.network(
                              model.shoppingCartProducts[index].imageUrl,
                              //height: 50,
                              alignment: Alignment.center,
                              fit: BoxFit.scaleDown,
                            ),
                          ),
                          IconButton(
                            onPressed: () => model.addToShoppingCart(
                                id: model.shoppingCartProducts[index].id,
                                count: 1),
                            icon: const Icon(Icons.exposure_plus_1),
                          ),
                        ],
                      ),
                      Column(
                        children: [
                          Text(
                            model.shoppingCartProducts[index].name,
                            style: const TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          const SizedBox(
                            height: 15,
                          ),
                          Text(
                            'RM${model.shoppingCartProducts[index].price.toStringAsFixed(2)}',
                            style: const TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.w400,
                            ),
                          ),
                        ],
                      ),
                      Padding(
                        padding: const EdgeInsets.all(20.0),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              '${model.shoppingCartProducts[index].countInCart} x',
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(
                              width: 15,
                            ),
                            Text(
                              'RM${model.shoppingCartProducts[index].price.toStringAsFixed(2)}',
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                            Text(
                              ' = RM${model.shoppingCartProducts[index].price * model.shoppingCartProducts[index].countInCart}',
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
                itemCount: model.shoppingCartProducts.length,
                separatorBuilder: (context, index) => const Divider(),
              ),
      ),
    );
  }
}

There will be quite a lot of unimplemented errors, which we will implement them now.

Head over to product.dart, and add a new variable called countInCart.

class Product {
  final String _id;
  final String _name;
  final double _price;
  final String _imageUrl;
  final String _description;
  int _countInCart;  // <--- new variable to count how many of this product is in the cart

  Product({
    required id,
    required name,
    required price,
    required imageUrl,
    required description,
  })  : _id = id,
        _name = name,
        _price = price,
        _imageUrl = imageUrl,
        _description = description,
        _countInCart = 0; // <--- initialize it with 0 in the initializing list

  String get id => _id;
  String get name => _name;
  double get price => _price;
  String get imageUrl => _imageUrl;
  String get description => _description;
  int get countInCart => _countInCart;
  void addCountInCart(int value) => _countInCart += value; // <--- at a method to increment countInCart with value
}

Next, go to products.dart, we are going to add some new methods.

import 'dart:collection';

import 'package:flutter/material.dart';
import 'package:shopping_app/core/models/product.dart';

class Products extends ChangeNotifier {
  List<Product> _products = [...]; 

  List<Product> _shoppingCartProducts = [];

  List<Product> get products => UnmodifiableListView(_products);
  List<Product> get shoppingCartProducts =>
      UnmodifiableListView(_shoppingCartProducts);   // <--- a getter for _shoppingCartProducts

  Product getProductById(String id) {
    return _products.firstWhere((product) => product.id == id);
  }

  void addToShoppingCart({required String id, required int count}) { // <--- a method to add product to cart
    final product;

    // first checking: if product exists
    if (_products.contains(getProductById(id))) {
      product = products.firstWhere((e) => e.id == id);
    } else {
      return;
    }
    
    // second checking: if product already in cart:
    // if so, add countInCart. If not, add to shoppingCartList and add countInCart.
    if (_shoppingCartProducts.contains(getProductById(id))) {
      product.addCountInCart(count);
    } else {
      _shoppingCartProducts.add(getProductById(id));
      product.addCountInCart(count);
    }

    // if countInCart is less than or equal to 0, remove it from the list
    if (product.countInCart <= 0) {
      removeFromShoppingCart(id: product.id);
    }

    notifyListeners();
  }

  // a method to remove a product from shoppingCartList
  void removeFromShoppingCart({required String id}) {
    if (_shoppingCartProducts.contains(getProductById(id))) {
      _shoppingCartProducts.removeWhere((e) => e.id == id);
      notifyListeners();
    }
  }
}

Now, let’s make sure that the “Add to Cart” button in SingleProductView works. Go to single_product_view.dart, under // TODO - implement addToShoppingCart, replace this line with

Provider.of<Products>(context, listen: false).addToShoppingCart(id: _product.id, count: 1);

Notice that we have listen set to false, since we only want to use the method, we do not need to listen to changes.

Congratulations! You have implemented Provider and utilize it in your app!

Let us review what we have done thus far:

  • Provider.of<changeNotifier>(context) : can be used to get access to the data in the ChangeNotifier class, and the relevant widget will also be rebuilt when notifyListeners() is called changeNotifier.
  • Provider.of(context, listen: false) : supply listen: false to indicate that you do not want to listen to changes.
  • Consumer<changeNotifier> is also another way to get access and get rebuilt when notifyListeners() is called.
  • A Consumer subscribe / listens to a ChangeNotifier provided, and requires an argument of builder, which takes in a Widget Function(BuildContext, <changeNotifier>, Widget?).
    • First argument is a BuildContext.
    • Second argument is the ChangeNotifier of type that you have provided. E.g. if Consumer<changeNotifier>, then changeNotifier.
    • Third argument is a widget, that you supply and will not be re-rendered when notifyListeners() is called. Works like Provider.of(context, listen: false).
    • Quick review over our Consumer implementation in ShoppingCartView:
Consumer<Products>( // <--- first we let Consumer listens to Products
        // the second argument of builder is now Products, you can access methods in Products with model,
        // as you would with Provider.of(context)
        builder: (context, model, child) => model.shoppingCartProducts.isEmpty
            // when the cart is empty, show this
            ? const Center(
                child: Text(
                  'Shopping Cart is Empty',
                  style: TextStyle(
                    fontSize: 30,
                    fontWeight: FontWeight.w400,
                  ),
                ),
              )
            // when the cart is not empty, show this
            : ListView.separated(
                itemBuilder: (context, index) => Card(
                  elevation: 5,
                  margin: const EdgeInsets.all(20),
                  child: Column(
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          IconButton(
                            onPressed: () => model.addToShoppingCart(
                                id: model.shoppingCartProducts[index].id,
                                count: -1),
                            icon: const Icon(Icons.exposure_minus_1),
                          ),
                          Container(
                            padding: const EdgeInsets.all(10),
                            height: 150,
                            width: 150,
                            child: Image.network(
                              model.shoppingCartProducts[index].imageUrl,
                              //height: 50,
                              alignment: Alignment.center,
                              fit: BoxFit.scaleDown,
                            ),
                          ),
                          IconButton(
                            onPressed: () => model.addToShoppingCart(
                                id: model.shoppingCartProducts[index].id,
                                count: 1),
                            icon: const Icon(Icons.exposure_plus_1),
                          ),
                        ],
                      ),
                      Column(
                        children: [
                          Text(
                            model.shoppingCartProducts[index].name,
                            style: const TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          const SizedBox(
                            height: 15,
                          ),
                          Text(
                            'RM${model.shoppingCartProducts[index].price.toStringAsFixed(2)}',
                            style: const TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.w400,
                            ),
                          ),
                        ],
                      ),
                      Padding(
                        padding: const EdgeInsets.all(20.0),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              '${model.shoppingCartProducts[index].countInCart} x',
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(
                              width: 15,
                            ),
                            Text(
                              'RM${model.shoppingCartProducts[index].price.toStringAsFixed(2)}',
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                            Text(
                              ' = RM${model.shoppingCartProducts[index].price * model.shoppingCartProducts[index].countInCart}',
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
                itemCount: model.shoppingCartProducts.length,
                separatorBuilder: (context, index) => const Divider(),
              ),
      ),

Now, let’s compile the code and test it out!

Final App Demo

Weaknesses of Provider

Now that we have learned theoritically and practically about Provider, now it’s time to discuss the weaknesses of Provider.

One of the weaknesses of Provider is that it is entirely reliant on Flutter. Because its widgets are used to give objects or states farther down the tree, it is entirely reliant on Flutter, resulting in a mix of UI code and dependency injections.

Provider also depends solely on the object type. You can only get one closer to the calling codeĀ if you give two of the same kind.

Provider invokes runtime errors if the given Object type is not provided.

What’s next

In no mean that Provider is the only way to manage state in Flutter. There are really no right or wrong to which state manager to implement to your project. Despite so, it is good to know what options you have and compare them so you get the best suiting one.

Here are some state management packages you may consider except for Provider:

Redux

Redux works with a unidirectional data flow. They call themselves predictable as Redux enables you to create applications that behave consistently across environments. They are also easy to debug thanks to their DevTools.

learn more about Redux here: https://pub.dev/packages/redux

BLoC

Everything in the app is represented as a stream of events with BLoC. If widgets submit events, for example, BLoC sits in the centre of the conversation, maintaining the conversation while other widgets reply. BLoC is easy to test and separated from UI logic hence improve the performance.

learn more about BLoC here: https://pub.dev/packages/flutter_bloc

Riverpod

The author of Riverpod is the same person with the author of Provider, in which the author realized the downfalls of Provider and had created Riverpod which solved the problems from Provider.

learn more about Riverpod here: https://pub.dev/packages/riverpod

Conclusion

As mentioned, there is no one perfect state management package, but ultimately, the idea is the same, you would definitely want your code able to react to a certain state and at the same time adopts a clean and neat codebase. And that is only possible with the help of State Managers like Provider, BLoC, Redux, and more. Happy Exploring!

Deep Link

0

Deep links are external URL’s that will lead you to a specific path or app state of your app. If someone installed your app and they tapped on a deep link, it will redirect them into your app with the intended app state that you want to show them.

For example : You’re looking for a product on your browser and you come across something that you like that is being sold in Shopee. If you have their app installed on your device, tapping on any link with their domain name will open the app on your device and redirect you to the product page.

We will learn how to setup deep linking for your app in this post!

The first button above will lead you to download the .apk file used to do deep link testing while the second button will navigate you to the Recipes tab of the app. By the end of this post, you should be able to set up deep links for your app.

Try this too. Click me!

This link will bring you to Google! Click me!