Skip to main content

ag_flow 0.3.0 copy "ag_flow: ^0.3.0" to clipboard
ag_flow: ^0.3.0 copied to clipboard

An opinionated Flutter framework: typed page-state, paginated list controllers, a type-keyed DI locator, guard-based routing over go_router, and a centralized networking layer — zero third-party state [...]

ag_flow #

The runtime half of AG Flow — an opinionated Flutter framework with no third-party state-management dependency. ag_flow gives every feature module the same page/controller/repo/service layering, page-state handling, pagination, and networking primitives, so the ag_flow_cli generator (and hand-written modules) build on a single, consistent contract.

State and DI are AG's own. Routing is AG's API over go_router, the Flutter team's own package — so every route is a real URL, every detail page is deep-linkable, and the system back button behaves correctly, without AG maintaining a router. go_router stays an implementation detail: it is not in your pubspec and it appears in no generated file.

Install #

dependencies:
  ag_flow:
    git:
      url: https://github.com/kripadevg-code/ag_flow.git
      path: packages/ag_flow
      ref: ag_flow-v0.1.0

Architecture #

Page → Controller → Repo → Service → ApiProvider

Each layer has one job, and only talks to the layer directly below it — see ag_framework.md for the full, binding spec this package implements.

Quick start — a collection module #

class ProductsController extends AgListController<Product, int> {
  ProductsController(this._repo) : super(initialPageKey: 0);
  final ProductsRepo _repo;

  @override
  Future<AgListPage<Product, int>> fetchPage(int pageKey) => _repo.getPage(pageKey);
}

class ProductsPage extends AgBasePage<ProductsController> {
  const ProductsPage({super.key});

  // Independently overridable — leaving loadingBuilder/emptyBuilder null
  // keeps the AG defaults; only Error is customized here.
  @override
  Widget Function(BuildContext, Object, StackTrace?, VoidCallback)? get errorBuilder =>
      (context, error, stackTrace, retry) => MyCustomError(error: error, onRetry: retry);

  @override
  Widget buildSuccess(BuildContext context) {
    return AgListBuilder<Product, int>(
      controller: controller,
      itemBuilder: (context, product, index) => ProductCard(product: product),
    );
  }
}

Quick start — a detail module #

class ProductDetailsController extends AgDetailController<Product, ProductDetailsArguments> {
  ProductDetailsController(this._repo);
  final ProductDetailsRepo _repo;

  @override
  Future<Product> fetch() => _repo.getById(arguments.productId);
}

class ProductDetailsPage extends AgBasePage<ProductDetailsController> {
  const ProductDetailsPage({super.key});

  @override
  Widget buildSuccess(BuildContext context) => ProductDetailsView(product: controller.state.dataOrNull!);
}

AgBasePage has a single type parameter regardless of whether the module is a collection or a detail page — the controller's own generics (T, and for detail controllers A) already carry everything AgBasePage needs, so there's no separate "detail" page base class and no argument type to state twice.

Networking #

abstract class ProductEndpoints {
  static const products = AgEndpoint('/products', methods: {AgHttpMethod.get, AgHttpMethod.post});
  static const productById = AgEndpoint('/products/{id}', methods: {AgHttpMethod.get, AgHttpMethod.put, AgHttpMethod.delete});
}

class ProductService extends AgBaseService with AgCrudService<Product, String> {
  ProductService(super.apiProvider);

  // Two distinct endpoints, not one shared between them: getAll/add have
  // no id to supply, and getById/update/delete need one — a single
  // {id}-shaped endpoint can't serve both correctly.
  @override
  AgEndpoint get collectionEndpoint => ProductEndpoints.products;
  @override
  AgEndpoint get resourceEndpoint => ProductEndpoints.productById;
  @override
  Product fromJson(Map<String, dynamic> json) => Product.fromJson(json);
  @override
  Map<String, dynamic> toJson(Product item) => item.toJson();

  // Feature-specific methods sit alongside the free add/getAll/getById/update/delete.
  Future<List<Product>> search(String query) => /* ... */;
}

AgCrudService is opt-in, not mandatory. AG never requires a fixed CRUD method set — a Service that doesn't fit add/getAll/getById/ update/delete (e.g. a paginated collection, which needs a getPage that mixin doesn't provide) just skips the mixin and writes exactly the methods the feature needs, named however reads best. Repo/Controller follow the same rule — a Controller is free to expose addProduct/ deleteProduct-style methods instead of a fixed shape, and AgPaginationMixin.updateItems is available as an emit()-style escape hatch for reflecting a mutation in the currently-displayed list without a full refresh() — useful when the mutation itself already returned the data, or against a backend that doesn't actually persist writes, where a refresh() would never show what was just created.

See example/ for a runnable app — entirely produced by ag_flow_cli (ag init + ag g m), not hand-wired — demonstrating a collection and a detail module wired end-to-end.

0
likes
140
points
436
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

An opinionated Flutter framework: typed page-state, paginated list controllers, a type-keyed DI locator, guard-based routing over go_router, and a centralized networking layer — zero third-party state-management dependency.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

collection, dio, flutter, go_router, meta

More

Packages that depend on ag_flow