diff --git a/app/components/wizard/PackageInfoPanel.tsx b/app/components/wizard/PackageInfoPanel.tsx index 2f2b1a8..87a7078 100644 --- a/app/components/wizard/PackageInfoPanel.tsx +++ b/app/components/wizard/PackageInfoPanel.tsx @@ -40,6 +40,10 @@ const PACKAGE_INFO: Record + { @@ -232,6 +234,51 @@ export function MiscStep() { + + + Dependency Injection + Choose how dependencies are wired in generated code. + + { + updateConfig({ dependencyInjection: value as DependencyInjectionStyle }) + }} + className="grid gap-2" + > + {dependencyInjectionOptions.map((option) => ( + + + + + {option.label} + {option.description} + + + { + e.preventDefault() + e.stopPropagation() + setSelectedItem(option.value) + }} + className="p-1.5 rounded-full hover:bg-primary/20 text-muted-foreground hover:text-primary transition-colors focus:outline-hidden cursor-pointer" + title="View details" + > + + + + ))} + + {categories.map((category) => ( +export const dependencyInjectionSchema = z.enum([ + "none", + "get_it", +]) +export type DependencyInjectionStyle = z.infer + const iconPackSchema = z.object({ default: z.literal(true), iconsax_plus: z.boolean(), @@ -173,6 +179,7 @@ export const scaffoldConfigSchema = z.object({ localization: localizationSchema, navigation: navigationSchema, architecture: architectureSchema, + dependencyInjection: dependencyInjectionSchema.default("none"), icons: iconPackSchema.default({ default: true, iconsax_plus: false, @@ -249,6 +256,7 @@ export const defaultConfig: ScaffoldConfig = { localization: { enabled: true, supportedLocales: ["en", "es"] }, navigation: "go_router", architecture: "feature-first", + dependencyInjection: "none", misc: { usesScreenutil: true, usesFlutterNativeSplash: true, @@ -337,6 +345,11 @@ export const architectureOptions = [ { value: "layer-first", label: "Layer-first", description: "Groups code by technical layers." }, ] as const satisfies Array<{ value: ArchitectureStyle; label: string; description: string }> +export const dependencyInjectionOptions = [ + { value: "none", label: "None", description: "Manual constructor wiring without a DI container." }, + { value: "get_it", label: "GetIt", description: "Service locator with centralized dependency registration." }, +] as const satisfies Array<{ value: DependencyInjectionStyle; label: string; description: string }> + export const navigationOptions = [ { value: "imperative", label: "Imperative (Navigator 1.0)", description: "Standard Navigator 1.0; simple for small apps." }, { value: "go_router", label: "go_router", description: "Declarative routing with deep linking support." }, diff --git a/app/lib/generator/index.ts b/app/lib/generator/index.ts index b8dd182..e975821 100644 --- a/app/lib/generator/index.ts +++ b/app/lib/generator/index.ts @@ -56,6 +56,7 @@ type TemplateContext = ScaffoldConfig & { usesDeviceInfoPlus: boolean usesAppVersionUpdate: boolean usesGeolocator: boolean + usesGetItDi: boolean } } @@ -98,7 +99,8 @@ export async function generateFlutterScaffold(input: unknown) { } } - const zipBuffer = await zipDirectory(workingDir) + const zipRootDir = context.flags.appSlug || "flutter-app" + const zipBuffer = await zipDirectory(workingDir, zipRootDir) return zipBuffer } finally { await fs.rm(workingDir, { recursive: true, force: true }).catch(() => { }) @@ -170,6 +172,7 @@ function buildTemplateContext(config: ScaffoldConfig): TemplateContext { usesDeviceInfoPlus: config.misc.usesDeviceInfoPlus, usesAppVersionUpdate: config.misc.usesAppVersionUpdate, usesGeolocator: config.misc.usesGeolocator, + usesGetItDi: config.dependencyInjection === "get_it", }, } } @@ -315,8 +318,9 @@ async function copyAndRenderDirectory( } } -async function zipDirectory(dir: string) { +async function zipDirectory(dir: string, rootFolderName: string) { const zip = new JSZip() + const zipRoot = rootFolderName.trim().replace(/[/\\]/g, "") || "flutter-app" async function walk(current: string) { const entries = await fs.readdir(current, { withFileTypes: true }) @@ -327,7 +331,7 @@ async function zipDirectory(dir: string) { await walk(fullPath) } else if (entry.isFile()) { const data = await fs.readFile(fullPath) - zip.file(relPath, data) + zip.file(`${zipRoot}/${relPath}`, data) } } } diff --git a/docs/configuration.md b/docs/configuration.md index 13f5a7c..a36f247 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -9,6 +9,7 @@ A complete reference of all available options, flags, and values you can configu | `appName` | `string` | The display name of your Flutter app. | `Flutter Starter` | | `packageId` | `string` | The bundled identifier (e.g. `com.example.app`). | *derived* | | `stateManagement` | `provider`, `riverpod`, `bloc`, `getx`, `mobx`, `none` | State management library for injected controllers. | `riverpod` | +| `dependencyInjection` | `none`, `get_it` | Dependency wiring strategy for repositories/services. | `none` | | `navigation` | `imperative`, `go_router`, `getx`, `auto_route` | Strategy used for routing and navigation flow. | `go_router` | | `architecture` | `mvc`, `mvvm`, `clean`, `feature-first`, `layer-first` | Parent folder structure and logic segregation pattern. | `feature-first` | diff --git a/package-lock.json b/package-lock.json index df4e58d..b10f2e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4655,7 +4655,7 @@ }, "node_modules/@types/node": { "version": "20.19.30", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4663,7 +4663,7 @@ }, "node_modules/@types/react": { "version": "19.2.9", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4671,7 +4671,7 @@ }, "node_modules/@types/react-dom": { "version": "19.2.3", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -8090,6 +8090,16 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hono": { + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -11912,7 +11922,7 @@ }, "node_modules/typescript": { "version": "5.9.3", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11976,7 +11986,7 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { diff --git a/templates/flutter/base/README.md.hbs b/templates/flutter/base/README.md.hbs index c935558..0c0c795 100644 --- a/templates/flutter/base/README.md.hbs +++ b/templates/flutter/base/README.md.hbs @@ -7,6 +7,7 @@ Generated with the Flutter Scaffolding Wizard. - Onboarding presentation starter - Routing scaffold {{#if flags.routerPackage}}using `{{flags.routerPackage}}`{{else}}with `MaterialApp` routes{{/if}} - {{> state_label}} +- Dependency injection: {{dependencyInjection}} - Backend: {{backend.provider}} ## Getting started @@ -16,4 +17,4 @@ flutter pub get flutter pub run build_runner build --delete-conflicting-outputs {{/if}} flutter run -``` \ No newline at end of file +``` diff --git a/templates/flutter/base/lib/main.dart.hbs b/templates/flutter/base/lib/main.dart.hbs index 5e49ef5..a822e8d 100644 --- a/templates/flutter/base/lib/main.dart.hbs +++ b/templates/flutter/base/lib/main.dart.hbs @@ -23,6 +23,7 @@ Future main() async { {{/if}} await AppConfig.init(); + await initDependencies(); {{#if flags.usesHive}} await HiveService.instance.init(); {{/if}} @@ -40,4 +41,4 @@ Future main() async { ){{/if}}, {{/if}} ); -} \ No newline at end of file +} diff --git a/templates/flutter/base/lib/src/di/service_locator.dart.hbs b/templates/flutter/base/lib/src/di/service_locator.dart.hbs new file mode 100644 index 0000000..eac908f --- /dev/null +++ b/templates/flutter/base/lib/src/di/service_locator.dart.hbs @@ -0,0 +1,30 @@ +import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}domain/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/domain/repositories/{{/if}}auth_repository.dart'; +import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}data/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/data/repositories/{{/if}}auth_repository_impl.dart'; +{{#if flags.usesGetItDi}} +import 'package:{{flags.appSnake}}/src/services/auth_service.dart'; +import 'package:get_it/get_it.dart'; + +final sl = GetIt.instance; +{{/if}} + +Future initDependencies() async { + {{#if flags.usesGetItDi}} + if (!sl.isRegistered()) { + sl.registerLazySingleton(() => AuthService.instance); + } + + if (!sl.isRegistered()) { + sl.registerLazySingleton( + () => AuthRepositoryImpl(authService: sl()), + ); + } + {{/if}} +} + +AuthRepository createAuthRepository() { + {{#if flags.usesGetItDi}} + return sl(); + {{else}} + return AuthRepositoryImpl(); + {{/if}} +} diff --git a/templates/flutter/base/lib/src/imports/core_imports.dart.hbs b/templates/flutter/base/lib/src/imports/core_imports.dart.hbs index 7f9b8bc..bddac31 100644 --- a/templates/flutter/base/lib/src/imports/core_imports.dart.hbs +++ b/templates/flutter/base/lib/src/imports/core_imports.dart.hbs @@ -18,6 +18,7 @@ export '../routing/global_navigator.dart'; {{#if flags.isGetX}} export '../routing/app_bindings.dart'; {{/if}} +export '../di/service_locator.dart'; export '../services/services.dart'; export '../shared/shared.dart'; diff --git a/templates/flutter/base/lib/src/routing/(isGetX)@app_bindings.dart.hbs b/templates/flutter/base/lib/src/routing/(isGetX)@app_bindings.dart.hbs index 6c5b3e6..7d1e005 100644 --- a/templates/flutter/base/lib/src/routing/(isGetX)@app_bindings.dart.hbs +++ b/templates/flutter/base/lib/src/routing/(isGetX)@app_bindings.dart.hbs @@ -1,5 +1,5 @@ import 'package:get/get.dart'; -import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}data/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/data/repositories/{{/if}}auth_repository_impl.dart'; +import 'package:{{flags.appSnake}}/src/di/service_locator.dart'; {{#if (eq architecture "layer-first")}} import 'package:{{flags.appSnake}}/src/presentation/controllers/auth/auth_controller.dart'; @@ -16,7 +16,7 @@ class AppBindings implements Bindings { void dependencies() { {{#if flags.isGetX}} Get.lazyPut( - () => AuthController(repository: AuthRepositoryImpl()), + () => AuthController(repository: createAuthRepository()), ); {{/if}} } diff --git a/templates/flutter/base/lib/src/shared/wrappers/state_wrapper.dart.hbs b/templates/flutter/base/lib/src/shared/wrappers/state_wrapper.dart.hbs index 9c52867..5d26ace 100644 --- a/templates/flutter/base/lib/src/shared/wrappers/state_wrapper.dart.hbs +++ b/templates/flutter/base/lib/src/shared/wrappers/state_wrapper.dart.hbs @@ -1,13 +1,10 @@ import '../../imports/imports.dart'; {{#if flags.isBloc}} -import '../../{{#if (eq architecture "layer-first")}}data/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/data/repositories/{{/if}}auth_repository_impl.dart'; import '../../{{#if (eq architecture "layer-first")}}presentation/providers/session_bloc.dart{{else if (eq architecture "mvc")}}controllers/auth/session_bloc.dart{{else if (eq architecture "mvvm")}}ui/auth/bloc/session_bloc.dart{{else}}features/auth/presentation/providers/session_bloc.dart{{/if}}'; {{else if flags.isProvider}} -import '../../{{#if (eq architecture "layer-first")}}data/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/data/repositories/{{/if}}auth_repository_impl.dart'; import '../../{{#if (eq architecture "layer-first")}}presentation/providers/session_provider.dart{{else if (eq architecture "mvc")}}controllers/auth/session_provider.dart{{else if (eq architecture "mvvm")}}ui/auth/providers/session_provider.dart{{else}}features/auth/presentation/providers/session_provider.dart{{/if}}'; {{else if flags.isMobX}} import 'package:provider/provider.dart'; -import '../../{{#if (eq architecture "layer-first")}}data/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/data/repositories/{{/if}}auth_repository_impl.dart'; import '../../{{#if (eq architecture "layer-first")}}presentation/providers/session_store.dart{{else if (eq architecture "mvc")}}controllers/auth/session_store.dart{{else if (eq architecture "mvvm")}}ui/auth/stores/session_store.dart{{else}}features/auth/presentation/providers/session_store.dart{{/if}}'; {{/if}} @@ -27,21 +24,21 @@ class StateWrapper extends StatelessWidget { {{else if flags.isProvider}} return MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => SessionProvider(repository: AuthRepositoryImpl())), + ChangeNotifierProvider(create: (_) => SessionProvider(repository: createAuthRepository())), ], child: child, ); {{else if flags.isBloc}} return MultiBlocProvider( providers: [ - BlocProvider(create: (_) => SessionBloc(repository: AuthRepositoryImpl())), + BlocProvider(create: (_) => SessionBloc(repository: createAuthRepository())), ], child: child, ); {{else if flags.isMobX}} return MultiProvider( providers: [ - Provider(create: (_) => SessionStore(repository: AuthRepositoryImpl())), + Provider(create: (_) => SessionStore(repository: createAuthRepository())), ], child: child, ); diff --git a/templates/flutter/base/pubspec.yaml.hbs b/templates/flutter/base/pubspec.yaml.hbs index e006cd9..f83800b 100644 --- a/templates/flutter/base/pubspec.yaml.hbs +++ b/templates/flutter/base/pubspec.yaml.hbs @@ -28,6 +28,9 @@ dependencies: # State Management fpdart: ^1.2.0 equatable: ^2.0.7 + {{#if flags.usesGetItDi}} + get_it: ^8.0.3 + {{/if}} {{#if flags.usesFlutterHooks}} flutter_hooks: ^0.20.5 {{/if}} diff --git a/templates/flutter/partials/features/auth/auth_logic.hbs b/templates/flutter/partials/features/auth/auth_logic.hbs index 5fdeed0..6c50919 100644 --- a/templates/flutter/partials/features/auth/auth_logic.hbs +++ b/templates/flutter/partials/features/auth/auth_logic.hbs @@ -3,11 +3,10 @@ import 'package:{{flags.appSnake}}/src/imports/packages_imports.dart'; {{#if flags.isRiverpod}} import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}domain/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/domain/repositories/{{/if}}auth_repository.dart'; -import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}data/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/data/repositories/{{/if}}auth_repository_impl.dart'; // Provides the single instance of AuthRepositoryImpl final authRepositoryProvider = Provider((ref) { - return AuthRepositoryImpl(); + return createAuthRepository(); }); final authControllerProvider = StateNotifierProvider((ref) { diff --git a/templates/flutter/partials/features/auth/auth_repository_impl.hbs b/templates/flutter/partials/features/auth/auth_repository_impl.hbs index d344a5c..6f2bc19 100644 --- a/templates/flutter/partials/features/auth/auth_repository_impl.hbs +++ b/templates/flutter/partials/features/auth/auth_repository_impl.hbs @@ -5,7 +5,10 @@ import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}do import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}domain/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/domain/repositories/{{/if}}auth_repository.dart'; class AuthRepositoryImpl implements AuthRepository { - final AuthService _authService = AuthService.instance; + final AuthService _authService; + + AuthRepositoryImpl({AuthService? authService}) + : _authService = authService ?? AuthService.instance; @override Stream get onAuthStateChanged { diff --git a/templates/flutter/partials/features/auth/forgot_password_screen.hbs b/templates/flutter/partials/features/auth/forgot_password_screen.hbs index 5dec2c4..3263e77 100644 --- a/templates/flutter/partials/features/auth/forgot_password_screen.hbs +++ b/templates/flutter/partials/features/auth/forgot_password_screen.hbs @@ -43,11 +43,11 @@ class ForgotPasswordScreen extends {{#if (and flags.isRiverpod flags.usesFlutter {{else if flags.isGetX}} final isLoading = controller.isLoading.value; {{else if flags.isMobX}} - final authStore = {{#if flags.usesFlutterHooks}}useMemoized{{else}}null; // Add your MobX store here{{/if}}(() => AuthStore(repository: AuthRepositoryImpl()), []); + final authStore = {{#if flags.usesFlutterHooks}}useMemoized{{else}}null; // Add your MobX store here{{/if}}(() => AuthStore(repository: createAuthRepository()), []); final isLoading = authStore.isLoading; {{else if flags.isNoneState}} {{#if flags.usesFlutterHooks}} - final viewModel = useMemoized(() => AuthViewModel(repository: AuthRepositoryImpl()), []); + final viewModel = useMemoized(() => AuthViewModel(repository: createAuthRepository()), []); useListenable(viewModel); final isLoading = viewModel.isLoading; {{else}} diff --git a/templates/flutter/partials/features/auth/login_screen.hbs b/templates/flutter/partials/features/auth/login_screen.hbs index cd718c4..0a3bad0 100644 --- a/templates/flutter/partials/features/auth/login_screen.hbs +++ b/templates/flutter/partials/features/auth/login_screen.hbs @@ -48,11 +48,11 @@ class LoginScreen extends {{#if (and flags.isRiverpod flags.usesFlutterHooks)}}H {{else if flags.isMobX}} // In a real app, MobX stores might be provided via Provider/GetIt. // Here we use a locally created memoized instance for the template. - final authStore = {{#if flags.usesFlutterHooks}}useMemoized{{else}}null; // Add your MobX store here{{/if}}(() => AuthStore(repository: AuthRepositoryImpl()), []); + final authStore = {{#if flags.usesFlutterHooks}}useMemoized{{else}}null; // Add your MobX store here{{/if}}(() => AuthStore(repository: createAuthRepository()), []); final isLoading = authStore.isLoading; {{else if flags.isNoneState}} {{#if flags.usesFlutterHooks}} - final viewModel = useMemoized(() => AuthViewModel(repository: AuthRepositoryImpl()), []); + final viewModel = useMemoized(() => AuthViewModel(repository: createAuthRepository()), []); useListenable(viewModel); final isLoading = viewModel.isLoading; {{else}} diff --git a/templates/flutter/partials/features/auth/session_provider.hbs b/templates/flutter/partials/features/auth/session_provider.hbs index 3598345..4e8e460 100644 --- a/templates/flutter/partials/features/auth/session_provider.hbs +++ b/templates/flutter/partials/features/auth/session_provider.hbs @@ -2,13 +2,14 @@ import 'dart:async'; import 'package:{{flags.appSnake}}/src/imports/packages_imports.dart'; import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}domain/entities/user.dart{{else if (eq architecture "mvc")}}models/user_model.dart{{else if (eq architecture "mvvm")}}data/models/user_model.dart{{else}}features/auth/domain/entities/user.dart{{/if}}'; import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}domain/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/domain/repositories/{{/if}}auth_repository.dart'; - {{#if flags.isRiverpod}} -import 'package:{{flags.appSnake}}/src/{{#if (eq architecture "layer-first")}}data/repositories/{{else if (eq architecture "mvc")}}services/{{else if (eq architecture "mvvm")}}data/repositories/{{else}}features/auth/data/repositories/{{/if}}auth_repository_impl.dart'; +import '{{#if (eq architecture "layer-first")}}../../di/service_locator.dart{{else if (eq architecture "mvc")}}../../di/service_locator.dart{{else if (eq architecture "mvvm")}}../../../di/service_locator.dart{{else}}../../../../di/service_locator.dart{{/if}}'; +{{/if}} +{{#if flags.isRiverpod}} /// Provides the AuthRepository instance final authRepositoryProvider = Provider((ref) { - return AuthRepositoryImpl(); + return createAuthRepository(); }); /// Provides a stream of auth state changes diff --git a/templates/flutter/partials/features/auth/signup_screen.hbs b/templates/flutter/partials/features/auth/signup_screen.hbs index d8742a3..3fedd07 100644 --- a/templates/flutter/partials/features/auth/signup_screen.hbs +++ b/templates/flutter/partials/features/auth/signup_screen.hbs @@ -53,11 +53,11 @@ class SignupScreen extends {{#if (and flags.isRiverpod flags.usesFlutterHooks)}} {{else if flags.isGetX}} final isLoading = controller.isLoading.value; {{else if flags.isMobX}} - final authStore = {{#if flags.usesFlutterHooks}}useMemoized{{else}}null; // Add your MobX store here{{/if}}(() => AuthStore(repository: AuthRepositoryImpl()), []); + final authStore = {{#if flags.usesFlutterHooks}}useMemoized{{else}}null; // Add your MobX store here{{/if}}(() => AuthStore(repository: createAuthRepository()), []); final isLoading = authStore.isLoading; {{else if flags.isNoneState}} {{#if flags.usesFlutterHooks}} - final viewModel = useMemoized(() => AuthViewModel(repository: AuthRepositoryImpl()), []); + final viewModel = useMemoized(() => AuthViewModel(repository: createAuthRepository()), []); useListenable(viewModel); final isLoading = viewModel.isLoading; {{else}}
Dependency Injection
Choose how dependencies are wired in generated code.
{option.description}