ydb_flutter_sdk 0.2.1 copy "ydb_flutter_sdk: ^0.2.1" to clipboard
ydb_flutter_sdk: ^0.2.1 copied to clipboard

Yandex Database SDK for Dart and Flutter with gRPC transport, complete type system, and authentication support

example/lib/main.dart

// ignore_for_file: avoid_print

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:ydb_flutter_sdk/ydb_flutter_sdk.dart';

void main() {
  // Enable logging
  Logger.root.level = Level.ALL;
  Logger.root.onRecord.listen((record) {
    print('${record.level.name}: ${record.time}: ${record.message}');
  });

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<DriverConfig>(
      future: _loadConfig(),
      builder: (BuildContext context, AsyncSnapshot<DriverConfig> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return MaterialApp(
            home: Scaffold(
              body: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: const <Widget>[
                    CircularProgressIndicator(),
                    SizedBox(height: 16),
                    Text('Loading configuration...'),
                  ],
                ),
              ),
            ),
          );
        }

        if (snapshot.hasError) {
          return MaterialApp(
            home: Scaffold(
              body: Center(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      const Icon(Icons.error, color: Colors.red, size: 48),
                      const SizedBox(height: 16),
                      const Text(
                        'Failed to load configuration',
                        style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        snapshot.error.toString(),
                        textAlign: TextAlign.center,
                        style: const TextStyle(color: Colors.grey),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          );
        }

        final DriverConfig config = snapshot.data!;
        final YdbDriver driver = YdbDriver(config);

        return MaterialApp(
          title: 'YDB Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: YdbConnectionWidget(
            driver: driver,
            builder:
                (
                  BuildContext context,
                  YdbConnectionState state,
                  Object? error,
                ) {
                  if (state == YdbConnectionState.connected) {
                    return const ContactListPage();
                  } else if (state == YdbConnectionState.error) {
                    return Scaffold(
                      body: YdbErrorWidget(
                        error: error!,
                        message: 'Failed to connect to YDB',
                        showStackTrace: true,
                      ),
                    );
                  }
                  return const Scaffold(
                    body: Center(
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          CircularProgressIndicator(),
                          SizedBox(height: 16),
                          Text('Connecting to YDB...'),
                        ],
                      ),
                    ),
                  );
                },
          ),
        );
      },
    );
  }

  Future<DriverConfig> _loadConfig() async {
    // Load Service Account credentials from assets
    final String jsonString = await rootBundle.loadString('assets/sa-key.json');
    final Map<String, dynamic> saData =
        jsonDecode(jsonString) as Map<String, dynamic>;

    final String serviceAccountId = saData['service_account_id'] as String;
    final String keyId = saData['id'] as String;

    // Parse private key properly
    String rawKey = saData['private_key'] as String;

    // Remove any existing whitespace and newlines
    rawKey = rawKey.replaceAll(RegExp(r'\s+'), '');

    // Extract the base64 content between headers
    final beginMarker = '-----BEGINPRIVATEKEY-----';
    final endMarker = '-----ENDPRIVATEKEY-----';

    final startIdx = rawKey.indexOf(beginMarker);
    final endIdx = rawKey.indexOf(endMarker);

    if (startIdx == -1 || endIdx == -1) {
      throw FormatException('Invalid PEM format: missing BEGIN or END markers');
    }

    final base64Content = rawKey.substring(
      startIdx + beginMarker.length,
      endIdx,
    );

    // Reconstruct proper PEM format with 64 chars per line
    final buffer = StringBuffer('-----BEGIN PRIVATE KEY-----\n');
    for (int i = 0; i < base64Content.length; i += 64) {
      final end = (i + 64 < base64Content.length)
          ? i + 64
          : base64Content.length;
      buffer.write(base64Content.substring(i, end));
      buffer.write('\n');
    }
    buffer.write('-----END PRIVATE KEY-----');

    final privateKey = buffer.toString();

    final signer = RsaJwtSigner.fromPem(privateKey, keyId: keyId);

    // Создаем HTTP client для IAM API
    final iamClient = IamHttpClient(
      baseUrl: 'https://iam.api.cloud.yandex.net',
    );

    return DriverConfig(
      endpoint: 'grpcs://ydb.serverless.yandexcloud.net:2135',
      database: '/ru-central1/b1.....',
      credentials: YdbAuth.serviceAccount(
        serviceAccountId: serviceAccountId,
        signer: signer,
        transport: iamClient,
      ),
    );
  }
}

