Skip to content

Quick Start Guide

Get up and running with LDK in just a few minutes! This guide will walk you through creating a simple Flutter app that connects to Strapi and performs basic operations.

🎯 What You'll Build

A simple blog reader app that: - Connects to a Strapi backend - Displays a list of posts - Shows post details - Handles user authentication

📋 Prerequisites

  • Flutter development environment set up
  • A running Strapi server (local or remote)
  • LDK installed in your project (Installation Guide)

🚀 Step 1: Initialize LDK

First, initialize LDK in your main.dart file:

import 'package:flutter/material.dart';
import 'package:ldk/ldk.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize LDK with your Strapi server
  await LDK.initialize(
    baseUrl: 'http://localhost:1337', // Replace with your Strapi URL
    enableLogging: true, // Enable logging for development

    // V2 features (enabled by default)
    enableGraphQL: true,
    enableRealtime: true,
    enableIntelligentCache: true,
    enableOfflineStorage: true,
    enableEnhancedAuth: true,
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LDK Quick Start',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: PostListScreen(),
    );
  }
}

📝 Step 2: Create a Post List Screen

Create a screen to display posts from your Strapi backend:

class PostListScreen extends StatefulWidget {
  @override
  _PostListScreenState createState() => _PostListScreenState();
}

class _PostListScreenState extends State<PostListScreen> {
  List<Map<String, dynamic>> posts = [];
  bool isLoading = true;
  String? error;

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

  Future<void> _loadPosts() async {
    try {
      setState(() {
        isLoading = true;
        error = null;
      });

      // Get posts collection
      final postsCollection = LDK.collection('posts');

      // Fetch posts with intelligent caching
      final response = await postsCollection.get();

      setState(() {
        posts = response.data;
        isLoading = false;
      });
    } catch (e) {
      setState(() {
        error = e.toString();
        isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Blog Posts'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _loadPosts,
          ),
          IconButton(
            icon: Icon(Icons.person),
            onPressed: () => _showAuthDialog(),
          ),
        ],
      ),
      body: _buildBody(),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _createPost(),
        child: Icon(Icons.add),
        tooltip: 'Create Post',
      ),
    );
  }

  Widget _buildBody() {
    if (isLoading) {
      return Center(child: CircularProgressIndicator());
    }

    if (error != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.error, size: 64, color: Colors.red),
            SizedBox(height: 16),
            Text('Error: $error'),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _loadPosts,
              child: Text('Retry'),
            ),
          ],
        ),
      );
    }

    if (posts.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.article, size: 64, color: Colors.grey),
            SizedBox(height: 16),
            Text('No posts found'),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _createPost,
              child: Text('Create First Post'),
            ),
          ],
        ),
      );
    }

    return RefreshIndicator(
      onRefresh: _loadPosts,
      child: ListView.builder(
        itemCount: posts.length,
        itemBuilder: (context, index) {
          final post = posts[index];
          final attributes = post['attributes'] as Map<String, dynamic>;

          return Card(
            margin: EdgeInsets.all(8),
            child: ListTile(
              title: Text(
                attributes['title'] ?? 'Untitled',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  if (attributes['content'] != null)
                    Text(
                      attributes['content'].toString(),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                  SizedBox(height: 4),
                  Text(
                    'ID: ${post['id']}',
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.grey[600],
                    ),
                  ),
                ],
              ),
              trailing: Icon(Icons.arrow_forward_ios),
              onTap: () => _viewPost(post),
            ),
          );
        },
      ),
    );
  }

  void _viewPost(Map<String, dynamic> post) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => PostDetailScreen(post: post),
      ),
    );
  }

  Future<void> _createPost() async {
    final result = await Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => CreatePostScreen(),
      ),
    );

    if (result == true) {
      _loadPosts(); // Refresh the list
    }
  }

  void _showAuthDialog() {
    showDialog(
      context: context,
      builder: (context) => AuthDialog(),
    );
  }
}

📖 Step 3: Create Post Detail Screen

class PostDetailScreen extends StatelessWidget {
  final Map<String, dynamic> post;

  const PostDetailScreen({Key? key, required this.post}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final attributes = post['attributes'] as Map<String, dynamic>;

    return Scaffold(
      appBar: AppBar(
        title: Text('Post Details'),
        actions: [
          IconButton(
            icon: Icon(Icons.edit),
            onPressed: () => _editPost(context),
          ),
          IconButton(
            icon: Icon(Icons.delete),
            onPressed: () => _deletePost(context),
          ),
        ],
      ),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              attributes['title'] ?? 'Untitled',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            SizedBox(height: 16),
            Text(
              'Post ID: ${post['id']}',
              style: TextStyle(color: Colors.grey[600]),
            ),
            SizedBox(height: 16),
            Expanded(
              child: SingleChildScrollView(
                child: Text(
                  attributes['content'] ?? 'No content',
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _editPost(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => EditPostScreen(post: post),
      ),
    );
  }

  Future<void> _deletePost(BuildContext context) async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Delete Post'),
        content: Text('Are you sure you want to delete this post?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: Text('Delete'),
            style: TextButton.styleFrom(foregroundColor: Colors.red),
          ),
        ],
      ),
    );

