The technology landscape is constantly changing with new and innovative solutions. This has allowed the industry to utilize modern engineering solutions such as Docker, Kubernetes, managed services, and serverless functions when building and deploying software.
In fact, serverless functions have leveled the playing field by giving developers the required flexibility to build and ship software fast at a reduced cost and with good developer experience.
In this post, we will explore how Appwrite is changing the game through its serverless function offering and support for multiple content types; we’ll also demonstrate how to use it in a Flutter application.
GitHub links
The project source codes are below:
Prerequisites
To fully grasp the concepts presented in this tutorial, the following are required:
Serverless functions on Appwrite
Appwrite provides a unique approach to managing and hosting serverless functions within either a secure cloud or self-hosted environment, offering an excellent user experience for developers. In addition, it also lets developers:
- Customize and extend code capabilities
- Simplify deployment complexities
- Effectively address scalability concerns
Beyond its extensibility, security features, and other valuable capabilities, Appwrite also facilitates the return of various content types as a response, such as JSON, HTML, Text, and more. This capability is transformative and a game-changer because it allows developers to extend their use cases and unlock new possibilities.
Below are the content types supported by Appwrite and the sample use case:
Empty
This response type in a serverless function does not yield any output when used in a function. A sample use case is a function responsible for initializing the database and corresponding collections.
import 'dart:async';
Future<dynamic> main(final context) async {
//call to another function can come here
return context.res.empty();
}
Redirect
This type is used to redirect users to another URL. A sample use case is a function that redirects users to a specified URL.
import 'dart:async';
import 'dart:convert';
Future<dynamic> main(final context) async {
final data = jsonDecode(context.req.body);
return context.res.redirect(data["url"], 301);
}
JSON
This type returns a JSON. A sample use case is a function that uploads files into storage and returns the upload information.
import 'dart:async';
Future<dynamic> main(final context) async {
//upload to storage code
return context.res.json({"upload_id" "erjkdkkSAMPLE_UPLOAD_2343ujej"});
}
HTML
This type returns HTML content. A sample use case is a function designed to manage an RSVP form.
import 'dart:async';
Future<dynamic> main(final context) async {
final html = '''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Contact Form</title>
</head>
<body>
<form action="/" method="POST">
<input type="email" id="email" name="email" placeholder="Email" required>
<button type="submit">Submit</button>
</form>
</body>
</html>
''';
return context.res.send(html, 200, {'content-type': 'text/html'});
}
Text
This type returns a text. A sample use case is a function designed to verify the operational status of a service.
import 'dart:async';
Future<dynamic> main(final context) async {
//api check to a service
return context.res.send("The service is running!");
}
Now that we understand Appwrite’s serverless function and its support for multiple content types, we can build our application and test multiple responses.
Create Appwrite’s serverless function to get multiple content types
Set up a project on Appwrite
To get started, we need to create a project on Appwrite’s console to deploy and test our function. To do this, log into the Appwrite console, click the Create project button, input appwrite_content_type
as the name, and Create.
Secondly, we must navigate to the Functions tab and create a function using the Dart
template.
Input appwrite_content_type
as the name and continue.
Check the Generate API key on completion option so that our function can securely connect to Appwrite and proceed.
Check the Create a new repository option and connect to GitHub by following the prompt.
On connection, input appwrite_content_type
as the repository name, accept the default branch suggestion and Create.
With that done, we should see our deployed function on Appwrite and the corresponding source code on GitHub.
Modify the generated source code on GitHub
Next, we must modify the generated function source code on GitHub by navigating to the lib/main.dart
file, click the Edit icon, and add the snippet below:
import 'dart:async';
import 'dart:convert';
Future<dynamic> main(final context) async {
final data = context.req.headers;
final responseType = json.encode(data['Content-Type']);
switch (responseType) {
case 'application/json':
return context.res.json(
{'type': 'This is a sample JSON response from Appwrite function'});
case 'text/html':
return context.res.send(
'<h1>This is a sample HTML response from Appwrite function</h1>',
200,
{'content-type': 'text/html'});
default:
return context.res
.send('This is a sample text response from Appwrite function');
}
}
The snippet above gets the request header based on a header and conditionally returns a matching response type.
Lastly, we must commit the changes by clicking the Commit changes… button, input appropriate commit message, and save.
With that done, Appwrite will automatically redeploy our function using the updated source code.
Leveraging deployed function in a Flutter application
To get started, we need to clone the project by navigating to the desired directory and running the command below:
git clone https://github.com/Mr-Malomz/flutter_appwrite_content
Running the project
We need to install the project dependencies by running the command below:
flutter pub get
Then, run the project using the following command:
flutter run
The command above will run the application on the selected device.
Add platform support and update function permission
To securely connect our Flutter application with Appwrite, we need to add it as a supported platform on the console. To do this, navigate to the Overview menu and click the Flutter button.
Next, we must modify the Flutter application as detailed below:
To obtain our Bundle ID, navigate to the path below:
ios > Runner.xcodeproj > project.pbxproj
Open the project.pbxproj
file and search for PRODUCT_BUNDLE_IDENTIFIER
.
To obtain our package name for Android, navigate to the path below:
android > app > src > debug > AndroidManifest.xml
Open the AndroidManifest.xml
file and copy the package
value.
We must also modify the AndroidManifext.xml
as shown below:
<manifest>
<application>
<activity android:name="com.linusu.flutter_web_auth_2.CallbackActivity" android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="appwrite-callback-[PROJECT_ID]" />
</intent-filter>
</activity>
</application>
</manifest>
We must also replace the [PROJECT_ID]
section with our actual Appwrite project ID. We can get our project ID from the Overview tab.
Lastly, we must update the deployed function permission to use it in our Flutter application. To do this, navigate to the Settings tab of the deployed function, scroll to the Execute Access section, select Any
, and Update.
PS: Appwrite ships with a robust role and access management to cater to individual and company needs.
Create a service
With that done, we need to create a service file to separate the application core logic from the UI. To do this, create a utils.dart
file inside the lib
directory. Then, add the snippet below:
import 'package:appwrite/appwrite.dart';
class _AppConstant {
final String endpoint = "https://cloud.appwrite.io/v1";
final String projectId = "REPLACE WITH PROJECT ID";
final String functionId = "REPLACE WITH FUNCTION ID";
}
class ContentService {
Client _client = Client();
ContentService() {
_init();
}
//initialize the application
_init() async {
_client
.setEndpoint(_AppConstant().endpoint)
.setProject(_AppConstant().projectId);
//get current session
Account account = Account(_client);
try {
await account.get();
} on AppwriteException catch (e) {
if (e.code == 401) {
account
.createAnonymousSession()
.then((value) => value)
.catchError((e) => e);
}
}
}
Future getContentType(String selectedType) async {
Map<String, String> contentTypes = {
"text": "text/plain",
"json": "application/json",
"html": "text/html"
};
Functions functions = Functions(_client);
try {
var result = await functions.createExecution(
functionId: _AppConstant().functionId,
headers: {'Content-Type': contentTypes[selectedType]},
);
return result.responseBody;
} catch (e) {
throw Exception('Error creating subscription');
}
}
}
The snippet above does the following:
- Creates a private class
_AppConstant
to save required properties - Creates a
ContentService
class with_client
and_account
properties to connect to the Appwrite instance - Creates an
_init
method that configures Appwrite using the properties - Creates a
getContentType
method that uses the_client
property to create an instance of the function and then use thecreateExecution
method to trigger the function with the required parameters
Consuming the service
First, we need to modify the home.dart
file inside the screens
folder by importing the required dependencies and using the service to perform required operations:
import 'package:flutter/material.dart';
import 'package:flutter_appwrite_content/utils.dart';
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
final _formKey = GlobalKey<FormState>();
var _selected = '';
var _dropdownItems = ["text", "json", "html"];
bool _isLoading = false;
var _contentValue = '';
_handleContentType() {
setState(() {
_isLoading = true;
_contentValue = '';
});
ContentService().getContentType(_selected).then((value) {
setState(() {
_isLoading = false;
_contentValue = value.toString();
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Content retrieved successfully!')),
);
}).catchError((e) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error retrieving content!')),
);
});
}
Widget build(BuildContext context) {
//UI code goes here
}
}
The snippet above does the following:
- Imports the required dependency
- Line 12-13: Creates the
_isLoading
and_contentValue
properties to manage application state and results from the service, respectively - Line 15-36: Creates an
_handleContentType
method that uses thegetContentType
service to get the required content
Lastly, we must update the UI to use created properties to manage the application state and display returned content.
//import goes here
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
//state goes here
_handleContentType() {
//code goes here
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Appwrite Function Content"),
backgroundColor: Colors.black,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 30.0),
child: Column(
children: [
Form(
key: _formKey,
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//title
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Data type',
style: TextStyle(
color: Colors.grey,
fontSize: 14.0,
),
),
const SizedBox(height: 5.0),
DropdownButtonFormField(
items: _dropdownItems.map((String item) {
return DropdownMenuItem(
value: item,
child: Text(item),
);
}).toList(),
onChanged: (value) {
setState(() => _selected = value!);
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 20),
hintText: "select data type",
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.grey),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
),
],
),
],
),
const SizedBox(height: 30.0),
SizedBox(
height: 45,
width: double.infinity,
child: TextButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_handleContentType();
}
},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.black),
),
child: const Text(
'Get content',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
),
),
),
],
),
),
const SizedBox(height: 40.0),
Container(
width: MediaQuery.of(context).size.width,
height: 200.0,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(color: Colors.blueAccent),
borderRadius: BorderRadius.circular(10),
),
child: Text(_contentValue),
)
],
),
),
);
}
}
With that done, we restart the application using the code editor or run the command below:
flutter run
Conclusion
This post discussed the support for multiple content types in Appwrite function and how to use it in a Flutter application. In addition to the use cases mentioned above, Appwrite's support for multiple content types unlocks exciting possibilities for developers by allowing them to dynamically build various types of applications.
These resources may also be helpful: