You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何全局复用服务端JSON数据定制多组件?

Hey there! Let's tackle your Flutter JSON component reuse issue. The core problem here is sharing asynchronously fetched JSON data across all your components (like modal bottom sheets) without re-fetching or getting stuck in async logic loops. Here are two solid solutions, plus a complete example to get you up and running.

Core Idea

No matter which approach you pick, the goal is to cache the parsed JSON data in a globally accessible spot after the first async fetch. This way, every component can access the ready-to-use data without repeating network calls or handling redundant async logic.


Solution 1: Singleton Class (Quick & Simple)

Perfect for small projects or when you don't want to add a full state management library. We'll create a singleton to handle data loading and caching.

Step 1: Create the Singleton Data Manager

import 'dart:convert';
import 'package:http/http.dart' as http;

class JsonDataManager {
  // Singleton instance (only one will ever exist)
  static final JsonDataManager _instance = JsonDataManager._internal();
  factory JsonDataManager() => _instance;
  JsonDataManager._internal();

  // Cache for parsed JSON data
  Map<String, dynamic>? _cachedData;

  // Get data: return cached data if exists, else fetch and cache
  Future<Map<String, dynamic>> getJsonData() async {
    if (_cachedData != null) return _cachedData!;
    
    // Replace this with your actual server API call
    final response = await http.get(Uri.parse('https://your-api-url.com/config.json'));
    
    if (response.statusCode == 200) {
      _cachedData = json.decode(response.body);
      return _cachedData!;
    } else {
      throw Exception('Failed to load config data');
    }
  }

  // Optional: Clear cache (e.g. for pull-to-refresh)
  void clearCache() => _cachedData = null;
}

Step 2: Use the Singleton in Components

Now any component (including your modal bottom sheet) can access the cached data with a single async call that won't re-fetch:

// In your main screen
@override
void initState() {
  super.initState();
  // Pre-load data on app start (optional but improves UX)
  _preloadData();
}

Future<void> _preloadData() async {
  try {
    await JsonDataManager().getJsonData();
    setState(() {}); // Trigger UI update once data is cached
  } catch (e) {
    // Handle error (e.g. show snackbar)
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to load data: $e')),
      );
    }
  }
}

// Open modal bottom sheet
void _showCustomModal() {
  showModalBottomSheet(
    context: context,
    builder: (context) {
      return FutureBuilder<Map<String, dynamic>>(
        future: JsonDataManager().getJsonData(), // Uses cached data instantly
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            // Build modal UI using parsed JSON
            final modalConfig = snapshot.data!['modalBottomSheet'];
            return Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Text(modalConfig['title'], style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                ),
                ...modalConfig['items'].map<Widget>((item) => ListTile(
                      title: Text(item),
                      onTap: () => Navigator.pop(context),
                    )).toList(),
              ],
            );
          } else if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }
          // Show loader only if data is still loading first time
          return const Center(child: CircularProgressIndicator());
        },
      );
    },
  );
}

For apps with complex state needs, use a library like Riverpod (Flutter's official recommendation) or Provider. This approach automatically handles UI updates when data loads or changes.

Example with Riverpod

  1. Add dependency:
flutter pub add flutter_riverpod
  1. Create a Provider for your JSON data:
final jsonConfigProvider = FutureProvider<Map<String, dynamic>>((ref) async {
  final response = await http.get(Uri.parse('https://your-api-url.com/config.json'));
  if (response.statusCode == 200) {
    return json.decode(response.body);
  } else {
    throw Exception('Failed to load config');
  }
});
  1. Use the Provider in your components:
class HomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final configAsync = ref.watch(jsonConfigProvider);
    
    return Scaffold(
      appBar: AppBar(title: const Text('Custom Components')),
      body: configAsync.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(child: Text('Error: $error')),
        data: (config) {
          return Center(
            child: ElevatedButton(
              onPressed: () => _showModal(context, ref),
              child: Text(config['mainLayout']['buttonText']),
            ),
          );
        },
      ),
    );
  }

  void _showModal(BuildContext context, WidgetRef ref) {
    showModalBottomSheet(
      context: context,
      builder: (context) {
        final configAsync = ref.watch(jsonConfigProvider);
        return configAsync.when(
          loading: () => const Center(child: CircularProgressIndicator()),
          error: (error, stack) => Center(child: Text('Error: $error')),
          data: (config) {
            final modalConfig = config['modalBottomSheet'];
            return ListView.builder(
              shrinkWrap: true,
              itemCount: modalConfig['items'].length + 1,
              itemBuilder: (context, index) {
                if (index == 0) {
                  return Padding(
                    padding: const EdgeInsets.all(16),
                    child: Text(modalConfig['title'], style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                  );
                }
                final item = modalConfig['items'][index - 1];
                return ListTile(title: Text(item), onTap: () => Navigator.pop(context));
              },
            );
          },
        );
      },
    );
  }
}

Complete Working Example (Singleton Approach)

Here's a full runnable app with mock JSON data to test immediately:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Custom Components',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const HomePage(),
    );
  }
}

class JsonDataManager {
  static final JsonDataManager _instance = JsonDataManager._internal();
  factory JsonDataManager() => _instance;
  JsonDataManager._internal();

  Map<String, dynamic>? _cachedData;

  Future<Map<String, dynamic>> getJsonData() async {
    if (_cachedData != null) return _cachedData!;
    
    // Mock API call (replace with your real endpoint)
    await Future.delayed(const Duration(seconds: 1));
    const mockJson = '''
    {
      "mainLayout": {
        "title": "Custom Component Demo",
        "buttonText": "Open Modal"
      },
      "modalBottomSheet": {
        "title": "Modal Options",
        "items": ["Profile Settings", "Help Center", "Logout"]
      }
    }
    ''';
    _cachedData = json.decode(mockJson);
    return _cachedData!;
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _isLoading = true;
  Map<String, dynamic>? _configData;

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

  Future<void> _loadConfig() async {
    try {
      _configData = await JsonDataManager().getJsonData();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to load data: $e')),
        );
      }
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  void _showModal() {
    showModalBottomSheet(
      context: context,
      builder: (context) {
        return FutureBuilder<Map<String, dynamic>>(
          future: JsonDataManager().getJsonData(),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              final modalConfig = snapshot.data!['modalBottomSheet'];
              return Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Padding(
                    padding: const EdgeInsets.all(16),
                    child: Text(modalConfig['title'], style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                  ),
                  ...modalConfig['items'].map<Widget>((item) => ListTile(
                        title: Text(item),
                        onTap: () => Navigator.pop(context),
                      )).toList(),
                ],
              );
            } else if (snapshot.hasError) {
              return Center(child: Text('Error: ${snapshot.error}'));
            }
            return const Center(child: CircularProgressIndicator());
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }
    final mainConfig = _configData!['mainLayout'];
    return Scaffold(
      appBar: AppBar(title: Text(mainConfig['title'])),
      body: Center(
        child: ElevatedButton(
          onPressed: _showModal,
          child: Text(mainConfig['buttonText']),
        ),
      ),
    );
  }
}

内容的提问来源于stack exchange,提问作者Lunedor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 20:52:30