Building Flutter Apps Without The Boilerplate
Flutter is a fast way to build cross-platform native applications with a beautiful UI. But the real cost of development often comes from the boilerplate required for everyday tasks. Navigating between screens, wiring up a controller, and showing alerts can demand verbose code that doesn't add business value.
Simply navigating to an AboutScreen requires passing a BuildContext through a chain of calls:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => AboutScreen()),
);
A more efficient and developer-friendly approach would be to call something like:
Get.to(AboutScreen());
Going back to the previous page has the same problem, also depending on the context property:
Navigator.pop(context);
With GetX Flutter, navigation doesn't need to be tied to the widget tree or a context at all. The same action can be expressed as:
Get.back();
GetX is a fast, stable, extra-light framework for Flutter that ships with high-performance state management, intelligent dependency injection, and route management. It targets common Flutter pain points with a simpler, more practical approach. At its core lie three principles:
- Performance — GetX features are built to consume as few resources as possible while keeping your app running efficiently.
- Productivity — With easy-to-remember syntax, GetX gets you building features quickly. It also provides a smart management system that removes controllers from memory when they are no longer in use — a task developers normally have to handle manually.
- Organization — GetX decouples the view from presentation logic, business logic, dependency injection, and navigation. You don't need context to navigate routes or to access your controllers/blocs through an
inheritedWidget, and you don't need to inject your controllers or models via multiproviders. GetX uses its own DI feature instead.
Core Features Included Out Of The Box
GetX covers a wide range of daily app development needs without requiring you to combine several separate packages:
- State management — Intuitive and minimal, achieved with little or no boilerplate.
- Route management — An API for navigation featuring a simple and concise syntax.
- Dependency management — Handles smart controller lifecycle management, removing controllers not in use from memory automatically.
- Internationalization — Provides i18n support out of the box for multi-language applications.
- Validation — Ships with validation methods for input validation, so you don't need to install a separate package.
- Storage — A fast, extra-light, synchronous in-memory key-value store that backs up data to disk after each operation. It's written entirely in Dart.
Setting Up The Demo App
The practical way to see GetX in action is to rebuild the default counter app Flutter scaffolds for you, with GetX managing its state. Let's start by creating a new Flutter project:
flutter create getx_demo
Next, clear main.dart to its minimal state:
# main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
This leaves the project broken, because there is no MyHomePage widget anymore. To fix that, create two directories inside lib/ to keep the UI and logic separate:
views/ | To hold the screens in our application. |
controllers/ | To hold all controllers for the screens in our application. |
Creating The View
Create a MyHomePage widget inside the views/ directory in a file named my_home_page.dart:
import 'package:flutter/material.dart';
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'0',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: null,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Now import it in main.dart, right below the Material import line:
import './views/my_home_page.dart';
Your main.dart should now look like this:
import 'package:flutter/material.dart';
import './views/my_home_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
This will fix the build errors. But the app is still missing its functionality: the UI hardcodes 0 and passes null to the button's onPressed handler. Time to integrate GetX.
Installing GetX
Install the get package (version 3.23.1 at the time of this writing) by adding this line to the dependencies section of your pubspec.yml file:
get: ^3.23.1
After saving the file, the package should install automatically. You can also install it manually via the terminal:
flutter pub get
Your dependencies section should now look like this:
dependencies:
flutter:
sdk: flutter
get: ^3.23.1
Moving Logic Into A Controller
GetX separates the UI from the business logic by providing the GetxController class, which you extend to create controller classes for specific views of your application. To create the controller for our app's single view, go to the controllers/ directory and open a file named my_home_page_controller.dart.
Start by importing the GetX package:
import 'package:get/get.dart';
Then, create a class that extends GetxController:
import 'package:get/get.dart';
class MyHomePageController extends GetxController {}
Add the count state to the class we've created:
final count = 0;
To make a variable observable — meaning that other parts of the application are notified when it changes — simply add .obs to the variable initialization:
final count = 0.obs;
The controller file looks like this so far:
import 'package:get/get.dart';
class MyHomePageController extends GetxController {
final count = 0.obs;
}
Finish the controller by implementing the increment method:
increment() => count.value++;
The .value suffix on count is needed because adding .obs to a variable wraps its value in an observable type that you access through its value property. With this in place, the controller holds the view's state and methods.
Connecting Controller And View
Next we're heading back to the view to instantiate this controller using GetX's dependency management feature. This ensures that the controller isn't kept in memory when it's no longer needed.
Import the get package and your controller file in views/my_home_page.dart:
import 'package:get/get.dart';
import '../controllers/my_home_page_controller.dart';
Instantiate the MyHomePageController inside the MyHomePage class:
final MyHomePageController controller = Get.put(MyHomePageController());
With the instance ready, you can access both the state variable and the method.
In GetX, you wrap a part of the UI that should rebuild when a state variable changes in an Obx widget. GetX supports other patterns for this, but Obx is the simplest and cleanest.
Wrap the Text widget inside the view with Obx:
Obx(() => Text('0',style: Theme.of(context).textTheme.headline4,),)
Then replace the hardcoded 0 with the controller's count variable:
Obx(() => Text('${controller.count.value}',
,style: Theme.of(context).textTheme.headline4,),)
Finally, call the increment method when the floatingActionButton is pressed:
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
),
Here's what the full view file looks like now:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/my_home_page_controller.dart';
class MyHomePage extends StatelessWidget {
final String title;
final MyHomePageController controller = Get.put(MyHomePageController());
MyHomePage({this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Obx(
() => Text(
'${controller.count.value}',
style: Theme.of(context).textTheme.headline4,
),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Save or rerun the app and the counter works as it initially did. Notice that the view no longer holds or maintains any state. It stays a stateless widget; all business logic lives in the controller class, and the state updates are cleanly decoupled from the UI.
Routing Without context
GetX also provides a navigation system that removes the need to pass context around for every route transition. To enable it, swap the root MaterialApp widget for GetMaterialApp in main.dart. After importing GetX at the top of the file, the change looks like this:
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import './views/my_home_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
Once the root widget is in place, any view can be pushed onto the stack with a single call. Create a new view in the views/ directory, for example about_page.dart:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/my_home_page_controller.dart';
class AboutPage extends StatelessWidget {
final MyHomePageController controller = Get.put(MyHomePageController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('About GetX'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'GetX is an extra-light and powerful solution for Flutter. It combines high performance state management, intelligent dependency injection, and route management in a quick and practical way.',
),
),
],
),
),
);
}
}
In MyHomePage, add a button below the existing Obx widget. Import the new page, and in the button’s onPressed callback call:
FlatButton(onPressed: () {}, child: Text('About GetX'))
import './about_page.dart';
Get.to(AboutPage());
FlatButton(
onPressed: () {
Get.to(AboutPage());
},
child: Text('About GetX'))
That is all it takes to navigate to AboutPage. For flows where the user should not be able to return to the previous screen—like a login page—use Get.off() instead. This removes the current route from the stack and replaces it with the new one:
Get.off(AboutPage());
Going back is just as direct. Add a button in AboutPage whose onPressed handler calls Get.back():
FlatButton(
onPressed: () {
Get.back();
},
child: Text('Go Home')
)
Snackbars And Dialogs
GetX also collapses the boilerplate for transient UI such as snackbars and dialogs. The conventional Flutter snackbar requires a ScaffoldMessenger and a context. The GetX version is a one-liner. Add another button to the home view and inside its handler write:
FlatButton(
onPressed: () {
// TODO: Implement Snackbar
},
child: Text('Show Snackbar'))
Get.snackbar('GetX Snackbar', 'Yay! Awesome GetX Snackbar');
That displays the snackbar at the top of the screen by default. You can move it to the bottom and alter its background color through the same API. The customized snackbar and its button become:
Get.snackbar('GetX Snackbar', 'Yay! Awesome GetX Snackbar',snackPosition:SnackPosition.BOTTOM,
);
Get.snackbar('GetX Snackbar', 'Yay! Awesome GetX Snackbar',snackPosition:SnackPosition.BOTTOM, backgroundColor: Colors.amberAccent
);
FlatButton(
onPressed: () {
Get.snackbar('GetX Snackbar', 'Yay! Awesome GetX Snackbar',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.amberAccent);
},
child: Text('Show Snackbar'))
Alert dialogs follow the same pattern. GetX exposes Get.defaultDialog() which shows a dismissible dialog in a single call:
FlatButton(
onPressed: () {
// TODO: Show alert dialog
},
child: Text('Show AlertDialog'))
Get.defaultDialog();
Customization is straightforward: pass title, middleText, and even custom action buttons for confirm and cancel flows. The resulting button code is compact and readable:
Get.defaultDialog(
title: 'GetX Alert', middleText: 'Simple GetX alert');
Get.defaultDialog(
title: 'GetX Alert',
middleText: 'Simple GetX alert',
textConfirm: 'Okay',
confirmTextColor: Colors.amberAccent,
textCancel: 'Cancel');
Wrapping Up
GetX is designed to remove repetitive boilerplate from Flutter development. State management, navigation, snackbars, and dialogs all have simple, context-free APIs that keep code concise without sacrificing performance. The demo project for this walkthrough is available on GitHub.




