F
Flutter Integration
Build cross-platform mobile apps with Calligraphy API
Cross-platformProduction ReadyDart
Quick Start
1. Add Dependencies
yaml
Click to copy
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.1.0
cached_network_image: ^3.3.0
path_provider: ^2.1.1
share_plus: ^7.2.1
permission_handler: ^11.0.12. Basic Service Implementation
dart
Click to copy
// lib/services/calligraphy_service.dart
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
class CalligraphyService {
static const String _baseUrl = 'https://api.calligraphymaker.com/api/v2';
static const String _apiKey = 'YOUR_API_KEY_HERE';
static Future<CalligraphyResponse?> generateCalligraphy({
required String text,
String language = 'hindi',
String fontStyle = 'calligraphy',
int count = 1,
String imageFormat = 'png',
}) async {
try {
final response = await http.post(
Uri.parse('$_baseUrl/generate'),
headers: {
'Content-Type': 'application/json',
'x-api-key': _apiKey,
},
body: jsonEncode({
'text': text,
'language': language,
'fontStyle': fontStyle,
'count': count,
'imageFormat': imageFormat,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success']) {
return CalligraphyResponse.fromJson(data['data']);
}
} else {
print('API Error: ${response.statusCode} - ${response.body}');
}
} catch (e) {
print('Error generating calligraphy: $e');
}
return null;
}
static Future<SvgDownloadResponse?> downloadSvg({
required String resultText,
required String fontName,
}) async {
try {
final response = await http.post(
Uri.parse('$_baseUrl/download-svg'),
headers: {
'Content-Type': 'application/json',
'x-api-key': _apiKey,
},
body: jsonEncode({
'resultText': resultText,
'fontName': fontName,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success']) {
return SvgDownloadResponse.fromJson(data['data']);
}
}
} catch (e) {
print('Error downloading SVG: $e');
}
return null;
}
static Future<String?> downloadAndSaveImage(String imageUrl) async {
try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
final directory = await getApplicationDocumentsDirectory();
final fileName = 'calligraphy_${DateTime.now().millisecondsSinceEpoch}.png';
final file = File('${directory.path}/$fileName');
await file.writeAsBytes(response.bodyBytes);
return file.path;
}
} catch (e) {
print('Error downloading image: $e');
}
return null;
}
}
// Data models
class CalligraphyResponse {
final List<CalligraphyResult> results;
final CalligraphyMetadata metadata;
final CalligraphyUsage usage;
CalligraphyResponse({
required this.results,
required this.metadata,
required this.usage,
});
factory CalligraphyResponse.fromJson(Map<String, dynamic> json) {
return CalligraphyResponse(
results: (json['results'] as List)
.map((item) => CalligraphyResult.fromJson(item))
.toList(),
metadata: CalligraphyMetadata.fromJson(json['metadata']),
usage: CalligraphyUsage.fromJson(json['usage']),
);
}
}
class CalligraphyResult {
final String id;
final String resultText;
final String fontFamily;
final String? cdnUrl;
final List<dynamic> appliedVariants;
CalligraphyResult({
required this.id,
required this.resultText,
required this.fontFamily,
this.cdnUrl,
required this.appliedVariants,
});
factory CalligraphyResult.fromJson(Map<String, dynamic> json) {
return CalligraphyResult(
id: json['id'],
resultText: json['resultText'],
fontFamily: json['fontFamily'],
cdnUrl: json['cdnUrl'],
appliedVariants: json['appliedVariants'] ?? [],
);
}
}
class CalligraphyMetadata {
final int totalVariations;
final double averageVariantsApplied;
final String fontFamily;
CalligraphyMetadata({
required this.totalVariations,
required this.averageVariantsApplied,
required this.fontFamily,
});
factory CalligraphyMetadata.fromJson(Map<String, dynamic> json) {
return CalligraphyMetadata(
totalVariations: json['totalVariations'],
averageVariantsApplied: (json['averageVariantsApplied'] as num).toDouble(),
fontFamily: json['fontFamily'],
);
}
}
class CalligraphyUsage {
final int creditsUsed;
final int remainingCredits;
CalligraphyUsage({
required this.creditsUsed,
required this.remainingCredits,
});
factory CalligraphyUsage.fromJson(Map<String, dynamic> json) {
return CalligraphyUsage(
creditsUsed: json['credits_used'],
remainingCredits: json['remaining_credits'],
);
}
}
class SvgDownloadResponse {
final String resultText;
final String fontFamily;
final String svgBase64;
final String svgString;
SvgDownloadResponse({
required this.resultText,
required this.fontFamily,
required this.svgBase64,
required this.svgString,
});
factory SvgDownloadResponse.fromJson(Map<String, dynamic> json) {
return SvgDownloadResponse(
resultText: json['resultText'],
fontFamily: json['fontFamily'],
svgBase64: json['svgBase64'],
svgString: json['svgString'],
);
}
}3. UI Implementation
dart
Click to copy
// lib/screens/calligraphy_screen.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:share_plus/share_plus.dart';
import '../services/calligraphy_service.dart';
class CalligraphyScreen extends StatefulWidget {
const CalligraphyScreen({super.key});
@override
State<CalligraphyScreen> createState() => _CalligraphyScreenState();
}
class _CalligraphyScreenState extends State<CalligraphyScreen> {
final TextEditingController _textController = TextEditingController(
text: 'नमस्ते दुनिया'
);
bool _isLoading = false;
List<CalligraphyResult> _results = [];
String? _errorMessage;
final List<String> _languages = ['hindi', 'marathi', 'gujarati', 'english'];
final List<String> _fontStyles = ['calligraphy', 'decorative', 'publication'];
String _selectedLanguage = 'hindi';
String _selectedFontStyle = 'calligraphy';
int _generationCount = 3;
@override
void dispose() {
_textController.dispose();
super.dispose();
}
Future<void> _generateCalligraphy() async {
if (_textController.text.trim().isEmpty) {
_showError('Please enter some text');
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
_results = [];
});
try {
final response = await CalligraphyService.generateCalligraphy(
text: _textController.text.trim(),
language: _selectedLanguage,
fontStyle: _selectedFontStyle,
count: _generationCount,
);
if (response != null) {
setState(() {
_results = response.results;
});
} else {
_showError('Failed to generate calligraphy');
}
} catch (e) {
_showError('Error: ${e.toString()}');
} finally {
setState(() {
_isLoading = false;
});
}
}
void _showError(String message) {
setState(() {
_errorMessage = message;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
Future<void> _shareImage(String imageUrl) async {
try {
final localPath = await CalligraphyService.downloadAndSaveImage(imageUrl);
if (localPath != null) {
await Share.shareXFiles(
[XFile(localPath)],
text: 'Beautiful calligraphy generated with Calligraphy API',
);
}
} catch (e) {
_showError('Failed to share image');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Calligraphy Generator'),
backgroundColor: Colors.sky.shade500,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Input Section
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Enter Text',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextField(
controller: _textController,
decoration: const InputDecoration(
hintText: 'Enter text to convert to calligraphy',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
// Language Selection
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Language'),
DropdownButton<String>(
value: _selectedLanguage,
isExpanded: true,
items: _languages.map((language) {
return DropdownMenuItem(
value: language,
child: Text(language.toUpperCase()),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedLanguage = value!;
});
},
),
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Font Style'),
DropdownButton<String>(
value: _selectedFontStyle,
isExpanded: true,
items: _fontStyles.map((style) {
return DropdownMenuItem(
value: style,
child: Text(style.toUpperCase()),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedFontStyle = value!;
});
},
),
],
),
),
],
),
const SizedBox(height: 16),
// Generate Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _generateCalligraphy,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.sky.shade500,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: const Text(
'Generate Calligraphy',
style: TextStyle(fontSize: 16),
),
),
),
],
),
),
),
const SizedBox(height: 16),
// Results Section
if (_results.isNotEmpty)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Generated Results (${_results.length})',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.2,
),
itemCount: _results.length,
itemBuilder: (context, index) {
final result = _results[index];
return Card(
child: Column(
children: [
Expanded(
child: result.cdnUrl != null
? CachedNetworkImage(
imageUrl: result.cdnUrl!,
fit: BoxFit.contain,
placeholder: (context, url) =>
const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
)
: const Center(
child: Text('No image available'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.share),
onPressed: result.cdnUrl != null
? () => _shareImage(result.cdnUrl!)
: null,
tooltip: 'Share',
),
IconButton(
icon: const Icon(Icons.download),
onPressed: result.cdnUrl != null
? () => CalligraphyService
.downloadAndSaveImage(
result.cdnUrl!)
: null,
tooltip: 'Download',
),
],
),
),
],
),
);
},
),
),
],
),
),
],
),
),
);
}
}Advanced Features
State Management with Provider
dart
Click to copy
// lib/providers/calligraphy_provider.dart
import 'package:flutter/foundation.dart';
import '../services/calligraphy_service.dart';
class CalligraphyProvider with ChangeNotifier {
bool _isLoading = false;
List<CalligraphyResult> _results = [];
String? _errorMessage;
CalligraphyUsage? _usage;
bool get isLoading => _isLoading;
List<CalligraphyResult> get results => _results;
String? get errorMessage => _errorMessage;
CalligraphyUsage? get usage => _usage;
Future<void> generateCalligraphy({
required String text,
String language = 'hindi',
String fontStyle = 'calligraphy',
int count = 3,
}) async {
_setLoading(true);
_clearError();
try {
final response = await CalligraphyService.generateCalligraphy(
text: text,
language: language,
fontStyle: fontStyle,
count: count,
);
if (response != null) {
_results = response.results;
_usage = response.usage;
} else {
_setError('Failed to generate calligraphy');
}
} catch (e) {
_setError('Error: ${e.toString()}');
} finally {
_setLoading(false);
}
}
void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
void _setError(String error) {
_errorMessage = error;
notifyListeners();
}
void _clearError() {
_errorMessage = null;
notifyListeners();
}
void clearResults() {
_results = [];
notifyListeners();
}
}
// Usage in main.dart
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CalligraphyProvider()),
],
child: const MyApp(),
),
);
}Image Caching & Offline Support
dart
Click to copy
// lib/services/cache_service.dart
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';
class CacheService {
static Future<String> get _cacheDirectory async {
final directory = await getApplicationCacheDirectory();
final cacheDir = Directory('${directory.path}/calligraphy_cache');
if (!await cacheDir.exists()) {
await cacheDir.create(recursive: true);
}
return cacheDir.path;
}
static String _generateCacheKey(String text, String language, String fontStyle) {
final input = '$text-$language-$fontStyle';
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}
static Future<void> cacheResults(
String text,
String language,
String fontStyle,
CalligraphyResponse response,
) async {
try {
final cacheDir = await _cacheDirectory;
final cacheKey = _generateCacheKey(text, language, fontStyle);
final file = File('$cacheDir/$cacheKey.json');
final cacheData = {
'timestamp': DateTime.now().millisecondsSinceEpoch,
'response': response.toJson(),
};
await file.writeAsString(jsonEncode(cacheData));
} catch (e) {
print('Error caching results: $e');
}
}
static Future<CalligraphyResponse?> getCachedResults(
String text,
String language,
String fontStyle, {
Duration maxAge = const Duration(hours: 24),
}) async {
try {
final cacheDir = await _cacheDirectory;
final cacheKey = _generateCacheKey(text, language, fontStyle);
final file = File('$cacheDir/$cacheKey.json');
if (!await file.exists()) return null;
final cacheContent = await file.readAsString();
final cacheData = jsonDecode(cacheContent);
final timestamp = DateTime.fromMillisecondsSinceEpoch(cacheData['timestamp']);
final age = DateTime.now().difference(timestamp);
if (age > maxAge) {
await file.delete();
return null;
}
return CalligraphyResponse.fromJson(cacheData['response']);
} catch (e) {
print('Error reading cache: $e');
return null;
}
}
static Future<void> clearCache() async {
try {
final cacheDir = await _cacheDirectory;
final directory = Directory(cacheDir);
await directory.delete(recursive: true);
} catch (e) {
print('Error clearing cache: $e');
}
}
}Error Handling & Validation
dart
Click to copy
// lib/utils/api_error_handler.dart
enum ApiErrorType {
network,
authentication,
rateLimit,
insufficientCredits,
validation,
server,
unknown,
}
class ApiError {
final ApiErrorType type;
final String message;
final int? statusCode;
ApiError({
required this.type,
required this.message,
this.statusCode,
});
static ApiError fromHttpResponse(int statusCode, String body) {
try {
final data = jsonDecode(body);
final errorMessage = data['error'] ?? 'Unknown error occurred';
switch (statusCode) {
case 401:
return ApiError(
type: ApiErrorType.authentication,
message: 'Invalid API key. Please check your credentials.',
statusCode: statusCode,
);
case 402:
return ApiError(
type: ApiErrorType.insufficientCredits,
message: 'Insufficient credits. Please upgrade your plan.',
statusCode: statusCode,
);
case 429:
return ApiError(
type: ApiErrorType.rateLimit,
message: 'Rate limit exceeded. Please try again later.',
statusCode: statusCode,
);
case 400:
return ApiError(
type: ApiErrorType.validation,
message: errorMessage,
statusCode: statusCode,
);
default:
return ApiError(
type: ApiErrorType.server,
message: errorMessage,
statusCode: statusCode,
);
}
} catch (e) {
return ApiError(
type: ApiErrorType.unknown,
message: 'Failed to parse error response',
statusCode: statusCode,
);
}
}
static ApiError networkError() {
return ApiError(
type: ApiErrorType.network,
message: 'Network error. Please check your internet connection.',
);
}
}
// Input validation utility
class TextValidator {
static String? validateCalligraphyText(String? text) {
if (text == null || text.trim().isEmpty) {
return 'Please enter some text';
}
if (text.trim().length < 2) {
return 'Text must be at least 2 characters long';
}
if (text.trim().length > 200) {
return 'Text must be less than 200 characters';
}
return null;
}
static bool isValidLanguage(String language) {
const validLanguages = ['hindi', 'marathi', 'gujarati', 'english'];
return validLanguages.contains(language.toLowerCase());
}
static bool isValidFontStyle(String fontStyle) {
const validStyles = ['calligraphy', 'decorative', 'publication'];
return validStyles.contains(fontStyle.toLowerCase());
}
}
// Enhanced service with error handling
class EnhancedCalligraphyService {
static Future<CalligraphyResponse> generateCalligraphyWithErrorHandling({
required String text,
String language = 'hindi',
String fontStyle = 'calligraphy',
int count = 1,
}) async {
// Validate input
final textError = TextValidator.validateCalligraphyText(text);
if (textError != null) {
throw ApiError(
type: ApiErrorType.validation,
message: textError,
);
}
if (!TextValidator.isValidLanguage(language)) {
throw ApiError(
type: ApiErrorType.validation,
message: 'Invalid language. Supported: hindi, marathi, gujarati, english',
);
}
try {
final response = await http.post(
Uri.parse('${CalligraphyService._baseUrl}/generate'),
headers: {
'Content-Type': 'application/json',
'x-api-key': CalligraphyService._apiKey,
},
body: jsonEncode({
'text': text.trim(),
'language': language,
'fontStyle': fontStyle,
'count': count,
'imageFormat': 'png',
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success']) {
return CalligraphyResponse.fromJson(data['data']);
} else {
throw ApiError(
type: ApiErrorType.server,
message: data['error'] ?? 'Generation failed',
);
}
} else {
throw ApiError.fromHttpResponse(response.statusCode, response.body);
}
} on SocketException {
throw ApiError.networkError();
} on FormatException {
throw ApiError(
type: ApiErrorType.unknown,
message: 'Invalid response format from server',
);
} catch (e) {
if (e is ApiError) rethrow;
throw ApiError(
type: ApiErrorType.unknown,
message: 'Unexpected error: ${e.toString()}',
);
}
}
}What You'll Build
Cross-platform App
Single codebase for iOS and Android
State Management
Provider pattern for scalable apps
Image Caching
Efficient image loading and caching
Offline Support
Cache results for offline viewing
Requirements
Flutter 3.0+
Dart 3.0+
API Key from Calligraphy API
iOS/Android development setup
Other Integrations