0.4.1

Tutorial

Follow this tutorial in video format or scroll down for the full written version!



🗒 Listing TO-DOs (“R” in CRUD)

Now that Flutter Data is ready to use, we have access to our Repository<Todo> via Provider’s context.watch.

Important: Make sure you went through the quickstart to get everything set up!

It’s a REST client with CRUD actions that has a findAll method. We can start by displaying JSON Placeholder User id=1's list of TO-DOs:

class TodoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final repository = context.watch<Repository<Todo>>();
    return FutureBuilder<List<Todo>>(
      future: repository.findAll(params: {'userId': '1'}),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return Center(child: const CircularProgressIndicator());
        }
        return ListView.separated(
          itemBuilder: (context, i) {
            final todo = snapshot.data[i];
            return Text(
                '${todo.completed ? "✅" : "◻️"} [id: ${todo.id}] ${todo.title}');
          },
          itemCount: snapshot.data.length,
          separatorBuilder: (context, i) => Divider(),
          padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
        );
      },
    );
  }
}

and call it from TodoApp:

builder: (context) {
  if (context.watch<DataManager>() == null) {
    return const CircularProgressIndicator();
  }
  return TodoScreen();
},

Bam!

Whoa! Hold on, how did we get that magic to work?

Well, Flutter Data made an HTTP GET request on https://my-json-server.typicode.com/flutterdata/demo/todos?userId=1 because we requested to findAll Todos for a User id=1:

For more information see the Repository docs.

Check out the full source code at https://github.com/flutterdata/flutter_data_todos

Limit the amount of TO-DO items

Let’s make the number of TO-DOs more manageable via the _limit query param:

  future: repository.findAll(params: {'userId': 1, '_limit': 5}),

One hot-reload later…

In case you were wondering…

Chopper and Retrofit are REST client generators. Flutter Data is a REST client generator, too, but goes way beyond.

➕ Creating a new TO-DO

In a FloatingActionButton callback, we instantiate a new Todo model with a totally random title and save it:

floatingActionButton: FloatingActionButton(
  onPressed: () {
    Todo(title: "Task number ${Random().nextInt(9999)}").save();
  },
  child: Icon(Icons.add),
),

(This goes in our app’s Scaffold!)

Done!

Clicking that button sends a request in the background to POST https://my-json-server.typicode.com/flutterdata/demo/todos

But… why can’t we see this new Todo in the list?!

⚡️ Reactivity to the rescue

It’s not there because we used a FutureBuilder which fetches the list just once.

The solution is making the list reactive – in other words, using watchAll(). Instead of returning a Future, it returns a specific StateNotifier:

class TodoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final repository = context.watch<Repository<Todo>>();
    return DataStateBuilder<List<Todo>>(
      notifier: () => repository.watchAll(params: {'userId': '1', '_limit': '5'}),
      builder: (context, state, notifier, _) {
        if (state.isLoading) {
          return Center(child: const CircularProgressIndicator());
        }
        return ListView.separated(
          itemBuilder: (context, i) {
            final todo = state.model[i];
            return Text(
                '${todo.completed ? "✅" : "◻️"} [id: ${todo.id}] ${todo.title}');
          },
          itemCount: state.model.length,
          separatorBuilder: (context, i) => Divider(),
          padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
        );
      },
    );
  }
}

What went on here?

We are using DataStateBuilder (pretty much like a ValueListenableBuilder) to access DataState objects. They give us:

Add flutter_data_state to your pubspec.yaml to install and restart the app!

Creating a new TO-DO will now show up:

Before, with an id=null (temporary model which hasn’t been persisted):

After, with an id=201 that was assigned by the API server:

Notice that we passed a _limit=5 query param, so we only got 5 items!

The new Todo appeared because watchAll() reflects the current local storage state. (As a matter of fact, JSON Placeholder does not actually persist anything!)

Models are fetched from the network in the background by default. This strategy can be changed by overriding methods in a custom adapter.

⛲️ Prefer a Stream API?

