GetX is a powerful Flutter framework with 3 main pillars:
MVVM = Model – View – ViewModel
VIEW (UI)
– Widget
– No business logic
– Only displays data
Observes (Obx/GetBuilder)
VIEWMODEL (Controller)
– Business Logic
– State Management
– Input Validation
– Communication with Repository
Retrieving/Saving Data
MODEL (Data Layer)
– Repository
– Provider (API, Database)
– Data Models
lib/
main.dart
app/
core/ # Code used throughout the app
utils/ # Utility functions
helpers.dart
validators.dart
values/ # Constants
colors.dart
strings.dart
text_styles.dart
widgets/ # Reusable widgets
custom_button.dart
loading_widget.dart
data/ # DATA LAYER (Model)
models/ # Data models
user_model.dart
product_model.dart
providers/ # API calls, local DB
api_provider.dart
local_storage_provider.dart
repositories/ # Bridge Controller Provider
user_repository.dart
product_repository.dart
modules/ # APPLICATION FEATURES
home/
bindings/
home_binding.dart
controllers/
home_controller.dart
views/
home_view.dart
widgets/
home_card.dart
login/
bindings/
login_binding.dart
controllers/
login_controller.dart
views/
login_view.dart
profile/
bindings/
profile_binding.dart
controllers/
profile_controller.dart
views/
profile_view.dart
routes/ # ROUTING
app_pages.dart # Definition of all routes
app_routes.dart # Constant house routes
pubspec.yaml
A model is a representation of data, usually from an API or database.
// lib/app/data/models/user_model.dart
class User {
final String id;
final String name;
final String email;
final String? avatar;
User({
required this.id,
required this.name,
required this.email,
this.avatar,
});
// From JSON to Object (parsing API response)
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json[‘id’] ?? ”,
name: json[‘name’] ?? ”,
email: json[’email’] ?? ”,
avatar: json[‘avatar’],
);
}
// From Object to JSON (to send to API)
Map<String, dynamic> toJson() {
return {
‘id’: id,
‘name’: name,
’email’: email,
‘avatar’: avatar,
};
}
// CopyWith for immutability
User copyWith({
String? id,
String? name,
String? email,
String? avatar,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
avatar: avatar ?? this.avatar,
);
}
}
Example of a Complex Model with Nested Objects:
// lib/app/data/models/product_model.dart
class Product {
final String id;
final String name;
final double price;
final Category category;
final List<String> images;
Product({
required this.id,
required this.name,
required this.price,
required this.category,
required this.images,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json[‘id’],
name: json[‘name’],
price: (json[‘price’] as num).toDouble(),
category: Category.fromJson(json[‘category’]),
images: List<String>.from(json[‘images’] ?? []),
);
}
}
class Category {
final String id;
final String name;
Category({required this.id, required this.name});
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
id: json[‘id’],
name: json[‘name’],
);
}
}
The provider is responsible for communication with external data sources (API, Database, etc.).
// lib/app/data/providers/api_provider.dart
import ‘package:dio/dio.dart’;
class ApiProvider {
final Dio _dio = Dio(BaseOptions(
baseUrl: ‘https://api.example.com’,
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
));
// GET request
Future<Response> get(String path) async {
try {
final response = await _dio.get(path);
return response;
} on DioException catch (e) {
throw _handleError(e);
}
}
// POST request
Future<Response> post(String path, Map<String, dynamic> data) async {
try {
final response = await _dio.post(path, data: data);
return response;
} on DioException catch (e) {
throw _handleError(e);
}
}
// PUT request
Future<Response> put(String path, Map<String, dynamic> data) async {
try {
final response = await _dio.put(path, data: data);
return response;
} on DioException catch (e) {
throw _handleError(e);
}
}
// DELETE request
Future<Response> delete(String path) async {
try {
final response = await _dio.delete(path);
return response;
} on DioException catch (e) {
throw _handleError(e);
}
}
// Error handling
Exception _handleError(DioException error) {
String errorMessage = ”;
switch (error.type) {
case DioExceptionType.connectionTimeout:
errorMessage = ‘Connection timeout’;
break;
case DioExceptionType.sendTimeout:
errorMessage = ‘Send timeout’;
break;
case DioExceptionType.receiveTimeout:
errorMessage = ‘Receive timeout’;
break;
case DioExceptionType.badResponse:
errorMessage = ‘Received invalid status code: ${error.response?.statusCode}’;
break;
case DioExceptionType.cancel:
errorMessage = ‘Request cancelled’;
break;
default:
errorMessage = ‘Connection error’;
}
return Exception(errorMessage);
}
}
Provider for Local Storage:
// lib/app/data/providers/local_storage_provider.dart
import ‘package:get_storage/get_storage.dart’;
class LocalStorageProvider {
final _storage = GetStorage();
// Save data
Future<void> write(String key, dynamic value) async {
await _storage.write(key, value);
}
// Read data
T? read<T>(String key) {
return _storage.read<T>(key);
}
// Delete data
Future<void> remove(String key) async {
await _storage.remove(key);
}
// Delete all data
Future<void> clearAll() async {
await _storage.erase();
}
}
Repository isbridgebetween the Controller and the Provider. This abstraction eliminates the need for the controller to know the details of how data is retrieved.
// lib/app/data/repositories/user_repository.dart
import ‘../models/user_model.dart’;
import ‘../providers/api_provider.dart’;
class UserRepository {
final ApiProvider _apiProvider;
UserRepository(this._apiProvider);
// Get user by ID
Future<User> getUser(String userId) async {
try {
final response = await _apiProvider.get(‘/users/$userId’);
return User.fromJson(response.data);
} catch (e) {
throw Exception(‘Failed to fetch user: $e’);
}
}
// Get all users
Future<List<User>> getAllUsers() async {
try {
final response = await _apiProvider.get(‘/users’);
final List<dynamic> usersJson = response.data;
return usersJson.map((json) => User.fromJson(json)).toList();
} catch (e) {
throw Exception(‘Failed to fetch users: $e’);
}
}
// Update user
Future<User> updateUser(String userId, Map<String, dynamic> data) async {
try {
final response = await _apiProvider.put(‘/users/$userId’, data);
return User.fromJson(response.data);
} catch (e) {
throw Exception(‘Failed to update user: $e’);
}
}
// Delete user
Future<void> deleteUser(String userId) async {
try {
await _apiProvider.delete(‘/users/$userId’);
} catch (e) {
throw Exception(‘Failed to delete user: $e’);
}
}
}
Repository with Caching:
// lib/app/data/repositories/product_repository.dart
import ‘../models/product_model.dart’;
import ‘../providers/api_provider.dart’;
import ‘../providers/local_storage_provider.dart’;
class ProductRepository {
final ApiProvider _apiProvider;
final LocalStorageProvider _localStorageProvider;
ProductRepository(this._apiProvider, this._localStorageProvider);
Future<List<Product>> getProducts({bool forceRefresh = false}) async {
// Check cache first
if (!forceRefresh) {
final cachedProducts = _localStorageProvider.read<List>(‘products’);
if (cachedProducts != null && cachedProducts.isNotEmpty) {
return cachedProducts
.map((json) => Product.fromJson(json))
.toList();
}
}
// If cache is empty or force refresh, get from API
try {
final response = await _apiProvider.get(‘/products’);
final List<dynamic> productsJson = response.data;
// Simple cache
await _localStorageProvider.write(‘products’, productsJson);
return productsJson.map((json) => Product.fromJson(json)).toList();
} catch (e) {
throw Exception(‘Failed to fetch products: $e’);
}
}
}
Controller isbrainfrom the application. This is where all the business logic is located.
// lib/app/modules/home/controllers/home_controller.dart
import ‘package:get/get.dart’;
import ‘../../../data/models/user_model.dart’;
import ‘../../../data/repositories/user_repository.dart’;
class HomeController extends GetxController {
final UserRepository repository;
HomeController(this.repository);
// Reactive variables
var isLoading = false.obs;
var users = <User>[].obs;
var errorMessage = ”.obs;
// Lifecycle: called when the controller is initialized
@override
void onInit() {
super.onInit();
fetchUsers();
}
// Lifecycle: called when the controller is ready to use
@override
void onReady() {
super.onReady();
print(‘HomeController is ready’);
}
// Lifecycle: called when the controller is deleted
@override
void onClose() {
print(‘HomeController is closed’);
super.onClose();
}
// Business Logic: Fetch users
Future<void> fetchUsers() async {
try {
isLoading.value = true;
errorMessage.value = ”;
final result = await repository.getAllUsers();
users.value = result;
} catch (e) {
errorMessage.value = e.toString();
Get.snackbar(
‘Error’,
‘Failed to load users’,
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isLoading.value = false;
}
}
// Business Logic: Refresh
Future<void> refreshUsers() async {
await fetchUsers();
}
}
// lib/app/modules/login/controllers/login_controller.dart
import ‘package:flutter/material.dart’;
import ‘package:get/get.dart’;
import ‘../../../data/repositories/auth_repository.dart’;
import ‘../../../routes/app_routes.dart’;
class LoginController extends GetxController {
final AuthRepository repository;
LoginController(this.repository);
// Form controllers
final emailController = TextEditingController();
final passwordController = TextEditingController();
// Reactive variables
var isLoading = false.obs;
var isPasswordHidden = true.obs;
// Validation
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return ‘Email cannot be empty’;
}
if (!GetUtils.isEmail(value)) {
return ‘Invalid email format’;
}
return null;
}
String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return ‘Password cannot be empty’;
}
if (value.length < 6) {
return ‘Password minimum 6 characters’;
}
return null;
}
// Toggle password visibility
void togglePasswordVisibility() {
isPasswordHidden.value = !isPasswordHidden.value;
}
// Login action
Future<void> login() async {
// Input validation
final emailError = validateEmail(emailController.text);
final passwordError = validatePassword(passwordController.text);
if (emailError != null || passwordError != null) {
Get.snackbar(‘Validation Error’, emailError ?? passwordError ?? ”);
return;
}
try {
isLoading.value = true;
await repository.login(
email: emailController.text,
password: passwordController.text,
);
// Navigate to home
Get.offAllNamed(Routes.HOME);
} catch (e) {
Get.snackbar(
‘Login Failed’,
e.toString(),
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isLoading.value = false;
}
}
@override
void onClose() {
emailController.dispose();
passwordController.dispose();
super.onClose();
}
}
// lib/app/modules/search/controllers/search_controller.dart
import ‘package:get/get.dart’;
import ‘../../../data/repositories/product_repository.dart’;
class SearchController extends GetxController {
final ProductRepository repository;
SearchController(this.repository);
var searchQuery = ”.obs;
var searchResults = [].obs;
var isSearching = false.obs;
@override
void onInit() {
super.onInit();
// Debounce: wait for the user to finish typing (1 second) then search
debounce(
searchQuery,
(_) => performSearch(),
time: Duration(seconds: 1),
);
// Ever: run every time searchQuery changes
ever(searchQuery, (_) {
print(‘Search query changed to: ${searchQuery.value}’);
});
// Once: run only once when first changed
once(searchQuery, (_) {
print(‘First search performed’);
});
}
void updateSearchQuery(String query) {
searchQuery.value = query;
}
Future<void> performSearch() async {
if (searchQuery.value.isEmpty) {
searchResults.clear();
return;
}
try {
isSearching.value = true;
final results = await repository.searchProducts(searchQuery.value);
searchResults.value = results;
} catch (e) {
Get.snackbar(‘Error’, ‘Search failed: $e’);
} finally {
isSearching.value = false;
}
}
}
View isUI/Widget. Its task is only to display data, there should be no business logic.
GetView<T>is a widget that automatically has access to the type controllerT.
// lib/app/modules/home/views/home_view.dart
import ‘package:flutter/material.dart’;
import ‘package:get/get.dart’;
import ‘../controllers/home_controller.dart’;
class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(‘Home’),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: controller.refreshUsers,
),
],
),
body: Obx(() {
// Loading state
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
// Error state
if (controller.errorMessage.value.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(controller.errorMessage.value),
SizedBox(height: 16),
ElevatedButton(
onPressed: controller.fetchUsers,
child: Text(‘Retry’),
),
],
),
);
}
// Empty state
if (controller.users.isEmpty) {
return Center(child: Text(‘No users found’));
}
// Success state
return ListView.builder(
itemCount: controller.users.length,
itemBuilder: (context, index) {
final user = controller.users[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.avatar ?? ”),
),
title: Text(user.name),
subtitle: Text(user.email),
onTap: () {
Get.toNamed(Routes.PROFILE, arguments: user.id);
},
);
},
);
}),
);
}
}
// lib/app/modules/login/views/login_view.dart
import ‘package:flutter/material.dart’;
import ‘package:get/get.dart’;
import ‘../controllers/login_controller.dart’;
class LoginView extends GetView<LoginController> {
const LoginView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
FlutterLogo(size: 100),
SizedBox(height: 48),
// Email field
TextField(
controller: controller.emailController,
decoration: InputDecoration(
labelText: ‘Email’,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
// Password field
Obx(() => TextField(
controller: controller.passwordController,
obscureText: controller.isPasswordHidden.value,
decoration: InputDecoration(
labelText: ‘Password’,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
controller.isPasswordHidden.value
? Icons.visibility_off
: Icons.visibility,
),
onPressed: controller.togglePasswordVisibility,
),
),
)),
SizedBox(height: 24),
// Login button
Obx(() => SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.login,
child: controller.isLoading.value
? CircularProgressIndicator(color: Colors.white)
: Text(‘Login’),
),
)),
],
),
),
),
);
}
}
1. Obx – Untuk reactive variables (.obs)
Obx(() => Text(‘Count: ${controller.count.value}’))
2. GetBuilder- For manual updates (more performance for complex data)
GetBuilder<HomeController>(
builder: (controller) => Text(‘Count: ${controller.count}’),
)
3. GetX Widget- Combination of dependency injection + reactive
GetX<HomeController>(
init: HomeController(),
builder: (controller) => Text(‘Count: ${controller.count.value}’),
)
Bindings connect the controller to its dependencies and perform lazy initialization.
// lib/app/modules/home/bindings/home_binding.dart
import ‘package:get/get.dart’;
import ‘../../../data/providers/api_provider.dart’;
import ‘../../../data/repositories/user_repository.dart’;
import ‘../controllers/home_controller.dart’;
class HomeBinding extends Bindings {
@override
void dependencies() {
// Lazy initialization: only created when needed
Get.lazyPut<ApiProvider>(() => ApiProvider());
Get.lazyPut<UserRepository>(() => UserRepository(Get.find()));
Get.lazyPut<HomeController>(() => HomeController(Get.find()));
}
}
// 1. lazyPut – Created when first needed (most frequently used)
Get.lazyPut(() => HomeController());
// 2. put – Created immediately, even if not used yet
Get.put(HomeController());
// 3. putAsync – For async initialization
Get.putAsync<SharedPreferences>(() async {
return await SharedPreferences.getInstance();
});
// 4. create – Creates a new instance every time Get.find() is called
Get.create(() => HomeController());
// lib/app/modules/profile/bindings/profile_binding.dart
import ‘package:get/get.dart’;
import ‘../../../data/providers/api_provider.dart’;
import ‘../../../data/providers/local_storage_provider.dart’;
import ‘../../../data/repositories/user_repository.dart’;
import ‘../../../data/repositories/auth_repository.dart’;
import ‘../controllers/profile_controller.dart’;
class ProfileBinding extends Bindings {
@override
void dependencies() {
// Providers
Get.lazyPut<ApiProvider>(() => ApiProvider());
Get.lazyPut<LocalStorageProvider>(() => LocalStorageProvider());
// Repositories
Get.lazyPut<UserRepository>(
() => UserRepository(Get.find()),
);
Get.lazyPut<AuthRepository>(
() => AuthRepository(Get.find(), Get.find()),
);
// Controller
Get.lazyPut<ProfileController>(
() => ProfileController(Get.find(), Get.find()),
);
}
}
// lib/app/routes/app_routes.dart
abstract class Routes {
static const SPLASH = ‘/splash’;
static const LOGIN = ‘/login’;
static const REGISTER = ‘/register’;
static const HOME = ‘/home’;
static const PROFILE = ‘/profile’;
static const SETTINGS = ‘/settings’;
static const PRODUCT_DETAIL = ‘/product-detail’;
}
// lib/app/routes/app_pages.dart
import ‘package:get/get.dart’;
import ‘../modules/home/bindings/home_binding.dart’;
import ‘../modules/home/views/home_view.dart’;
import ‘../modules/login/bindings/login_binding.dart’;
import ‘../modules/login/views/login_view.dart’;
import ‘../modules/profile/bindings/profile_binding.dart’;
import ‘../modules/profile/views/profile_view.dart’;
import ‘app_routes.dart’;
class AppPages {
static const INITIAL = Routes.LOGIN;
static final routes = [
GetPage(
name: Routes.LOGIN,
page: () => LoginView(),
binding: LoginBinding(),
),
GetPage(
name: Routes.HOME,
page: () => HomeView(),
binding: HomeBinding(),
transition: Transition.fadeIn,
),
GetPage(
name: Routes.PROFILE,
page: () => ProfileView(),
binding: ProfileBinding(),
transition: Transition.rightToLeft,
),
];
}
// Navigate to new page
Get.to(() => NextPage());
Get.toNamed(Routes.HOME);
// Navigation with arguments
Get.toNamed(Routes.PROFILE, arguments: {‘userId’: ‘123’});
// In the destination controller, accept the arguments:
final userId = Get.arguments[‘userId’];
// Navigate and delete previous page
Get.off(() => HomePage());
Get.offNamed(Routes.HOME);
// Navigate and clear all previous pages (clear stack)
Get.offAll(() => HomePage());
Get.offAllNamed(Routes.HOME);
// Back to previous page
Get.back();
// Return with data
Get.back(result: {‘success’: true});
// Return until a certain condition is met
Get.until((route) => route.settings.name == Routes.HOME);
Middleware is useful for guard routes (e.g. login check).
// lib/app/middlewares/auth_middleware.dart
import ‘package:flutter/material.dart’;
import ‘package:get/get.dart’;
import ‘../routes/app_routes.dart’;
class AuthMiddleware extends GetMiddleware {
@override
int? get priority => 1;
@override
RouteSettings? redirect(String? route) {
// Check if the user is logged in
final isLoggedIn = Get.find<AuthService>().isLoggedIn;
if (!isLoggedIn) {
return RouteSettings(name: Routes.LOGIN);
}
return null; // null = continue to the requested route
}
}
// Use in app_pages.dart:
GetPage(
name: Routes.HOME,
page: () => HomeView(),
binding: HomeBinding(),
middlewares: [AuthMiddleware()],
),
// lib/app/data/models/todo_model.dart
class Todo {
final String id;
final String title;
final String description;
final bool isCompleted;
final DateTime createdAt;
Todo({
required this.id,
required this.title,
required this.description,
this.isCompleted = false,
required this.createdAt,
});
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json[‘id’],
title: json[‘title’],
description: json[‘description’] ?? ”,
isCompleted: json[‘isCompleted’] ?? false,
createdAt: DateTime.parse(json[‘createdAt’]),
);
}
Map<String, dynamic> toJson() {
return {
‘id’: id,
‘title’: title,
‘description’: description,
‘isCompleted’: isCompleted,
‘createdAt’: createdAt.toIso8601String(),
};
}
Todo copyWith({
String? id,
String? title,
String? description,
bool? isCompleted,
DateTime? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
);
}
}
// lib/app/data/repositories/todo_repository.dart
import ‘../models/todo_model.dart’;
import ‘../providers/api_provider.dart’;
class TodoRepository {
final ApiProvider _apiProvider;
TodoRepository(this._apiProvider);
Future<List<Todo>> getAllTodos() async {
try {
final response = await _apiProvider.get(‘/todos’);
final List<dynamic> todosJson = response.data;
return allJson.map((json) => All.fromJson(json)).toList();
} catch (e) {
throw Exception(‘Failed to fetch todos: $e’);
}
}
Future<Todo> createTodo(Todo todo) async {
try {
final response = await _apiProvider.post(‘/todos’, todo.toJson());
return Todo.fromJson(response.data);
} catch (e) {
throw Exception(‘Failed to create todo: $e’);
}
}
Future<Todo> updateTodo(String id, Todo todo) async {
try {
final response = await _apiProvider.put(‘/todos/$id’, todo.toJson());
return Todo.fromJson(response.data);
} catch (e) {
throw Exception(‘Failed to update todo: $e’);
}
}
Future<void> deleteTodo(String id) async {
try {
await _apiProvider.delete(‘/todos/$id’);
} catch (e) {
throw Exception(‘Failed to delete todo: $e’);
}
}
}
// lib/app/modules/todo/controllers/todo_controller.dart
import ‘package:get/get.dart’;
import ‘../../../data/models/todo_model.dart’;
import ‘../../../data/repositories/todo_repository.dart’;
class TodoController extends GetxController {
final TodoRepository repository;
TodoController(this.repository);
var todos = <Todo>[].obs;
var isLoading = false.obs;
var filter = TodoFilter.all.obs;
@override
void onInit() {
super.onInit();
fetchTodos();
}
// Get filtered todos
List<Todo> get filteredTodos {
switch (filter.value) {
case TodoFilter.completed:
return todos.where((todo) => todo.isCompleted).toList();
case TodoFilter.active:
return todos.where((all) => !all.isCompleted).toList();
default:
return all;
}
}
// Get count
int get completedCount => todos.where((t) => t.isCompleted).length;
int get activeCount => todos.where((t) => !t.isCompleted).length;
Future<void> fetchTodos() async {
try {
isLoading.value = true;
final result = await repository.getAllTodos();
everyone.value = result;
} catch (e) {
Get.snackbar(‘Error’, ‘Failed to fetch todos’);
} finally {
isLoading.value = false;
}
}
Future<void> addTodo(String title, String description) async {
try {
final newTodo = Todo(
id: DateTime.now().toString(),
title: title,
description: description,
createdAt: DateTime.now(),
);
final created = await repository.createTodo(newTodo);
todos.add(created);
Get.snackbar(‘Success’, ‘Todo added successfully’);
} catch (e) {
Get.snackbar(‘Error’, ‘Failed to add todo’);
}
}
Future<void> toggleTodo(Todo todo) async {
try {
final updated = todo.copyWith(isCompleted: !todo.isCompleted);
await repository.updateAll(all.id, updated);
final index = todos.indexWhere((t) => t.id == todo.id);
todos[index] = updated;
} catch (e) {
Get.snackbar(‘Error’, ‘Failed to update todo’);
}
}
Future<void> deleteTodo(String id) async {
try {
await repository.deleteTodo(id);
everyone.removeWhere((everything) => everything.id == id);
Get.snackbar(‘Success’, ‘Todo deleted’);
} catch (e) {
Get.snackbar(‘Error’, ‘Failed to delete todo’);
}
}
void setFilter(TodoFilter newFilter) {
filter.value = newFilter;
}
}
enum TodoFilter { all, active, completed }
// lib/app/modules/todo/views/todo_view.dart
import ‘package:flutter/material.dart’;
import ‘package:get/get.dart’;
import ‘../controllers/todo_controller.dart’;
class TodoView extends GetView<TodoController> {
const TodoView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(‘My Todos’),
bottom: PreferredSize(
preferredSize: Size.fromHeight(50),
child: _buildFilterTabs(),
),
),
body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
if (controller.filteredTodos.isEmpty) {
return Center(child: Text(‘No todos found’));
}
return ListView.builder(
itemCount: controller.filteredTodos.length,
itemBuilder: (context, index) {
final todo = controller.filteredTodos[index];
return _buildTodoItem(todo);
},
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _showAddTodoDialog,
child: Icon(Icons.add),
),
bottomNavigationBar: _buildBottomBar(),
);
}
Widget _buildFilterTabs() {
return Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFilterChip(‘All’, TodoFilter.all),
_buildFilterChip(‘Active’, TodoFilter.active),
_buildFilterChip(‘Completed’, TodoFilter.completed),
],
));
}
Widget _buildFilterChip(String label, TodoFilter filter) {
final isSelected = controller.filter.value == filter;
return ChoiceChip(
label: Text(label),
selected: isSelected,
onSelected: (_) => controller.setFilter(filter),
);
}
Widget _buildTodoItem(Todo todo) {
return Dismissible(
key: Key(todo.id),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 16),
child: Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (_) => controller.deleteTodo(todo.id),
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => controller.toggleTodo(todo),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
subtitle: Text(todo.description),
),
);
}
Widget _buildBottomBar() {
return Obx(() => Container(
padding: EdgeInsets.all(16),
child: Text(
‘${controller.activeCount} active ${controller.completedCount} completed’,
textAlign: TextAlign.center,
),
));
}
void _showAddTodoDialog() {
final titleController = TextEditingController();
final descController = TextEditingController();
Get.dialog(
AlertDialog(
title: Text(‘Add New Todo’),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: InputDecoration(labelText: ‘Title’),
),
SizedBox(height: 8),
TextField(
controller: descController,
decoration: InputDecoration(labelText: ‘Description’),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(‘Cancel’),
),
ElevatedButton(
onPressed: () {
if (titleController.text.isNotEmpty) {
controller.addTodo(
titleController.text,
descController.text,
);
Get.back();
}
},
child: Text(‘Add’),
),
],
),
);
}
}
// lib/app/modules/todo/bindings/todo_binding.dart
import ‘package:get/get.dart’;
import ‘../../../data/providers/api_provider.dart’;
import ‘../../../data/repositories/todo_repository.dart’;
import ‘../controllers/todo_controller.dart’;
class TodoBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiProvider>(() => ApiProvider());
Get.lazyPut<TodoRepository>(() => TodoRepository(Get.find()));
Get.lazyPut<TodoController>(() => TodoController(Get.find()));
}
}
// lib/main.dart
import ‘package:flutter/material.dart’;
import ‘package:get/get.dart’;
import ‘app/routes/app_pages.dart’;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: ‘Every App’,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
initialRoute: AppPages.INITIAL,
getPages: AppPages.routes,
debugShowCheckedModeBanner: false,
);
}
}
// File names: snake_case
user_model.dart
home_controller.dart
api_provider.dart
// Class names: PascalCase
class UserModel {}
class HomeController {}
// Variables & methods: camelCase
var userName = ”;
void fetchUsers() {}
// Constants: UPPER_SNAKE_CASE
const API_BASE_URL = ‘https://api.example.com’;
// Private members: prefix with _
var _isLoading = false;
void _handleError() {}
// GOOD: Use .obs for primitive data
var count = 0.obs;
var name = ”.obs;
var isLoading = false.obs;
// GOOD: Use Rx<Type> for objects and nullables
var user = Rx<User?>(null);
var selectedDate = Rx<DateTime>(DateTime.now());
// GOOD: Use RxList, RxMap for collections
var items = <String>[].obs;
var settings = <String, dynamic>{}.obs;
// BAD: Don’t use .obs for non-reactive data
class MyController extends GetxController {
// 1. Constructor
MyController(this.repository);
// 2. onInit – Initial setup
@override
void onInit() {
super.onInit();
fetchData(); // Load data for the first time
setupListeners(); // Setup workers
}
// 3. onReady – After the widget is ready
@override
void onReady() {
super.onReady();
// Action after UI is ready
}
// 4. onClose – Cleanup
@override
void onClose() {
// Dispose controllers, cancel subscriptions
textController.dispose();
super.onClose();
}
}
Future<void> fetchData() async {
try {
isLoading.value = true;
errorMessage.value = ”;
final result = await repository.getData();
data.value = result;
} on NetworkException catch (e) {
errorMessage.value = ‘Network error: ${e.message}’;
Get.snackbar(‘Network Error’, e.message);
} on ValidationException catch (e) {
errorMessage.value = ‘Validation error: ${e.message}’;
} catch (e) {
errorMessage.value = ‘Unexpected error: $e’;
Get.snackbar(‘Error’, ‘Something went wrong’);
} finally {
isLoading.value = false;
}
}
// BAD: Business logic di View
class HomeView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
// Don’t be like this!
final response = await http.get(‘…’);
final data = jsonDecode(response.body);
},
child: Text(‘Fetch’),
);
}
}
// GOOD: Business logic di Controller
class HomeController extends GetxController {
Future<void> fetchData() async {
final response = await repository.getData();
// Process data
}
}
class HomeView extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: controller.fetchData,
child: Text(‘Fetch’),
);
}
}
// GOOD: Use GetView if you only need 1 controller
class HomeView extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
return Text(controller.title); // Directly access the controller
}
}
// If you need multiple controllers:
class ComplexView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final homeCtrl = Get.find<HomeController>();
final authCtrl = Get.find<AuthController>();
return Column(
children: [
Text(homeCtrl.title),
Text(authCtrl.userName),
],
);
}
}
// GOOD: LazyPut – only created when needed
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => HomeController());
}
}
// AVOID: Put – created immediately even if not used yet
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.put(HomeController()); // Created directly
}
}
// lib/app/core/widgets/custom_button.dart
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final bool isLoading;
const CustomButton({
required this.text,
required this.onPressed,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
child: isLoading
? CircularProgressIndicator()
: Text(text),
);
}
}
Snack bar
Get.snackbar(
‘Title’,
‘Message’,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: Duration(seconds: 3),
);
// Dialog
Get.defaultDialog(
title: ‘Alert’,
middleText: ‘Are you sure?’,
onConfirm: () => Get.back(),
onCancel: () => Get.back(),
);
// Custom Dialog
Get.dialog(
AlertDialog(
title: Text(‘Custom Dialog’),
content: Text(‘This is a custom dialog’),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(‘OK’),
),
],
),
);
// Bottom Sheet
Get.bottomSheet(
Container(
color: Colors.white,
child: Column(
children: [
ListTile(
leading: Icon(Icons.camera),
title: Text(‘Camera’),
onTap: () => Get.back(),
),
ListTile(
leading: Icon(Icons.photo),
title: Text(‘Gallery’),
onTap: () => Get.back(),
),
],
),
),
);
// Email validation
if (GetUtils.isEmail(email)) {
// Valid email
}
// Phone validation
if (GetUtils.isPhoneNumber(phone)) {
// Valid phone
}
// URL validation
if (GetUtils.isURL(url)) {
// Valid URL
}
// Null check
if (GetUtils.isNull(value)) {
// Is null
}
// Number check
if (GetUtils.isNum(value)) {
// Is number
}
if (GetPlatform.isAndroid) {
// Android specific code
}
if (GetPlatform.isIOS) {
// iOS specific code
}
if (GetPlatform.isWeb) {
// Web specific code
}
if (GetPlatform.isMobile) {
// Mobile (Android or iOS)
}
// lib/app/translations/app_translations.dart
class AppTranslations extends Translations {
@override
Map<String, Map<String, String>> get keys => {
‘en_US’: {
‘hello’: ‘Hello’,
‘welcome’: ‘Welcome @name’,
},
‘id_ID’: {
‘hello’: ‘Halo’,
‘welcome’: ‘Welcome @name’,
},
};
}
// In main.dart
GetMaterialApp(
translations: AppTranslations(),
locale: Locale(‘id’, ‘ID’),
fallbackLocale: Locale(‘in’, ‘US’),
);
// Use in code
Text(‘hello’.tr); // Output: Halo
Text(‘welcome’.trParams({‘name’: ‘John’})); // Output: Welcome John
// Change language
Get.updateLocale(Locale(‘en’, ‘US’));
// Change theme
Get.changeTheme(ThemeData.dark());
Get.changeTheme(ThemeData.light());
// Cek current theme
if (Get.isDarkMode) {
// Dark mode active
}
class MyController extends GetxController {
Future<void> refreshData() async {
await fetchData();
update([‘my-list’]); // Update hanya widget dengan id ‘my-list’
}
}
// In view
GetBuilder<MyController>(
id: ‘my-list’,
builder: (controller) => ListView(…),
)
class MyController extends GetxController with StateMixin<List<User>> {
@override
void onInit() {
super.onInit();
fetchUsers();
}
Future<void> fetchUsers() async {
change(null, status: RxStatus.loading());
try {
final users = await repository.getUsers();
change(users, status: RxStatus.success());
} catch (e) {
change(null, status: RxStatus.error(‘Failed to load’));
}
}
}
// In view
controller.obx(
(users) => ListView.builder(…), // Success
onLoading: CircularProgressIndicator(),
onError: (error) => Text(error ?? ‘Error’),
onEmpty: Text(‘No data’),
)
// For controllers used in multiple places
class AppController extends GetxController {
var theme = ThemeMode.light.obs;
var locale = Locale(‘en’, ‘US’).obs;
void toggleTheme() {
theme.value = theme.value == ThemeMode.light
? ThemeMode.dark
: ThemeMode.light;
Get.changeThemeMode(theme.value);
}
}
// In main.dart
void main() {
Get.put(AppController(), permanent: true); // permanent = not auto dispose
runApp(MyApp());
}
// Access from anywhere
final appCtrl = Get.find<AppController>();
appCtrl.toggleTheme();
Made with for Flutter Developers
Requirements:
Select your paper details and see how much our professional writing services will cost.
Our custom human-written papers from top essay writers are always free from plagiarism.
Your data and payment info stay secured every time you get our help from an essay writer.
Your money is safe with us. If your plans change, you can get it sent back to your card.
We offer more than just hand-crafted papers customized for you. Here are more of our greatest perks.
Get instant answers to the questions that students ask most often.
See full FAQOur professional writing service focuses on giving you the right specialist so the one assigned will have the knowledge about the right topic. However, if you’ve used our essay service before, you can ask us to assign you the expert writer who used to complete papers for you in the past. We can easily do so if the specialist in question is available at the moment.
If you’re ordering from our essay writing service for the first time, we will assign you a suitable expert ourselves and ensure that your academic essay writer is a pro. Moreover, let us know how complex your assignment is so that we can find the best match for your order.
We’ve hired the best writers in 80+ academic subjects to complete any paper you need. As soon as we hear, “Write my essays,” our support team assigns you the writer who understands your needs and subject.
In case you need to make sure we’ve picked a great specialist to deal with your paper, you can chat with the expert writers directly. We do our best to make sure you’re happy with the writer we’ve selected for you.
We have been selling original essays for more than 15 years. To prove that we are a trustworthy custom essay writing company, we provide quick delivery and a money-back guarantee. If we can’t complete your paper for any reason, we’ll send your money back to the credit card. We want to deliver the finest services, so you can decide if the paper is good enough; from our side, we’ll edit it according to your primary requirements to make the writing perfect. Our online paper writing service is about both giving you the materials you need when you need them and ensuring that your private data is safe. Check out our guarantees to see how we control the quality of your assignment and protect you as a customer.