class ContactListPage extends StatefulWidget {
  const ContactListPage({super.key});

  @override
  State<ContactListPage> createState() => _ContactListPageState();
}

class _ContactListPageState extends State<ContactListPage> {
  bool _tableInitialized = false;

  @override
  void initState() {
    super.initState();
    _initializeTable();
  }

  Future<void> _initializeTable() async {
    // Note: In production, check if table exists first
    setState(() {
      _tableInitialized = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!_tableInitialized) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('YDB Contacts'),
        actions: <Widget>[
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              setState(() {}); // Trigger rebuild to refresh query
            },
          ),
        ],
      ),
      body: YdbQueryBuilder<Contact>(
        driver: context
            .findAncestorWidgetOfExactType<YdbConnectionWidget>()!
            .driver,
        query: 'SELECT * FROM contacts ORDER BY id',
        mapper: Contact.fromRow,
        builder: (BuildContext context, AsyncSnapshot<List<Contact>> snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return YdbErrorWidget(
              error: snapshot.error!,
              stackTrace: snapshot.stackTrace,
              message: 'Failed to load contacts',
              showStackTrace: true,
              onRetry: () {
                setState(() {}); // Trigger refresh
              },
            );
          }

          final List<Contact> contacts = snapshot.data ?? <Contact>[];

          if (contacts.isEmpty) {
            return const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(Icons.contacts_outlined, size: 64, color: Colors.grey),
                  SizedBox(height: 16),
                  Text('No contacts found'),
                  SizedBox(height: 8),
                  Text(
                    'Tap + to add a contact',
                    style: TextStyle(color: Colors.grey),
                  ),
                ],
              ),
            );
          }

          return ListView.builder(
            itemCount: contacts.length,
            itemBuilder: (BuildContext context, int index) {
              final Contact contact = contacts[index];
              return ListTile(
                leading: CircleAvatar(
                  child: Text(contact.userName[0].toUpperCase()),
                ),
                title: Text(contact.userName),
                subtitle: Text(
                  '${contact.contactValue} (Ch: ${contact.channelId})',
                ),
                trailing: contact.isActive
                    ? const Icon(
                        Icons.check_circle,
                        color: Colors.green,
                        size: 20,
                      )
                    : const Icon(Icons.cancel, color: Colors.grey, size: 20),
                onTap: () {
                  _showContactDetails(context, contact);
                },
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _showAddContactDialog(context);
        },
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showContactDetails(BuildContext context, Contact contact) {
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(contact.userName),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('ID: ${contact.id}'),
              Text('Channel ID: ${contact.channelId}'),
              Text('Contact: ${contact.contactValue}'),
              Text('Active: ${contact.isActive ? "Yes" : "No"}'),
              Text('Created: ${contact.createdAt}'),
              Text('Updated: ${contact.updatedAt}'),
            ],
          ),
          actions: <Widget>[
            TextButton(
              onPressed: () async {
                Navigator.of(context).pop();
                await _deleteContact(context, contact.id);
              },
              child: const Text('Delete', style: TextStyle(color: Colors.red)),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  void _showAddContactDialog(BuildContext context) {
    // Get driver before showing dialog
    final YdbDriver driver = context
        .findAncestorWidgetOfExactType<YdbConnectionWidget>()!
        .driver;

    final TextEditingController userNameController = TextEditingController();
    final TextEditingController channelIdController = TextEditingController();
    final TextEditingController contactValueController =
        TextEditingController();
    bool isActive = true;

    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return AlertDialog(
              title: const Text('Add Contact'),
              content: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  TextField(
                    controller: userNameController,
                    decoration: const InputDecoration(labelText: 'User Name'),
                  ),
                  TextField(
                    controller: channelIdController,
                    decoration: const InputDecoration(labelText: 'Channel ID'),
                    keyboardType: TextInputType.number,
                  ),
                  TextField(
                    controller: contactValueController,
                    decoration: const InputDecoration(
                      labelText: 'Contact Value',
                    ),
                  ),
                  CheckboxListTile(
                    title: const Text('Active'),
                    value: isActive,
                    onChanged: (bool? value) {
                      setState(() {
                        isActive = value ?? true;
                      });
                    },
                  ),
                ],
              ),
              actions: <Widget>[
                TextButton(
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                  child: const Text('Cancel'),
                ),
                TextButton(
                  onPressed: () async {
                    // Validate required fields
                    if (userNameController.text.trim().isEmpty) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('User Name is required')),
                      );
                      return;
                    }
                    if (contactValueController.text.trim().isEmpty) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(
                          content: Text('Contact Value is required'),
                        ),
                      );
                      return;
                    }

                    final BuildContext dialogContext = context;
                    await _addContact(
                      dialogContext,
                      driver,
                      userNameController.text,
                      int.tryParse(channelIdController.text) ?? 0,
                      contactValueController.text,
                      isActive,
                    );
                  },
                  child: const Text('Add'),
                ),
              ],
            );
          },
        );
      },
    );
  }

  Future<void> _addContact(
    BuildContext context,
    YdbDriver driver,
    String userName,
    int channelId,
    String contactValue,
    bool isActive,
  ) async {
    try {
      // Generate a simple ID (in production, use proper ID generation)
      final int id = DateTime.now().millisecondsSinceEpoch;
      final DateTime now = DateTime.now();

      await driver.queryClient.execute(
        'INSERT INTO contacts (id, user_name, channel_id, contact_value, is_active, created_at, updated_at) '
        'VALUES (\$id, \$user_name, \$channel_id, \$contact_value, \$is_active, \$created_at, \$updated_at)',
        parameters: <String, IYdbValue>{
          '\$id': YdbUint64(id),
          '\$user_name': YdbUtf8(userName),
          '\$channel_id': YdbUint64(channelId),
          '\$contact_value': YdbUtf8(contactValue),
          '\$is_active': YdbBool(isActive),
          '\$created_at': YdbTimestamp(now),
          '\$updated_at': YdbTimestamp(now),
        },
      );

      if (context.mounted) {
        Navigator.of(context).pop(); // Close dialog
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Contact added successfully')),
        );
        setState(() {}); // Refresh the list
      }
    } catch (e) {
      if (context.mounted) {
        Navigator.of(context).pop(); // Close dialog on error too
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Error adding contact: $e')));
      }
    }
  }

  Future<void> _deleteContact(BuildContext context, int id) async {
    try {
      final YdbDriver driver = context
          .findAncestorWidgetOfExactType<YdbConnectionWidget>()!
          .driver;

      await driver.queryClient.execute(
        'DELETE FROM contacts WHERE id = \$id',
        parameters: <String, IYdbValue>{'\$id': YdbUint64(id)},
      );

      if (context.mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Contact deleted successfully')),
        );
        setState(() {}); // Refresh the list
      }
    } catch (e) {
      if (context.mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Error deleting contact: $e')));
      }
    }
  }
}

