Flutter启动初始化变量问题:SharedPreferences异步致空值崩溃
这个问题我太熟悉了——Flutter里异步操作和Widget构建的顺序冲突确实很容易踩坑!你遇到的核心问题就是初始化依赖异步操作,但build方法会在异步结果返回前就执行,导致空值引发崩溃。不管是SharedPreferences还是JSON文件读写,本质都是异步操作(哪怕你用了同步方法,获取目录的getApplicationDocumentsDirectory()还是异步的),所以得从「等待异步完成再构建UI」或者「提前完成异步初始化」这两个方向入手。下面给你几个靠谱的解决思路:
方案1:用FutureBuilder等待异步结果(最推荐的局部UI方案)
FutureBuilder专门用来处理「依赖异步数据构建UI」的场景,它会自动监听Future的状态,在异步完成前显示加载状态,完成后再用数据构建UI,完美避开空值问题。
修改你的SharedPreferences示例代码:
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class TestingApp extends StatefulWidget { @override State<StatefulWidget> createState() => _CupertinoNavigationState(); } class _CupertinoNavigationState extends State<TestingApp> { Future<int>? _itemNoFuture; @override void initState() { super.initState(); // 提前准备好获取数据的Future _itemNoFuture = SharedPreferences.getInstance().then((sp) { return sp.getInt("itemNo") ?? 0; // 给个默认值避免null }); } @override Widget build(BuildContext context) { return FutureBuilder<int>( future: _itemNoFuture, builder: (context, snapshot) { // 异步还在进行时,显示加载指示器 if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } // 异步完成,获取数据(这里已经不会是null了) int itemNo = snapshot.data!; print("item number on build " + itemNo.toString()); return Text("当前itemNo: $itemNo"); }, ); } }
方案2:在initState中用异步函数初始化(需处理加载状态)
initState本身不能标记为async,但你可以在里面启动一个异步函数,获取到值后调用setState更新。不过这个方法会先执行一次build(此时变量为空),所以必须在build里处理空值的情况,比如显示加载中。
示例代码:
class _CupertinoNavigationState extends State<TestingApp> { int? itemNo; @override void initState() { super.initState(); // 立即执行异步初始化函数 _loadItemNo(); } Future<void> _loadItemNo() async { final sp = await SharedPreferences.getInstance(); setState(() { itemNo = sp.getInt("itemNo") ?? 0; }); } @override Widget build(BuildContext context) { // 变量未初始化时显示加载 if (itemNo == null) { return const CircularProgressIndicator(); } print("item number on build " + itemNo.toString()); return Text("当前itemNo: $itemNo"); } }
方案3:全局提前初始化(适合全局通用配置)
如果这个变量是整个APP都需要用到的,你可以在main函数里先完成异步初始化,再启动APP。这样APP启动后变量就已经准备好了,不会有空值问题。
示例代码:
void main() async { // 必须加这句,确保Flutter初始化完成 WidgetsFlutterBinding.ensureInitialized(); // 提前初始化SharedPreferences并获取值 final sp = await SharedPreferences.getInstance(); int globalItemNo = sp.getInt("itemNo") ?? 0; // 启动APP,把全局值传递给需要的页面 runApp(MyApp(initialItemNo: globalItemNo)); } class MyApp extends StatelessWidget { final int initialItemNo; const MyApp({super.key, required this.initialItemNo}); @override Widget build(BuildContext context) { return MaterialApp( home: TestingApp(itemNo: initialItemNo), ); } } // 页面直接使用初始化好的值 class TestingApp extends StatefulWidget { final int itemNo; const TestingApp({super.key, required this.itemNo}); @override State<StatefulWidget> createState() => _CupertinoNavigationState(); } class _CupertinoNavigationState extends State<TestingApp> { late int itemNo; @override void initState() { super.initState(); itemNo = widget.itemNo; } @override Widget build(BuildContext context) { print("item number on build " + itemNo.toString()); return Text("当前itemNo: $itemNo"); } }
为什么你的JSON方案不行?
你在initState里调用fetchSavedItemNo(),但这个方法里用了getApplicationDocumentsDirectory().then(...),是异步操作,所以fetchSavedItemNo()返回的其实是Future<int>,而不是实际的int值,直接赋值给itemNo自然是null。你可以把fetchSavedItemNo()改成async函数,然后用方案2的方式调用它。
内容的提问来源于stack exchange,提问作者Simon

