Category: Programming

  • 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:

  • Pelajaran harus di tanggapi dengan cepat

    Bergerak harus cepat

    Requirements:

  • Mengapa biaa terjadi gempa

    JebekemjemjdHebsksvsbhsbebsbsvss

    Requirements:

  • I am fresher

    Yes I am fresher

    Requirements:

  • Pengertian akutansi

    1. Pengertian wirausaha. 2. Pengertian dari bisnis. 3. Pengertian hukum. 4. Contoh hukum yang paling penting

    Requirements:

  • Watch programing

    I am a watch repairer, there is a piece in my watch that I need someone to help me in programming it and give me the codes, We need program for this (IC ATMEGA 32A) so that we can operate the watch the same as the video in the file. there also images in file all the watch components.

    I need someone who is highly skilled in programming.

    Requirements: long

  • Pengertian akutansi

    1. Pengertian wirausaha. 2. Pengertian dari bisnis. 3. Pengertian hukum. 4. Contoh hukum yang paling penting

    Requirements:

  • Programming Question

    At UC, it is a priority that students are provided with strong educational programs and courses that allow them to be servant-leaders in their disciplines and communities, linking research with practice and knowledge with ethical decision-making. This assignment is a written assignment where students will demonstrate how this course research has connected and put into practice within their own career.

    Assignment:

    Provide a reflection of at least 500 words (or 2 pages double spaced) of how the knowledge, skills, or theories of this course have been applied, or could be applied, in a practical manner to your current work environment. If you are not currently working, share times when you have or could observe these theories and knowledge could be applied to an employment opportunity in your field of study.

    Requirements:

    • Provide a 500 word (or 2 pages double spaced) minimum reflection.
    • Use of proper APA formatting and citations. If supporting evidence from outside resources is used those must be properly cited.
    • Share a personal connection that identifies specific knowledge and theories from this course.
    • Demonstrate a connection to your current work environment. If you are not employed, demonstrate a connection to your desired work environment.
    • You should not provide an overview of the assignments assigned in the course. The assignment asks that you reflect how the knowledge and skills obtained through meeting course objectives were applied or could be applied in the workplace.
    • and this sysbuss for this coures :

    This course explores the foundational statistical concepts and methods essential for developing, analyzing, and applying artificial intelligence (AI) models. Students will gain a deep understanding of probability, hypothesis testing, regression, and data distribution as they pertain to AI and machine learning algorithms. Through hands-on exercises and practical applications, learners will develop skills to interpret data patterns, assess model performance, and make data-driven decisions in real-world AI scenarios. The course is designed for students seeking to strengthen their quantitative and analytical skills in preparation for advanced AI and machine learning coursework.

    Course Objectives

    Upon completion of this course:

    1. Explain and apply core statistical concepts, including probability, descriptive statistics, and data distribution, in the context of AI model development.

    2. Use statistical tools to analyze datasets, identify trends, and interpret results relevant to AI and machine learning projects.

    3. Utilize hypothesis testing, confidence intervals, and goodness-of-fit tests to validate AI models and assess their performance.

    4. Analyze relationships between variables using regression and correlation methods and incorporate these techniques into machine learning workflows.

    5. Critically evaluate AI models’ statistical assumptions and limitations and propose methods for improving their robustness and accuracy.

    Requirements: immediate

  • Programming Question

    Image Classification with Convolutional Neural Networks

    Example Dataset: link :

    Overview:

    Students will implement a Convolutional Neural Network (CNN) for an image classification task. The goal is to build a model that can accurately classify images from a dataset such as CIFAR-10.

    Instructions:
    1. Data Collection: Use a standard image dataset, such as CIFAR-10, which contains labeled images of different objects.
    2. Preprocessing: Preprocess the images by normalizing pixel values, resizing, and augmenting the dataset to improve model performance.
    3. Model Design: Design a CNN architecture using Python and a deep learning library such as TensorFlow or PyTorch.
    4. Training: Train the CNN on the preprocessed dataset, using techniques such as batch normalization and dropout to prevent overfitting.
    5. Evaluation: Evaluate the model’s performance on a test dataset, calculating metrics such as accuracy, precision, and recall.
    6. Optimization: Optimize the model by tuning hyperparameters and experimenting with different architectures.
    7. Reporting: Document the entire process, including the design choices, training process, evaluation results, and insights gained, adhering to the Neural Networks Implementation Project Rubric.
    Submission Instructions:

    Code File(s):

    • Submit your full implementation as either:
    • A Jupyter Notebook (.ipynb)
    • A Python script (.py)
    • Your code must include:
    • Data loading and preprocessing
    • CNN architecture design
    • Training loop and loss function
    • Evaluation metrics
    • Hyperparameter tuning/experiments
    • Use TensorFlow or PyTorch for model implementation.

    Report (.pdf or .docx):

    Structure your report according to the Neural Networks Implementation Project Rubric and include:

    • Introduction: Problem description and dataset overview
    • Methodology:
    • Preprocessing steps
    • CNN architecture design (include diagrams if helpful)
    • Training setup and hyperparameters
    • Results:
    • Performance metrics (accuracy, precision, recall, etc.)
    • Confusion matrix and/or classification report
    • Training/validation loss and accuracy curves
    • Discussion:
    • Observations, challenges, and insights
    • Justification for design and optimization decisions
    • Potential improvements and future work
    • Conclusion: Summary of outcomes and takeaways

    Requirements: i need answer and video explaination for this assigment