fetcher 3.1.0 copy "fetcher: ^3.1.0" to clipboard
fetcher: ^3.1.0 copied to clipboard

Flutter package to Fetch and display asynchronous data easily

fetcher

Pub License

Async task UI made easy 😄

Minimalist framework to easily handle UI states (loading, error and data) for asynchonous tasks (like network calls), in an unified way.

It provides two main widgets, with automatic handling of all common UI states:

  • FetchBuilder fetch then display data (example: a weather info page).
  • SubmitBuilder submit data (example: a form page).

Simplicity in mind: directly provides a Future (likely network call, which may throw), the widget handles the rest.

Package developed with the KISS principle: no fuss, no glitter, just an easy-to-use API, using easy-to-read code.

Fetcher Bloc

fetcher package was designed with BLoC pattern in mind. We recommend using fetcher with the provided BlocProvider to split UI and business logic, and with the value_stream package to handle synchronous UI changes (based on StreamBuilder). A handy export file is provided in that purpose.

Features #

  • Minimalist library: mostly use native Flutter components & logic
  • Ready to use: default widgets provided
  • Basic usage should be very simple and straighforward, while advanced usage is possible
  • Global configuration with local overrides
  • Error & retry handling, with common UX behavior in mind
  • Can be plugged into an error reporting service
  • Fade transition between states to allow smooth UI
  • Optional components to use with BLoC pattern (recommended)

Main Widgets #

  • FetchBuilder fetch then display data
    • Handle loading, error and data states
    • Retry system
  • SubmitBuilder submit data
    • Handle loading and error states
    • Display barrier to prevent user interaction while loading (avoid double clicks)

Additional Widgets #

  • EventFetchBuilder listen to an EventStream and display data

    • It's like FetchBuilder but instead of directly calling a task once, it will listen to a stream and his updates.
  • PagedListViewFetcher paginated version of FetchBuilder

    • with infinite scrolling
  • SubmitFormBuilder submit data with automatic form validation

    • use default Flutter Form system
  • AsyncEditBuilder fetch then display data, and submit a change if needed (example: an async switch)

Fetcher Bloc #

  • BlocProvider mixin to make a Bloc class easily accessible from widget's state.
  • Exports value_stream package, recommended way to handle synchonous UI changes, based on StreamBuilder.

Usage examples #

Fetch data #

This example fetch a data from a server, then display data directly

FetchBuilder.basic<Weather>(
  task: api.getWeather,
  builder: (context, weather) => Text('Weather: ${weather.temperature}')
)

Were getWeather is an async function that return a Future<Weather>, and may throw (not internet, bad request, etc).

By default it will use global (or default) config. To override locally you can use config parameter:

FetchBuilder.basic<Weather>(
  task: api.getWeather,
  config: FetcherConfig(
    fetchingBuilder: (context) => const CircularProgressIndicator(),
  ),
  builder: (context, weather) => Text('Weather: ${weather.temperature}')
)

Submit data #

This example submit data to server, and then pop the page.

SubmitBuilder<void>(
  task: () => api.submitData('new data value'),
  onSuccess: (_) => Navigator.pop(context),
  builder: (context, runTask) => ElevatedButton(
    onPressed: runTask,
    child: const Text('Submit'),
  ),
)

Were submitData is an async function that send new data to server, and may throw (not internet, bad request, etc). If task throws, it will call onDisplayMessage callback (see config) and stay on the page to allow user to try again: onSuccess is only called it task return without errors. task can optionally return an object, that will be passed to the onSuccess callback, for advanced usage.

If task depends of the child context (for instance, if you have 2 buttons that starts 2 different tasks), you can pass the desired task in the runTask callback, instead of the task argument of SubmitBuilder:

SubmitBuilder<void>(
  onSuccess: (_) => Navigator.pop(context),
  builder: (context, runTask) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: runTask(() => api.submitData('data 1')),
          child: const Text('Submit 1'),
        ),
        ElevatedButton(
          onPressed: runTask(() => api.submitData('data 2')),
          child: const Text('Submit 2'),
        ),
      ],
    );
  },
)

Fetcher Bloc #

This example use BlocProvider mixin to provide a bloc class to the widget state.

The bloc class, that exposes anything you need (business logic), here a simple value:

class MyBloc with Disposable {
  final String value = 'Hello';
}

The widget (generally the page widget), that uses a BlocProvider mixin to give access to the bloc from the state:

class MyPage extends StatefulWidget {
  const MyPage({super.key});

  @override
  State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> with BlocProvider<MyPage, MyBloc> {
  @override
  MyBloc initBloc() => MyBloc();

  @override
  Widget build(BuildContext context) {
    // You have access to the bloc instance anywhere from the state
    return Text(bloc.value);
  }
}

More #

See the example project for more usage examples.

Getting started #