    if (confirmed == true) {
      try {
        final postsCollection = LDK.collection('posts');
        await postsCollection.delete(post['id']);

        Navigator.pop(context);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Post deleted successfully')),
        );
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error deleting post: $e')),
        );
      }
    }
  }
}

✍️ Step 4: Create Post Form

class CreatePostScreen extends StatefulWidget {
  @override
  _CreatePostScreenState createState() => _CreatePostScreenState();
}

class _CreatePostScreenState extends State<CreatePostScreen> {
  final _formKey = GlobalKey<FormState>();
  final _titleController = TextEditingController();
  final _contentController = TextEditingController();
  bool _isSubmitting = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Create Post'),
        actions: [
          TextButton(
            onPressed: _isSubmitting ? null : _submitPost,
            child: _isSubmitting 
              ? SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : Text('SAVE', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: Form(
        key: _formKey,
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: InputDecoration(
                  labelText: 'Title',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a title';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),
              Expanded(
                child: TextFormField(
                  controller: _contentController,
                  decoration: InputDecoration(
                    labelText: 'Content',
                    border: OutlineInputBorder(),
                    alignLabelWithHint: true,
                  ),
                  maxLines: null,
                  expands: true,
                  textAlignVertical: TextAlignVertical.top,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _submitPost() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() => _isSubmitting = true);

    try {
      final postsCollection = LDK.collection('posts');

      await postsCollection.create({
        'title': _titleController.text,
        'content': _contentController.text,
      });

      Navigator.pop(context, true); // Return success
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Post created successfully!')),
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error creating post: $e')),
      );
    } finally {
      setState(() => _isSubmitting = false);
    }
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }
}

🔐 Step 5: Add Authentication

class AuthDialog extends StatefulWidget {
  @override
  _AuthDialogState createState() => _AuthDialogState();
}

class _AuthDialogState extends State<AuthDialog> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLogin = true;
  bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(_isLogin ? 'Login' : 'Sign Up'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: _emailController,
            decoration: InputDecoration(labelText: 'Email'),
            keyboardType: TextInputType.emailAddress,
          ),
          TextField(
            controller: _passwordController,
            decoration: InputDecoration(labelText: 'Password'),
            obscureText: true,
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => setState(() => _isLogin = !_isLogin),
          child: Text(_isLogin ? 'Need an account?' : 'Have an account?'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: _isLoading ? null : _authenticate,
          child: _isLoading 
            ? SizedBox(
                width: 20,
                height: 20,
                child: CircularProgressIndicator(strokeWidth: 2),
              )
            : Text(_isLogin ? 'Login' : 'Sign Up'),
        ),
      ],
    );
  }

  Future<void> _authenticate() async {
    setState(() => _isLoading = true);

    try {
      final auth = LDK.instance.auth;

      if (_isLogin) {
        await auth.signIn(
          email: _emailController.text,
          password: _passwordController.text,
        );
      } else {
        await auth.signUp(
          email: _emailController.text,
          password: _passwordController.text,
        );
      }

      Navigator.pop(context);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(_isLogin ? 'Logged in successfully!' : 'Account created!'),
        ),
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Authentication failed: $e')),
      );
    } finally {
      setState(() => _isLoading = false);
    }
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

🎉 Step 6: Run Your App

  1. Make sure your Strapi server is running
  2. Create a posts content type in Strapi with title and content fields
  3. Run your Flutter app:
flutter run

✨ What You've Accomplished

Congratulations! You've just built a complete Flutter app with LDK that:

  • ✅ Connects to Strapi backend
  • ✅ Displays data with intelligent caching
  • ✅ Creates, reads, updates, and deletes posts
  • ✅ Handles user authentication
  • ✅ Provides offline support (automatic)
  • ✅ Shows loading states and error handling

🚀 Next Steps

Now that you have the basics working, explore more advanced features:

Real-time Updates

// Subscribe to real-time post updates
LDK.collection('posts').subscribeToUpdates().listen((event) {
  print('Post ${event.type}: ${event.data}');
  // Update your UI automatically
});

Advanced Querying

// Use advanced query builder
final query = LDKQueryBuilder()
  .where('title', containsi: 'flutter')
  .populate(['author'])
  .sort(['-createdAt'])
  .limit(10);

final posts = await LDK.collection('posts').get(query: query);

Offline-First Development

// Enable optimistic updates
await LDK.collection('posts').create(
  {'title': 'New Post', 'content': 'Content'},
  optimistic: true, // UI updates immediately
);

GraphQL Queries

// Use GraphQL for complex queries
final result = await LDK.instance.graphql.query('''
  query GetPostsWithAuthors {
    posts {
      id
      title
      content
      author {
        username
        email
      }
    }
  }
''');

📚 Learn More

🆘 Need Help?


Next: Configuration Options →