Flutter Riverpod Filters-img

Flutter Riverpod Filters

This guide will cover how to link multiple providers to filter data in other providers. The UI functionality will be placed in a drawer
In this guide, we will go over how to use riverpod providers within each other to create filters. This guide will not go through explaining riverpod providers. Please visit this guide if you need to get familiar with the types of providers that riverpod offers. For big lists, it is obviously better to do filtering on the server and just send the response back. This guide is to show how we can use providers together

We're going to create a Flight list with the ability to filter if the flights are direct and set a max price.

Getting Started

We need to add riverpod to our app.
flutter_riverpod: ^0.12.3+1
we also need to wrap our app with ProviderScope in main.dart
void main() {
  runApp(ProviderScope(child: MyApp()));
}

Let's create our Flight model first. models/flight.dart
class Flight {
  String tripTitle;
  String flightClass;
  num price;
  bool direct;
  Flight({this.direct, this.price, this.tripTitle, this.flightClass});
}
Now let's create a list of flights. flights.dart
final List<Flight> flightsList = [
  new Flight(
      price: 500, flightClass: 'Economy', tripTitle: 'ATL - LAX', direct: true),
  new Flight(
      price: 800,
      flightClass: 'Business',
      tripTitle: 'DXB - HND',
      direct: true),
  new Flight(
      price: 300, flightClass: 'Economy', tripTitle: 'ORD - LHR', direct: true),
  new Flight(
      price: 440,
      flightClass: 'Economy',
      tripTitle: 'LHR - LAX',
      direct: false),
  new Flight(
      price: 700, flightClass: 'Economy', tripTitle: 'CDG - LAX', direct: true),
  new Flight(
      price: 1800,
      flightClass: 'Business',
      tripTitle: 'CAN - LAX',
      direct: false),
];

Show the flights

Now that we have things set up we can create our first provider and have it return the list of flights. I like to place my providers in a separate folder providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_filters/models/flight.dart';

import 'flights.dart';

final flightsProvider = Provider<List<Flight>>((ref) => flightsList);
Let's create a filteredFlights provider and use the value from this provider. We will update this provider as we go along to filter the results.
final filteredFlights = Provider<List<Flight>>((ref) {
  final flights = ref.watch(flightsProvider); // 1

  var filteredFlightsList = flights;

  return filteredFlightsList;
});
  1. We use ref.watch in order to use flightsProvider in another provider and listen to changes that happen to it.

Now that we can listen to this provider and create a list. Let's create a Scaffold that has a list of all the flights.
class FlightPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flights')),
      body: Consumer( // 1
        builder: (context, watch, child) {
          List<Flight> flights = watch(filteredFlights); // 2
          return ListView.builder(
            itemCount: flights.length,
            itemBuilder: (BuildContext context, int index) {
              final flight = flights[index];
              return ListTile(
                title: Text(flight.tripTitle),
                subtitle: Text(
                    '${flight.flightClass} - ${flight.price} USD - direct: ${flight.direct}'),
              );
            },
          );
        },
      ),
    );
  }
}
  1. Here we use a Consumer instead of just making the entire widget extend ConsumeWidget in order not to rerender the entire scaffold when the filters change. For more information on these please go through this guide.
  2. We watch (listen) to the filteredFlights provider and list them in a ListView

Adding filter providers

In order to get our filters working they would also need providers to store the values. Let's create 2 new providers. providers.dart
final directFilterProvider = StateProvider<bool>((_) => false);
final priceFilterProvider = StateProvider<num>((_) => 0);
We can now use these values to filter out our flightsProvider. Let's change the filteredFlights provider to look like this
final filteredFlights = Provider<List<Flight>>((ref) {
  final flights = ref.watch(flightsProvider);
  final direct = ref.watch(directFilterProvider).state; // 1
  final price = ref.watch(priceFilterProvider).state;

  var filteredFlightsList = flights.where((flight) => flight.direct == direct); // 2

  if (price > 0) { // 3
    filteredFlightsList =
        filteredFlightsList.where((flight) => flight.price < price);
  }

  return filteredFlightsList.toList(); // 4
});
  1. We use ref.watch to use our new providers inside filteredFlights
  2. We filter out the list based on if the flights are direct or not
  3. We use 0 as a base to show all. If it is greater then we filter again based on the price
  4. where returns an iterable but not a list. We just convert it to a list and return the value.

The non UI logic behind riverpod filters is done, but we still need to implement the UI in order to see the results.

Creating the filter drawer

Let's create a StatefulWidget that manages our filters and syncs them with the providers. Create this widget in filters.dart and call it Filters. Due to the guide not being focused on the UI the snippet below will contain the full widget and functionality. Don't forget to import!
class Filters extends StatefulWidget {
  Filters({Key key}) : super(key: key);

  @override
  _FiltersState createState() => _FiltersState();
}

class _FiltersState extends State<Filters> {
  // 1
  bool _direct;
  TextEditingController priceController = new TextEditingController();

  // 2
  @override
  void initState() {
    super.initState();
    _direct = context.read(directFilterProvider).state;
    priceController.text = context.read(priceFilterProvider).state.toString();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: EdgeInsets.zero,
      children: <Widget>[
        DrawerHeader( // 3
          child: Text(
            'Filters',
            style: TextStyle(fontSize: 24, color: Colors.white),
          ),
          decoration: BoxDecoration(
            color: Colors.blue,
          ),
        ),
        Row(
          children: [
            Checkbox( // 4
              value: _direct,
              onChanged: (bool value) {
                setState(() {
                  _direct = value;
                });
              },
            ),
            Text('Direct')
          ],
        ),
        Padding(
          padding: EdgeInsets.only(left: 16, right: 16),
          child: TextFormField( // 5
            controller: priceController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(
              icon: Icon(Icons.attach_money),
              labelText: 'Max Price',
            ),
          ),
        ),
        ListTile( // 6
          title: Text('Apply Filters'),
          onTap: () {
            context.read(priceFilterProvider).state =
                double.parse(priceController.text);
            context.read(directFilterProvider).state = _direct;
            Navigator.pop(context);
          },
        ),
        ListTile( // 7
          title: Text('Clear Filters'),
          onTap: () {
            setState(() {
              _direct = false;
              priceController.text = '0';
            });
            context.read(priceFilterProvider).state = 0;
            context.read(directFilterProvider).state = false;
            Navigator.pop(context);
          },
        ),
      ],
    );
  }
}
  1. We create a boolean property and a TextEditingController to manage the state of our filters
  2. On init we read the values of the providers and assign them to our boolean property and set the TextField text.
  3. DrawerHeader is just used for design purposes and to make the drawer look more full
  4. A checkbox that changes the value of our direct property when it's tapped
  5. A text field that loads the initial value from the controller
  6. A button that applies the values to the providers using context.read in order not to listen to changes.
  7. A button that resets the values of the providers using the same approach as above.

For more details on forms, you can visit this guide.

Creating the drawer

Now that we have the widget that has our filters linked to the providers. Let's add a Drawer to our Scaffold.
drawer: Drawer(
  child: Filters(),
),
This should now show a button that opens the drawer with our working filters.

Conclusion

In this guide, we went over how to create providers that are linked and get filtered based on each other. The focus was to show the filter functionality across widgets without diving too much into the UI.

Like the post?

feel free to share it anywhere

Related Guides

Flutter Riverpod State Management Explained-img

Flutter Riverpod State Management Explained

A Powerful state management solution