No problem. If you don’t like DataState or StateNotifier you can totally bypass them and use good ol’ streams:

class TodoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final repository = context.watch<Repository<Todo>>();
    return StreamBuilder<List<Todo>>(
      stream: repository.watchAll(params: {'userId': '1', '_limit': '5'}).stream,
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return Center(child: const CircularProgressIndicator());
        }
        return ListView.separated(
          itemBuilder: (context, i) {
            final todo = snapshot.data[i];
            return Text(
                '${todo.completed ? "✅" : "◻️"} [id: ${todo.id}] ${todo.title}');
          },
          itemCount: snapshot.data.length,
          separatorBuilder: (context, i) => Divider(),
          padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
        );
      },
    );
  }
}

Just call watchAll().stream.

Check out the app’s full source code: https://github.com/flutterdata/flutter_data_todos

♻ Reloading

For a minute, let’s change that floating action button to overwrite one of our TO-DOs. For example, Todo with id=1.

floatingActionButton: FloatingActionButton(
  onPressed: () {
    Todo(id: 1, title: "OVERWRITING TASK!", completed: true).save();
  },
  child: Icon(Icons.add),
),

If we hot-reload and click on the + button we get:

As discussed before, JSON Placeholder does not persist any data. We’ll verify that claim by reloading our data with a RefreshIndicator and the very handy reload() from DataStateNotifier.

First, let’s place our list UI in a separate TodoList widget:

class TodoList extends StatelessWidget {
  final DataState<List<Todo>> state;
  const TodoList(this.state, {Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (state.isLoading) {
      return Center(child: const CircularProgressIndicator());
    }
    return ListView.separated(
      itemBuilder: (context, i) {
        final todo = state.model[i];
        return Text(
            '${todo.completed ? "✅" : "◻️"} [id: ${todo.id}] ${todo.title}');
      },
      itemCount: state.model.length,
      separatorBuilder: (context, i) => Divider(),
      padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
    );
  }
}

Next, we simply wrap the new widget and add the onRefresh function:

class TodoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final repository = context.watch<Repository<Todo>>();
    return DataStateBuilder<List<Todo>>(
      notifier: () => repository.watchAll(params: {'userId': '1', '_limit': '5'}),
      builder: (context, state, notifier, _) {
        return RefreshIndicator(
          onRefresh: () async {
            await notifier.reload();
          },
          child: TodoList(state),
        );
      },
    );
  }
}

Pull-to-refresh…

And all states have been reset!

⛔️ Deleting a TO-DO

There’s stuff “User 1” just doesn’t want to do!

We can delete a Todo on dismiss by wrapping the text with a Dismissible and calling todo.delete():

class TodoList extends StatelessWidget {
  final DataState<List<Todo>> state;
  const TodoList(this.state, {Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (state.isLoading) {
      return Center(child: const CircularProgressIndicator());
    }
    return ListView.separated(
      itemBuilder: (context, i) {
        final todo = state.model[i];
        return Dismissible(
          child: Text(
              '${todo.completed ? "✅" : "◻️"} [id: ${todo.id}] ${todo.title}'),
          key: ValueKey(todo),
          direction: DismissDirection.endToStart,
          background: Container(
            color: Colors.red,
            child: Icon(Icons.delete, color: Colors.white),
          ),
          onDismissed: (_) async {
            await todo.delete();
          },
        );
      },
      itemCount: state.model.length,
      separatorBuilder: (context, i) => Divider(),
      padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
    );
  }
}

Hot-reload and swipe left…

Done! (well, not really “done” 😄)

✅ Marking it as done!

Lastly, we’ll add a GestureDetector to our list’s tiles so we can easily toggle the completed state:

class TodoList extends StatelessWidget {
  final DataState<List<Todo>> state;
  const TodoList(this.state, {Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (state.isLoading) {
      return Center(child: const CircularProgressIndicator());
    }
    return ListView.separated(
      itemBuilder: (context, i) {
        final todo = state.model[i];
        return GestureDetector(
          onDoubleTap: () {
            Todo(id: todo.id, title: todo.title, completed: !todo.completed).save();
          },
          child: Dismissible(
            child: Text(
                '${todo.completed ? "✅" : "◻️"} [id: ${todo.id}] ${todo.title}'),
            key: ValueKey(todo),
            direction: DismissDirection.endToStart,
            background: Container(
              color: Colors.red,
              child: Icon(Icons.delete, color: Colors.white),
            ),
            onDismissed: (_) async {
              await todo.delete();
            },
          ),
        );
      },
      itemCount: state.model.length,
      separatorBuilder: (context, i) => Divider(),
      padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
    );
  }
}

Hot-reload and double-click any TO-DO…

All tasks done!

🎎 Relationships

Let’s now slightly rethink our query. Instead of “fetching all TO-DOs for user 1” we are going to “get user 1 with all their TO-DOs”.

Flutter Data has great support for relationships.

First, in models/user.dart, we’ll create the User model:

import 'package:flutter_data/flutter_data.dart';
import 'package:json_annotation/json_annotation.dart';

import 'todo.dart';

part 'user.g.dart';

@JsonSerializable()
@DataRepository([StandardJSONAdapter, JSONPlaceholderAdapter])
class User extends DataSupport<User> {
  @override
  final int id;
  final String name;
  final HasMany<Todo> todos;

