Docs Menu

Docs HomeAtlas App Services

Flutter Tutorial

On this page

  • Prerequisites
  • Start with the Template
  • Set up the Template App
  • Open the App
  • Explore the App Structure
  • Run the App
  • Check the Backend
  • Modify the Application
  • Add a New Property
  • Set the Priority when Creating and Updating Items
  • Run and Test
  • Update the subscription
  • Run and Test
  • Update Flexible Sync on the Server
  • Test the changes
  • Conclusion
  • What's Next?

The Realm Flutter SDK allows you to create a multi-platform applications with Dart and Flutter. This tutorial is based on the Flutter Flexible Sync Template App, named flutter.todo.flex, which illustrates the creation of a Todo application. This application enables users to:

  • Register their email as a new user account.

  • Sign in to their account with their email and password (and sign out later).

  • View, create, modify, and delete task items.

This tutorial adds on to the Template App. You will add a new priority field to the existing Item model and update the Flexible Sync subscription to only show items within a range of priorities.

Depending on your experience with Flutter, this tutorial should take around 30 minutes.

If you prefer to get started with your own application rather than follow a guided tutorial, check out the Flutter Quick Start. It includes copyable code examples and the essential information that you need to set up a Realm Flutter SDK application.

Note

Supported Platforms

You can build this tutorial app on the following platforms:

  • iOS

  • Android

  • macOS

  • Windows

  • Linux

The Realm Flutter SDK does not support building web applications.

  • You should have previous experience deploying a Flutter app to an Android Emulator, iOS Simulator, and/or a physical device.

  • This tutorial starts with a Template App. You need an Atlas Account, an API key, and realm-cli to create a Template App.

    • You can learn more about creating an Atlas account in the Atlas Getting Started documentation. For this tutorial, you need an Atlas account with a free-tier cluster.

    • You also need an Atlas API key for the MongoDB Cloud account you wish to log in with. You must be a Project Owner to create a Template App using realm-cli.

    • To learn more about installing realm-cli, see Install realm-cli. After you have installed realm-cli, login using the API key for your Atlas project.

This tutorial is based on the Flutter Flexible Sync Template App named "flutter.todo.flex". We start with the default app and build new features on it.

If you already have the latest Realm CLI installed, you can run the realm-cli apps create command to set up the backend and create the Flutter base app. The following command creates a new app based on the flutter.todo.flex template. The app is created in a development environment, is named "MyTutorialApp", and is deployed in the US-VA region:

realm-cli apps create -n MyTutorialApp --template flutter.todo.flex \
--deployment-model global --location us-va --environment development

To learn more about the Template Apps, and to install the Template App that this tutorial uses in the Atlas App Services UI, see Template Apps.

1

Navigate to the MyTutorialApp/frontend/flutter.todo.flex directory, which contains the template Realm Flutter app. Open the Flutter app with your code editor.

2

In your code editor, take a few minutes to explore how the project is organized. This is a standard multi-platform Flutter application that has been modified for our specific use. Specifically, the following files contain important uses of the Realm Flutter SDK:

File
Purpose
lib/main.dart
Entry point into the app. Contains routing and state management.
lib/realm/schemas.dart
Defines Realm Database schema.
lib/realm/schemas.g.dart
Generated Realm object class.
lib/realm/app_services.dart
Handles interaction with Atlas App Services.
lib/realm/init_realm.dart
Initializes the realm and adds a subscription to sync data with MongoDB Atlas using Device Sync.
lib/viewmodels/todo_viewmodel.dart
View model class that controls interaction between UI and Realm.
lib/components/
Component parts of app featuring Flutter widgets.
lib/screens/
Pages of the app.
3

Without making any changes to the code, you should be able to run the app in either the Android emulator, iOS Simulator, physical mobile device, or desktop emulator. When you installed the template app, the Realm CLI also set up a new backend for you and populated the realm.json file with the correct App ID.

Attach to a device and run the Flutter application.

Once the app is running, register a new user account, and then add a new Item to your todo list.

Tip

For more information on running a Flutter app with development tools, refer to the Flutter Test Drive documentation.

