我对 Flutter 很陌生,想知道如何在 Widget 中正确加载异步数据。这是我到目前为止的代码:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../../../api/api_result.dart';
import '../../../common/application.dart';
import '../../../models/survey.dart';
import '../../common/list_material_ink_well.dart';
import '../../common/loading_widget.dart';
import '../../common/main_layout.dart';
import '../../common/status_bar.dart';
import '../../common/status_bar_icon.dart';
import 'survey_poll.dart';
class SurveysPollsWidget extends StatefulWidget {
const SurveysPollsWidget({super.key});
@override
State<StatefulWidget> createState() => SurveysPollsWidgetState();
}
class SurveysPollsWidgetState extends State<SurveysPollsWidget> {
bool isEditMode = false;
Map<String, bool> activity = <String, bool>{};
Future<List<Survey>?> getSurveys(Application application) async {
ApiResult<List<Survey>>? apiResult =
await application.repositories?.survey.getSurveys();
if (apiResult?.error != null) {
await application.auth.logout();
}
if (apiResult?.value != null && application.caches != null) {
application.caches?.survey.saveSurveys(apiResult!.value!);
}
return apiResult?.value;
}
Future<List<Survey>?> getSurveysFromCache(Application application) async {
List<Survey>? result = await application.caches?.survey.getSurveys();
return result;
}
final MaterialStateProperty<Icon?> thumbIcon =
MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const Icon(Icons.check);
}
return const Icon(Icons.close);
},
);
Widget _drawItem(Survey survey) => isEditMode
? ListMaterialInkWell(
survey.name,
navItem: Switch(
thumbIcon: thumbIcon,
value: activity[survey.id] ?? false,
onChanged: (bool value) {
setState(() {
activity[survey.id] = value;
});
},
),
)
: ListMaterialInkWell(
survey.name,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SurveyPollWebViewWidget(survey: survey),
),
),
);
void toggleEditMode() => setState(() {
isEditMode = !isEditMode;
});
Widget _draw(BuildContext context, AsyncSnapshot<List<Survey>?> cachedData) =>
Material(
child: MainLayout(
title: AppLocalizations.of(context)!.surveysAndPolls,
iconLeft: isEditMode
? StatusBarIcon(
StatusBar.goBackIcon.icon, (context) => toggleEditMode())
: StatusBar.goBackIcon,
iconRight: !isEditMode
? StatusBarIcon(
Icons.edit_outlined, (context) => toggleEditMode())
: null,
showLoading: !cachedData.hasData,
children: cachedData.hasData ? cachedData.data!.map(_drawItem).toList() : []),
);
@override
Widget build(BuildContext context) => Consumer<Application>(
builder: (_, application, __) => () {
if (application.repositories == null) {
return const LoadingWidget();
}
return FutureBuilder<List<Survey>?>(
future: getSurveysFromCache(application),
builder: (context, cachedData) => cachedData
.connectionState !=
ConnectionState.done
? Container()
: cachedData.data != null
? _draw(
context, cachedData)
: FutureBuilder<List<Survey>?>(
future: getSurveys(application),
builder: (context, snapshot) => _draw(
context, snapshot),
));
}());
}
这里的问题是,每次状态改变时,通过切换 editMode 或开关,屏幕会闪烁黑色(默认背景),因为整个小部件都会重新渲染,并且在调用
getSurveysFromCache
时会在短时间内, Container()
被渲染,那么黑。
一个肮脏的解决方案是为调查添加一个状态:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../../../api/api_result.dart';
import '../../../common/application.dart';
import '../../../models/survey.dart';
import '../../common/list_material_ink_well.dart';
import '../../common/loading_widget.dart';
import '../../common/main_layout.dart';
import '../../common/status_bar.dart';
import '../../common/status_bar_icon.dart';
import 'survey_poll.dart';
class SurveysPollsWidget extends StatefulWidget {
const SurveysPollsWidget({super.key});
@override
State<StatefulWidget> createState() => SurveysPollsWidgetState();
}
class SurveysPollsWidgetState extends State<SurveysPollsWidget> {
bool isEditMode = false;
Map<String, bool> activity = <String, bool>{};
List<Survey>? surveys;
Future<List<Survey>?> getSurveys(Application application) async {
ApiResult<List<Survey>>? apiResult =
await application.repositories?.survey.getSurveys();
if (apiResult?.error != null) {
await application.auth.logout();
}
if (apiResult?.value != null && application.caches != null) {
application.caches?.survey.saveSurveys(apiResult!.value!);
}
if (apiResult?.value != null) {
for (Survey element in apiResult!.value!) {
activity[element.id] = element.isActive;
}
}
setState(() {
surveys = apiResult?.value;
});
return apiResult?.value;
}
Future<List<Survey>?> getSurveysFromCache(Application application) async {
List<Survey>? result = await application.caches?.survey.getSurveys();
setState(() {
surveys = result;
});
return result;
}
final MaterialStateProperty<Icon?> thumbIcon =
MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const Icon(Icons.check);
}
return const Icon(Icons.close);
},
);
Widget _drawItem(Survey survey) => isEditMode
? ListMaterialInkWell(
survey.name,
navItem: Switch(
thumbIcon: thumbIcon,
value: activity[survey.id] ?? false,
onChanged: (bool value) {
setState(() {
activity[survey.id] = value;
});
},
),
)
: ListMaterialInkWell(
survey.name,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SurveyPollWebViewWidget(survey: survey),
),
),
);
void toggleEditMode() => setState(() {
isEditMode = !isEditMode;
});
Widget _draw(BuildContext context, List<Survey>? surveys, bool hasData) =>
Material(
child: MainLayout(
title: AppLocalizations.of(context)!.surveysAndPolls,
iconLeft: isEditMode
? StatusBarIcon(
StatusBar.goBackIcon.icon, (context) => toggleEditMode())
: StatusBar.goBackIcon,
iconRight: !isEditMode
? StatusBarIcon(
Icons.edit_outlined, (context) => toggleEditMode())
: null,
showLoading: !hasData,
children: hasData ? surveys!.map(_drawItem).toList() : []),
);
@override
Widget build(BuildContext context) => Consumer<Application>(
builder: (_, application, __) => () {
if (application.repositories == null) {
return const LoadingWidget();
}
return surveys != null
? _draw(context, surveys, true)
: FutureBuilder<List<Survey>?>(
future: getSurveysFromCache(application),
builder: (context, cachedData) => cachedData
.connectionState !=
ConnectionState.done
? Container()
: cachedData.data != null
? _draw(
context, cachedData.data, cachedData.hasData)
: FutureBuilder<List<Survey>?>(
future: getSurveys(application),
builder: (context, snapshot) => _draw(
context, snapshot.data, cachedData.hasData),
));
}());
}
一个干净的解决方案是在
getSurveysFromCache
中调用 getSurveys
和 initState
,但我需要由 Application
返回的 Consumer<Application>
实例对象。
一些提示如何正确操作和清洁?
首先我建议使用 Riverpod 而不是 Provider。有几篇关于优点和缺点的文章:
使用 Provider,你基本上受到 BuildContext 的限制,但正如我一样,你只使用一些函数调用来加载数据,所以: