Tutorial: Atlas Device Sync for C++ with FTXUI
On this page
- Learning Objectives
- Prerequisites
- Start with the Template
- Explore the Template App
- Open the App
- Build the App
- Explore the App Structure
- Run the App
- Check the Backend
- Modify the Application
- Add a New Property
- Add a Property to the Model
- Add an Element to the UI to Set the Priority
- Save the New Property to the Database
- Run and Test
- Change the Subscription
- Update the subscription
- Run and Test
- Conclusion
- What's Next?
Estimated time to complete: 30 minutes, depending on your experience with C++
Atlas Device SDK for C++ enables you to store and sync data across phones,
tablets, wearables, or IoT devices. This tutorial is based on the C++ Template
App, named cpp.todo.flex
, which illustrates the creation of a to-do list
terminal GUI application built with
FTXUI. 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 their own tasks.
View all tasks, even where the user is not the owner.
The template app also provides a toggle that simulates the device being in "Offline Mode." This toggle lets you quickly test Device Sync functionality on the simulator, emulating the user having no internet connection. However, you would likely remove this toggle in a production application.
This tutorial builds on 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.
Learning Objectives
This tutorial illustrates how you might adapt the template app for your own needs. You would not necessarily make this change given the current structure of the template app.
In this tutorial, you will learn how to:
Update a C++ object model with a non-breaking change.
Update a Device Sync subscription.
Add a queryable field to the Device Sync configuration on the server to change which data is synchronized.
Tip
If you prefer to get started with your own application rather than follow a guided tutorial, check out the C++ Quick Start. It includes copyable code examples and the essential information that you need to set up an Atlas App Services backend.
Prerequisites
Ensure that you have the necessary software installed. The C++ Template App assumes you have:
CMake version 3.25 or newer.
C++ 17 or newer.
This tutorial starts with a Template App. You need an Atlas Account, an API key, and App Services 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 App Services CLI.
To learn more about installing App Services CLI, see Install App Services CLI. After installing, run the login command using the API key for your Atlas project.
Start with the Template
This tutorial is based on the C++ Sync Template App named
cpp.todo.flex
. We start with the default app and build new features
on it.
To learn more about the Template Apps, see Template Apps.
If you don't already have an Atlas account, sign-up to deploy a Template App.
Follow the procedure described in the Create an App Services App guide, and select Create App from Template. Select the Real-time Sync template. This creates an App Services App pre-configured to use with one of the Device Sync template app clients.
After you create a template app, the UI displays a modal labeled
Get the Front-end Code for your Template. This modal
provides instructions for downloading the template app client code
as a .zip
file or using App Services CLI to get the client.
The C++ Template App is not yet available to download in the App Services UI. Use the CLI or clone the repository from GitHub to get the client code.
The appservices apps create command sets up the backend and creates a C++ template app for you to use as a base for this tutorial.
Run the following command in a terminal window to create an app
named "MyTutorialApp" that is deployed in the US-VA
region
with its environment set to "development" (instead of production
or QA).
appservices app create \ --name MyTutorialApp \ --template cpp.todo.flex \ --deployment-model global \ --environment development
The command creates a new directory in your current path with the
same name as the value of the --name
flag.
You can fork and clone a GitHub repository that contains the Device Sync client code. The C++ client code is available at https://github.com/mongodb/template-app-cpp-todo.
If you use this process to get the client code, you must create a template app to use with the client. Follow the instructions at Create a Template App to use the Atlas App Services UI, App Services CLI, or Admin API to create a Device Sync template app.
Explore the Template App
Open the App
Open the frontend client code in your preferred IDE.
If you cloned the client from a GitHub repository, you must manually
insert the App Services App ID in the appropriate place in your client.
Follow the Configuration instructions in the client
README.md
to learn where to insert your App ID.
Build the App
Make a directory in which to build the app. For convenience, the
.gitignore
packaged with the template app ignores abuild
directory within the client directory. Navigate into the build directory.mkdir build && cd build Use CMake to create the Makefile. Assuming you're building from a
build
directory within the client directory:cmake ../ Use CMake to build the app executable. This takes a few moments as it installs the dependencies and compiles the executable.
cmake --build .
Explore the App Structure
Take a few minutes to explore how the project is organized while CMake builds the executable.
You won't work directly with these files during this tutorial, but they contain code that demonstrates using the C++ SDK:
File | Purpose |
---|---|
controllers/app_controller.cpp | Use the To learn more about how you can customize your app configuration, see: Connect to an Atlas App Services Backend. This code also sets up the |
managers/auth_manager.cpp | Logic to register an email/password user, log them in or out, and
display an error message when authentication errors occur. |
In this tutorial, you'll work in the following files:
File | Purpose |
---|---|
state/item.hpp | Define the Item object we store in the database. |
state/home_controller_state.hpp | Manage the app state for the Home view. |
controllers/home_controller.hpp | Contains important definitions for the Home view controller. |
controllers/home_controller.cpp | Implements the Home view. This is the view where a logged-in user
can work with the app. |
managers/database_manager.hpp | Contains important definitions for Device Sync and database
operations. |
managers/database_manager.cpp | Implements some Device Sync and database operations, such as
creating items, changing Device Sync query subscriptions, and
handling Sync errors. |
Run the App
Without making any changes to the code, you should be able to run the app
in the terminal. Pass the path to the atlasConfig.json
as an argument
when you run the application:
./sync_todo /path-to-file/atlasConfig.json
Run the app, register a new user account, and then add a new Item to your todo list.
Tip
Expand the terminal window, if needed.
The top of the home screen contains a row of buttons and a toggle to hide completed tasks. If your terminal window is too small, the button text labels don't display. To see the labels, make the terminal window bigger, and FTXUI re-renders the content to fit the larger window.
Check the Backend
Log in to Atlas App Services. In the Data Services 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.
Modify the Application
Add a New Property
Add a Property to the Model
Now that you have confirmed everything is working as expected, you can
add changes. In this tutorial, you add a priority
property to each
Item
so that you can filter the items by their priorities.
In a production app, you might add a PriorityLevel
enum to constrain
the possible values. For this tutorial, we'll use a number property to
simplify working with the UI framework.
To do this, follow these steps:
Open the client code in your preferred IDE.
In the
state/
directory, ppen theitem.hpp
file.Add the following property to the
Item
struct:int64_t priority; Add the new
priority
property to theREALM_SCHEMA()
:REALM_SCHEMA(Item, _id, isComplete, summary, owner_id, priority) The
Item
model should now resemble:namespace realm { struct Item { realm::primary_key<realm::object_id> _id{realm::object_id::generate()}; bool isComplete; std::string summary; std::string owner_id; int64_t priority; }; REALM_SCHEMA(Item, _id, isComplete, summary, owner_id, priority) } // namespace realm
Add an Element to the UI to Set the Priority
In the
state
directory, go tohome_controller_state.hpp
. Add a newint
property under the existingnewTaskIsComplete
property. Then, add astatic const int
to store the default int value for this property.The
HomeControllerState
struct may now resemble:struct HomeControllerState { static const int DEFAULT_TASK_PRIORITY = 3; // Used for creating a new task. std::string newTaskSummary; bool newTaskIsComplete{false}; int newTaskPriority{DEFAULT_TASK_PRIORITY}; ...more code here... }; In the
controllers
directory, go tohome_controller.hpp
. This controller renders the main view of the app.Import the string and vector libraries:
Create an array of string labels for the new priority UI element:
std::vector<std::string> priorityLevelLabels = { "Severe", "High", "Medium", "Low" }; Still in the
controllers
directory, go tohome_controller.cpp
. This is where we add a UI element to allow the user to set the priority for the item. FTXUI offers two UI elements you could use for this functionality:Radiobox
orDropdown
. In this tutorial, we'll useDropdown
, but you might preferRadiobox
if you don't like the way the UI reflows to render the dropdown.Add this new UI element input after the
auto newTaskCompletionStatus
line:auto newTaskPriorityDropdown = ftxui::Dropdown( &priorityLevelLabels, &_homeControllerState.newTaskPriority ); In the
auto saveButton
function closure, pass the task priority selection to the_dbManager.addNew()
function call:_dbManager.addNew( _homeControllerState.newTaskIsComplete, _homeControllerState.newTaskSummary, _homeControllerState.newTaskPriority); And then add a line to reset the priority selection to the default value:
_homeControllerState.newTaskPriority = HomeControllerState::DEFAULT_TASK_PRIORITY; Add the dropdown selector to the
auto newTaskLayout
that sets the layout for the interactive elements in the item row container:auto newTaskLayout = ftxui::Container::Horizontal( {inputNewTaskSummary, newTaskCompletionStatus, newTaskPriorityDropdown, saveButton}); This section of your code should now resemble:
auto newTaskCompletionStatus = ftxui::Checkbox("Complete", &_homeControllerState.newTaskIsComplete); auto newTaskPriorityDropdown = ftxui::Dropdown( &priorityLevelLabels, &_homeControllerState.newTaskPriority); auto saveButton = ftxui::Button("Save", [this] { _dbManager.addNew( _homeControllerState.newTaskIsComplete, _homeControllerState.newTaskSummary, _homeControllerState.newTaskPriority); _homeControllerState.newTaskSummary = ""; _homeControllerState.newTaskIsComplete = false; _homeControllerState.newTaskPriority = HomeControllerState::DEFAULT_TASK_PRIORITY; }); auto newTaskLayout = ftxui::Container::Horizontal( {inputNewTaskSummary, newTaskCompletionStatus, newTaskPriorityDropdown, saveButton}); Finally, farther down in the
home_controller.cpp
file, add the new UI element to theauto itemListRenderer
:inputNewTaskSummary->Render() | ftxui::flex, newTaskCompletionStatus->Render() | ftxui::center, newTaskPriorityDropdown->Render(), saveButton->Render(), This renders the new element right before the Save button in the UI.
Save the New Property to the Database
In the
managers
directory, go todatabase_manager.hpp
. Update theaddNew()
function signature to include theint newItemPriority
we pass in from thehome_controller.cpp
:void addNew( bool newItemIsComplete, std::string newItemSummary, int newItemPriority); Now go to
database_manager.cpp
, and update theaddNew()
implementation. Addint newItemProperty
to the function arguments:void DatabaseManager::addNew( bool newItemIsComplete, std::string newItemSummary, int newItemPriority) { ...implementation... } Add a new line in the function to set the value of the
priority
property when we save theItem
to the database:.priority = newItemPriority Your
addNew()
implementation should now resemble:void DatabaseManager::addNew(bool newItemIsComplete, std::string newItemSummary, int newItemPriority) { auto item = realm::Item { .isComplete = newItemIsComplete, .summary = std::move(newItemSummary), .owner_id = _userId, .priority = newItemPriority }; _database->write([&]{ _database->add(std::move(item)); }); }
Run and Test
At this point, build and run the application again. In your build directory, rebuild the executable with the changes you made:
cmake --build .
And run the app:
./sync_todo /path-to-file/atlasConfig.json
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. The existing Item does not have a priority
field.
Note
Why Didn't This Break Sync?
Adding a property to a SDK client 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 object are automatically reflected in the server-side schema. For more information, see Development Mode and Update Your Data Model.
Change the Subscription
In the database_manager.cpp
file in the managers
directory, we
create the Flexible Sync subscription that defines which documents we
sync with the user's device and account. By default, we subscribe to all
items. You can see items that other people create, but server-side rules
prevent you from writing to them. You can see this logic in the block
where we create the initial subscription. If there are no subscriptions
when the app opens, we add a subscription for all Item
objects:
_database->subscriptions().update([this](realm::mutable_sync_subscription_set& subs) { // By default, we show all items. if (!subs.find(_allItemSubscriptionName)) { subs.add<realm::Item>(_allItemSubscriptionName); } }).get();
In the toggleSubscriptions()
function, we switch the subscription,
depending on the current subscription state. In the UI, the user can
toggle between showing all items, or showing only their own items.
Within this function, find the _myItemSubscriptionName
logic.
If there isn't already a subscription for this subscription name, the app
adds a subscription to all documents where the owner_id
property
matches the authenticated user's id.
For this tutorial, we want to maintain that, but only sync Items that are marked as "High" or "Severe" priority.
This is why we used an int64_t
for the priority
property, and
labeled the priority levels in the UI from most to least important. 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 a number
and the priority property.
Update the subscription
To change the subscription, go to the managers
directory and open the
database_manager.cpp
file. Update the query statement to include
documents where the priority is equal to or less than 1. This should
only include items of "Severe" (0) or "High" (1) priority.
if (!subs.find(_myItemSubscriptionName)) { subs.add<realm::Item>( _myItemSubscriptionName, [&](auto &item){ return item.owner_id == _userId && item.priority <= 1; } ); }
Run and Test
Run the application again. Log in using the account you created earlier
in this tutorial. In the Subscription
box, press the Switch to
Mine
button. After an initial moment when the SDK resyncs the document
collection, you will only see the new Item of High priority that you
created. You may need to move your mouse or use the arrow key to cause
the UI to re-render with the new items that have been synced in the
background.
The Item document you initially created does not show on the device,
because it does not have a priority
field. If you want this Item to
sync to the device, you can edit the document in the Atlas UI and add a
value for the priority
field.
Tip
Changing Subscriptions with Developer Mode Enabled
In this tutorial, when you change the subscription and query on the priority field for the first time, the field is automatically added to the Device Sync Collection Queryable Fields. This occurs because the template app has Development Mode enabled by default. If Development Mode was not enabled, you would have to manually add the field as a queryable field to use it in a client-side Sync query.
For more information, refer to Queryable Fields.
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. The Sync error handler helpfully provides a message describing this behavior:
A sync error occurred. Message: "Client attempted a write that is not allowed; it has been reverted"
In this scenario, the SDK creates the Item locally, syncs it with the backend, and then reverts the write because it doesn't meet the subscription rules.
Note
Known UI Issue
If the error modal displays, and you move your mouse over the item
list in the terminal prior to dismissing the error modal, the UI
rendering breaks. This is related to limitations with the FTXUI
library. If this occurs, quit the app using ctrl + c
, and re-run
it. You can avoid this issue by using the enter key to press the
Dismiss
button in the error modal before moving the mouse.
Conclusion
Adding a property to an existing SDK object is a non-breaking change, and Development Mode ensures that the schema change is reflected server-side.
What's Next?
Read our C++ SDK documentation.
Find developer-oriented blog posts and integration tutorials on the MongoDB Developer Hub.
Join the MongoDB Community forum to learn from other MongoDB developers and technical experts.
Explore engineering and expert-provided example projects.
Note
Share Feedback
How did it go? Use the Rate this page widget at the bottom right of the page to rate its effectiveness. Or file an issue on the GitHub repository if you had any issues.