In GetX, how do Get.put, Get.lazyPut and Bindings differ, and when do a GetxController's onInit, onReady and onClose run?
answer
- eager versus on first find
- fenix keeps the factory
- Bindings.dependencies per route
- onReady one frame after onInit
- onClose on delete or route removal
basics
~20 sGet.put registers and starts an instance immediately; Get.lazyPut registers a builder that runs on the first Get.find. Bindings group those calls per route. onInit runs when the instance starts, onReady one frame later, onClose when it is deleted.
solid answer
~40 s`Get.put(HabitController())` registers a singleton in GetX's global registry and immediately looks it up, so the controller starts and `onInit` runs at once. `Get.lazyPut(() => HabitController())` registers only a builder; the instance is created, and `onInit` runs, on the first `Get.find<HabitController>()`. With `fenix: true` the builder survives deletion so a later find recreates the instance. A `Bindings` subclass puts these calls in `dependencies()` and is attached to a route through `GetPage(binding: ...)` or `Get.to(..., binding: ...)`. Lifecycle: `onInit` when the instance starts, `onReady` one frame later through a post-frame callback (the place for snackbars, dialogs or navigation), `onClose` just before deletion, by `Get.delete` or, under the default `SmartManagement.full`, when the GetX route it was first used on is removed. `permanent: true` and `GetxService` opt out of that automatic removal.
code
dart · 33 linesimport 'dart:async';
import 'package:get/get.dart';
abstract class HabitRepository {
Stream<int> watchStreak();
}
class HabitController extends GetxController {
HabitController(this._repo);
final HabitRepository _repo;
final streak = 0.obs;
StreamSubscription<int>? _sub;
@override
void onInit() {
super.onInit(); // also schedules onReady for the next frame
_sub = _repo.watchStreak().listen((s) => streak.value = s);
}
@override
void onReady() {
super.onReady();
Get.snackbar('Welcome back', 'Current streak: ${streak.value}');
}
@override
void onClose() {
_sub?.cancel();
super.onClose();
}
}go deeper
Recall that Get.put registers now, Get.lazyPut registers on first use, and Get.find retrieves the instance.
Explain the lifecycle order and timing, onReady's post-frame scheduling, and how Bindings attach registrations to routes.
Reason about when SmartManagement deletes instances, where permanent and GetxService are justified, and what leaks when onClose skips cleanup.
Treat the global registry as a design decision: set rules for what may be permanent and keep Get.find at route boundaries rather than inside business logic.
## The registry GetX keeps a **global registry** of instances keyed by type (plus an optional `tag`). Code anywhere can call `Get.find<HabitController>()` to obtain the registered instance; if nothing is registered, it throws '"HabitController" not found. You need to call "Get.put(HabitController())" or "Get.lazyPut(()=>HabitController())"'. ## Ways to register | Call | Instance created | Notes | |---|---|---| | `Get.put(x)` | now, by you | returns the registered instance; starts its lifecycle immediately | | `Get.lazyPut(() => X())` | on the first `Get.find` | `fenix: true` keeps the builder after deletion to recreate later | | `Get.putAsync(() async => X())` | after the future completes | for services that need async setup | | `Get.create(() => X())` | a new one on every `Get.find` | not a singleton | Options shared by several of these: - `tag:` registers several instances of one type under different keys. - `permanent: true` exempts the instance from automatic removal. - A second `Get.put` of an already-registered type and tag does **not** replace the live instance; `Get.replace` deletes and re-puts. ## Bindings A `Bindings` subclass implements one method, `dependencies()`, that performs the registrations a route needs. GetX runs it when the route is opened: ```dart class HabitBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => HabitRepository()); Get.lazyPut(() => HabitController(Get.find<HabitRepository>())); } } GetPage(name: '/habits', page: () => const HabitPage(), binding: HabitBinding()) ``` `BindingsBuilder(() { ... })` avoids a class for one-off bindings, and `GetMaterialApp(initialBinding: ...)` registers app-wide dependencies at start. Bindings keep registrations out of widgets, which is also what makes them replaceable in tests. ## The lifecycle `GetxController` inherits three hooks: 1. **`onInit()`** runs when the instance starts: immediately for `Get.put`, on first find for `Get.lazyPut`. Initialise fields and subscriptions here; call `super.onInit()`. 2. **`onReady()`** runs **one frame after** `onInit`, because `onInit` schedules it with a post-frame callback. The source recommends it for navigation events, snackbars, dialogs and async requests. 3. **`onClose()`** runs just before the instance is deleted. Dispose `TextEditingController`s, `AnimationController`s, stream subscriptions and workers here. ## When is an instance deleted? - Explicitly, by `Get.delete<HabitController>()`. - By a `GetBuilder` or `GetX` widget that created it through `init:` when that widget is disposed (with `autoRemove: true`, the default). - Automatically under `SmartManagement.full`, the default: an instance first used while a GetX route (`GetPageRoute` or `GetDialogRoute`) was current is linked to that route and deleted when the route is removed. This needs `GetMaterialApp` and GetX navigation. Exceptions: `permanent: true` instances are skipped unless deleted with `force: true`; a `GetxService` is not deleted by `Get.delete` without `force: true`, and `Get.reset()` clears everything. ## SmartManagement modes `GetMaterialApp(smartManagement: ...)` sets how aggressively unused instances are removed: | Mode | Behaviour | |---|---| | `SmartManagement.full` (default) | removes instances that are unused and not permanent, including ones registered with `Get.put` | | `SmartManagement.onlyBuilder` | removes only controllers started through `init:` or registered lazily in a Binding; `Get.put` instances stay | | `SmartManagement.keepFactory` | removes unused instances like `full` but keeps their factories, as if every `lazyPut` used `fenix: true` | The source's own advice is to leave `full` in place unless you have a reason; changing the mode changes when every `onClose` in the app runs, which is easy to underestimate. ## Picking a registration style - Use `lazyPut` inside Bindings for screen controllers, so nothing is created before the screen needs it. - Use `put` for objects that must start immediately, such as a controller whose `onInit` opens a connection. - Use `GetxService` or `permanent: true` for app-lifetime services. - Avoid calling `Get.put` inside `build`: it runs on every rebuild; it is harmless only because a second put of a live instance is ignored.
- Why is onReady, not onInit, recommended for showing a snackbar or navigating?`onInit` often runs while a widget is building, for example on the first `Get.find` from a `build` method. Showing UI or navigating then interferes with the frame in progress. `onReady` is scheduled with a post-frame callback, so it runs after that frame has been built.
- What does fenix: true change for Get.lazyPut?When the instance is deleted, for example because its route was removed, GetX keeps the builder instead of dropping the registration. The next `Get.find` builds a fresh instance and runs `onInit` again. `SmartManagement.keepFactory` makes that the default for lazy registrations.
- How do you keep a service alive for the whole app in GetX?Register it with `permanent: true`, or extend `GetxService`. Neither is removed by route-linked smart management; `Get.delete` refuses both unless called with `force: true`, and `Get.reset()` clears them along with everything else.
GetX's registry is a hotel front desk: Get.put checks a guest in right away, Get.lazyPut leaves a reservation that turns into a guest the first time someone asks for them, and SmartManagement is housekeeping that checks guests out when the floor they arrived on closes.
saying these in an interview costs you the question
- Get.lazyPut creates the instance immediately but delays onInit
- onReady runs in the same frame as onInit
- A second Get.put of the same type replaces the existing instance
- GetX never deletes controllers unless you call Get.delete
- Bindings run once at app start for every route