Imagepicker 在 form 重建时重建,丢失所选图像
Imagepicker is rebuilt when form does, losing the selected image
我正在使用 无状态屏幕,其中包含两个有状态小部件、一个图像选择器和一个包含许多字段的表单。当我打开键盘时,如果我之前 select 编辑了一张图片,它就会消失,整个图片选择器小部件将重新初始化。
这意味着提交图像的唯一方法是 select 当键盘关闭并且永远不会重新打开它时。我已经尝试设置一个密钥和我在这里找到的其他解决方案,但没有任何效果。我不能完全理解这种行为,当然我需要图像留在那儿,即使我打开和关闭键盘。
一个快速的解决方案可能是简单地移动表单本身中的图像选择器,但我更愿意将它们保留在不同的小部件中。我真的需要了解我所缺少的东西。
主页:
class ProductAddScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GlobalKey<ProductAddUpdateFormState> _keyForm = GlobalKey();
final GlobalKey<ImageInputProductState> _keyImage = GlobalKey();
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).padding.top,
),
TitleHeadline(
title: 'Add',
backBtn: true,
trailingBtn: Icons.info,
trailingBtnAction: () =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(builder: (context) => InfoScreen()),
),
),
const SizedBox(height: 8),
ImageInputProduct(key: _keyImage),
ProductAddUpdateForm(key: _keyForm),
const SizedBox(height: 16),
ButtonWide(
action: () => _keyForm.currentState.submit(
screenContext: context,
newImage: _keyImage.currentState.storedImage),
text: 'Confirm',
),
],
),
),
);
}
}
图像选择器:
class ImageInputProduct extends StatefulWidget {
final String preImage;
ImageInputProduct({Key key, this.preImage = ''}) : super(key: key);
@override
ImageInputProductState createState() => ImageInputProductState();
}
class ImageInputProductState extends State<ImageInputProduct> {
File _storedImage;
// Get the selected file
File get storedImage {
return _storedImage;
}
// Take an image from camera
Future<void> _takePicture() async {
final picker = ImagePicker();
final imageFile = await picker.getImage(
source: ImageSource.camera,
maxHeight: 1280,
maxWidth: 1280,
);
setState(() {
_storedImage = File(imageFile.path);
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: 130,
width: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
width: 1,
color: Colors.black12,
),
),
child: _storedImage == null
? widget.preImage.isEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image,
color:
Theme.of(context).primaryColor.withOpacity(0.4),
size: 48,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Text(
'No image selected',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyText2,
),
)
],
)
: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
topLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Image.network(
widget.preImage,
fit: BoxFit.cover,
width: double.infinity,
),
)
: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
topLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Image.file(
_storedImage,
fit: BoxFit.cover,
width: double.infinity,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
child: ButtonWideOutlined(
action: _takePicture,
text: 'Select image',
),
),
],
);
}
}
表格只是一个标准表格,这个问题已经太长了。我真的很感激任何建议。我唯一知道的是每次打开键盘时都会在图像选择器中调用 initState(因此表单状态会改变)。
当抽屉或软键盘打开时,屏幕状态发生变化,有时构建方法会自动重新加载,请查看此了解更多信息。
构建方法的设计方式应该是 pure/without 副作用。这是因为许多外部因素可以触发新的小部件构建,例如:
路由 pop/push 屏幕大小调整,通常是由于键盘外观或方向改变 Parent 小部件重新创建了它的 child 小部件所依赖的 InheritedWidget (Class.of(context) pattern) change 这意味着构建方法不应触发 http 调用或修改任何状态。
这与问题有什么关系?
您面临的问题是您的构建方法 side-effects/is 不纯,使得无关的构建调用很麻烦。
与其阻止构建调用,不如让构建方法更纯净,这样它就可以随时调用而不会受到影响。
在您的示例中,您需要将小部件转换为 StatefulWidget,然后提取对 State 的 initState 的 HTTP 调用:
class Example extends StatefulWidget {
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
@override
void initState() {
future = Future.value(42);
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
}
我已经知道了。我来这里是因为我真的很想优化重建
也可以使小部件能够重建,而无需强制其 children 也进行构建。
当小部件的实例保持不变时; Flutter 故意不重建 children。这意味着您可以缓存部分小部件树以防止不必要的重建。
最简单的方法是使用 dart const 构造函数:
@override
Widget build(BuildContext context) {
return const DecoratedBox(
decoration: BoxDecoration(),
child: Text("Hello World"),
);
}
多亏了那个 const 关键字,DecoratedBox 的实例将保持不变,即使构建被调用了数百次。
但您可以手动获得相同的结果:
@override
Widget build(BuildContext context) {
final subtree = MyWidget(
child: Text("Hello World")
);
return StreamBuilder<String>(
stream: stream,
initialData: "Foo",
builder: (context, snapshot) {
return Column(
children: <Widget>[
Text(snapshot.data),
subtree,
],
);
},
);
}
在此示例中,当 StreamBuilder 收到新值通知时,即使 StreamBuilder/Column 重建,子树也不会重建。发生这种情况是因为,由于闭包,MyWidget 的实例没有改变。
这种模式在动画中被大量使用。典型用途是 AnimatedBuilder 和所有过渡,例如 AlignTransition。
您也可以将子树存储到 class 的字段中,但不太推荐,因为它会破坏 hot-reload 功能。
我从 abbas jafary 的建议开始,并尝试重组屏幕以不自动重建图像选择器。我无法在不进行一些侧面更改的情况下将其初始化为变量,因为我将密钥传递给了图像选择器本身。这是最终代码:
class ProductAddScreen extends StatelessWidget {
static final GlobalKey<ProductAddUpdateFormState> _keyForm = GlobalKey();
static final GlobalKey<ImageInputProductState> _keyImage = GlobalKey();
final imageInput = ImageInputProduct(key: _keyImage);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).padding.top,
),
TitleHeadline(
title: 'Add',
backBtn: true,
trailingBtn: Icons.info,
trailingBtnAction: () =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(builder: (context) => InfoScreen()),
),
),
const SizedBox(height: 8),
imageInput,
ProductAddUpdateForm(key: _keyForm),
const SizedBox(height: 16),
ButtonWide(
action: () => _keyForm.currentState.submit(
screenContext: context,
newImage: _keyImage.currentState.storedImage),
text: 'Confirm',
),
],
),
),
);
}
}
我不确定这是不是最佳做法,但它确实有效,而且无论如何图像选择器都会保持其状态。
我正在使用 无状态屏幕,其中包含两个有状态小部件、一个图像选择器和一个包含许多字段的表单。当我打开键盘时,如果我之前 select 编辑了一张图片,它就会消失,整个图片选择器小部件将重新初始化。
这意味着提交图像的唯一方法是 select 当键盘关闭并且永远不会重新打开它时。我已经尝试设置一个密钥和我在这里找到的其他解决方案,但没有任何效果。我不能完全理解这种行为,当然我需要图像留在那儿,即使我打开和关闭键盘。
一个快速的解决方案可能是简单地移动表单本身中的图像选择器,但我更愿意将它们保留在不同的小部件中。我真的需要了解我所缺少的东西。
主页:
class ProductAddScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GlobalKey<ProductAddUpdateFormState> _keyForm = GlobalKey();
final GlobalKey<ImageInputProductState> _keyImage = GlobalKey();
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).padding.top,
),
TitleHeadline(
title: 'Add',
backBtn: true,
trailingBtn: Icons.info,
trailingBtnAction: () =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(builder: (context) => InfoScreen()),
),
),
const SizedBox(height: 8),
ImageInputProduct(key: _keyImage),
ProductAddUpdateForm(key: _keyForm),
const SizedBox(height: 16),
ButtonWide(
action: () => _keyForm.currentState.submit(
screenContext: context,
newImage: _keyImage.currentState.storedImage),
text: 'Confirm',
),
],
),
),
);
}
}
图像选择器:
class ImageInputProduct extends StatefulWidget {
final String preImage;
ImageInputProduct({Key key, this.preImage = ''}) : super(key: key);
@override
ImageInputProductState createState() => ImageInputProductState();
}
class ImageInputProductState extends State<ImageInputProduct> {
File _storedImage;
// Get the selected file
File get storedImage {
return _storedImage;
}
// Take an image from camera
Future<void> _takePicture() async {
final picker = ImagePicker();
final imageFile = await picker.getImage(
source: ImageSource.camera,
maxHeight: 1280,
maxWidth: 1280,
);
setState(() {
_storedImage = File(imageFile.path);
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: 130,
width: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
width: 1,
color: Colors.black12,
),
),
child: _storedImage == null
? widget.preImage.isEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image,
color:
Theme.of(context).primaryColor.withOpacity(0.4),
size: 48,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Text(
'No image selected',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyText2,
),
)
],
)
: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
topLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Image.network(
widget.preImage,
fit: BoxFit.cover,
width: double.infinity,
),
)
: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
topLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Image.file(
_storedImage,
fit: BoxFit.cover,
width: double.infinity,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
child: ButtonWideOutlined(
action: _takePicture,
text: 'Select image',
),
),
],
);
}
}
表格只是一个标准表格,这个问题已经太长了。我真的很感激任何建议。我唯一知道的是每次打开键盘时都会在图像选择器中调用 initState(因此表单状态会改变)。
当抽屉或软键盘打开时,屏幕状态发生变化,有时构建方法会自动重新加载,请查看此
构建方法的设计方式应该是 pure/without 副作用。这是因为许多外部因素可以触发新的小部件构建,例如:
路由 pop/push 屏幕大小调整,通常是由于键盘外观或方向改变 Parent 小部件重新创建了它的 child 小部件所依赖的 InheritedWidget (Class.of(context) pattern) change 这意味着构建方法不应触发 http 调用或修改任何状态。
这与问题有什么关系?
您面临的问题是您的构建方法 side-effects/is 不纯,使得无关的构建调用很麻烦。
与其阻止构建调用,不如让构建方法更纯净,这样它就可以随时调用而不会受到影响。
在您的示例中,您需要将小部件转换为 StatefulWidget,然后提取对 State 的 initState 的 HTTP 调用:
class Example extends StatefulWidget {
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
@override
void initState() {
future = Future.value(42);
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
}
我已经知道了。我来这里是因为我真的很想优化重建
也可以使小部件能够重建,而无需强制其 children 也进行构建。
当小部件的实例保持不变时; Flutter 故意不重建 children。这意味着您可以缓存部分小部件树以防止不必要的重建。
最简单的方法是使用 dart const 构造函数:
@override
Widget build(BuildContext context) {
return const DecoratedBox(
decoration: BoxDecoration(),
child: Text("Hello World"),
);
}
多亏了那个 const 关键字,DecoratedBox 的实例将保持不变,即使构建被调用了数百次。
但您可以手动获得相同的结果:
@override
Widget build(BuildContext context) {
final subtree = MyWidget(
child: Text("Hello World")
);
return StreamBuilder<String>(
stream: stream,
initialData: "Foo",
builder: (context, snapshot) {
return Column(
children: <Widget>[
Text(snapshot.data),
subtree,
],
);
},
);
}
在此示例中,当 StreamBuilder 收到新值通知时,即使 StreamBuilder/Column 重建,子树也不会重建。发生这种情况是因为,由于闭包,MyWidget 的实例没有改变。
这种模式在动画中被大量使用。典型用途是 AnimatedBuilder 和所有过渡,例如 AlignTransition。
您也可以将子树存储到 class 的字段中,但不太推荐,因为它会破坏 hot-reload 功能。
我从 abbas jafary 的建议开始,并尝试重组屏幕以不自动重建图像选择器。我无法在不进行一些侧面更改的情况下将其初始化为变量,因为我将密钥传递给了图像选择器本身。这是最终代码:
class ProductAddScreen extends StatelessWidget {
static final GlobalKey<ProductAddUpdateFormState> _keyForm = GlobalKey();
static final GlobalKey<ImageInputProductState> _keyImage = GlobalKey();
final imageInput = ImageInputProduct(key: _keyImage);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).padding.top,
),
TitleHeadline(
title: 'Add',
backBtn: true,
trailingBtn: Icons.info,
trailingBtnAction: () =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(builder: (context) => InfoScreen()),
),
),
const SizedBox(height: 8),
imageInput,
ProductAddUpdateForm(key: _keyForm),
const SizedBox(height: 16),
ButtonWide(
action: () => _keyForm.currentState.submit(
screenContext: context,
newImage: _keyImage.currentState.storedImage),
text: 'Confirm',
),
],
),
),
);
}
}
我不确定这是不是最佳做法,但它确实有效,而且无论如何图像选择器都会保持其状态。