Once you have the SDK added as a dependency, initialize the SDK as early as possible in your application (e.g. in your application class) like this:
await Parse().initialize(
keyApplicationId,
keyParseServerUrl,
);
If you want to use secure storage or use the Flutter web/desktop SDK, please change to the below instance of CoreStorage
as it has no dependencies on Flutter. CoreStoreSembastImp
does not encrypt the data on the web and Web is not safe anyway. Encrypt fields manually as needed.
await Parse().initialize(
keyParseApplicationId,
keyParseServerUrl,
coreStore: await CoreStoreSembastImp.getInstance("/data"));
It’s possible to add other parameters to work with your instance of Parse Server:
await Parse().initialize(
keyApplicationId,
keyParseServerUrl,
clientKey: keyParseClientKey, // Required for some setups
debug: true, // When enabled, prints logs to console
liveQueryUrl: keyLiveQueryUrl, // Required if using LiveQuery
autoSendSessionId: true, // Required for authentication and ACL
securityContext: securityContext, // Again, required for some setups
coreStore: CoreStoreMemoryImp()); // Non persistent mode (default): Sdk will store everything in memory instead of using Sembast as an internal DB.
⚠️ The master key should only be used in safe environments and never on client side. Using this package on a server should be fine.
Due to Cross-Origin Resource Sharing (CORS) restrictions, web support requires adding X-Parse-Installation-Id
as an allowed header in the Parse Server configuration:
PARSE_SERVER_ALLOW_HEADERS=X-Parse-Installation-Id
.allowHeaders: ['X-Parse-Installation-Id']
.The security entitlements posed by the macOS framework require that your app is granted permission to open outgoing network connections, so that the Parse Flutter SDK can communicate with Parse Server. To grant this permission, add the following code:
<key>com.apple.security.network.client</key>
<true/>
to the following files:
/macOS/Runner/Release.entitlements
/macOS/Runner/DebugProfile.entitlements
By default, this SDK uses the ParseHTTPClient
. Another option is use ParseDioClient
. This client supports the most features (for example a progress callback at the file upload), but a benchmark has shown that dio is slower than http on web.
If you want to use the ParseDioClient
, which uses the dio network library, you can provide a custom ParseClientCreator
at the initialization of the SDK:
await Parse().initialize(
//...
clientCreator: ({bool? sendSessionId, SecurityContext? securityContext}) => ParseDioClient(sendSessionId: sendSessionId, securityContext: securityContext),
);
You can create a custom object like this:
var dietPlan = ParseObject('DietPlan')
..set('Name', 'Ketogenic')
..set('Fat', 65);
await dietPlan.save();
You can update an existing object via its objectId
:
var dietPlan = ParseObject('DietPlan')
..objectId = 'R5EonpUDWy'
..set('Fat', 70);
await dietPlan.save();
Check the response to verify that the object has been successfully saved:
var response = await dietPlan.save();
if (response.success) {
dietPlan = response.results.first;
}
You can create your own ParseObjects
or convert your existing objects into ParseObjects
:
class DietPlan extends ParseObject implements ParseCloneable {
DietPlan() : super(_keyTableName);
DietPlan.clone(): this();
/// Mimic a clone due to Flutter not using reflection
@override clone(Map map) => DietPlan.clone()..fromJson(map);
static const String _keyTableName = 'Diet_Plans';
static const String keyName = 'Name';
String get name => get<String>(keyName);
set name(String name) => set<String>(keyName, name);
}
When receiving a ParseObject
you can often provide an instance of your custom object as an object copy. To always use your custom object class, register your subclass at the initialization of the SDK:
Parse().initialize(
...,
registeredSubClassMap: <String, ParseObjectConstructor>{
'Diet_Plans': () => DietPlan(),
},
parseUserConstructor: (username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress),
);
Additionally you can register SubClasses
after the initialization of the SDK:
ParseCoreData().registerSubClass('Diet_Plans', () => DietPlan());
ParseCoreData().registerUserSubClass((username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress));
Providing a ParseObject
as described above should still work, even if you have registered a different SubClass
.
To add a new object property:
dietPlan.set<int>('RandomInt', 8);
var randomInt = dietPlan.get<int>('RandomInt');
You can save an object by calling .pin()
:
dietPlan.pin();
and to retrieve it
var dietPlan = DietPlan().fromPin('OBJECT ID OF OBJECT');
Retrieve it, call
var response = await dietPlan.increment("count", 1);
or using with save function
dietPlan.setIncrement('count', 1);
dietPlan.setDecrement('count', 1);
var response = dietPlan.save()
Retrieve it, call
var response = await dietPlan.add("listKeywords", ["a", "a","d"]);
var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]);
var response = await dietPlan.remove("listKeywords", ["a"]);
or using with save function
dietPlan.setAdd('listKeywords', ['a','a','d']);
dietPlan.setAddUnique('listKeywords', ['a','a','d']);
dietPlan.setRemove('listKeywords', ['a']);
var response = dietPlan.save()
For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object.
To support this type of security, each object has an access control list, implemented by the ParseACL
class.
If ParseACL
is not specified (with the exception of the ParseUser
class) all objects are set to Public for read and write.
The simplest way to use a ParseACL
is to specify that an object may only be read or written by a single user.
To create such an object, there must first be a logged in ParseUser
. Then, new ParseACL(user)
generates a ParseACL
that limits access to that user. An object’s ACL is updated when the object is saved, like any other property.
ParseUser user = await ParseUser.currentUser() as ParseUser;
ParseACL parseACL = ParseACL(owner: user);
ParseObject parseObject = ParseObject("TestAPI");
...
parseObject.setACL(parseACL);
var apiResponse = await parseObject.save();
Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL
using setReadAccess
and setWriteAccess
ParseUser user = await ParseUser.currentUser() as ParseUser;
ParseACL parseACL = ParseACL();
// Grant total access to current user
parseACL.setReadAccess(userId: user.objectId, allowed: true);
parseACL.setWriteAccess(userId: user.objectId, allowed: true);
// Grant read access to userId: 'TjRuDjuSAO'
parseACL.setReadAccess(userId: 'TjRuDjuSAO', allowed: true);
parseACL.setWriteAccess(userId: 'TjRuDjuSAO', allowed: false);
ParseObject parseObject = ParseObject("TestAPI");
...
parseObject.setACL(parseACL);
var apiResponse = await parseObject.save();
You can also grant permissions to all users at once using setPublicReadAccess
and setPublicWriteAccess
.
ParseACL parseACL = ParseACL();
parseACL.setPublicReadAccess(allowed: true);
parseACL.setPublicWriteAccess(allowed: true);
ParseObject parseObject = ParseObject("TestAPI");
...
parseObject.setACL(parseACL);
var apiResponse = await parseObject.save();
Operations that are forbidden, such as deleting an object that you do not have write access to, result in a ParseError
with code 101
: ObjectNotFound
.
For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which object ids do not exist at all.
You can retrieve the ACL list of an object using:
ParseACL parseACL = parseObject.getACL();
To set the ACL to ParseRole
use:
parseACL.setReadAccess(userId: "role:ROLE_NAME", allowed: true);
parseACL.setWriteAccess(userId: "role:ROLE_NAME", allowed: true);
Once you have setup the project and initialized the instance, you can then retrieve data from your server by calling:
var apiResponse = await ParseObject('ParseTableName').getAll();
if (apiResponse.success){
for (var testObject in apiResponse.result) {
print(ApplicationConstants.APP_NAME + ": " + testObject.toString());
}
}
Or you can get an object by its objectId
:
var dietPlan = await DietPlan().getObject('R5EonpUDWy');
if (dietPlan.success) {
print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString());
} else {
print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message);
}
The standard query method query()
returns a ParseResponse
that contains the result or the error. As an alternative, you can also use Future<List<T>> find()
for receiving options.
This method returns an Future
that either resolves in an error (equivalent to the error in the ParseResponse
) or an List
containing the queried objects. One difference, you should be aware of, is the fact that Future<List<T>> find()
will return an empty list instead of the No results
error you receive in case no object matches your query.
Choosing between query()
and find()
comes down to personal preference. Both methods can be used for querying a ParseQuery
, just the output method differs.
Similar to find()
the QueryBuilder
also has a function called Future<T?> first()
. Just like find()
first()
is just a convenience method that makes querying the first object satisfying the query simpler. first()
returns an Future
, that resolves in an error or the first object matching the query. In case no object satisfies the query, the result will be null
.
You can create complex queries to really put your database to the test:
var queryBuilder = QueryBuilder<DietPlan>(DietPlan())
..startsWith(DietPlan.keyName, "Keto")
..greaterThan(DietPlan.keyFat, 64)
..lessThan(DietPlan.keyFat, 66)
..equals(DietPlan.keyCarbs, 5);
var response = await queryBuilder.query();
if (response.success) {
print(ApplicationConstants.keyAppName + ": " + ((response.results as List<dynamic>).first as DietPlan).toString());
} else {
print(ApplicationConstants.keyAppName + ": " + response.exception.message);
}
If you want to find objects that match one of several queries, you can use the QueryBuilder.or
method to construct a query that is an OR of the queries passed in. For instance if you want to find players who either have a lot of wins or a few wins, you can do:
ParseObject playerObject = ParseObject("Player");
QueryBuilder<ParseObject> lotsOfWins =
QueryBuilder<ParseObject>(playerObject))
..whereGreaterThan('wins', 50);
QueryBuilder<ParseObject> fewWins =
QueryBuilder<ParseObject>(playerObject)
..whereLessThan('wins', 5);
QueryBuilder<ParseObject> mainQuery = QueryBuilder.or(
playerObject,
[lotsOfWins, fewWins],
);
var apiResponse = await mainQuery.query();
To find objects that match several queries use QueryBuilder.and
. To find objects that do not match any given query use QueryBuilder.nor
.
The features available are:
If you want to retrieve objects where a field contains an object that matches another query, you can use the whereMatchesQuery
condition.
For example, imagine you have a post class and a comment class, where each comment has a pointer to its parent Post.
You can find comments on posts with images by doing:
QueryBuilder<ParseObject> queryPost =
QueryBuilder<ParseObject>(ParseObject('Post'))
..whereValueExists('image', true);
QueryBuilder<ParseObject> queryComment =
QueryBuilder<ParseObject>(ParseObject('Comment'))
..whereMatchesQuery('post', queryPost);
var apiResponse = await queryComment.query();
If you want to retrieve objects where a field contains an object that does not match another query, you can use the whereDoesNotMatchQuery
condition.
Imagine you have a post class and a comment class, where each comment has a pointer to its parent post.
You can find comments on posts without images by doing:
QueryBuilder<ParseObject> queryPost =
QueryBuilder<ParseObject>(ParseObject('Post'))
..whereValueExists('image', true);
QueryBuilder<ParseObject> queryComment =
QueryBuilder<ParseObject>(ParseObject('Comment'))
..whereDoesNotMatchQuery('post', queryPost);
var apiResponse = await queryComment.query();
You can use the whereMatchesKeyInQuery
method to get objects where a key matches the value of a key in a set of objects resulting from another query. For example, if you have a class containing sports teams and you store a user’s hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like:
QueryBuilder<ParseObject> teamQuery =
QueryBuilder<ParseObject>(ParseObject('Team'))
..whereGreaterThan('winPct', 0.5);
QueryBuilder<ParseUser> userQuery =
QueryBuilder<ParseUser>ParseUser.forQuery())
..whereMatchesKeyInQuery('hometown', 'city', teamQuery);
var apiResponse = await userQuery.query();
Conversely, to get objects where a key does not match the value of a key in a set of objects resulting from another query, use whereDoesNotMatchKeyInQuery
. For example, to find users whose hometown teams have losing records:
QueryBuilder<ParseObject> teamQuery =
QueryBuilder<ParseObject>(ParseObject('Team'))
..whereGreaterThan('winPct', 0.5);
QueryBuilder<ParseUser> losingUserQuery =
QueryBuilder<ParseUser>ParseUser.forQuery())
..whereDoesNotMatchKeyInQuery('hometown', 'city', teamQuery);
var apiResponse = await losingUserQuery.query();
To filter rows based on objectId
from pointers in a second table, you can use dot notation:
QueryBuilder<ParseObject> rolesOfTypeX =
QueryBuilder<ParseObject>(ParseObject('Role'))
..whereEqualTo('type', 'x');
QueryBuilder<ParseObject> groupsWithRoleX =
QueryBuilder<ParseObject>(ParseObject('Group')))
..whereMatchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX);
var apiResponse = await groupsWithRoleX.query();
If you only care about the number of games played by a particular player:
QueryBuilder<ParseObject> queryPlayers =
QueryBuilder<ParseObject>(ParseObject('GameScore'))
..whereEqualTo('playerName', 'Jonathan Walsh');
var apiResponse = await queryPlayers.count();
if (apiResponse.success && apiResponse.result != null) {
int countGames = apiResponse.count;
}
This tool allows you to subscribe to a QueryBuilder
you are interested in. Once subscribed, the server will notify clients whenever a ParseObject
that matches the QueryBuilder
is created or updated, in real-time.
Parse LiveQuery
contains two parts, the LiveQuery
server and the LiveQuery
clients.
In order to use live queries, you need to set up both of them.
The Parse Server configuration guide on the server is found here and is not part of this documentation.
Initialize the Parse Live Query by entering the parameter liveQueryUrl
in Parse().initialize
:
Parse().initialize(
keyApplicationId,
keyParseServerUrl,
clientKey: keyParseClientKey,
debug: true,
liveQueryUrl: keyLiveQueryUrl,
autoSendSessionId: true);
Declare LiveQuery:
final LiveQuery liveQuery = LiveQuery();
Set the QueryBuilder
that will be monitored by LiveQuery:
QueryBuilder<ParseObject> query =
QueryBuilder<ParseObject>(ParseObject('TestAPI'))
..whereEqualTo('intNumber', 1);
You’ll get the LiveQuery events through this subscription.
The first time you call subscribe
, we’ll try to open the WebSocket connection to the LiveQuery
server for you.
Subscription subscription = await liveQuery.client.subscribe(query);
We define several types of events you’ll get through a subscription object:
When a new ParseObject
is created and it fulfills the QueryBuilder
you subscribe to, you’ll get this event.
The object is the ParseObject
which was created.
subscription.on(LiveQueryEvent.create, (value) {
print('*** CREATE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
When an existing ParseObject
which fulfills the QueryBuilder
you subscribe to is updated (The ParseObject
fulfills the QueryBuilder
before and after changes), you’ll get this event.
The object is the ParseObject
which was updated. Its content is the latest value of the ParseObject
.
subscription.on(LiveQueryEvent.update, (value) {
print('*** UPDATE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
When an existing ParseObject’s
old value does not fulfill the QueryBuilder
but its new value fulfills the QueryBuilder
, you’ll get this event. The object is the ParseObject
which enters the QueryBuilder
.
Its content is the latest value of the ParseObject
.
subscription.on(LiveQueryEvent.enter, (value) {
print('*** ENTER ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
When an existing ParseObject’s
old value fulfills the QueryBuilder
but its new value doesn’t fulfill the QueryBuilder
, you’ll get this event. The object is the ParseObject
which leaves the QueryBuilder
.
Its content is the latest value of the ParseObject
.
subscription.on(LiveQueryEvent.leave, (value) {
print('*** LEAVE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
When an existing ParseObject
which fulfills the QueryBuilder
is deleted, you’ll get this event.
The object is the ParseObject
which is deleted.
subscription.on(LiveQueryEvent.delete, (value) {
print('*** DELETE ***: ${DateTime.now().toString()}\n $value ');
print((value as ParseObject).objectId);
print((value as ParseObject).updatedAt);
print((value as ParseObject).createdAt);
print((value as ParseObject).get('objectId'));
print((value as ParseObject).get('updatedAt'));
print((value as ParseObject).get('createdAt'));
});
If you would like to stop receiving events from a QueryBuilder
, you can just unsubscribe
the subscription
.
After that, you won’t get any events from the subscription
object and will close the WebSocket connection to the LiveQuery server.
liveQuery.client.unSubscribe(subscription);
In case the client’s connection to the server breaks, LiveQuery
will automatically try to reconnect.
LiveQuery
will wait at increasing intervals between reconnection attempts.
By default, these intervals are set to [0, 500, 1000, 2000, 5000, 10000]
for mobile and [0, 500, 1000, 2000, 5000]
for web.
You can change these by providing a custom list using the liveListRetryIntervals
parameter at Parse.initialize()
(-1
means do not try to reconnect
).
ParseLiveList
makes implementing a dynamic List as simple as possible.
It ships with the ParseLiveList
class itself, this class manages all elements of the list, sorts them,
keeps itself up to date and Notifies you of changes.
ParseLiveListWidget
is a widget that handles all the communication with the ParseLiveList
for you.
Using ParseLiveListWidget
you can create a dynamic List by just providing a QueryBuilder
.
ParseLiveListWidget<ParseObject>(
query: query,
);
To customize the List Elements, you can provide a childBuilder
.
ParseLiveListWidget<ParseObject>(
query: query,
reverse: false,
childBuilder:
(BuildContext context, ParseLiveListElementSnapshot<ParseObject> snapshot) {
if (snapshot.failed) {
return const Text('something went wrong!');
} else if (snapshot.hasData) {
return ListTile(
title: Text(
snapshot.loadedData.get("text"),
),
);
} else {
return const ListTile(
leading: CircularProgressIndicator(),
);
}
},
);
Similar to the standard ListView
, you can provide arguments like reverse
or shrinkWrap
.
By providing the listLoadingElement
, you can show the user something while the list is loading.
ParseLiveListWidget<ParseObject>(
query: query,
childBuilder: childBuilder,
listLoadingElement: Center(
child: CircularProgressIndicator(),
),
);
By providing the duration argument, you can change the animation speed.
ParseLiveListWidget<ParseObject>(
query: query,
childBuilder: childBuilder,
duration: Duration(seconds: 1),
);
By default, ParseLiveQuery
will provide you with all the objects you included in your Query like this:
queryBuilder.includeObject(/* List of all the included sub-objects */);
ParseLiveList
will not listen for updates on these objects by default.
To activate listening for updates on all included objects, add listenOnAllSubItems: true
to your ParseLiveListWidgets
constructor.
If you want ParseLiveList
to listen for updates on only some sub-objects, use listeningIncludes: const <String>[/* all the included sub-objects */]
instead.
Just as QueryBuilder
, ParseLiveList
supports nested sub-objects too.
By default, ParseLiveList
lazy loads the content.
You can avoid that by setting lazyLoading: false
.
In case you want to use lazyLoading
but you need some columns to be preloaded, you can provide a list of preloadedColumns
.
Preloading fields of a pointer is supported by using the dot-notation.
You can access the preloaded data stored in the preLoadedData
field of the ParseLiveListElementSnapshot
.
ParseLiveListWidget<ParseObject>(
query: query,
lazyLoading: true,
preloadedColumns: ["test1", "sender.username"],
childBuilder:
(BuildContext context, ParseLiveListElementSnapshot<ParseObject> snapshot) {
if (snapshot.failed) {
return const Text('something went wrong!');
} else if (snapshot.hasData) {
return ListTile(
title: Text(
snapshot.loadedData.get<String>("text"),
),
);
} else {
return ListTile(
title: Text(
"loading comment from: ${snapshot.preLoadedData?.get<ParseObject>("sender")?.get<String>("username")}",
),
);
}
},
);
NOTE: To use these features you have to enable Live Queries first.
The SDK supports Relation.
To add relation to object:
dietPlan.addRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]);
To remove relation to object:
dietPlan.removeRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]);
To retrieve a relation instance for user, call:
final relation = dietPlan.getRelation('fruits');
and then you can add a relation to the passed in object:
relation.add(dietPlan);
final result = await user.save();
To retrieve objects that are members of Relation field of a parent object:
QueryBuilder<ParseObject> query =
QueryBuilder<ParseObject>(ParseObject('Fruits'))
..whereRelatedTo('fruits', 'DietPlan', DietPlan.objectId);
The SDK supports calling Cloud Functions.
Execute a Cloud Function that returns a ParseObject
:
final ParseCloudFunction function = ParseCloudFunction('hello');
final ParseResponse result =
await function.executeObjectFunction<ParseObject>();
if (result.success) {
if (result.result is ParseObject) {
final ParseObject parseObject = result.result;
print(parseObject.className);
}
}
Execute a Cloud Function with parameters:
final ParseCloudFunction function = ParseCloudFunction('hello');
final Map<String, String> params = <String, String>{'plan': 'paid'};
function.execute(parameters: params);
The SDK supports Parse Config. A map of config parameters can be retrieved from Parse Server with:
var response = await ParseConfig().getConfigs();
To add a new parameter to Parse Config:
ParseConfig().addConfig('TestConfig', 'testing');
There are three different file classes in this SDK:
ParseFileBase
is an abstract class and is the foundation of every file class that can be handled by the SDK.ParseFile
extends ParseFileBase
and is by default used as the file class on every platform but web. This class uses a File
from dart:io
for storing the raw file. The class was formerly the only file class in the SDK.ParseWebFile
is the equivalent of ParseFile
used for Flutter Web. This class uses a Uint8List
for storing the raw file data.The classes above are used by default to represent files, but you can also build your own class extending ParseFileBase
and provide a custom ParseFileConstructor
similar to the SubClasses
.
Have a look at the example application for a small (non web) example.
When uploading or downloading a file, you can use the progressCallback
parameter to track the progress of the HTTP request.
The following is an example for showing an image from a ParseFileBase
:
Widget buildImage(ParseFileBase image){
return FutureBuilder<ParseFileBase>(
future: image.download(),
builder: (BuildContext context,
AsyncSnapshot<ParseFileBase> snapshot) {
if (snapshot.hasData) {
if (kIsWeb) {
return Image.memory((snapshot.data as ParseWebFile).file);
} else {
return Image.file((snapshot.data as ParseFile).file);
}
} else {
return CircularProgressIndicator();
}
},
);
}
A short example for storing a selected image:
// Libraries: image_picker (https://pub.dev/packages/image_picker), image_picker_for_web (https://pub.dev/packages/image_picker_for_web)
PickedFile pickedFile = await ImagePicker().getImage(source: ImageSource.gallery);
ParseFileBase parseFile;
if (kIsWeb) {
// Get data from selected file as an Uint8List
ParseWebFile file = ParseWebFile(null, name: null, url: pickedFile.path);
await file.download();
parseFile = ParseWebFile(file.file, name: file.name);
} else {
parseFile = ParseFile(File(pickedFile.path));
}
someParseObject.set("image", parseFile);
// Save ParseObject and its children like the ParseFileBase
await someParseObject.save();
Example for using the progress callback:
file.upload(progressCallback: (int count, int total) => print("$count of $total"));
Push notifications are a great way to keep your users engaged and informed about your app. You can reach your user base quickly and effectively. This guide will help you through the setup process and the general usage of Parse Platform to send push notifications.
To configure push notifications in Parse Server, check out the push notification guide.
Install Firebase Core and Cloud Messaging. For more details review the Firebase Core Manual.
Add the following code after Parse().initialize(...);
:
ParsePush.instance.initialize(FirebaseMessaging.instance);
FirebaseMessaging.onMessage.listen((message) => ParsePush.instance.onMessage(message));
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
Future<void> onBackgroundMessage(RemoteMessage message) async => ParsePush.instance.onMessage(message);
The following is a code example for a simple implementation of push notifications:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase Core
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// Initialize Parse
await Parse().initialize("applicationId", "serverUrl",
clientKey: "clientKey", debug: true);
// Initialize Parse push notifications
ParsePush.instance.initialize(FirebaseMessaging.instance);
FirebaseMessaging.onMessage
.listen((message) => ParsePush.instance.onMessage(message));
// Process push notifications while app is in the background
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
runApp(const MyApp());
}
Future<void> onBackgroundMessage(RemoteMessage message) async =>
ParsePush.instance.onMessage(message);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
...
Main:
User:
Objects:
We now have 2 types of storage, secure and unsecure. We currently rely on 2 third party options:
Sembast offers secured storage, whilst SharePreferences
wraps NSUserDefaults
(on iOS) and SharedPreferences
(on Android).
The storage method is defined in the parameter coreStore
in Parse().initialize
You can create and control users just as normal using this SDK.
To register a user, first create one:
var user = ParseUser().create("TestFlutter", "TestPassword123", "[email protected]");
Then have the user sign up:
var response = await user.signUp();
if (response.success) user = response.result;
You can also login with the user:
var response = await user.login();
if (response.success) user = response.result;
You can also logout with the user:
var response = await user.logout();
if (response.success) {
print('User logout');
}
Also, once logged in you can manage sessions tokens. This feature can be called after Parse().init()
on startup to check for a logged in user.
user = ParseUser.currentUser();
To add additional columns to the user:
var user = ParseUser("TestFlutter", "TestPassword123", "[email protected]")
..set("userLocation", "FlutterLand");
Other user features are:
Usually, each provider will provide their own library for logins, but the loginWith
method on ParseUser
accepts a name of provider, then a Map<String, dynamic>
with the authentication details required.
For Facebook and the example below, we used the library provided at flutter_facebook_login
Future<void> goToFacebookLogin() async {
final FacebookLogin facebookLogin = FacebookLogin();
final FacebookLoginResult result = await facebookLogin.logInWithReadPermissions(['email']);
switch (result.status) {
case FacebookLoginStatus.loggedIn:
final ParseResponse response = await ParseUser.loginWith(
'facebook',
facebook(result.accessToken.token,
result.accessToken.userId,
result.accessToken.expires));
if (response.success) {
// User is logged in, test with ParseUser.currentUser()
}
break;
case FacebookLoginStatus.cancelledByUser:
// User canceled
break;
case FacebookLoginStatus.error:
// Error
break;
}
}
For Google and the example below, we used the library provided at google_sign_in
class OAuthLogin {
final GoogleSignIn _googleSignIn = GoogleSignIn( scopes: ['email', 'https://www.googleapis.com/auth/contacts.readonly'] );
sigInGoogle() async {
GoogleSignInAccount account = await _googleSignIn.signIn();
GoogleSignInAuthentication authentication = await account.authentication;
await ParseUser.loginWith(
'google',
google(authentication.accessToken,
_googleSignIn.currentUser.id,
authentication.idToken));
}
}