/// Contact model class
class Contact {
  final int id;
  final String userName;
  final int channelId;
  final String contactValue;
  final bool isActive;
  final DateTime createdAt;
  final DateTime updatedAt;

  Contact({
    required this.id,
    required this.userName,
    required this.channelId,
    required this.contactValue,
    required this.isActive,
    required this.createdAt,
    required this.updatedAt,
  });

  static Contact fromRow(Map<String, IYdbValue> row) {
    return Contact(
      id: row.getUint('id') ?? 0,
      userName: row.getString('user_name') ?? '',
      channelId: row.getUint('channel_id') ?? 0,
      contactValue: row.getString('contact_value') ?? '',
      isActive: row.getBool('is_active') ?? false,
      createdAt: row.getTimestamp('created_at') ?? DateTime.now(),
      updatedAt: row.getTimestamp('updated_at') ?? DateTime.now(),
    );
  }
}
0
likes
140
points
213
downloads

Publisher

verified publisherflutter-lab.pro

Weekly Downloads

Yandex Database SDK for Dart and Flutter with gRPC transport, complete type system, and authentication support

Repository (GitHub)
View/report issues

Documentation

Documentation
API reference

License

unknown (license)

Dependencies

dart_jsonwebtoken, fixnum, flutter, grpc, http, logging, protobuf

More

Packages that depend on ydb_flutter_sdk