GetX Complete Guide with MVVM and Clean Architecture

GetX Complete Guide with MVVM and Clean Architecture

List of contents


GetX Introduction

GetX is a powerful Flutter framework with 3 main pillars:

1. State Management

  • Reactive and easy to use
  • High performance
  • Minimal boilerplate

2. Route Management (Navigation)

  • Navigation withoutcontext
  • Named routes
  • Middleware support

3. Dependency Management (Inject Dependencies)

  • Lazy loading
  • Auto dispose
  • Smart management

MVVM concept at GetX

What is MVVM?

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

Mapping to GetX:

  • Model = models/, providers/, repositories/
  • View = views/ (Widget)
  • ViewModel = controllers/ (GetxController)

Structure Folder

Complete and Organized Structure

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

Structure Explanation:

1. /core – Shared Resources

  • utils/: Helper functions, validators, formatters
  • values/: Constants seperti colors, strings, API URLs
  • widgets/: Reusable widget for all apps

2. /data – Data Layer (MODEL)

  • models/: Class model for data (User, Product, etc.)
  • providers/: Place for API calls, database operations
  • repositories/: Abstraction between controller and provider

3. /modules – Feature Modules

  • Each feature has its own folder
  • Each module has:bindings, controllers, views

4. /routes – Navigation

  • Centralized routing configuration

Detailed Explanation of Each Layer

1. MODEL LAYER

a. Data Models (/data/models)

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’],

);

}

}

b. Providers (/data/providers)

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();

}

}

c. Repositories (/data/repositories)

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’);

}

}

}


2. VIEWMODEL LAYER (Controller)

Controller isbrainfrom the application. This is where all the business logic is located.

Basic Controller

// 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();

}

}

Controller with Form Validation

// 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();

}

}

Controller dengan Workers (Side Effects)

// 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;

}

}

}

Types of Workers in GetX:

  1. ever: Called every time the observable changes
  2. once: Called only once when the observable changes for the first time
  3. debounce: Waits for the user to finish performing an action (e.g. typing) before the action is executed
  4. interval: Ignore changes within a specified time interval

3. VIEW LAYER

View isUI/Widget. Its task is only to display data, there should be no business logic.

Basic View with GetView

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);

},

);

},

);

}),

);

}

}

View with Form

// 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’),

),

)),

],

),

),

),

);

}

}

Reactive State: Obx vs GetBuilder

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}’),

)


4. BINDINGS (Dependency Injection)

Bindings connect the controller to its dependencies and perform lazy initialization.

Basic Binding

// 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()));

}

}

Types of Dependency Injection in GetX:

// 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());

Binding with Multiple Dependencies

// 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()),

);

}

}


5. ROUTING

a. Routes Constants

// 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’;

}

b. Pages Configuration

// 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,

),

];

}

c. Navigation Methods

// 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);

d. Middleware

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()],

),


Complete Implementation

Case Study: Todo App

1. Model

// 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,

);

}

}

2. Repository

// 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’);

}

}

}

3. Controller

// 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 }

4. View

// 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’),

),

],

),

);

}

}

5. Binding

// 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()));

}

}

6. Main.dart

// 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,

);

}

}


Best Practices

1. Naming Conventions

// 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() {}

2. Reactive Variables

// 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

3. Controller Lifecycle

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();

}

}

4. Error Handling Pattern

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;

}

}

5. Separation of Concerns

// 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’),

);

}

}

6. Use GetView When Possible

// 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),

],

);

}

}

7. Lazy Loading Dependencies

// 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

}

}

8. Reusable Widgets

// 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),

);

}

}


Tips & Tricks

1. Snackbar, Dialog, BottomSheet without Context

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(),

),

],

),

),

);

2. GetUtils Helper Functions

// 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

}

3. Platform Checks

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)

}

4. Internationalization (i18n)

// 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’));

5. Theme Management

// Change theme

Get.changeTheme(ThemeData.dark());

Get.changeTheme(ThemeData.light());

// Cek current theme

if (Get.isDarkMode) {

// Dark mode active

}

6. Smart Refresh

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(…),

)

7. StateMixin for Loading States

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’),

)

8. Global Controllers

// 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();


Conclusion

GetX with MVVM provides:

  1. Clean Architecture- Clear separation between UI, Logic, and Data
  2. Testability- Easy to test because it is separate
  3. Scalability- Easy to develop for large projects
  4. Maintainability- Code is easy for the team to maintain and understand

Key to Success:

  • Consistent with folder structure
  • Separate concerns (View, Controller, Repository)
  • Use Binding for dependency injection
  • Take advantage of GetX’s reactive programming
  • Follow best practices
  • Well documented code

Resources:


Made with for Flutter Developers

Requirements:

Calculate the price of your order

Select your paper details and see how much our professional writing services will cost.

We`ll send you the first draft for approval by at
Price: $36
  • Freebies
  • Format
  • Formatting (MLA, APA, Chicago, custom, etc.)
  • Title page & bibliography
  • 24/7 customer support
  • Amendments to your paper when they are needed
  • Chat with your writer
  • 275 word/double-spaced page
  • 12 point Arial/Times New Roman
  • Double, single, and custom spacing
  • We care about originality

    Our custom human-written papers from top essay writers are always free from plagiarism.

  • We protect your privacy

    Your data and payment info stay secured every time you get our help from an essay writer.

  • You control your money

    Your money is safe with us. If your plans change, you can get it sent back to your card.

How it works

  1. 1
    You give us the details
    Complete a brief order form to tell us what kind of paper you need.
  2. 2
    We find you a top writer
    One of the best experts in your discipline starts working on your essay.
  3. 3
    You get the paper done
    Enjoy writing that meets your demands and high academic standards!

Samples from our advanced writers

Check out some essay pieces from our best essay writers before your place an order. They will help you better understand what our service can do for you.

  • Essay (any type)
    Direct Social Work Practice and Helping Process
    Undergrad. (yrs 3-4)
    Social Work and Human Services
    APA

Get your own paper from top experts

Order now

Perks of our essay writing service

We offer more than just hand-crafted papers customized for you. Here are more of our greatest perks.

  • Swift delivery
    Our writing service can deliver your short and urgent papers in just 4 hours!
  • Professional touch
    We find you a pro writer who knows all the ins and outs of your subject.
  • Easy order placing/tracking
    Create a new order and check on its progress at any time in your dashboard.
  • Help with any kind of paper
    Need a PhD thesis, research project, or a two-page essay? For you, we can do it all.
  • Experts in 80+ subjects
    Our pro writers can help you with anything, from nursing to business studies.
  • Calculations and code
    We also do math, write code, and solve problems in 30+ STEM disciplines.

Frequently asked questions

Get instant answers to the questions that students ask most often.

See full FAQ
  • How do you find the best essay writer for me?

    Our 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.

  • How can you prove that your paper writing service is not a scam?

    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.

See full FAQ

Take your studies to the next level with our experienced specialists