Kanban Boards — cdx_kanban_view
cdx_kanban_view is the CDX kanban package. Use it when an entity list needs to be grouped into operational columns such as "Queued", "In Review", "Revision", and "Done".
The package composes CDX UI styling, but it is imported directly:
import 'package:cdx_kanban_view/cdx_kanban_view.dart';Live Example
Open the full route: Kanban View.
When To Use It
Use a kanban board when the primary question is where is this item in the flow? Each column is a server-side filter, so a column can represent one value, several values, or a richer cdx_query expression.
Good fits:
| Scenario | Why kanban fits |
|---|---|
| Workflow tasks | Show user tasks by lifecycle state or assignment stage |
| Activities | Group records by execution/review/revision state |
| Equipment or documents | Track operational readiness or approval state |
| Entity dashboards | Offer an alternate layout beside the default table |
Model
KanbanQuery
-> KanbanDataSource<T>
-> KanbanController<T>
-> KanbanBoard<T>KanbanQuery declares the shared base filter, sorting, search, columns, and optional per-column segments. KanbanDataSource<T> executes those queries against your backend. KanbanController<T> owns raw MobX state, pagination, refreshes, and realtime reconciliation. KanbanBoard<T> renders the result.
Query And Columns
final query = KanbanQuery(
baseFilter: myActivitiesFilter,
orderBy: const [
OrderExpr(aliasOrField: 'updated_at', direction: SortDirection.desc),
],
columns: [
KanbanColumnSpec(
key: 'in_review',
title: 'In Review',
accent: Colors.amber,
filter: statusIn(['submitted', 'under_review', 'revision_requested']),
segments: [
KanbanSegment(
key: 'submitted',
label: 'Submitted',
filter: statusIn(['submitted', 'under_review']),
),
KanbanSegment(
key: 'revision',
label: 'Revision',
filter: statusIn(['revision_requested']),
),
],
),
],
);The effective query is:
base filter AND column filter AND active segment filterThe board does not evaluate filters locally. Filtering remains server-side through cdx_query.
Data Source
class ActivityKanbanDataSource extends KanbanDataSource<Activity> {
ActivityKanbanDataSource(super.query, this.api);
final ActivityApi api;
@override
Future<List<KanbanColumnData<Activity>>> fetchColumns({
int limit = 10,
int offset = 0,
}) {
return api.fetchKanbanColumns(
query: query,
limit: limit,
offset: offset,
);
}
@override
Future<KanbanColumnPage<Activity>> fetchColumn(
String key, {
int limit = 10,
int offset = 0,
String? segmentKey,
Filter? extraFilter,
}) {
return api.fetchKanbanColumn(
filter: query.columnFilter(key, segmentKey: segmentKey),
extraFilter: extraFilter,
limit: limit,
offset: offset,
);
}
}Each column result carries a column-global total. That lets the header count and the per-column pager remain honest even when only one page of items is loaded.
API Parameters
| Type | Parameter | Purpose |
|---|---|---|
KanbanQuery | columns | Ordered board columns. Required. |
KanbanQuery | baseFilter | Predicate applied by the data source to every column. |
KanbanQuery | search | Optional free-text search applied by the data source. |
KanbanQuery | orderBy | Shared ordering inside every column. |
KanbanColumnSpec | key, title | Stable column id and header label. |
KanbanColumnSpec | filter | Server-side predicate for the column. |
KanbanColumnSpec | segments | Optional per-column narrowing filters; "All" is synthetic. |
KanbanColumnSpec | isPlaceholder | Renders a ghost slot without fetching data. |
KanbanDataSource.fetchColumn | extraFilter | User-selected per-column filter; implementations must AND it with base, column, and segment filters. |
KanbanController | idOf, columnOf | Stable identity and realtime placement. |
KanbanBoard | onFilterRequested | Optional hook that shows a host-owned filter picker and applies its returned filter. |
Rendering
final controller = KanbanController<Activity>(
dataSource: ActivityKanbanDataSource(query, api),
idOf: (activity) => activity.id,
columnOf: (activity) => columnKeyForStatus(activity.status),
pageSize: 10,
);
await controller.load();
KanbanBoard<Activity>(
controller: controller,
itemBuilder: (context, activity) => KanbanItemView(
item: KanbanItem(
id: activity.id,
title: activity.name,
subtitle: activity.statusLabel,
),
),
onItemTap: (activity) => openActivity(activity.id),
);Return KanbanItemView for the default card, or provide a fully custom widget from itemBuilder.
Pagination, Refresh, Realtime
Pagination is per column:
controller.nextPage('in_review');
controller.prevPage('in_review');
controller.goToPage('in_review', 2);
controller.pageCount('in_review');Refresh and realtime updates are granular:
controller.refreshColumn('in_review');
controller.refreshColumns(['todo', 'done']);
controller.refresh();
controller.applyUpsert(changedActivity);
controller.applyRemoval(removedId);applyUpsert uses idOf and columnOf to move an item between columns and keep counts in sync without forcing a full board reload.
Customization
KanbanBoard<Activity>(
controller: controller,
itemBuilder: buildActivityCard,
theme: const KanbanTheme(
columnMinWidth: 320,
itemSpacing: 8,
),
headerBuilder: (context, column, state) => MyHeader(column, state),
footerBuilder: (context, column, state) => MyFooter(column, state),
emptyBuilder: (context, column) => EmptyState(title: 'No items'),
errorBuilder: (context, column, error, stackTrace) => ErrorState(error: error),
);Use columnBuilder only when the whole column chrome needs replacing. Prefer slot builders when you want CDX defaults plus custom header, footer, empty, loading, or error states.
Entity-System Relationship
Entity UI can expose a kanban layout through generated or hand-written EntityKanbanConfig<T>. The kanban package itself stays generic: it does not depend on vyuh_entity_system_ui, and entity-system code adapts its list APIs into KanbanDataSource<T>.
Cross-links
- Data Tables — use when comparison and sorting matter more than state grouping
- Tree Views — pair hierarchy context with any detail surface
- Search and Filters — shared
cdx_queryfilter vocabulary