  User({this.id, this.name, this.todos});
}
Notice that HasMany<Todo> field: it’s a Flutter Data relationship!

But… User needs JSONPlaceholderAdapter. Since users and TO-DOs share the same base URL (exactly the one defined in JSONPlaceholderAdapter) we’ll make this adapter generic.

Move the existing mixin to its own file at models/_adapters.dart and add a type T extends DataSupport:

import 'package:flutter_data/flutter_data.dart';

mixin JSONPlaceholderAdapter<T extends DataSupport<T>> on RemoteAdapter<T> {
  @override
  String get baseUrl => 'http://jsonplaceholder.typicode.com';
}

Ready! Now import this file in both User and Todo models.

Time to run code generation and get a brand-new Repository<User>:

flutter packages pub run build_runner build

Great. We are now going to request the API, via watchOne(), to embed the related Todo models:

class TodoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final repository = context.watch<Repository<User>>();
    return DataStateBuilder<User>(
      notifier: () => repository.watchOne('1', params: {'_embed': 'todos'}),
      builder: (context, state, notifier, _) {
        return RefreshIndicator(
          onRefresh: () async {
            await notifier.reload();
          },
          child: TodoList(DataState(
            model: state.model?.todos,
            isLoading: state.isLoading,
          )),
        );
      },
    );
  }
}

(With a little DataState massaging we can reuse the TodoList widget 😄)

Yep, relationships between models are automagically linked!

They work even when data comes in at different times: when new models are loaded, relationships are automatically wired up.

You will notice that double-clicking or swiping left don’t work properly.

The reason why?

The notifier is listening for new users, not TO-DOs

In the next episode, we will see how to make relationships reactive.

If we were to add a BelongsTo<User>, this is how our Todo would look like:

import 'package:flutter_data/flutter_data.dart';
import 'package:json_annotation/json_annotation.dart';

import '_adapters.dart';

part 'todo.g.dart';

@JsonSerializable()
@DataRepository([StandardJSONAdapter, JSONPlaceholderAdapter])
class Todo extends DataSupport<Todo> {
  @override
  final int id;
  final String title;
  final bool completed;
  final BelongsTo<User> user;

  Todo({this.id, this.title, this.completed = false, this.user});
}

Great! But remember JSON Placeholder’s format:

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

Since StandardJSONAdapter identifies relationships in the user_id snake-case format (Github API style), we have to override it:

import 'package:flutter_data/flutter_data.dart';

mixin JSONPlaceholderAdapter<T extends DataSupport<T>>
    on StandardJSONAdapter<T> {
  @override
  String get baseUrl => 'http://jsonplaceholder.typicode.com';

  @override
  String get identifierSuffix => 'Id';
}

And it works both ways now!