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()); }, ); }, ); }
Solution 2: State Management Library (Recommended for Medium/Large Apps)
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
- Add dependency:
flutter pub add flutter_riverpod
- 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'); } });
- 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