4

Log in to Atlas App Services. In the Atlas tab, click on Browse Collections. In the list of databases, find and expand the todo database, and then the Item collection. You should see the document you created in this collection.

1

Now that you have confirmed everything is working as expected, we can add changes. In this tutorial, we have decided that we want to add a "priority" property to each Item so that we can filter Items by their priorities.

To do this, follow these steps:

  1. In the flutter.todo.flex project, open the file lib/realm/schemas.dart.

  2. Add the following property to the _Item class:

    lib/realm/schemas.dart
    late int? priority;
  3. Regenerate the Item Realm object class:

    flutter pub run realm generate
2
  1. In lib/viewmodels/item_viewmodel.dart, add logic to set and update priority. Also add a PriorityLevel abstract class below the ItemViewModel class to constrain the possible values.

    lib/viewmodels/item_viewmodel.dart
    import 'package:realm/realm.dart';
    import 'package:flutter_todo/realm/schemas.dart';
    class ItemViewModel {
    final ObjectId id;
    String summary;
    bool isComplete;
    int priority;
    final String ownerId;
    late Item item;
    final Realm realm;
    ItemViewModel._(this.realm, this.item, this.id, this.summary, this.ownerId,
    this.isComplete, this.priority);
    ItemViewModel(Realm realm, Item item)
    : this._(realm, item, item.id, item.summary, item.ownerId,
    item.isComplete, item.priority ?? PriorityLevel.low);
    static ItemViewModel create(Realm realm, Item item) {
    final itemInRealm = realm.write<Item>(() => realm.add<Item>(item));
    return ItemViewModel(realm, item);
    }
    void delete() {
    realm.write(() => realm.delete(item));
    }
    void update({String? summary, bool? isComplete, int? priority}) {
    realm.write(() {
    if (summary != null) {
    this.summary = summary;
    item.summary = summary;
    }
    if (isComplete != null) {
    this.isComplete = isComplete;
    item.isComplete = isComplete;
    }
    if (priority != null) {
    this.priority = priority;
    item.priority = priority;
    }
    });
    }
    }
    abstract class PriorityLevel {
    static int severe = 0;
    static int high = 1;
    static int medium = 2;
    static int low = 3;
    }
  2. Add a new file to contain a widget to set the priority for an Item. Create the file lib/components/select_priority.dart.

    lib/components/select_priority.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_todo/viewmodels/item_viewmodel.dart';
    class SelectPriority extends StatefulWidget {
    int priority;
    void Function(int priority) setFormPriority;
    SelectPriority(this.priority, this.setFormPriority, {Key? key})
    : super(key: key);
    @override
    State<SelectPriority> createState() => _SelectPriorityState();
    }
    class _SelectPriorityState extends State<SelectPriority> {
    @override
    Widget build(BuildContext context) {
    return Padding(
    padding: const EdgeInsets.only(top: 15),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text('Priority'),
    DropdownButtonFormField<int>(
    onChanged: ((int? value) {
    final valueOrDefault = value ?? PriorityLevel.low;
    widget.setFormPriority(valueOrDefault);
    setState(() {
    widget.priority = valueOrDefault;
    });
    }),
    value: widget.priority,
    items: [
    DropdownMenuItem(
    child: const Text("Low"), value: PriorityLevel.low),
    DropdownMenuItem(
    child: const Text("Medium"), value: PriorityLevel.medium),
    DropdownMenuItem(
    child: const Text("High"), value: PriorityLevel.high),
    DropdownMenuItem(
    child: const Text("Severe"), value: PriorityLevel.severe),
    ],
    ),
    ],
    ),
    );
    }
    }
  3. Now add the SelectPriority widget to the CreateItem and ModifyItem widgets. You also need to add some additional logic to handle setting the priority. The code you must add is shown below.

    Some sections of the files you are adding to are replaced with comments in the below code examples to focus on the relevant sections of code that are changed.

    Edit the CreateItemForm widget in lib/components/create_item.dart:

    lib/components/create_item.dart
    // ... other imports
    import 'package:flutter_todo/viewmodels/item_viewmodel.dart';
    import 'package:flutter_todo/components/select_priority.dart';
    // ... CreateItem widget
    // _CreateItemFormWrapper widget
    class CreateItemForm extends StatefulWidget {
    const CreateItemForm({Key? key}) : super(key: key);
    @override
    _CreateItemFormState createState() => _CreateItemFormState();
    }
    class _CreateItemFormState extends State<CreateItemForm> {
    int _priority = PriorityLevel.low;
    final _formKey = GlobalKey<FormState>();
    var itemEditingController = TextEditingController();
    void _setPriority(int priority) {
    setState(() {
    _priority = priority;
    });
    }
    @override
    Widget build(BuildContext context) {
    TextTheme myTextTheme = Theme.of(context).textTheme;
    final currentUser = Provider.of<AppServices>(context).currentUser;
    return Form(
    key: _formKey,
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
    // ... Text and TextFormField widgets
    SelectPriority(_priority, _setPriority),
    // .. other widgets
    // Set priority when creating an Item
    Container(
    margin: const EdgeInsets.symmetric(horizontal: 10),
    child: Consumer<Realm>(
    builder: (context, realm, child) {
    return ElevatedButton(
    child: const Text('Create'),
    onPressed: () {
    if (_formKey.currentState!.validate()) {
    final summary = itemEditingController.text;
    ItemViewModel.create(
    realm,
    Item(ObjectId(), summary, currentUser!.id,
    priority: _priority));
    Navigator.pop(context);
    }
    },
    );
    },
    ),
    ),
    // ...closing brackets and parentheses

    Edit the ModifyItemForm widget in lib/components/modify_item.dart:

    lib/components/modify_item.dart
    // ... other imports
    import 'package:flutter_todo/components/select_priority.dart';
    // showModifyItemModal function
    class ModifyItemForm extends StatefulWidget {
    final ItemViewModel item;
    const ModifyItemForm(this.item, {Key? key}) : super(key: key);
    @override
    _ModifyItemFormState createState() => _ModifyItemFormState();
    }
    class _ModifyItemFormState extends State<ModifyItemForm> {
    final _formKey = GlobalKey<FormState>();
    late bool _isComplete;
    late String _summary;
    late int _priority;
    @override
    void initState() {
    super.initState();
    _summary = widget.item.summary;
    _isComplete = widget.item.isComplete;
    _priority = widget.item.priority;
    }
    @override
    Widget build(BuildContext context) {
    TextTheme myTextTheme = Theme.of(context).textTheme;
    final item = widget.item;
    void updateItem() {
    item.update(
    summary: _summary, isComplete: _isComplete, priority: _priority);
    }
    // deleteItem and handleItemRadioChange functions
    void _setPriority(int priority) {
    setState(() {
    _priority = priority;
    });
    }
    return Padding(
    padding:
    EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
    child: Container(
    color: Colors.grey.shade100,
    padding: const EdgeInsets.only(
    top: 25,
    bottom: 25,
    left: 30,
    right: 30,
    ),
    child: Center(
    child: Form(
    key: _formKey,
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
    // ... Text and TextFormField widgets
    SelectPriority(_priority, _setPriority),
    // ... other widgets
    ],
    ),
    ),
    ),
    ),
    );
    }
    }
  4. Now add a visual indicator for priority in the ItemCard widget in lib/components/item_card.dart. Create a new widget _PriorityIndicator that gives a visual indicator of the Item's priority.

    Add a _PriorityIndicator widget you just created to the ItemCard widget.

    lib/components/item_card.dart
    // ...imports
    class ItemCard extends StatelessWidget {
    final ItemViewModel viewModel;
    final Animation<double> animation;
    const ItemCard(this.viewModel, this.animation, {Key? key}) : super(key: key);
    @override
    Widget build(BuildContext context) {
    final realm = Provider.of<Realm>(context);
    void deleteItem() {
    viewModel.delete();
    }
    return FadeTransition(
    key: key ?? ObjectKey(viewModel),
    opacity: animation,
    child: SizeTransition(
    sizeFactor: animation,
    child: AnimatedSwitcher(
    duration: const Duration(milliseconds: 300),
    child: Slidable(
    // endActionPane property and children widgets
    child: Card(
    child: ListTile(
    title: Row(
    children: [
    Padding(
    padding: const EdgeInsets.only(right: 8.0),
    child: _PriorityIndicator(viewModel.priority),
    ),
    SizedBox(width: 175, child: Text(viewModel.summary)),
    ],
    ),
    subtitle:
    Text(viewModel.isComplete ? 'Completed' : 'Incomplete'),
    leading: _CompleteCheckbox(viewModel),
    ),
    ),
    ),
    ),
    ),
    );
    }
    }
    // _CompleteCheckbox widget
    class _PriorityIndicator extends StatelessWidget {
    final int? priority;
    const _PriorityIndicator(this.priority, {Key? key}) : super(key: key);
    Widget getIconForPriority(int? priority) {
    if (priority == PriorityLevel.low) {
    return const Icon(Icons.keyboard_arrow_down, color: Colors.blue);
    } else if (priority == PriorityLevel.medium) {
    return const Icon(Icons.circle, color: Colors.grey);
    } else if (priority == PriorityLevel.high) {
    return const Icon(Icons.keyboard_arrow_up, color: Colors.orange);
    } else if (priority == PriorityLevel.severe) {
    return const Icon(
    Icons.block,
    color: Colors.red,
    );
    } else {
    return const SizedBox.shrink();
    }
    }
    @override
    Widget build(BuildContext context) {
    return getIconForPriority(priority);
    }
    }
3

Before you run the application again, perform a hot restart. This makes sure that the sync session restarts with the new schema and prevents sync errors.

Then, Log in using the account you created earlier in this tutorial. You will see the one Item you previously created. Add a new Item, and you will see that you can now set the priority. Choose High for the priority and save the Item.

Now switch back to the Atlas data page in your browser, and refresh the Item collection. You should now see the new Item with the priority field added and set to 1. You will also notice that the existing Item now also has a priority field, and it is set to null, as shown in the following screenshot:

Two items in a collection

Note

Why Didn't This Break Sync?

Adding a property to a Realm object is not a breaking change and therefore does not require a client reset. The template app has Development Mode enabled, so changes to the client Realm object are reflected in the server-side schema. For more information, see Development Mode and Update Your Data Model.

Now that we added the priority field, we want to update the Device Sync subscription to only sync Items marked as a High or Severe priority.

1

In the lib/realm/init_realm.dart file, we define the Flexible Sync subscription that defines which documents we sync with the user's device and account. Currently, we are syncing all all documents where the owner property matches the authenticated user.

The current subscription:

lib/realm/init_realm.dart
final userItemSub = realm.subscriptions.findByName('getUserItems');
if (userItemSub == null) {
realm.subscriptions.update((mutableSubscriptions) {
// server-side rules ensure user only downloads own items
mutableSubscriptions.add(realm.all<Item>(), name: 'getUserItems');
});
}

Now we're going to change the subscription to only sync High and Severe priority Items. As you may recall, the priority field is of type int, where the highest priority ("Severe") has a value of 0, and the lowest priority ("Low") has a value of 3. We can make direct comparisons between an int and the priority property. To do so, we're going to refactor the subscription query to include Items where the priority is less than or equal to PriorityLevel.high (or 1). We will also give the subscription the new name "getUserItemsWithHighPriority".

First, import lib/viewmodels/item_viewmodel.dart:

lib/realm/init_realm.dart
import 'package:flutter_todo/viewmodels/item_viewmodel.dart';

Update the subscription to delete the old subscription and add a new one that uses priority:

lib/realm/init_realm.dart
final userItemSub = realm.subscriptions.findByName('getUserItems');
final userItemSubWithPriority =
realm.subscriptions.findByName('getUserItemsWithPriority');
if (userItemSubWithPriority == null) {
realm.subscriptions.update((mutableSubscriptions) {
if (userItemSub != null) {
mutableSubscriptions.remove(userItemSub);
}
// server-side rules ensure user only downloads own items
mutableSubscriptions.add(
realm.query<Item>(
'priority <= \$0',
[PriorityLevel.high],
),
name: 'getUserItemsWithPriority');
});
}
2

Run the application again. Log in using the account you created earlier in this tutorial. You would expect to not see the first Item created, since doesn't have a priority. But it is in the list. If you check your application logs in the terminal, you will see an entry that looks something like this:

"Client provided query with bad syntax: unsupported query for table "Item":
key "priority" is not a queryable field" (error_code=300, query_version=1)

This message tells us that we have added a field to our subscription without configuring Flexible Sync to use that field.

3
  1. Switch back to the Atlas page in your browser. Select the Atlas App Services tab and open the app you are using.

  2. In the left-hand navigation, choose Device Sync, and then click OK in the dialog box about Development Mode being enabled.

  3. Scroll down to the Select Queryable Fields section. In the dropdown labeled Select or create a queryable field, choose "priority". The priority field will be added to the fields shown:

    Priority field is now queryable.
  4. Save your changes.

4

Return to your Flutter app. To ensure the subscription is re-run, log out the current user, and then log in again.

After an initial moment when Realm resyncs the document collection, you might see an error message resembling the following:

The following RangeError was thrown building StreamBuilder<RealmResultsChanges<Item>>(dirty, state:
_StreamBuilderBaseState<RealmResultsChanges<Item>, AsyncSnapshot<RealmResultsChanges<Item>>>#387c4):
RangeError (index): Invalid value: Only valid value is 0: 3

This error can occur with the StreamBuilder widget as the subscription updates. In a production app, you could add error handling. But for the sake of this tutorial, just perform a hot refresh and the error will go away.

Now you should see the new Item of High priority that you created.

If you want to further test the functionality, you can create Items of various priorities. You will see that a new Item with a lower priority briefly appears in the list of Items and then disappears. Realm creates the Item locally, syncs it with the backend, and then filters out the Item because it doesn't meet the subscription rules.

You'll note, too, that the document you initially created is not synced, because it has a priority of null. If you want this Item to be synced, you can edit the document in the Atlas UI and add a value for the priority field, or you can change your subscription to include documents with null values. You must use the Realm Query Language nil type to represent the null value in your query. We will also give the subscription the new name "getUserItemsWithHighOrNoPriority".

lib/realm/init_realm.dart
// old subscriptions
final userItemSub = realm.subscriptions.findByName('getUserItems');
final userItemSubWithPriority =
realm.subscriptions.findByName('getUserItemsWithPriority');
final userItemSubWithPriorityOrNothing =
realm.subscriptions.findByName('getUserItemsWithPriorityOrNothing');
if (userItemSubWithPriorityOrNothing == null) {
realm.subscriptions.update((mutableSubscriptions) {
if (userItemSub != null) {
mutableSubscriptions.remove(userItemSub);
}
if (userItemSubWithPriority != null) {
mutableSubscriptions.remove(userItemSubWithPriority);
}
// server-side rules ensure user only downloads own items
mutableSubscriptions.add(
realm.query<Item>(
'priority <= \$0 OR priority == nil',
[PriorityLevel.high],
),
name: 'getUserItemsWithPriorityOrNothing');
});
// Syncs in background
// realm.subscriptions.waitForSynchronization();
}

Again, when a StreamBuilder error occurs the first time you open the app with the new subscription, perform a hot refresh to see the expected data.

Adding a property to an existing Realm object is a non-breaking change, and Development Mode ensures that the schema change is reflected server-side. If you add or change a subscription to use an additional field, whether newly added or previously existent, you need to modify the Flexible Sync settings to enable querying against that field.

Note

Give Feedback

How did it go? Use the Give Feedback tab at the bottom right of the page to let us know if this tutorial was helpful or if you had any issues.

←  React Native TutorialWrite a Serverless GitHub Contribution Tracker →
Give Feedback
© 2022 MongoDB, Inc.

About

  • Careers
  • Investor Relations
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2022 MongoDB, Inc.