Оглавление
- 1 Next steps
- 2 Use networking
- 3 User authentication
- 4 Summary
- 5 Write data offline
- 6 Saving Lists of Data
- 7 Store data
- 8 Updating Saved Data
- 9 Updating or deleting data
- 10 Write, update, or delete data at a reference
- 11 Updating Data with Conditional Requests
- 12 Write data offline
- 13 Security in a virtual machine
- 14 Writing Data with PUT
- 15 Save data as transactions
- 16 Implementation tips and best practices
- 17 Save data as transactions
Next steps
After a user signs in for the first time, a new user account is created and
linked to the credentials—that is, the user name and password, phone
number, or auth provider information—the user signed in with. This new
account is stored as part of your Firebase project, and can be used to identify
a user across every app in your project, regardless of how the user signs in.
-
In your apps, you can get the user's basic profile information from the
object. See
Manage Users. -
In your Firebase Realtime Database and Cloud Storage
Security Rules, you can
get the signed-in user's unique user ID from the variable,
and use it to control what data a user can access.
You can allow users to sign in to your app using multiple authentication
providers by linking auth provider credentials to an
existing user account.
To sign out a user, call :
Use networking
Network transactions are inherently risky for security, because they involve transmitting
data that is potentially private to the user. People are increasingly aware of the privacy
concerns of a mobile device, especially when the device performs network transactions,
so it's very important that your app implement all best practices toward keeping the user's
data secure at all times.
Use IP networking
Networking on Android is not significantly different from other Linux
environments. The key consideration is making sure that appropriate protocols
are used for sensitive data, such as for
secure web traffic. You should use HTTPS over HTTP anywhere that HTTPS is
supported on the server, because mobile devices frequently connect on networks
that are not secured, such as public Wi-Fi hotspots.
Authenticated, encrypted socket-level communication can be easily
implemented using the
class. Given the frequency with which Android devices connect to unsecured
wireless networks using Wi-Fi, the use of secure networking is strongly
encouraged for all applications that communicate over the network.
Some applications use localhost network ports for
handling sensitive IPC. You should not use this approach because these interfaces are
accessible by other applications on the device. Instead, use an Android IPC
mechanism where authentication is possible, such as with a .
Binding to INADDR_ANY is worse than using loopback because then your application
may receive requests from anywhere.
Make sure that you don't
trust data downloaded from HTTP or other insecure protocols. This includes
validation of input in and
any responses to intents issued against HTTP.
Use telephony networking
The SMS protocol was primarily designed for
user-to-user communication and is not well-suited for apps that want to transfer data.
Due to the limitations of SMS, you should use Google Cloud Messaging (GCM)
and IP networking for sending data messages from a web server to your app on a user device.
Beware that SMS is neither encrypted nor strongly
authenticated on either the network or the device. In particular, any SMS receiver
should expect that a malicious user may have sent the SMS to your application. Don't
rely on unauthenticated SMS data to perform sensitive commands.
Also, you should be aware that SMS may be subject to spoofing and/or
interception on the network. On the Android-powered device itself, SMS
messages are transmitted as broadcast intents, so they may be read or captured
by other applications that have the
permission.
User authentication
When an authenticated user performs a request against Cloud Storage,
the variable is populated with the user's
() as well as the claims of the Firebase Authentication JWT
().
Additionally, when using custom authentication, additional claims are surfaced
in the field.
When an unauthenticated user performs a request, the variable is
.
Using this data, there are several common ways to use authentication to secure
files:
- Public: ignore
- Authenticated private: check that is not
- User private: check that equals a path
- Group private: check the custom token's claims to match a chosen claim, or
read the file metadata to see if a metadata field exists
Public
Any rule that doesn't consider the context can be considered a
rule, since it doesn't consider the authentication context of the user.
These rules can be useful for surfacing public data such as game assets, sound
files, or other static content.
// Anyone to read a public image if the file is less than 100kB // Anyone can upload a public file ending in '.txt' match /public/{imageId} { allow read: if resource.size
Authenticated private
In certain cases, you may want data to be viewable by all authenticated users of
your application, but not by unauthenticated users. Since the
variable is for all unauthenticated users, all you have to do is check
the variable exists in order to require authentication:
// Require authentication on all internal image reads match /internal/{imageId} { allow read: if request.auth != null; }
User private
By far the most common use case for will be to provide individual
users with granular permissions on their files: from uploading profile pictures
to reading private documents.
Since files in Cloud Storage have a full path to the file, all it takes
to make a file controlled by a user is a piece of unique, user identifying
information in the file path (such as the user's ) that can be checked when
the rule is evaluated:
// Only a user can upload their profile picture, but anyone can view it match /users/{userId}/profilePicture.png { allow read; allow write: if request.auth.uid == userId; }
Group private
Another equally common use case will be to allow group permissions on an object,
such as allowing several team members to collaborate on a shared document. There
are several approaches to doing this:
- Mint a Firebase Authentication custom token
that contains additional information about a group member (such as a group ID) - Include group information (such as a group ID or list of authorized s) in
the file metadata
Once this data is stored in the token or file metadata, it can be referenced
from within a rule:
// Allow reads if the group ID in your token matches the file metadata's `owner` property // Allow writes if the group ID is in the user's custom token match /files/{groupId}/{fileName} { allow read: if resource.metadata.owner == request.auth.token.groupId; allow write: if request.auth.token.groupId == groupId; }
Summary
The header does not have much nuance; it is either on or off, and
the application bears the burden of providing appropriate experiences based on
its setting, regardless of the reason.
For example, some users might not allow data saving mode if they suspect there
will be a loss of app content or function, even in a poor connectivity
situation. Conversely, some users might enable it as a matter of course to keep
pages as small and simple as possible, even in a good connectivity situation.
It's best for your app to assume that the user wants the full and unlimited
experience until you have a clear indication otherwise via an explicit user
action.
As site owners and web developers, let's take on the responsibility of managing
our content to improve the user experience for data- and cost-constrained users.
For more detail on and excellent practical examples, see Help Your
Users .
Write data offline
If a client loses its network connection, your app will continue functioning
correctly.
Every client connected to a Firebase database maintains its own internal version
of any active data. When data is written, it's written to this local version
first. The Firebase client then synchronizes that data with the remote database
servers and with other clients on a «best-effort» basis.
As a result, all writes to the database trigger local events immediately, before
any data is written to the server. This means your app remains
responsive regardless of network latency or connectivity.
Once connectivity is reestablished, your app receives the appropriate set of
events so that the client syncs with the current server state, without having to
write any custom code.
Saving Lists of Data
When creating lists of data, it is important to keep in mind the multi-user nature of most applications and
adjust your list structure accordingly. Expanding on the example above, let's add blog posts to your app. Your
first instinct might be to use set to store children with auto-incrementing integer indexes, like
the following:
// NOT RECOMMENDED - use push() instead! { "posts": { "0": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "1": { "author": "alanisawesome", "title": "The Turing Machine" } } }
If a user adds a new post it would be stored as . This would work if only a single author
were adding posts, but in your collaborative blogging application many users may add posts at the same time. If
two authors write to simultaneously, then one of the posts would be deleted by the other.
To solve this, the Firebase clients provide a function that generates a
unique key for each new child. By using unique child keys, several clients can
add children to the same location at the same time without worrying about write conflicts.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
Because Firebase generates a unique key for each blog post, no write conflicts will occur if multiple users
add a post at the same time. Your database data now looks like this:
{ "posts": { "-JRHTHaIs-jNPLXOQivY": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "-JRHTHaKuITFIhnj02kE": { "author": "alanisawesome", "title": "The Turing Machine" } } }
In JavaScript, Python and Go, the pattern of calling and then immediately calling is
so common that the Firebase SDK lets you combine them by passing the data to be set directly to as follows:
Getting the unique key generated by push ()
Calling will return a reference to the new data path, which you can use to get the key or set data to it. The following code will result in the same data as the above example, but now we'll have access to the unique key that was generated:
As you can see, you can get the value of the unique key from your reference.
In the next section on Retrieving Data, we'll learn how to read this data from a Firebase database.
Store data
The most common security concern for an application on Android is whether the data
that you save on the device is accessible to other apps. There are three fundamental
ways to save data on the device:
- Internal storage.
- External storage.
- Content providers.
Use internal storage
By default, files that you create on are accessible only to your app.
Android implements this protection, and it's sufficient for most applications.
Generally, avoid the or
modes for
IPC files because they do not provide
the ability to limit data access to particular applications, nor do they
provide any control of data format. If you want to share your data with other
app processes, instead consider using a
content provider, which
offers read and write permissions to other apps and can make
dynamic permission grants on a case-by-case basis.
To provide additional protection for sensitive data, you can
encrypt local files using the Security
library. This measure can provide protection for a lost device without file system
encryption.
Use external storage
Files created on , such as SD cards, are globally readable and writable. Because
external storage can be removed by the user and also modified by any
application, don't store sensitive information using
external storage.
To read and write files on external storage in a more secure way, consider
using the Security library, which provides
the
class.
You should when handling
data from external storage as you would with data from any untrusted source.
You should not store executables or
class files on external storage prior to dynamic loading. If your app
does retrieve executable files from external storage, the files should be signed and
cryptographically verified prior to dynamic loading.
Use content providers
Content providers
offer a structured storage mechanism that can be limited
to your own application or exported to allow access by other applications.
If you do not intend to provide other
applications with access to your , mark them as in the application manifest. Otherwise, set the
attribute to to allow other apps to access the stored data.
When creating a
that is exported for use by other applications, you can specify a single
for reading and writing, or you can specify distinct permissions for reading and writing.
You should limit your permissions to those
required to accomplish the task at hand. Keep in mind that it’s usually
easier to add permissions later to expose new functionality than it is to take
them away and impact existing users.
If you are using a content provider
for sharing data between only your own apps, it is preferable to use the
attribute set to protection.
Signature permissions do not require user confirmation,
so they provide a better user experience and more controlled access to the
content provider data when the apps accessing the data are
signed with
the same key.
Content providers can also provide more granular access by declaring the attribute and using the and flags in the
object
that activates the component. The scope of these permissions can be further
limited by the element.
When accessing a content provider, use parameterized query methods such as
,
, and
to avoid
potential SQL injection from untrusted sources. Note that using parameterized methods is not
sufficient if the argument is built by concatenating user data
prior to submitting it to the method.
Don't have a false sense of security about the write permission.
The write permission allows SQL statements that make it possible for some
data to be confirmed using creative clauses and parsing the
results. For example, an attacker might probe for the presence of a specific phone
number in a call log by modifying a row only if that phone number already
exists. If the content provider data has predictable structure, the write
permission may be equivalent to providing both reading and writing.
Updating Saved Data
If you want to write to multiple children of a database location at the same time without overwriting other
child nodes, you can use the update method as shown below:
This will update Grace's data to include her nickname. If you had used set here instead of update,
it would have deleted both and from your .
The Firebase Realtime Database also supports multi-path updates. This means that update can now update
values at multiple locations in your database at the same time, a powerful feature which allows
helps you denormalize
your data. Using multi-path updates, you can add nicknames to both Grace and Alan at the same
time:
After this update, both Alan and Grace have had their nicknames added:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing", "nickname": "Alan The Machine" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper", "nickname": "Amazing Grace" } } }
Note that trying to update objects by writing objects with the paths included will result in different behavior. Let's take a look at what happens if you instead try to update Grace and Alan this way:
This results in different behavior, namely overwriting the entire node:
{ "users": { "alanisawesome": { "nickname": "Alan The Machine" }, "gracehop": { "nickname": "Amazing Grace" } } }
Adding a Completion Callback
In Node.js and Java Admin SDKs,
if you'd like to know when your data has been committed, you can add a completion callback.
Both set and update methods in these SDKs take an optional completion callback that is called
when the write has been committed to the database. If the call was unsuccessful for some
reason, the callback is passed an error object indicating why the failure occurred.
In Python and Go Admin SDKs, all write methods are blocking. That is, the write methods do not return
until the writes are committed to the database.
Updating or deleting data
Update specific fields
To simultaneously write to specific children of a node without overwriting other
child nodes, use the method.
When calling , you can update lower-level child values by
specifying a path for the key. If data is stored in multiple locations to scale
better, you can update all instances of that data using
.
For example, a social blogging app might create a post and simultaneously update
it to the recent activity feed and the posting user's activity feed using
code like this:
function writeNewPost(uid, username, picture, title, body) { // A post entry. var postData = { author: username, uid: uid, body: body, title: title, starCount: 0, authorPic: picture }; // Get a key for a new Post. var newPostKey = firebase.database().ref().child('posts').push().key; // Write the new post's data simultaneously in the posts list and the user's post list. var updates = {}; updates['/posts/' + newPostKey] = postData; updates['/user-posts/' + uid + '/' + newPostKey] = postData; return firebase.database().ref().update(updates); }
This example uses to create a post in the node containing posts for
all users at and simultaneously retrieve the key. The key can
then be used to create a second entry in the user's
posts at .
Using these paths, you can perform simultaneous updates to multiple locations in
the JSON tree with a single call to , such as how this example
creates the new post in both locations. Simultaneous updates made this way
are atomic: either all updates succeed or all updates fail.
Add a Completion Callback
If you want to know when your data has been committed, you can add a
completion callback. Both and take an optional completion
callback that is called when the write has been committed to the database. If
the call was unsuccessful, the callback is passed an
error object indicating why the failure occurred.
firebase.database().ref('users/' + userId).set({ username: name, email: email, profile_picture : imageUrl }, function(error) { if (error) { // The write failed... } else { // Data saved successfully! } }); }
Delete data
The simplest way to delete data is to call on a reference to the
location of that data.
You can also delete by specifying as the value for another write
operation such as or . You can use this technique
with to delete multiple children in a single API call.
Receive a
To know when your data is committed to the Firebase Realtime Database server, you
can use a
.
Both and can return a you can use to know when the
write is committed to the database.
Write, update, or delete data at a reference
Basic write operations
For basic write operations, you can use to save data to a
specified reference, replacing any existing data at that path. You can use this
method to pass types that correspond to the available JSON types as follows:
If you use a typed C# object, you can use the built in
to convert the object to raw Json and call .
For example, you may have a User class that looked as follows:
public class User { public string username; public string email; public User() { } public User(string username, string email) { this.username = username; this.email = email; } }
You can add a user with as follows:
private void writeNewUser(string userId, string name, string email) { User user = new User(name, email); string json = JsonUtility.ToJson(user); mDatabaseRef.Child("users").Child(userId).SetRawJsonValueAsync(json); }
Using or in this way overwrites data
at the specified location, including any child nodes. However, you can still
update a child without rewriting the entire object. If you want to allow users
to update their profiles you could update the username as follows:
Append to a list of data
Use the method to append data to a list in multiuser applications.
The method generates a unique key every time a new
child is added to the specified Firebase reference. By using these
auto-generated keys for each new element in the list, several clients can
add children to the same location at the same time without write conflicts. The
unique key generated by is based on a timestamp, so list items are
automatically ordered chronologically.
You can use the reference to the new data returned by the method to get
the value of the child's auto-generated key or set data for the child. Calling
on a reference returns the value of the
auto-generated key.
Update specific fields
To simultaneously write to specific children of a node without overwriting other
child nodes, use the method.
When calling , you can update lower-level child values by
specifying a path for the key. If data is stored in multiple locations to scale
better, you can update all instances of that data using
. For example, a
game might have a class like this:
public class LeaderboardEntry { public string uid; public int score = 0; public LeaderboardEntry() { } public LeaderboardEntry(string uid, int score) { this.uid = uid; this.score = score; } public Dictionary<string, Object> ToDictionary() { Dictionary<string, Object> result = new Dictionary<string, Object>(); result = uid; result = score; return result; } }
To create a LeaderboardEntry and simultaneously update it to the recent score
feed and the user's own score list, the game uses code like this:
private void WriteNewScore(string userId, int score) { // Create new entry at /user-scores/$userid/$scoreid and at // /leaderboard/$scoreid simultaneously string key = mDatabase.Child("scores").Push().Key; LeaderBoardEntry entry = new LeaderBoardEntry(userId, score); Dictionary<string, Object> entryValues = entry.ToDictionary(); Dictionary<string, Object> childUpdates = new Dictionary<string, Object>(); childUpdates["/scores/" + key] = entryValues; childUpdates["/user-scores/" + userId + "/" + key] = entryValues; mDatabase.UpdateChildrenAsync(childUpdates); }
This example uses to create an entry in the node containing entries for
all users at and simultaneously retrieve the key with
. The key can then be used to create a second entry in the user's
scores at .
Using these paths, you can perform simultaneous updates to multiple locations in
the JSON tree with a single call to , such as how this
example creates the new entry in both locations. Simultaneous updates made this
way are atomic: either all updates succeed or all updates fail.
Delete data
The simplest way to delete data is to call on a reference to the
location of that data.
You can also delete by specifying as the value for another write
operation such as or . You can use this
technique with to delete multiple children in a single API
call.
Know when your data is committed.
To know when your data is committed to the Firebase Realtime Database server, you
can add a continuation. Both and
return a that allows you to know when the operation is complete. If the
call is unsuccessful for any reason, the Tasks will be true with the
property indicating why the failure occurred.
Updating Data with Conditional Requests
-
To perform a conditional request at a location, get the unique identifier
for the current data at that location, or the ETag. If the data changes at
that location, the ETag changes, too. You can request an ETag with any
method other than . The following example uses a request.curl -i 'https://test.example.com/posts/12345/upvotes.json' -H 'X-Firebase-ETag: true'
Specifically calling the ETag in the header returns the ETag of the
specified location in the HTTP response.HTTP/1.1 200 OK Content-Length: 6 Content-Type: application/json; charset=utf-8 Access-Control-Allow-Origin: * ETag: Cache-Control: no-cache 10 // Current value of the data at the specified location
-
Include the returned ETag in your next or
request to update
data that specifically matches that ETag value. Following our example, to
update the counter to 11, or 1 larger than the initial fetched value of 10,
and fail the request if the value no longer matches, use the following code:curl -iX PUT -d '11' 'https://.firebaseio.com/posts/12345/upvotes.json' -H 'if-match:'
If the value of the data at the specified location is still 10, the ETag in the
request matches, and the request succeeds, writing 11 to the database.HTTP/1.1 200 OK Content-Length: 6 Content-Type: application/json; charset=utf-8 Access-Control-Allow-Origin: * Cache-Control: no-cache 11 // New value of the data at the specified location, written by the conditional request
If the location no longer matches the ETag, which might occur if another user
wrote a new value to the database, the request fails without writing to the
location. The return response includes the new value and ETag.HTTP/1.1 412 Precondition Failed Content-Length: 6 Content-Type: application/json; charset=utf-8 Access-Control-Allow-Origin: * ETag: Cache-Control: no-cache 12 // New value of the data at the specified location
- Use the new information if you decide to retry the request. Realtime Database
does not automatically retry conditional requests that have failed. However,
you can use the new value and ETag to build a new conditional request with
the information returned by the fail response.
REST-based conditional requests implement the HTTP
if-match
standard. However, they differ from the standard in the following ways:
- You can only supply one ETag value for each if-match request, not multiple.
- While the standard suggests that ETags be returned with all requests,
Realtime Database only returns ETags with requests including the
header. This reduces billing costs for standard requests.
Write data offline
If a client loses its network connection, your app will continue functioning
correctly.
Every client connected to a Firebase database maintains its own internal version
of any active data. When data is written, it's written to this local version
first. The Firebase client then synchronizes that data with the remote database
servers and with other clients on a «best-effort» basis.
As a result, all writes to the database trigger local events immediately, before
any data is written to the server. This means your app remains
responsive regardless of network latency or connectivity.
Once connectivity is reestablished, your app receives the appropriate set of
events so that the client syncs with the current server state, without having to
write any custom code.
Security in a virtual machine
Dalvik is Android's runtime virtual machine (VM). Dalvik was built specifically for Android,
but many of the concerns regarding secure code in other virtual machines also apply to Android.
In general, you shouldn't concern yourself with security issues relating to the virtual machine.
Your application runs in a secure sandbox environment, so other processes on the system can't
access your code or private data.
If you're interested in learning more about virtual machine security,
familiarize yourself with some
existing literature on the subject. Two of the more popular resources are:
- Securing Java
This document focuses on areas that are Android specific or
different from other VM environments. For developers experienced with VM
programming in other environments, there are two broad issues that may be
different about writing apps for Android:
- Some virtual machines, such as the JVM or .NET runtime, act as a security
boundary, isolating code from the underlying operating system capabilities. On
Android, the Dalvik VM is not a security boundary—the application sandbox is
implemented at the OS level, so Dalvik can interoperate with native code in the
same application without any security constraints. - Given the limited storage on mobile devices, it’s common for developers
to want to build modular applications and use dynamic class loading. When
doing this, consider both the source where you retrieve your application logic
and where you store it locally. Do not use dynamic class loading from sources
that are not verified, such as unsecured network sources or external storage,
because that code might be modified to include malicious behavior.
Writing Data with PUT
The basic write operation through the REST API is . To
demonstrate saving data, we'll build a blogging application with posts and users. All of the
data for our application will be stored at the Firebase database URL
https://docs-examples.firebaseio.com/rest/saving-data/fireblog.
Let's start by saving some user data to our Firebase database. We'll store each user by a unique
username, and we'll also store their full name and date of birth. Since each user will have a
unique username, it makes sense to use here instead of since
we already have the key and don't need to create one.
Using , we can write a string, number, boolean, array or any JSON object to
our Firebase database. In this case we'll pass it an object:
curl -X PUT -d '{ "alanisawesome": { "name": "Alan Turing", "birthday": "June 23, 1912" } }' 'https://docs-examples.firebaseio.com/rest/saving-data/fireblog/users.json'
When a JSON object is saved to the database, the object properties are automatically
mapped to child locations in a nested fashion. If we navigate to
the
newly created node, we'll see the value «Alan Turing». We can also save data directly to a
child location:
curl -X PUT -d '"Alan Turing"' \ 'https://docs-examples.firebaseio.com/rest/saving-data/fireblog/users/alanisawesome/name.json'
curl -X PUT -d '"June 23, 1912"' \ 'https://docs-examples.firebaseio.com/rest/saving-data/fireblog/users/alanisawesome/birthday.json'
The above two examples—writing the value at the same time as an object and writing them
separately to child locations—will result in the same data being saved to our Firebase
database:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing" } } }
A successful request will be indicated by a HTTP status code, and the
response will contain the data we wrote to the database. The first example will only
trigger one event on clients that are watching the data, whereas the second example will trigger
two. It is important to note that if data already existed at the users path, the first approach
would overwrite it, but the second method would only modify the value of each separate child
node while leaving other children unchanged. is equivalent to
in our JavaScript SDK.
Save data as transactions
When working with data that could be corrupted by concurrent
modifications, such as incremental counters, you can use a
.
You can give this operation an update function and an optional
completion callback. The update function takes the current state of the data as
an argument and returns the new desired state you would like to write. If
another client writes to the location before your new value is successfully
written, your update function is called again with the new current value, and
the write is retried.
For instance, in the example social blogging app, you could allow users to
star and unstar posts and keep track of how many stars a post has received
as follows:
function toggleStar(postRef, uid) { postRef.transaction(function(post) { if (post) { if (post.stars && post.stars) { post.starCount--; post.stars = null; } else { post.starCount++; if (!post.stars) { post.stars = {}; } post.stars = true; } } return post; }); }
Using a transaction prevents star counts from being incorrect if multiple
users star the same post at the same time or the client had stale data. If the
transaction is rejected, the server returns
the current value to the client, which runs the transaction again with the
updated value. This repeats until the transaction is accepted or you abort
the transaction.
Implementation tips and best practices
- When using , provide some UI devices that support it and allow users
to easily toggle between experiences. For example:- Notify users that is supported and encourage them to use it.
- Allow users to identify and choose the mode with appropriate prompts and
intuitive on/off buttons or checkboxes. - When data saving mode is selected, announce and provide an easy and obvious
way to disable it and revert back to the full experience if desired.
- Remember that lightweight applications are not lesser applications. They don't
omit important functionality or data, they're just more cognizant of the
involved costs and the user experience. For example:- A photo gallery application may deliver lower resolution previews, or use a less
code-heavy carousel mechanism. - A search application may return fewer results at a time, limit the number of
media-heavy results, or reduce the number of dependencies required to render
the page. - A news-oriented site may surface fewer stories, omit less popular categories,
or provide smaller media previews.
- A photo gallery application may deliver lower resolution previews, or use a less
- Provide server logic to check for the request header and consider
providing an alternate, lighter page response when it is enabled — e.g.,
reduce the number of required resources and dependencies, apply more aggressive
resource compression, etc. - If you use a service worker, your application can detect when the data saving
option is enabled by checking for the presence of the request
header, or by checking the value of the
property. If enabled, consider whether you can rewrite the request to fetch
fewer bytes, or use an already fetched response. - Consider augmenting with other signals, such as information about
the user's connection type and technology (see ). For example, you might
want to serve the lightweight experience to any user on a 2G connection even if
is not enabled. Conversely, just because the user is on a «fast» 4G
connection doesn't mean they aren't interested in saving data — for
example, when roaming. Additionally, you could augment the presence of
with the client hint to further adapt to users on
devices with limited memory. User device memory is also advertised in the
client hint.
Save data as transactions
When working with data that could be corrupted by concurrent
modifications, such as incremental counters, you can use a
.
You give this operation two arguments: an update function and an optional
completion callback. The update function takes the current state of the data as
an argument and returns the new desired state you would like to write.
For instance, in the example social blogging app, you could allow users to
star and unstar posts and keep track of how many stars a post has received
as follows:
Using a transaction prevents star counts from being incorrect if multiple
users star the same post at the same time or the client had stale data. The
value contained in the class is initially the client's last
known value for the path, or if there is none. The server compares the
initial value against it's current value and accepts the transaction if the
values match, or rejects it. If the transaction is rejected, the server returns
the current value to the client, which runs the transaction again with the
updated value. This repeats until the transaction is accepted or too many
attempts have been made.