nano_core 0.4.0
nano_core: ^0.4.0 copied to clipboard
A lightweight reactive architecture framework and design system toolkit for Flutter multiplatform applications.
Nano Core #
A lightweight reactive architecture framework and design system toolkit for Flutter multiplatform applications.
Features #
- 📱 NanoApp: Zero-boilerplate root application widget automatically configuring
NanoRouter,MaterialApp, themes, and localizations. - 🧭 NanoRouter & Declarative Routes: Intuitive zero-dependency declarative router supporting public routes (
NanoRoute), custom animated transitions (NanoAnimatedRoute), route groups (NanoGroupRoute), typed sub-routes (NanoDetailsRoute<T>), access-guarded routes (NanoProtectedRoute), and redirects (NanoRedirectRoute). - 🔭 NanoRouteObserver: Granular navigation observer for screen tracking, Firebase Analytics, Datadog, breadcrumbs, and route lifecycle telemetry.
- 🚀 NanoScaffold & NanoStateObservable: Decoupled reactive base page scaffold supporting Web/Desktop headers, mobile AppBars, loading overlays, toasts, and universal state observation (
NanoController, BLoC, Cubit, MobX adapters). - ⚡ NanoController & NanoState: Clean, reactive state management built on
ChangeNotifierandListenableBuilder. - 📊 NanoViewState: Base class for structured, immutable and equatable view/page state data models.
- 🛠️ NanoCommand & NanoCommandBuilder: Encapsulated async commands for user actions and operations.
- 🌐 NanoHttpClient & NanoHttpInterceptor: Standardized generic contract for decoupled HTTP communication, request/response interceptors (JWT injection, refresh tokens), built-in traffic logging (
NanoHttpLogInterceptor), and helper extensions (isSuccess,isClientError,isServerError). - 📦 NanoRepository, NanoSearchRepository & NanoQueryAdapter: Automated generic CRUD repository layer, type-safe search query serialization, and domain model adapters.
- 📄 Pagination & NanoPaginator: Pluggable strategies (
NanoOffsetPagination,NanoCursorPagination), reactive controller (NanoPaginator), automatic infinite scrolling widget (NanoPaginatedListView), and customizable navigation bar (NanoPaginationBar). - ⚡ NanoCache & Smart Caching: Zero-dependency in-memory caching (
NanoMemoryCache) with configurable policies (cacheFirst,networkFirst,networkOnly,cacheOnly), TTL expiration, and automatic invalidation on CRUD mutations. - 🛡️ Functional Results (NanoResult): Modern Dart 3
sealed classhierarchy (NanoSuccess,NanoFailure) with compile-time pattern matching,fold,map, andrunAsyncsafe execution. - 📝 NanoForm & Validators: Strongly-typed form models, automatic field disposal,
BuildContexti18n support, and reactiveNanoTextFieldcomponent. - 🏷️ NanoEntity & NanoEquatable: Base domain entity with unique identification and value-based equality.
- 🪵 NanoLogger: Central structured console logger with ANSI styling, severity levels (
debug,info,success,warning,error,http), method context tracking, data payloads, and telemetry hooks. - 💉 NanoInjections, NanoDefaultInjections & NanoStatePage: Dependency injection scoping with
GetIt, default framework services registration (NanoDefaultInjections.init), modular composition, and page lifecycle binding. - 🧩 Design System Components: Standalone reusable UI widgets such as
NanoLoadingOverlay,NanoToast,NanoPaginatedListView,NanoPaginationBar, andNanoTextField. - 🖥️ NanoDeviceType: Real-time cross-platform environment and responsive viewport width inspection.
Getting Started #
Add nano_core to your pubspec.yaml:
dependencies:
nano_core: ^0.3.0
Quick Example #
1. Domain Entity & Adapter #
import 'package:nano_core/nano_core.dart';
class User extends NanoEntity<String> {
final String name;
const User({required super.id, required this.name});
}
class UserAdapter implements NanoAdapter<User> {
const UserAdapter();
@override
User fromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
name: json['name'] as String,
);
@override
Map<String, dynamic> toJson(User user) => {
'id': user.id,
'name': user.name,
};
}
class UserRepository extends NanoRepository<User, String> {
UserRepository([super.client])
: super(
endpoint: '/users',
adapter: const UserAdapter(),
);
}
// Type-Safe Search with NanoSearchRepository:
class UserFilter {
final String? role;
final int page;
const UserFilter({this.role, this.page = 1});
}
class UserFilterAdapter extends NanoQueryAdapter<UserFilter> {
const UserFilterAdapter();
@override
Map<String, dynamic> toQueryParams(UserFilter query) => {
'page': query.page,
if (query.role != null) 'role': query.role,
};
}
class UserSearchRepository
extends NanoSearchRepository<User, String, UserFilter> {
UserSearchRepository([super.client])
: super(
endpoint: '/users',
adapter: const UserAdapter(),
queryAdapter: const UserFilterAdapter(),
);
}
2. View State, Controller & Injections #
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:nano_core/nano_core.dart';
// 1. Structured View State
class UsersState extends NanoViewState {
final List<User> users;
const UsersState({this.users = const []});
@override
List<Object?> get props => [users];
}
// 2. Reactive Controller
class MyController extends NanoController<UsersState> {
final UserRepository repository;
MyController({required this.repository});
@override
Future<void> init(String? id) async {
await loadUsers();
}
Future<void> loadUsers() async {
execute(() async {
final users = await repository.getAll();
return UsersState(users: users);
});
}
}
// 3. Page Injections Scope
class UsersInjections extends NanoInjections {
UsersInjections() : super(scope: 'users');
@override
void binds(GetIt i) {
// 1. Initialize default core framework services:
NanoDefaultInjections.init(i, client: DioHttpClient(Dio()));
// 2. Register repository (client is automatically injected via GetIt!):
i.registerLazySingleton<UserRepository>(() => UserRepository());
// 3. Register page controller:
i.registerFactory<MyController>(
() => MyController(repository: i<UserRepository>()),
);
}
}
// 4. Page with NanoStatePage & NanoScaffold
class UsersPage extends StatefulWidget {
const UsersPage({super.key});
@override
State<UsersPage> createState() => _UsersPageState();
}
class _UsersPageState
extends NanoStatePage<UsersPage, MyController> {
@override
NanoInjections get injections => UsersInjections();
@override
Widget build(BuildContext context) {
return NanoScaffold<UsersState, NanoMessageKey>(
controller: controller,
headerBuilder: (context, state) => AppBar(
title: Text(
state.data?.users.isNotEmpty == true
? 'Users (${state.data!.users.length})'
: 'Users List',
),
),
builder: (context, state) {
final users = state.data?.users ?? [];
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
);
},
);
},
);
}
}
3. HTTP Client Implementation with Dio (Optional) #
nano_core remains 100% agnostic to third-party HTTP dependencies. To use Dio as your HTTP client, implement NanoHttpClient in your app:
import 'package:dio/dio.dart';
import 'package:nano_core/nano_core.dart';
class DioHttpClient implements NanoHttpClient {
final Dio _dio;
DioHttpClient(this._dio);
@override
Future<NanoHttpResponse<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
}) async {
final response = await _dio.get<T>(
path,
queryParameters: queryParameters,
options: Options(headers: headers),
);
return NanoHttpResponse<T>(
data: response.data,
statusCode: response.statusCode,
statusMessage: response.statusMessage,
);
}
@override
Future<NanoHttpResponse<T>> post<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
}) async {
final response = await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: Options(headers: headers),
);
return NanoHttpResponse<T>(
data: response.data,
statusCode: response.statusCode,
statusMessage: response.statusMessage,
);
}
@override
Future<NanoHttpResponse<T>> put<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
}) async {
final response = await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: Options(headers: headers),
);
return NanoHttpResponse<T>(
data: response.data,
statusCode: response.statusCode,
statusMessage: response.statusMessage,
);
}
@override
Future<NanoHttpResponse<T>> delete<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
}) async {
final response = await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: Options(headers: headers),
);
return NanoHttpResponse<T>(
data: response.data,
statusCode: response.statusCode,
statusMessage: response.statusMessage,
);
}
@override
Future<NanoHttpResponse<T>> patch<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
}) async {
final response = await _dio.patch<T>(
path,
data: data,
queryParameters: queryParameters,
options: Options(headers: headers),
);
return NanoHttpResponse<T>(
data: response.data,
statusCode: response.statusCode,
statusMessage: response.statusMessage,
);
}
}
Register it with interceptors at app startup with GetIt / NanoInjections:
void main() {
final client = DioHttpClient(Dio(BaseOptions(baseUrl: 'https://api.example.com')));
// Add traffic logging or custom authentication interceptors:
client.addInterceptor(const NanoHttpLogInterceptor());
GetIt.I.registerLazySingleton<NanoHttpClient>(() => client);
runApp(const MyApp());
}
4. Declarative Routing with NanoRouter, Observers & NanoApp #
Define all application routes and analytics observers in a single declarative router file:
import 'package:nano_core/nano_core.dart';
final appRouter = NanoRouter(
initialRoute: '/', // Optional: defaults to '/'
observers: [
// 🔭 Track screens automatically with Firebase Analytics / Datadog:
NanoRouteObserver(
onRouteChange: (from, to, args) {
debugPrint('Navigated from: $from -> to: $to');
},
),
],
routes: [
// Public dashboard route with smooth fade transition:
NanoAnimatedRoute.fade(
name: 'showcase',
path: '/',
builder: (context, args) => const ShowcasePage(),
),
// Users list with nested typed detail route:
NanoRoute(
name: 'users',
path: '/users',
builder: (context, args) => const UsersPage(),
routes: [
// Sub-route: /users/detail with automatic argument typing
NanoDetailsRoute<User>(
name: 'user_detail',
builder: (context, user) => UserDetailPage(user: user),
),
],
),
// Protected area with route guard wrapping admin routes:
NanoProtectedRoute(
hasAccess: (context, args) => AuthService.isAdmin,
redirectTo: 'login',
routes: [
NanoGroupRoute(
path: '/admin',
routes: [
NanoRoute(
name: 'admin',
path: '/panel',
builder: (context, args) => const AdminPage(),
),
],
),
],
),
// Redirect / Alias route:
NanoRedirectRoute(
path: '/home',
redirectTo: 'showcase',
),
],
);
Then plug it directly into NanoApp in main.dart:
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return NanoApp(
title: 'My Nano App',
router: appRouter, // 🧭 Configures navigatorKey, initialRoute, and onGenerateRoute
theme: AppTheme.darkTheme,
);
}
}
Navigating anywhere:
// Navigate by route name:
context.toNamed('user_detail', arguments: user);
// Navigate by path:
context.toNamed('/users/detail', arguments: user);
// Replace current screen:
context.toReplacementNamed('login');
// Pop screen:
context.back();
5. Type-Safe Search, Query Adapters & Pagination with NanoPaginator #
Handle URL query parameter serialization, pagination strategies, and infinite scroll lists with zero boilerplate:
1. Define Typed Filter and Adapter
class UserFilter {
final String? name;
final String? role;
const UserFilter({this.name, this.role});
}
class UserFilterAdapter implements NanoQueryAdapter<UserFilter> {
const UserFilterAdapter();
@override
Map<String, dynamic> toQueryParams(UserFilter query) => {
if (query.name != null && query.name!.isNotEmpty) 'name': query.name,
if (query.role != null && query.role != 'all') 'role': query.role,
};
}
2. Create Search Repository
class UserRepository extends NanoSearchRepository<User, String, UserFilter> {
UserRepository([super.client])
: super(
endpoint: '/users',
adapter: const UserAdapter(),
queryAdapter: const UserFilterAdapter(),
);
}
3. Automatic Infinite Scroll (Mobile) or Page Navigation Bar (Web)
// In Controller:
late final paginator = NanoPaginator<User>(
fetcher: (pagination) => userRepository.getAll(pagination: pagination),
);
// Option A: Mobile Infinite Scroll:
NanoPaginatedListView<User>(
paginator: controller.paginator,
itemBuilder: (context, user, index) => ListTile(title: Text(user.name)),
);
// Option B: Web / Desktop Navigation Bar:
NanoPaginationBar(
paginator: controller.paginator,
showPageSizeSelector: true,
availablePageSizes: const [5, 10, 20, 50],
);
4. Instantaneous Caching (0ms latency & Offline fallback)
Works seamlessly across Web, iOS, Android, macOS, Windows, and Linux:
// 1. Configure in-memory cache globally at startup:
NanoDefaultInjections.init(
i,
client: DioHttpClient(Dio()),
cache: NanoMemoryCache(defaultTtl: const Duration(minutes: 5)),
);
// 2. Fetch using cache-first (instant response on subsequent visits):
final users = await userRepository.getAll(cachePolicy: NanoCachePolicy.cacheFirst);
// 3. Force network update during pull-to-refresh:
final freshUsers = await userRepository.getAll(cachePolicy: NanoCachePolicy.networkOnly);
💾 Custom Persistent Cache (e.g., SharedPreferences / LocalStorage)
You can persist cached data across app restarts simply by implementing NanoCache:
import 'dart:convert';
import 'package:nano_core/nano_core.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPrefsCache implements NanoCache {
final SharedPreferences prefs;
const SharedPrefsCache(this.prefs);
@override
T? get<T>(String key) {
final raw = prefs.getString(key);
if (raw == null) return null;
return jsonDecode(raw) as T?;
}
@override
void set<T>(String key, T value, {Duration? ttl}) {
prefs.setString(key, jsonEncode(value));
}
@override
void delete(String key) => prefs.remove(key);
@override
void clear({String? prefix}) {
final keys = prefs.getKeys();
for (final k in keys) {
if (prefix == null || k.startsWith(prefix)) {
prefs.remove(k);
}
}
}
@override
bool has(String key) => prefs.containsKey(key);
}
6. Type-Safe Functional Results with NanoResult #
Handle operations with typed business errors without throwing exceptions, using modern Dart 3 sealed class pattern matching:
// 1. Return typed results from UseCases or Services:
Future<NanoResult<User, String>> login(String email, String password) async {
if (password.length < 6) {
return const NanoResult.failure('Password too short');
}
try {
final user = await authApi.authenticate(email, password);
return NanoResult.success(user);
} catch (e) {
return NanoResult.failure('Invalid credentials');
}
}
// 2. Consume with Dart 3 Pattern Matching:
final result = await login('dev@nano.core', 'secret123');
final message = switch (result) {
NanoSuccess(:final data) => 'Welcome back, ${data.name}!',
NanoFailure(:final error) => 'Login failed: $error',
};
// 3. Or wrap any existing async call safely:
final safeResult = await NanoResult.runAsync(() => userRepository.getAll());
7. Reactive Forms, Internationalized Validators & NanoTextField #
Build robust, strongly-typed forms with immutable entities, automatic view state updates via updateForm, and BuildContext i18n support:
1. Define Form Entity & View State
class UserFormEntity extends NanoFormEntity {
const UserFormEntity({
this.name = '',
this.email = '',
});
final String name;
final String email;
UserFormEntity copyWith({
String Function()? name,
String Function()? email,
}) =>
UserFormEntity(
name: name != null ? name() : this.name,
email: email != null ? email() : this.email,
);
@override
List<Object?> get props => [name, email];
}
class RegisterViewState extends NanoFormState<UserFormEntity> {
const RegisterViewState({super.form = const UserFormEntity()});
RegisterViewState copyWith({UserFormEntity? form}) =>
RegisterViewState(form: form ?? this.form);
}
2. Manage via Controller with submit & reset
class RegisterController
extends NanoFormController<RegisterViewState, UserFormEntity> {
final UserRepository userRepository;
RegisterController(this.userRepository)
: super(initialData: const RegisterViewState());
void saveUser() {
// 🎯 submit automatically validates all fields before execution:
submit((form) {
execute(() => userRepository.create(form));
});
}
}
3. Render with NanoForm & Reactive NanoTextField in View
NanoForm(
controller: controller,
child: Column(
children: [
NanoTextField(
value: state.data?.form.name,
label: 'Full Name',
prefixIcon: const Icon(Icons.person_outline),
validators: [
NanoValidator.required((context) => 'Name is required'),
NanoValidator.minLength(3, (context) => 'Minimum 3 characters'),
],
autoValidateMode: NanoAutoValidateMode.onUserInteraction,
onChanged: (text) => controller.updateForm(
(s) => s.copyWith(form: s.form.copyWith(name: () => text)),
),
),
const SizedBox(height: 20),
FilledButton(
onPressed: controller.saveUser,
child: const Text('Save User'),
),
],
),
)
8. Structured Logging with NanoLogger #
Log formatted, color-coded, and tagged events with method tracking and data inspection:
import 'package:nano_core/nano_core.dart';
// Info with method tracking and data payload:
NanoLogger.info(
'User authenticated successfully',
tag: 'AuthService',
method: 'loginWithEmail',
data: {'userId': '123', 'role': 'admin'},
);
// Success notification (using the short alias NanoLog or NLog):
NanoLog.success('Cache synchronized', tag: 'UserRepository');
NLog.info('Shortest syntax!');
// HTTP event with httpMethod and statusCode:
NLog.http(
'/users',
httpMethod: 'GET',
statusCode: 200,
tag: 'NanoHttp',
method: 'getUsers',
data: {'count': 2},
);
// Error reporting with exception, statusCode and stack trace:
NanoLog.error(
'Failed to fetch user profile',
statusCode: 404,
tag: 'UserRepository',
method: 'getById',
data: {'id': '123'},
error: exception,
stackTrace: stackTrace,
);
Tip: You can use
NanoLogger,NanoLog, orNLoginterchangeably as concise aliases.
Hook errors directly into Crashlytics or Sentry:
NanoLogger.onError = (entry) {
FirebaseCrashlytics.instance.recordError(
entry.error,
entry.stackTrace,
reason: entry.message,
);
};
6. Universal State Management (BLoC, Cubit, MobX, GetX, Signals) #
NanoScaffold can observe any external state management library via the lightweight NanoStateObservable contract or using out-of-the-box generic adapters:
⚡ Option A: Out-of-the-Box Generic Adapters (Zero Boilerplate)
// 1. Any Stream (BLoC, Cubit, RxDart, WebSockets):
final blocController = NanoStreamAdapter<UserState, BlocState>(
stream: userBloc.stream,
initialState: InitialState(),
mapper: (blocState) => switch (blocState) {
UserLoading() => LoadingState(),
UserSuccess(:final user) => SuccessState(data: user),
_ => InitialState(),
},
);
// 2. Any Listenable (MobX, Signals, ValueNotifier, Provider):
final storeController = NanoListenableAdapter<UserState>(
listenable: userStore,
stateGetter: () => userStore.isBusy
? LoadingState()
: SuccessState(data: userStore.user),
);
// Use directly in NanoScaffold:
NanoScaffold(
controller: blocController,
builder: (context, state) => Text('User: ${state.data?.name}'),
);
🛠️ Option B: Custom Class Implementation
1. BLoC / Cubit Class Adapter
class UserCubitAdapter extends ChangeNotifier
implements NanoStateObservable<UserState> {
final UserCubit cubit;
late final StreamSubscription _sub;
UserCubitAdapter(this.cubit) {
_sub = cubit.stream.listen((_) => notifyListeners());
}
@override
NanoState<UserState> get state => switch (cubit.state) {
UserLoading() => LoadingState(),
UserSuccess(:final user) => SuccessState(data: user),
UserError() => ErrorState(),
_ => InitialState(),
};
@override
void dispose() {
_sub.cancel();
super.dispose();
}
}
2. MobX Class Adapter
class UserMobxAdapter extends ChangeNotifier
implements NanoStateObservable<UserState> {
final UserStore store;
late final ReactionDisposer _disposer;
UserMobxAdapter(this.store) {
_disposer = autorun((_) => notifyListeners());
}
@override
NanoState<UserState> get state {
if (store.isLoading) return LoadingState();
if (store.user != null) return SuccessState(data: store.user!);
return InitialState();
}
@override
void dispose() {
_disposer();
super.dispose();
}
}
3. GetX Class Adapter
class UserGetxAdapter extends ChangeNotifier
implements NanoStateObservable<UserState> {
final UserController getxController;
late final Worker _worker;
UserGetxAdapter(this.getxController) {
_worker = ever(getxController.stateRx, (_) => notifyListeners());
}
@override
NanoState<UserState> get state => getxController.stateRx.value;
@override
void dispose() {
_worker.dispose();
super.dispose();
}
}
4. Signals / ValueNotifier Class Adapter
class UserSignalsAdapter extends ChangeNotifier
implements NanoStateObservable<UserState> {
final Signal<NanoState<UserState>> signalState;
late final VoidCallback _cleanup;
UserSignalsAdapter(this.signalState) {
_cleanup = effect(() {
signalState.value; // register dependency
notifyListeners();
});
}
@override
NanoState<UserState> get state => signalState.value;
@override
void dispose() {
_cleanup();
super.dispose();
}
}
License #
This project is licensed under the MIT License - see the LICENSE file for details.