问题描述
原始答案
我在 Flutter 上使用 Getx 状态管理。
尽可能简化:
我构建了一个 GetxController 来控制我的页面,并且在这个控制器中,我有一个 StatefulWidget 实例来激发 http 请求。
class MyController extends GetxController {
Player player;
}
class Player extends StatefulWidget {
PlayerState state;
@override
PlayerState createState() {
state = PlayerState();
return state;
}
}
class PlayerState extends State<Player> {
void methodName async() {
futureRequest().then((data) {
// when the error ocurrs
setState(() {});
});
}
}
当用户在请求结束之前关闭移动页面,触发控制器的 close 方法时,就会出现问题。
这样,当 setState 被触发时,页面实例就没有了,错误就发生了。
我认为解决方案是在调用控制器关闭方法时中断与此 GetxController 相关的所有请求并“删除”此 StatefulWidget 实例。
我不知道这是否正确,也不知道该怎么做..
================================================ ====================
更新答案
主要问题是 getDetails() 方法中的异步请求,即使在控制器被释放后也返回响应,甚至使用 GetBuilder,并且此响应携带来自启动视频的 url通过 videoPlayerController(一个 video_player 插件实例)。
因此,用户在另一个屏幕上,但继续收听在后台播放的视频。
作为将良好实践应用于代码的解决方法和思考,我进行了重构以仅使用无状态小部件,遵循 GetX 规则。我解决了这个问题,但我不得不将 Future's 转换为 Stream's
正在使用 Get.lazyPut() 创建绑定以执行依赖项注入:
class Binding implements Bindings {
Get.lazyPut<PlayerController>(() {
return PlayerController(videoRepository: VideoRepository(VideoProvider(Dio())));
});
}
此绑定链接到页面路由器,基于 GetX 文档。
class AppPages {
static final routes = [
GetPage(name: Routes.MyRoute,page: () => MyPage(),binding: MyBinding()),];
}
为了防止控制器在处理之前进行操作,我必须创建一个流并在控制器处理时取消它。
class MyController extends GetxController {
MyController({@required this.repository}) : assert(repository != null);
StreamSubscription<bool> stream;
// Instance of plugin video_player
VideoPlayerController videoPlayerController;
@override
void onClose() {
if (streamGetVideo != null) streamGetVideo.cancel();
super.onClose();
if (videoPlayerController != null) videoPlayerController?.dispose();
}
// This is the method called by the user on screen
void loadVideo() {
stream = getDetails().asStream().listen((bool response) {
// This code is canceled on onClose() method by the stream
if (response) update();
});
}
Future<bool> getDetails() async {
return await repository.getDetails().then((data) async {
videoPlayerController = VideoPlayerController.network(data);
initFuture = videoPlayerController.initialize();
await initFuture.whenComplete(() { return true; });
});
}
}
我认为 Flutter/GetX 应该有更好的方法来做到这一点,而不需要我做的这些变通方法。如果有人有更好的方法或提示,我愿意接受建议。
解决方法
一种解决方案可能是将您的 setState 包裹起来
if(mounted){
setState(() {});
}
,
GetBuilder + 更新()
在 GetX
中使用 GetBuilder
和 update()
负责生命周期检查/处理,因此您不必这样做。
以下是在 HTTP 调用完成和调用 setState() 之前关闭屏幕/路由的示例,没有抛出异常。
(在第二个屏幕上,快速点击 Go Back! 按钮来模拟一个已经释放的 StatefulWidget。)
下面,update()
调用用于更新屏幕,而不是 setState(),但它们在 GetBuilder 中是相同的。 GetBuilder 是(扩展)一个 StatefulWidget。
GetBuilder 通过 init:
构造函数 arg 或 GetBuilder<Type>
参数(如果 Controller 在别处/更早的时候初始化)向您传递它的 Controller 添加侦听器。
如果 StatefulWidget(即 GetBuilder
)被释放,该监听器将被释放。
(请参阅 GetBuilder
的 dispose()
函数以了解一些魔法。添加侦听器时,添加该侦听器的返回值是一个处理/取消订阅该侦听的函数。非常聪明。)
因此,如果该小部件已被释放,则 GetBuilder/StatefulWidget 将永远不会调用其 update() / setState()
,因为这些调用的侦听器已被释放。因此,缓慢返回的 HTTP 调用不会尝试更新/设置小部件树中不再存在的小部件。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HttpX extends GetxController {
String slowValue = 'loading...';
@override
void onInit() {
slowCall();
}
/// Simulate a slow,long running HTTP call
Future<void> slowCall() async {
slowValue = 'Slow call STARTED!';
print(slowValue);
update(); // update the screen to show started message
await Future.delayed(Duration(seconds: 5),() {
slowValue = 'Slow call FINISHED!';
print(slowValue);
update(); // won't call setState() if GetBuilder is disposed
});
}
}
class GetXDisposePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose'),),body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,children: [
Text('awaiting http call to finish'),RaisedButton(
child: Text('Go Call Page'),onPressed: () => Get.to(SlowCallPage()),// using Get.to ↑ requires GetMaterialApp in place of MaterialApp in MyApp
)
],);
}
}
class SlowCallPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose - Go Back!'),children: [
GetBuilder<HttpX>(
init: HttpX(),// fake slow http call starts on init
builder: (hx) => Text(hx.slowValue),RaisedButton(
child: Text('Go Back!'),onPressed: () => Get.back(),],);
}
}