  1. Add package as dependency in pubspec.yaml
  2. Import
    1. fetcher_bloc.dart to use fetcher with BLoC pattern (recommended)
    2. Or fetcher.dart to just use fetcher widgets directly. You then may use extra.dart to use additional components.
  3. [Optional] Wrap your app widget with DefaultFetcherConfig to set global configuration:
DefaultFetcherConfig(
  config: FetcherConfig(
    fetchingBuilder: (context) => const Center(child: CircularProgressIndicator(color: Colors.red)),
    onDisplayError: (context, error) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text(error.toString()),
      backgroundColor: Colors.red,
    )),
    onError: (exception, stack, {reason}) {
      // Report error
    },
  ),
  child: MaterialApp(
    title: 'Fetcher Example',
    home: const MyHomePage(),
  ),
)

From there you're good to go 🎉️

Fetcher Bloc complete example / tutorial #

A detailed example to illustrate how to use Fetcher Bloc for a common use-case: a basic news reader app.

  • Fetch latest news article from server
  • User has the option to either like or dislike the article

Full source code is available in the example project.

1. Fetch data #

First, let's build the bloc with the method that will fetch last article from server

import 'package:fetcher/fetcher_bloc.dart';

class NewsReaderBloc with Disposable {
  Future<NewsArticle> fetchArticle() async {
    // Simulate network request
    await Future.delayed(const Duration(seconds: 2));

    // Return an article
    return NewsArticle('Title 1', 'Random content generated for page 1, at ${DateTime.now()}');
  }
}

We just need a class that extends Disposable, with an async method that returns the fetched data (here NewsArticle). No need for any error handling here, all is handled by FetchBuilder.

With corresponding data object:

class NewsArticle {
  const NewsArticle(this.title, this.content);

  final String title;
  final String content;
}

UI side, we need to create a new stateful widget for the page, that holds the bloc in his state:

import 'package:fetcher/fetcher_bloc.dart';
import 'package:flutter/material.dart';

import 'news_reader.bloc.dart';

class NewsReaderPage extends StatefulWidget {
  const NewsReaderPage({super.key});

  @override
  State<NewsReaderPage> createState() => _NewsReaderPageState();
}

class _NewsReaderPageState extends State<NewsReaderPage> with BlocProvider<NewsReaderPage, NewsReaderBloc> {
  @override
  NewsReaderBloc initBloc() => NewsReaderBloc();

  @override
  Widget build(BuildContext context) {
    return SizedBox();
  }
}

Now NewsReaderBloc instance is accessible from widget's state.

Let's build a basic UI to fetch and then display data:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('News reader #1'),
    ),
    body: FetchBuilder.basic<NewsArticle>(
      task: bloc.fetchArticle,
      builder: (context, article) {
        return Padding(
          padding: const EdgeInsets.all(20),
          child: Column(
            children: [
              // Title
              Text(
                article.title,
                style: Theme.of(context).textTheme.headlineSmall,
              ),
  
              // Content
              const SizedBox(height: 15),
              Text(
                article.content,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ],
          ),
        );
      }
    ),
  );
}

Key part here is the FetchBuilder.basic<NewsArticle> widget, that do all the job: we just give it the task (network request from bloc), it handles the rest. While it's loading, a default loading widget will be displayed. If task throws, a default error widget will be displayed, with a retry button. When data is available, builder will be called, with direct access to the data.

We now have a basic page that fetch data, than displays article.title and article.content.

we can go further and customize the fetching widget (optional). We have a config argument of type FetcherConfig, which allows to customize this widget. Here we use fetchingBuilder argument to override default loading widget:

...
task: bloc.fetchArticle,
config: FetcherConfig(
  fetchingBuilder: (context) => Padding(
    padding: const EdgeInsets.all(20),
    child: Center(
      child: Column(
        mainAxisSize: MainAxisSize.max,
        children: [
          const CircularProgressIndicator(),
          const SizedBox(height: 10),
          Text('Fetching article 1...'),
        ],
      ),
    ),
  ),
),
builder: ...

2. Dynamic widget #

If you need some widget to change dynamically, for instance on user interaction, and the related task is synchronous and safe (does not throws), you can use a basic DataStream and his widget DataStreamBuilder. DataStream is just a Dart Stream that holds the lattest value for easy access, and also guaranties that the value is alway accessible (no error handling). In our example, we can use that for the user to select a vote (like or dislike).

First add an enum on the bloc:

enum ArticleVote {
  like,
  dislike,
}

Then instantiate a new DataStream, and close it on the dispose method:

  final selectedVote = DataStream<ArticleVote?>(null);

  @override
  void dispose() {
    selectedVote.close();
    super.dispose();
  }

Because vote can be null (nothing selected), DataStream type must be nullable: ArticleVote? Initial value is null (unselected), we must explicitly pass it to the constructor.

You can add an extra handy bloc method to apply a vote (optional):

void selectVote(ArticleVote vote) => selectedVote.add(vote, skipSame: true);

On the UI side, use corresponding DataStreamBuilder:

DataStreamBuilder<ArticleVote?>(
  stream: bloc.selectedVote,
  builder: (context, selectedVote) {
    return ToggleButtons(
      isSelected: [
        selectedVote == ArticleVote.like,
        selectedVote == ArticleVote.dislike,
      ],
      onPressed: (index) => bloc.selectVote(index == 0 ? ArticleVote.like : ArticleVote.dislike),
      children: const [
        Icon(Icons.thumb_up),
        Icon(Icons.thumb_down),
      ],
    );
  },
),

When bloc.selectedVote stream updates (using the strem's add() method), DataStreamBuilder.builder part will be rebuild. So you alway should wrap the smallest possible part with a DataStreamBuilder, to only rebuild the part that changes.

Now we have a working ToggleButtons 😄

3. Submit data #

Now we want to submit that vote to the server. And because it's an asynchonous task (and unsafe), we have to handle all states properly. So we will use the perfectly adapted SubmitBuilder widget.

First add a new method to submit the vote to the server on the bloc:

Future<ArticleVote> voteArticle([ArticleVote? vote]) async {
  // If no vote is provided, use the selected vote
  vote ??= selectedVote.value;

  // If still no vote, throw an error
  if (vote == null) throw Exception('Select a vote first');

  // Simulate network request
  await Future.delayed(const Duration(seconds: 1));

  // Return the vote
  return vote;
}

Method takes the current stream value of selectedVote, then send it to the network. If selection is empty, we throw an error with a displayable message (by default it will be displayed by user).

Now on UI side, just wrap your page with SubmitBuilder:

Widget build(BuildContext context) {
  return SubmitBuilder<ArticleVote>(
    task: bloc.voteArticle,
    onSuccess: (vote) {
      // Display a success message
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text('Voted successfully: ${vote.name}'),
        backgroundColor: Colors.green,
      ));

      // Navigate to the next page
      Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => NewsReaderPage()));
    },
    builder: (context, runTask) {
      ...
      ElevatedButton(
        onPressed: runTask,
        child: const Text('Vote'),
      ),
      ...
    }
  );
}

task is executed when runTask (in builder) is called (usually from a button). While task is executed, a barrier with a loader is displayed, blocking user interaction (that's why we need the SubmitBuilder to be quite high on the tree). If task throws, a message is displayed, and state is reverted, so user can retry. When task is completed with success, onSuccess is called. This is where you should put navigation logic (all logic that needs a BuildContext).

If you need to pass an argument to task from the builder (or if you have different buttons calling different tasks), you may pass a task to the runTask callback, that will override the widget's task.

In our example, let's call a secondary task from a second button.

In the bloc:

  Future<ArticleVote> voteArticleWithError() => voteArticle(ArticleVote.error);

In the page:

ElevatedButton(
  onPressed: () => runTask(bloc.voteArticleWithError),
  child: const Text('Vote with error'),
),

That's all ! This example illustrate the base of fetcher_bloc usage.

Test & see full example code in the example project.

FAQ #

Controller #

In a FetchBuilder, if you need to call a task programmatically (for instance to refresh data from a pull-to-refresh), you can use the controller argument.

  1. Instanciate a new BasicFetchBuilderController (or ParameterizedFetchBuilderController) in the bloc or in a state.
  2. Pass instance to controller parameter of FetchBuilder.
  3. Call controller.refresh() from a function.

FetchBuilder with extra argument #

For advanced usage, you can use FetchBuilder.parameterized constructor to pass an object to the task.

For instance, it can be usefull for a search feature, passing the search string in the refresh method of the controller, each time searcg field changes.

Fetch vs Submit #

Both corresponding widgets handle an asynchonous task states (loading, error, data, etc). But differences in usage is important as state flow is a bit different.

Fetch is when you need to fetch a data before displaying it to the user. So while data is fetching, you have nothing to display but a loader. Same when an error occurs, an error widget will be displayed instead of the data widget.

Submit is when you need to send/post/submit/commit a data asynchonously OR just call an asynchonous task. Typical example is a form submission. Task is usually called after a user interaction (i.e. a button), displaying a barrier with a loader ABOVE all content. If an error occurs, barrier is removed and state reverted, so user can try again using same context (you stay on the form page). If it's a success, you usually want to navigate elsewhere.

You may look at the news reader example to get deeper.

5
likes
140
points
2.1k
downloads

Publisher

verified publishericysun.fr

Weekly Downloads

Flutter package to Fetch and display asynchronous data easily

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, value_stream

More

Packages that depend on fetcher