Back to Posts

Creating a List of Cards from JSON in Flutter

15 min read

Creating a list of cards from JSON data is a common requirement in Flutter applications. This comprehensive guide will show you how to efficiently parse JSON data and display it in an attractive card layout.

Basic Implementation

1. Define the Data Model

First, create a model class to represent your data:

class CardItem {
  final String title;
  final String description;
  final String imageUrl;
  final DateTime date;

  CardItem({
    required this.title,
    required this.description,
    required this.imageUrl,
    required this.date,
  });

  factory CardItem.fromJson(Map<String, dynamic> json) {
    return CardItem(
      title: json['title'] ?? '',
      description: json['description'] ?? '',
      imageUrl: json['imageUrl'] ?? '',
      date: DateTime.parse(json['date'] ?? DateTime.now().toIso8601String()),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'description': description,
      'imageUrl': imageUrl,
      'date': date.toIso8601String(),
    };
  }
}

2. Create a Custom Card Widget

class CustomCard extends StatelessWidget {
  final CardItem item;

  const CustomCard({Key? key, required this.item}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ClipRRect(
            borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
            child: Image.network(
              item.imageUrl,
              height: 200,
              width: double.infinity,
              fit: BoxFit.cover,
              errorBuilder: (context, error, stackTrace) {
                return Container(
                  height: 200,
                  color: Colors.grey[300],
                  child: Icon(Icons.error, color: Colors.red),
                );
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  item.title,
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 8),
                Text(
                  item.description,
                  style: TextStyle(
                    fontSize: 16,
                    color: Colors.grey[600],
                  ),
                ),
                SizedBox(height: 8),
                Text(
                  DateFormat('MMM dd, yyyy').format(item.date),
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[500],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

JSON Data Handling

1. Load JSON Data

class CardListScreen extends StatefulWidget {
  @override
  _CardListScreenState createState() => _CardListScreenState();
}

class _CardListScreenState extends State<CardListScreen> {
  List<CardItem> cards = [];
  bool isLoading = true;
  String? error;

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

  Future<void> loadCards() async {
    try {
      // Simulating API call
      final response = await http.get(
        Uri.parse('https://api.example.com/cards'),
      );

      if (response.statusCode == 200) {
        final List<dynamic> jsonData = json.decode(response.body);
        setState(() {
          cards = jsonData.map((json) => CardItem.fromJson(json)).toList();
          isLoading = false;
        });
      } else {
        throw Exception('Failed to load cards');
      }
    } catch (e) {
      setState(() {
        error = e.toString();
        isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (isLoading) {
      return Center(child: CircularProgressIndicator());
    }

    if (error != null) {
      return Center(child: Text('Error: $error'));
    }

    return ListView.builder(
      itemCount: cards.length,
      itemBuilder: (context, index) {
        return CustomCard(item: cards[index]);
      },
    );
  }
}

Advanced Implementation

1. Animated Card List

class AnimatedCardList extends StatelessWidget {
  final List<CardItem> cards;

  const AnimatedCardList({Key? key, required this.cards}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: cards.length,
      itemBuilder: (context, index) {
        return AnimatedContainer(
          duration: Duration(milliseconds: 300),
          curve: Curves.easeInOut,
          transform: Matrix4.translationValues(
            0,
            index * 10.0,
            0,
          ),
          child: CustomCard(item: cards[index]),
        );
      },
    );
  }
}

2. Card with Interaction

class InteractiveCard extends StatelessWidget {
  final CardItem item;

  const InteractiveCard({Key? key, required this.item}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => DetailScreen(item: item),
          ),
        );
      },
      child: Hero(
        tag: 'card_${item.title}',
        child: Card(
          margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(12),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.1),
                  blurRadius: 8,
                  offset: Offset(0, 2),
                ),
              ],
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                ClipRRect(
                  borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
                  child: Image.network(
                    item.imageUrl,
                    height: 200,
                    width: double.infinity,
                    fit: BoxFit.cover,
                  ),
                ),
                Padding(
                  padding: EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        item.title,
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      SizedBox(height: 8),
                      Text(
                        item.description,
                        style: TextStyle(
                          fontSize: 16,
                          color: Colors.grey[600],
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Best Practices

  1. Performance Optimization

    • Use const constructors where possible
    • Implement pagination for large lists
    • Cache network images
    • Use lazy loading techniques
  2. Error Handling

    • Implement proper error states
    • Show loading indicators
    • Handle network timeouts
    • Provide retry mechanisms
  3. User Experience

    • Add pull-to-refresh functionality
    • Implement smooth animations
    • Provide visual feedback
    • Handle edge cases gracefully

Common Issues and Solutions

Issue 1: Memory Management

// Bad Practice
ListView(
  children: cards.map((card) => CustomCard(item: card)).toList(),
);

// Good Practice
ListView.builder(
  itemCount: cards.length,
  itemBuilder: (context, index) => CustomCard(item: cards[index]),
);

Issue 2: Image Loading

// Handle image loading errors
Image.network(
  imageUrl,
  errorBuilder: (context, error, stackTrace) {
    return Container(
      color: Colors.grey[300],
      child: Icon(Icons.error),
    );
  },
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;
    return Center(child: CircularProgressIndicator());
  },
);

Conclusion

Creating a list of cards from JSON in Flutter requires:

  1. Proper data modeling
  2. Efficient JSON parsing
  3. Optimized list rendering
  4. Error handling
  5. Performance considerations

Remember these key points:

  • Use appropriate widgets for list rendering
  • Implement proper error handling
  • Optimize performance for large lists
  • Consider user experience
  • Follow Flutter best practices