Responsive Layouts
The Vyuh Entity System adapts entity views across mobile, tablet, and desktop form factors. This page covers EntitySelectionMode, breakpoint- driven layout decisions, column priority, and the workspace shell.
EntitySelectionMode
EntitySelectionMode controls how a row tap in the list is handled:
enum EntitySelectionMode {
/// No navigation — only fires the controller's selection callback.
/// Use when the list is a picker or selection-only surface.
none,
/// Navigate to the detail route. The default.
navigate,
/// Adapt by viewport: navigate to a route on phone, show a side panel
/// (overlay or docked) on tablet/desktop.
responsive,
}Set it on the EntityRouting configuration:
EntityRouting<Course>(
path: NavigationPathBuilder.collection(prefix: '/lms/courses'),
builder: StandardRouteBuilder<Course>(),
mode: EntitySelectionMode.responsive,
)| Mode | Phone (< 600) | Tablet (600 - 1200) | Desktop (> 1200) |
|---|---|---|---|
none | Callback only | Callback only | Callback only |
navigate | Detail route | Detail route | Detail route |
responsive | Detail route | Overlay panel | Docked panel |
Breakpoints
The entity system uses three breakpoint tiers:
| Breakpoint | Width | Typical device |
|---|---|---|
| Small | < 600 | Phone |
| Medium | 600 – 1200 | Tablet, narrow desktop |
| Large | > 1200 | Standard desktop |
The Workspace Shell
EntityWorkspace<T> (mounted by EntityWorkspaceRouteBuilder<T>) is the container that drives the responsive experience. It provides:
- The navigation sidebar (collapsible by category)
- The command palette (Cmd/Ctrl+K)
- The list pane and the detail pane (docked or overlay)
- Per-form-factor breakpoint handling
EntityShellLayout (mounted by StandardRouteBuilder<T>) remains the standard route-builder container. The workspace route builders wrap the same list/detail behavior with richer workspace chrome and lifecycle hooks, so new entity surfaces usually start there.
Panel Modes
| Mode | When |
|---|---|
| Docked | Desktop default — pane is fixed alongside the list |
| Overlay | Tablet default — pane floats above the list |
| Full-page | Phone default — selection navigates to a detail route |
The mode is chosen automatically from the breakpoint and the EntitySelectionMode.
Mobile Behavior
On phones (< 600px):
EntityTableConfig<T>falls back to a compact mobile list. Each row shows the entity's name, subtitle, key properties, and any status indicator.- Tapping an entity opens a bottom sheet with the detail layout (when
mode: responsive) or navigates to the detail route (whenmode: navigate). EntityGridConfig<T>collapses to a single column.
final courseGridLayout = EntityGridConfig<Course>(
identifier: 'grid',
title: 'Cards',
icon: FluentIcons.grid_24_regular,
defaultColumns: 3,
minColumns: 1,
maxColumns: 5,
userAdjustableColumns: true,
card: EntityCardLayout<Course>(
title: (c) => c.name,
subtitle: (c) => c.level,
leading: (context, c) => const Icon(FluentIcons.book_24_regular),
properties: [
(FluentIcons.tag_24_regular, 'Level', (c) => c.level),
(FluentIcons.checkmark_circle_24_regular, 'Status', (c) => c.status),
],
),
);Tablet Behavior
On medium screens (600 - 1200px):
- The table renders fewer columns. Columns with
priority: ColumnPriority.lowhide first, thennormal. - The detail pane appears as an overlay above the list.
- The sidebar collapses to icons by default.
Desktop Behavior
On large screens (> 1200px):
- All columns are visible.
- The detail pane docks alongside the list.
- A layout switcher appears when the entity has more than one
CollectionLayout<T>registered (e.g. table + grid).
EntityLayouts<Course>(
list: [courseTableLayout, courseGridLayout], // Switcher available
details: [courseDetailLayout],
)Column Priority
ColumnPriority constants control which fields hide first as space tightens:
| Constant | Value | Behavior |
|---|---|---|
ColumnPriority.essential | 0 | Never hidden |
ColumnPriority.high | 1 | Hidden only on very narrow screens |
ColumnPriority.normal | 2 | Hidden on medium screens (default) |
ColumnPriority.low | 3 | Hidden first |
Annotation form:
@Field(label: 'Name', priority: ColumnPriority.essential)
final String name;
@Field(label: 'Duration (min)', priority: ColumnPriority.low)
final int durationMinutes;Hand-written form:
static final name = TextFieldDef<Course>(
field: 'name',
label: 'Name',
priority: ColumnPriority.essential,
);Layout Selection per Form Factor
The framework chooses a single CollectionLayout<T> to render. When you register more than one, the first one in the list is the default and the user can switch between them via the layout switcher.
A common pattern:
EntityLayouts<Course>(
list: [
courseTableLayout, // Default — best for desktop
courseGridLayout, // Visual scan — great for tablet
],
details: [
courseDetailLayout,
courseParticipantsLayout,
],
)To choose programmatically based on a layout-builder (LayoutBuilder / MediaQuery), wrap your custom layout's build in a LayoutBuilder:
class CourseHybridLayout extends CollectionLayout<Course> {
@override
Widget build(BuildContext context, EntityListController<Course> controller) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return CourseMobileList(controller: controller);
}
return CourseTable(controller: controller);
},
);
}
}Workspace Shape Selection
EntityWorkspaceConfig<T> lets you pick the shape of the workspace:
- Master-detail (default) — list pane + detail pane.
- Singleton — one entity, no list (for
Settings-style entities markedisSingleton: true). - Grouped — a master pane plus an embedded detail with multiple sub-resources (used by Inbox).
- Dashboard — a single dashboard layout with no list at all.
The generator emits a $courseWorkspaceConfig() factory that picks the right shape from the @Entity declaration. Override routing.builder if you need a different shape:
routing: EntityRouting<Settings>(
path: NavigationPathBuilder.singleton(prefix: '/config/settings'),
builder: EntityWorkspaceRouteBuilder<Settings>(
workspaceConfig: $settingsWorkspaceConfig(),
),
),LMS Example: Browsing Courses Across Form Factors
class CourseFields extends EntityFieldRegistry<Course> {
static final name = TextFieldDef<Course>(
field: 'name',
label: 'Name',
primary: true,
priority: ColumnPriority.essential,
);
static final level = EnumFieldDef<Course>(
field: 'level',
label: 'Level',
priority: ColumnPriority.high,
options: const {
'beginner': 'Beginner',
'intermediate': 'Intermediate',
'advanced': 'Advanced',
},
);
static final status = EnumFieldDef<Course>(
field: 'status',
label: 'Status',
priority: ColumnPriority.high,
options: const {
'draft': 'Draft',
'published': 'Published',
'archived': 'Archived',
},
);
static final duration = NumberFieldDef<Course>(
field: 'duration_minutes',
label: 'Duration',
suffix: 'min',
priority: ColumnPriority.low, // Desktop only
);
static final instructor = ReferenceFieldDef<Course>.auto(
foreignKeyField: 'instructor_id',
targetField: 'name',
targetEntityType: 'trainers',
label: 'Instructor',
priority: ColumnPriority.normal, // Hidden on tablet
);
@override
List<FieldDefinition<Course>> get all =>
[name, level, status, duration, instructor];
}
final courseConfig = EntityConfiguration<Course>(
metadata: EntityMetadata(
identifier: 'courses',
name: 'Course',
pluralName: 'Courses',
icon: FluentIcons.book_24_regular,
),
api: const CourseApi(),
routing: EntityRouting<Course>(
path: NavigationPathBuilder.collection(prefix: '/lms/courses'),
builder: StandardRouteBuilder<Course>(),
mode: EntitySelectionMode.responsive,
),
layouts: EntityLayouts<Course>(
list: [courseTableLayout, courseGridLayout],
details: [courseDetailLayout, courseParticipantsLayout],
),
fields: CourseFields().all,
actions: EntityActions<Course>(
inline: StandardEntityActions.inline<Course>(),
header: StandardEntityActions.header<Course>(),
),
);Result:
- Phone — compact list, bottom-sheet detail with tabs.
- Tablet — table with Name, Level, and Status; overlay detail pane.
- Desktop — full table with all columns; docked detail pane; layout switcher.
Next Steps
- Master-Detail — selection model and panel transitions
- Layouts concept page — the layout type hierarchy
- Building UI —
EntityListViewand friends