π‘οΈ Zard Documentation
Zard is a schema validation and transformation library for Dart, inspired by the popular Zod library for JavaScript. With Zard, you can define schemas to validate and transform data easily and intuitively.
If you find Zard useful, please consider supporting its development π Buy Me a Coffee π. Your support helps us improve the framework and make it even better!
Add the following line to your pubspec.yaml:
dependencies:
zard: ^0.0.25Then, run:
flutter pub getOr run:
dart pub add zardZard allows you to define schemas for various data types. Below are several examples of how to use Zard, including handling errors either by using parse (which throws errors) or safeParse (which returns a success flag and error details).
import 'package:zard/zard.dart';
void main() {
// String validations with minimum and maximum length and email format check.
final schema = z.string().min(3).max(10).email(message: "Invalid email address");
// Using parse (throws if there is an error)
try {
final result = schema.parse("example@example.com");
print("Parsed Value: $result"); // example@example.com
} catch (e) {
print("Errors (parse): ${schema.getErrors()}");
}
// Using safeParse (doesn't throw; returns error info in result object)
final safeResult = schema.safeParse("Hi"); // "Hi" is too short
if (!safeResult['success']) {
safeResult['errors'].forEach((error) => print("Safe Error: $error")); // Output error messages π±
} else {
print("Safe Parsed Value: ${safeResult['data']}");
}
}Zard acrescentou validaΓ§Γ΅es especΓficas para emails usando padrΓ΅es (RegExp) reutilizΓ‘veis. Por padrΓ£o, z.string().email() valida usando o padrΓ£o HTML5 (compatΓvel com a validaΓ§Γ£o dos navegadores) que permite domΓnios de etiqueta ΓΊnica (ex: john@example). Γ possΓvel passar um pattern para escolher outro comportamento.
PadrΓ΅es disponΓveis em z.regexes:
html5Emailβ padrΓ£o usado por navegadores (permitejohn@example).emailβ mais estrito, exige um TLD (ex:example.com).rfc5322Emailβ implementaΓ§Γ£o mais completa que segue a especificaΓ§Γ£o RFC 5322 (aceita local-parts com aspas, tags, etc.).unicodeEmailβ permissivo para caracteres nΓ£o-ASCII (bom para emails internacionais), mas simples.
Exemplos rΓ‘pidos:
// 1) padrΓ£o HTML5 (padrΓ£o do navegador)
final html5 = z.string().email();
print(html5.parse('john@example')); // vΓ‘lido com html5Email
// 2) forΓ§ar padrΓ£o HTML5 explicitamente
final html5explicit = z.string().email(pattern: z.regexes.html5Email);
print(html5explicit.parse('john@example'));
// 3) padrΓ£o mais estrito (exige TLD)
final strict = z.string().email(pattern: z.regexes.email);
print(strict.parse('john@example.com')); // vΓ‘lido
// strict.parse('john@example'); // lanΓ§a erro
// 4) RFC5322 (mais completo)
final rfc = z.string().email(pattern: z.regexes.rfc5322Email);
print(rfc.parse('"john.doe"@example.co.uk')); // vΓ‘lido se atender RFC
// 5) Unicode (aceita caracteres nΓ£o-ASCII)
final uni = z.string().email(pattern: z.regexes.unicodeEmail);
print(uni.parse('usuΓ‘rio@exemplo.com'));Use o pattern quando quiser controlar exatamente quais formatos de e-mail sΓ£o aceitos no seu domΓnio ou aplicaΓ§Γ£o.
Zard adiciona um validador conveniente para URLs via z.string().url() com opΓ§Γ΅es para restringir hostname e protocol usando RegExp personalizados.
Exemplos:
import 'package:zard/zard.dart';
void main() {
// 1) PadrΓ£o: aceita http(s) opcional e hostname genΓ©rico
final urlSchema = z.string().url();
print(urlSchema.parse('https://www.example.com'));
// 2) ForΓ§ar hostname que termine com .example.com
final urlWithHostnameSchema =
z.string().url(hostname: RegExp(r'^[\w\.-]+\.example\.com$'));
print(urlWithHostnameSchema.parse('https://api.example.com/path'));
// 3) ForΓ§ar protocolo (por exemplo: somente https)
final urlProtocolSchema = z.string().url(protocol: RegExp(r'^https:\/\/'));
print(urlProtocolSchema.parse('https://secure.example.com'));
// 4) Hostname + protocolo personalizados simultaneamente
final urlAllSchema = z.string().url(
hostname: RegExp(r'^[\w\.-]+\.example\.com$'),
protocol: RegExp(r'^https:\/\/'),
);
print(urlAllSchema.parse('https://api.example.com/endpoint'));
}ObservaΓ§Γ΅es importantes:
- VocΓͺ pode passar
RegExpemhostnamee/ouprotocol. PadrΓ΅es com Γ’ncoras^e$sΓ£o aceitos β o validador remove esses anchors internamente ao compor a regex final para evitar conflitos. - A sensibilidade a maiΓΊsculas/minΓΊsculas (
isCaseSensitive) dosRegExpque vocΓͺ fornecer Γ© respeitada; por padrΓ£o a validaΓ§Γ£o Γ© case-insensitive quando nenhumRegExpespecifica o contrΓ‘rio. - O comportamento padrΓ£o permite protocole opcional (
http/https). Para forΓ§ar protocolo, forneΓ§a umRegExpapropriado (por exemploRegExp(r'^https:\/\/')).
Zard adiciona helpers e validadores convenientes para operaΓ§Γ΅es comuns em strings:
- uppercase() β validador: exige que o valor jΓ‘ esteja todo em maiΓΊsculas.
- lowercase() β validador: exige que o valor jΓ‘ esteja todo em minΓΊsculas.
- toUpperCase() β transform: converte o valor para maiΓΊsculas.
- toLowerCase() β transform: converte o valor para minΓΊsculas.
- trim() β transform: remove espaΓ§os do inΓcio/fim da string.
- normalize() β transform: remove acentos/diacrΓticos (usa package string_normalizer), remove caracteres de controle, faz trim e colapsa mΓΊltiplos whitespace em um ΓΊnico espaΓ§o.
Exemplos:
import 'package:zard/zard.dart';
void main() {
// 1) Validador uppercase: aceita apenas strings jΓ‘ em MAIΓSCULAS
final mustBeUpper = z.string().uppercase();
expect(mustBeUpper.parse('ABC'), equals('ABC'));
// mustBeUpper.parse('AbC'); // lanΓ§a ZardError
// 2) Validador lowercase: aceita apenas strings jΓ‘ em minΓΊsculas
final mustBeLower = z.string().lowercase();
expect(mustBeLower.parse('abc'), equals('abc'));
// mustBeLower.parse('aBc'); // lanΓ§a ZardError
// 3) Transform toUpperCase / toLowerCase
final toUpper = z.string().toUpperCase();
expect(toUpper.parse('hello'), equals('HELLO'));
final toLower = z.string().toLowerCase();
expect(toLower.parse('HELLO'), equals('hello'));
// 4) Trim
final trimmed = z.string().trim();
expect(trimmed.parse(' hello '), equals('hello'));
// 5) Normalize (remove acentos/diacrΓticos, trim, collapse whitespace)
final normalized = z.string().normalize();
// 'ÑéΓ' -> 'aei', ' e\n outra ' -> 'e outra'
expect(normalized.parse(' Ñéà '), equals('aei'));
expect(normalized.parse(' linha\n final '), equals('linha final'));
}ObservaΓ§Γ΅es:
- Os mΓ©todos validators (uppercase/lowercase) apenas validam o estado atual do valor; se quiser transformar automaticamente, use toUpperCase()/toLowerCase().
- normalize() depende do package string_normalizer para remoΓ§Γ£o de acentos/diacrΓticos e aplica limpeza adicional descrita acima.
Zard oferece um schema conveniente para interpretar strings como booleanos via z.stringbool().
Ele aceita valores booleanos, numΓ©ricos e strings que representam estados verdadeiros ou falsos.
Tokens reconhecidos (case-insensitive, com trim):
- Verdadeiros:
1,true,yes,on,y,enabled - Falsos:
0,false,no,off,n,disabled
Exemplos:
import 'package:zard/zard.dart';
void main() {
final strbool = z.stringbool();
// Valores que resultam em true
print(strbool.parse('1')); // true
print(strbool.parse('yes')); // true
print(strbool.parse('ON')); // true
print(strbool.parse(' enabled ')); // true (trim + case-insensitive)
// Valores que resultam em false
print(strbool.parse('0')); // false
print(strbool.parse('no')); // false
print(strbool.parse('Off')); // false
print(strbool.parse('disabled')); // false
// TambΓ©m aceita bool e nΓΊmeros
print(strbool.parse(true)); // true
print(strbool.parse(0)); // false
// Valores nΓ£o reconhecidos lanΓ§am ZardError
// strbool.parse('maybe'); // throws ZardError
}ObservaΓ§Γ΅es:
- O mΓ©todo
parse()retorna umboolquando a entrada Γ© reconhecida; caso contrΓ‘rio lanΓ§aZardErrorcom detalhes do problema. - Se precisar de comportamento de coerΓ§Γ£o mais permissivo (por exemplo tratar qualquer valor nΓ£o-vazio como
true), usez.coerce.boolean().
Zard fornece uma sΓ©rie de validadores especializados para tipos comuns de strings (URLs, IPs, hashes, etc.):
Identificadores e UUIDs:
guid()β GUID/UUID v4uuid(version)β UUID genΓ©rico (v1-v8) ou versΓ£o especΓficananoid()β Nano ID (21 caracteres)ulid()β ULID (Universally Unique Lexicographically Sortable Identifier)
Redes e Protocolos:
httpUrl()β URLs HTTP/HTTPS apenashostname()β Hostname vΓ‘lidoipv4()β EndereΓ§o IPv4ipv6()β EndereΓ§o IPv6mac()β EndereΓ§o MAC (ex:AA:BB:CC:DD:EE:FF)cidrv4()β Bloco CIDR IPv4 (ex:192.168.1.0/24)cidrv6()β Bloco CIDR IPv6
CodificaΓ§Γ΅es e Hashes:
base64()β Base64 padrΓ£obase64url()β Base64 URL-safehex()β Hexadecimalhash(algorithm)β Hash validado por algoritmo (suportasha1,sha256,sha384,sha512,md5)jwt()β JSON Web Token
Outros Formatos:
emoji()β Um ΓΊnico caractere emoji
Exemplos:
import 'package:zard/zard.dart';
void main() {
// UUIDs
final guidSchema = z.string().guid();
print(guidSchema.parse('550e8400-e29b-41d4-a716-446655440000')); // vΓ‘lido
final uuidSchema = z.string().uuid(version: 'v4');
print(uuidSchema.parse('550e8400-e29b-41d4-a716-446655440000')); // vΓ‘lido
// Identificadores
final nanoidSchema = z.string().nanoid();
print(nanoidSchema.parse('V1StGXR_Z5j3eK4CFLQ')); // 21 caracteres
final ulidSchema = z.string().ulid();
print(ulidSchema.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')); // vΓ‘lido
// URLs e Hosts
final httpUrlSchema = z.string().httpUrl();
print(httpUrlSchema.parse('https://example.com')); // vΓ‘lido
final hostnameSchema = z.string().hostname();
print(hostnameSchema.parse('api.example.com')); // vΓ‘lido
// Redes
final ipv4Schema = z.string().ipv4();
print(ipv4Schema.parse('192.168.1.1')); // vΓ‘lido
final ipv6Schema = z.string().ipv6();
print(ipv6Schema.parse('2001:0db8:85a3:0000:0000:8a2e:0370:7334')); // vΓ‘lido
final macSchema = z.string().mac();
print(macSchema.parse('AA:BB:CC:DD:EE:FF')); // vΓ‘lido
final cidrv4Schema = z.string().cidrv4();
print(cidrv4Schema.parse('192.168.1.0/24')); // vΓ‘lido
// CodificaΓ§Γ΅es
final base64Schema = z.string().base64();
print(base64Schema.parse('SGVsbG8gV29ybGQ=')); // vΓ‘lido
final base64urlSchema = z.string().base64url();
print(base64urlSchema.parse('SGVs-bG8tV29ybGQ')); // vΓ‘lido (URL-safe)
final hexSchema = z.string().hex();
print(hexSchema.parse('48656C6C6F')); // vΓ‘lido (paridade par)
// Hashes
final sha256Schema = z.string().hash('sha256');
print(sha256Schema.parse('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')); // vΓ‘lido
final md5Schema = z.string().hash('md5');
print(md5Schema.parse('5d41402abc4b2a76b9719d911017c592')); // vΓ‘lido
// JWT
final jwtSchema = z.string().jwt();
print(jwtSchema.parse('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ')); // vΓ‘lido
// Emoji
final emojiSchema = z.string().emoji();
print(emojiSchema.parse('π')); // vΓ‘lido
}Zard fornece validadores especializados para formatos ISO 8601, acessΓveis via namespace z.iso.*:
z.iso.date()β Data ISO (YYYY-MM-DD)z.iso.time()β Hora ISO (HH:mm:ss ou com milissegundos)z.iso.datetime()β Data e hora ISO 8601 (com ou sem Z)z.iso.duration()β DuraΓ§Γ£o ISO 8601 (ex: P1DT2H3M4S)
Exemplos:
import 'package:zard/zard.dart';
void main() {
// ISO Date
final isoDateSchema = z.iso.date();
print(isoDateSchema.parse('2021-01-01')); // vΓ‘lido
// isoDateSchema.parse('01/01/2021'); // erro
// ISO Time
final isoTimeSchema = z.iso.time();
print(isoTimeSchema.parse('12:30:45')); // vΓ‘lido
print(isoTimeSchema.parse('12:30:45.123')); // vΓ‘lido (com milissegundos)
// ISO DateTime
final isoDatetimeSchema = z.iso.datetime();
print(isoDatetimeSchema.parse('2021-01-01T12:30:45Z')); // vΓ‘lido
print(isoDatetimeSchema.parse('2021-01-01T12:30:45')); // vΓ‘lido
// ISO Duration
final isoDurationSchema = z.iso.duration();
print(isoDurationSchema.parse('P1Y2M3DT4H5M6S')); // 1 ano, 2 meses, 3 dias, 4h, 5m, 6s
print(isoDurationSchema.parse('P1D')); // 1 dia
print(isoDurationSchema.parse('PT5H')); // 5 horas
print(isoDurationSchema.parse('P1W')); // 1 semana
}Formatos ISO 8601 Duration:
P= perΓodoY= yearsM= months (antes deT) ou minutes (apΓ³sT)W= weeksD= daysT= separador (hora/minutos/segundos)H= hoursS= seconds
Exemplos vΓ‘lidos: P3Y, P2M, P1W, P1D, PT1H, PT30M, PT45S, P1DT2H30M
import 'package:zard/zard.dart';
void main() {
// Integer validations with minimum and maximum checks.
final schema = z.int().min(1).max(100);
// Using parse
try {
final result = schema.parse(50);
print("Parsed Value: $result"); // 50
} catch (e) {
print("Errors (parse): ${schema.getErrors()}");
}
// Using safeParse with error handling
final safeResult = schema.safeParse(5); // example: if 5 is below the minimum, it returns errors
if (!safeResult['success']) {
safeResult['errors'].forEach((error) => print("Safe Error: $error")); // Output error messages
} else {
print("Safe Parsed Value: ${safeResult['data']}");
}
}import 'package:zard/zard.dart';
void main() {
// Double validations with minimum and maximum checks.
final schema = z.doubleType().min(1.0).max(100.0);
try {
final result = schema.parse(50.5);
print("Parsed Value: $result"); // 50.5
} catch (e) {
print("Errors (parse): ${schema.getErrors()}");
}
final safeResult = schema.safeParse(0.5);
if (!safeResult['success']) {
safeResult['errors'].forEach((error) => print("Safe Error: $error")); // Outputs error message if invalid
} else {
print("Safe Parsed Value: ${safeResult['data']}");
}
}import 'package:zard/zard.dart';
void main() {
// Boolean validations
final schema = z.boolean();
try {
final result = schema.parse(true);
print("Parsed Value: $result"); // true
} catch (e) {
print("Errors (parse): ${schema.getErrors()}");
}
final safeResult = schema.safeParse(false);
if (!safeResult['success']) {
safeResult['errors'].forEach((error) => print("Safe Error: $error"));
} else {
print("Safe Parsed Value: ${safeResult['data']}");
}
}import 'package:zard/zard.dart';
void main() {
// List validations with inner string schema validations.
final schema = z.list(z.string().min(3));
try {
final result = schema.parse(["abc", "def"]);
print("Parsed Value: $result"); // [abc, def]
} catch (e) {
print("Errors (parse): ${schema.getErrors()}");
}
final safeResult = schema.safeParse(["ab", "def"]); // "ab" is too short
if (!safeResult['success']) {
safeResult['errors'].forEach((error) => print("Safe Error: $error"));
} else {
print("Safe Parsed Value: ${safeResult['data']}");
}
}import 'package:zard/zard.dart';
void main() {
// Map validations combining multiple schemas
final schema = z.map({
'name': z.string().min(3).nullable(),
'age': z.int().min(1).nullable(),
'email': z.string().email()
}).refine((value) {
return value['age'] > 18;
}, message: 'Age must be greater than 18');
final result = schema.safeParse({
'name': 'John Doe',
'age': 20,
'email': 'john.doe@example.com',
});
print(result);
final result2 = schema.safeParse({
'name': 'John Doe',
'age': 10,
'email': 'john.doe@example.com',
});
print(result2);
}import 'package:zard/zard.dart';
void main() {
// Date validations
final schema = z.date();
try {
final result = schema.parse(DateTime.now());
print("Parsed Value: $result"); // 2025-11-26T10:30:00.000
} catch (e) {
print("Errors (parse): ${schema.getErrors()}");
}
final safeResult = schema.safeParse("2025-11-26");
if (!safeResult.success) {
print("Safe Error: ${safeResult.error}");
} else {
print("Safe Parsed Value: ${safeResult.data}");
}
}import 'package:zard/zard.dart';
void main() {
// Enum validations with allowed values
final schema = z.$enum(['pending', 'active', 'inactive']);
try {
final result = schema.parse('active');
print("Parsed Value: $result"); // active
} catch (e) {
print("Errors (parse): ${schema.getErrors()}");
}
final safeResult = schema.safeParse('unknown');
if (!safeResult.success) {
print("Safe Error: Value must be one of [pending, active, inactive]");
} else {
print("Safe Parsed Value: ${safeResult.data}');
}
// Extract or exclude values from enum
final extractedSchema = schema.extract(['active', 'pending']);
print("Extracted: $extractedSchema"); // Only allows 'active' and 'pending'
final excludedSchema = schema.exclude(['inactive']);
print("Excluded: $excludedSchema"); // Allows everything except 'inactive'
}import 'package:zard/zard.dart';
void main() {
// Define default values for schemas
final schema = z.map({
'name': z.string(),
'status': z.string().$default('active'),
'age': z.int().$default(18),
});
// When 'status' and 'age' are omitted, defaults are used
final result = schema.parse({
'name': 'John Doe',
});
print(result); // {name: John Doe, status: active, age: 18}
// When values are explicitly null, defaults are applied
final result2 = schema.parse({
'name': 'Jane Doe',
'status': null,
'age': null,
});
print(result2); // {name: Jane Doe, status: active, age: 18}
// When values are provided, they override defaults
final result3 = schema.parse({
'name': 'Bob Smith',
'status': 'inactive',
'age': 30,
});
print(result3); // {name: Bob Smith, status: inactive, age: 30}
}import 'package:zard/zard.dart';
void main() {
// Coerce converts values to the expected type
final intSchema = z.coerce.int().parse("123");
print("Coerced int: $intSchema"); // 123
final doubleSchema = z.coerce.double().parse("3.14");
print("Coerced double: $doubleSchema"); // 3.14
final boolSchema = z.coerce.bool().parse("true");
print("Coerced bool: $boolSchema"); // true
final stringSchema = z.coerce.string().parse(123);
print("Coerced string: $stringSchema"); // "123"
final dateSchema = z.coerce.date().parse("2025-11-26");
print("Coerced date: $dateSchema"); // 2025-11-26T00:00:00.000
}import 'package:zard/zard.dart';
void main() {
// Lazy schemas are useful for recursive or circular schema definitions
late Schema<Map<String, dynamic>> userSchema;
userSchema = z.map({
'name': z.string(),
'email': z.string().email(),
'friends': z.lazy(() => userSchema).list().optional(),
});
final user = userSchema.parse({
'name': 'John Doe',
'email': 'john@example.com',
'friends': [
{
'name': 'Jane Doe',
'email': 'jane@example.com',
}
],
});
print(user); // Recursively parsed user with friends
}import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'email': z.string().email().transform((value) => value.toLowerCase()),
'name': z.string().transform((value) => value.toUpperCase()),
});
final result = schema.parse({
'email': 'JOHN@EXAMPLE.COM',
'name': 'john doe',
});
print(result); // {email: john@example.com, name: JOHN DOE}
}import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'name': z.string(),
'nickname': z.string().optional(), // Can be omitted
'middleName': z.string().nullable(), // Can be null if provided
'age': z.int().nullish(), // Can be omitted or null
});
final result = schema.safeParse({
'name': 'John Doe',
'age': null,
});
if (result.success) {
print(result.data); // {name: John Doe, age: null}
}
}import 'package:zard/zard.dart';
void main() {
final schema = z.map({
'name': z.string(),
'email': z.string().email(),
}).strict(); // Disallow extra fields
// This will throw an error due to the extra 'phone' field
try {
final result = schema.parse({
'name': 'John Doe',
'email': 'john@example.com',
'phone': '123-456-7890', // Extra field not allowed
});
} catch (e) {
print("Error: Unexpected key 'phone' found in object");
}
}When a validation fails, Zard provides detailed error information via the ZardError class. Each error object contains:
- message: A descriptive message about what went wrong.
- type: The type of error (e.g.,
min_error,max_error,type_error). - value: The unexpected value that failed validation.
Zard supports two methods for validation:
parse(): Throws an exception if any validation fails.safeParse(): Returns an object with asuccessflag and a list of errors without throwing exceptions.
Zard now supports additional methods to handle asynchronous validations and custom refine checks for Map schemas. These new methods help you integrate asynchronous operations and write custom validations easily!
-
Asynchronous Validation
parseAsync(): Returns aFuturethat resolves with the parsed value or throws an error if validation fails.safeParseAsync(): Works likesafeParse(), but returns aFuturewith a success flag and error details.- These methods ensure that if your input is a
Future, Zard waits for its resolution before parsing.
-
Refine Method on Map Schemas
refine(): Allows you to add custom validation logic onMapschemas.- It accepts a function that receives the parsed value and returns a boolean. If the function returns
false, arefine_erroris added with a custom message. - This feature is especially useful for validating inter-dependent fieldsβfor example, ensuring that an
agefield is greater than 18 in a user profile map.
-
InferType Method
inferType(): Allows you to create a typed schema that validates a Map and transforms it into a specific model instance.- It combines a Map validation schema with a conversion function, enabling type-safe validation and transformation in a single operation.
- This feature is especially useful for creating strongly-typed schemas for your data models while maintaining all validation capabilities including
refine().
Example usage of refine() in a Map schema:
final schema = z.map({
'name': z.string(),
'age': z.int(),
'email': z.string().email()
}).refine((value) {
return value['age'] > 18;
}, message: 'Age must be greater than 18');
final result = schema.safeParse({
'name': 'John Doe',
'age': 20,
'email': 'john.doe@example.com',
});
print(result); // {success: true, data: {...}}
final result2 = schema.safeParse({
'name': 'John Doe',
'age': 10,
'email': 'john.doe@example.com',
});
print(result2); // {success: false, errors: [...]}Example usage of inferType():
final userSchema = z.inferType<User>(
fromMap: (map) => User.fromMap(map),
mapSchema: schema,
).refine(
(value) => value.age >= 18,
message: 'User must be at least 18 years old',
);
final user = userSchema.parse({
'name': 'John Doe',
'age': 25,
});
print(user.name); // John DoeZard was inspired by Zod, a powerful schema validation library for JavaScript. Just like Zod, Zard provides an easy-to-use API for defining and transforming schemas. The main difference is that Zard is built specifically for Dart and Flutter, harnessing the power of Dart's language features.
Contributions are welcome! Feel free to open issues and pull requests on the GitHub repository.
This project is licensed under the MIT License. See the LICENSE file for more details.
Made with β€οΈ for Dart/Flutter developers! π―β¨
