flutter-master-detail-view
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFlutter Master-detail view
Flutter 主详情视图
When building mobile, desktop and web applications with Flutter often times you are faced with what to do with lists and the content when selected. Depending on the data you may have a list that renders another list before resolving to a detail view. On tablet or desktop this can be achieved with multi-column layouts.
On mobile you will still need to push to the details screen since the space is constrained.
How to build a Master-detail view with Flutter:
import 'package:flutter/material.dart';
class MasterDetail<T> extends StatefulWidget {
const MasterDetail({
Key? key,
required this.listBuilder,
required this.detailBuilder,
required this.onPush,
this.emptyBuilder,
}) : super(key: key);
final Widget Function(BuildContext, ValueChanged<T?>, T?) listBuilder;
final Widget Function(BuildContext, T, bool) detailBuilder;
final void Function(BuildContext, T) onPush;
final WidgetBuilder? emptyBuilder;
@override
State<MasterDetail<T>> createState() => _MasterDetailState<T>();
}
class _MasterDetailState<T> extends State<MasterDetail<T>> {
final selected = ValueNotifier<T?>(null);
double? detailsWidth;
@override
Widget build(BuildContext context) {
return Scaffold(
primary: false,
body: LayoutBuilder(
builder: (context, dimens) {
const double minWidth = 350;
final maxWidth = dimens.maxWidth - minWidth;
if (detailsWidth != null) {
if (detailsWidth! > maxWidth) {
detailsWidth = maxWidth;
}
if (detailsWidth! < minWidth) {
detailsWidth = minWidth;
}
}
return ValueListenableBuilder<T?>(
valueListenable: selected,
builder: (context, item, child) {
final canShowDetails = dimens.maxWidth > 800;
final showDetails = item != null && canShowDetails;
return Row(
children: [
Expanded(
child: widget.listBuilder(context, (item) {
if (canShowDetails) {
selected.value = item;
} else {
selected.value = null;
if (item != null) widget.onPush(context, item);
}
}, selected.value),
),
if (canShowDetails)
MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragUpdate: (details) {
if (mounted) {
setState(() {
double w = detailsWidth ?? maxWidth;
w -= details.delta.dx;
// Check for min width
if (w < minWidth) {
w = minWidth;
}
// Check for max width
if (w > maxWidth) {
w = maxWidth;
}
detailsWidth = w;
});
}
},
child: const SizedBox(
width: 5,
height: double.infinity,
child: VerticalDivider(),
),
),
),
if (canShowDetails)
SizedBox(
width: detailsWidth ?? maxWidth,
height: double.infinity,
child: showDetails
? widget.detailBuilder(context, item, false)
: widget.emptyBuilder?.call(context) ??
const Center(
child: Text('Select a item to view details'),
),
),
],
);
},
);
},
),
);
}
}This widget will size itself after layout and try to size the list as small as possible with the details filling up the rest. This is important for later when we nest multiple of these to create progressively adapting layouts.
在使用Flutter构建移动、桌面和Web应用时,你经常会遇到这样的问题:当列表项被选中时,该如何展示对应的内容。根据数据的不同,你可能需要先展示一个列表,再展示另一个列表,最终进入详情视图。在平板或桌面设备上,这可以通过多列布局来实现。
在移动设备上,由于空间有限,你仍然需要跳转到详情页面。
如何使用Flutter构建主详情视图:
import 'package:flutter/material.dart';
class MasterDetail<T> extends StatefulWidget {
const MasterDetail({
Key? key,
required this.listBuilder,
required this.detailBuilder,
required this.onPush,
this.emptyBuilder,
}) : super(key: key);
final Widget Function(BuildContext, ValueChanged<T?>, T?) listBuilder;
final Widget Function(BuildContext, T, bool) detailBuilder;
final void Function(BuildContext, T) onPush;
final WidgetBuilder? emptyBuilder;
@override
State<MasterDetail<T>> createState() => _MasterDetailState<T>();
}
class _MasterDetailState<T> extends State<MasterDetail<T>> {
final selected = ValueNotifier<T?>(null);
double? detailsWidth;
@override
Widget build(BuildContext context) {
return Scaffold(
primary: false,
body: LayoutBuilder(
builder: (context, dimens) {
const double minWidth = 350;
final maxWidth = dimens.maxWidth - minWidth;
if (detailsWidth != null) {
if (detailsWidth! > maxWidth) {
detailsWidth = maxWidth;
}
if (detailsWidth! < minWidth) {
detailsWidth = minWidth;
}
}
return ValueListenableBuilder<T?>(
valueListenable: selected,
builder: (context, item, child) {
final canShowDetails = dimens.maxWidth > 800;
final showDetails = item != null && canShowDetails;
return Row(
children: [
Expanded(
child: widget.listBuilder(context, (item) {
if (canShowDetails) {
selected.value = item;
} else {
selected.value = null;
if (item != null) widget.onPush(context, item);
}
}, selected.value),
),
if (canShowDetails)
MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragUpdate: (details) {
if (mounted) {
setState(() {
double w = detailsWidth ?? maxWidth;
w -= details.delta.dx;
// Check for min width
if (w < minWidth) {
w = minWidth;
}
// Check for max width
if (w > maxWidth) {
w = maxWidth;
}
detailsWidth = w;
});
}
},
child: const SizedBox(
width: 5,
height: double.infinity,
child: VerticalDivider(),
),
),
),
if (canShowDetails)
SizedBox(
width: detailsWidth ?? maxWidth,
height: double.infinity,
child: showDetails
? widget.detailBuilder(context, item, false)
: widget.emptyBuilder?.call(context) ??
const Center(
child: Text('Select a item to view details'),
),
),
],
);
},
);
},
),
);
}
}这个组件会根据布局自动调整尺寸,尽量让列表部分占用最小空间,详情部分填充剩余空间。这一点很重要,因为之后我们可以嵌套多个这样的组件,创建逐步自适应的布局。