flutter-arquitectura-servicios-ui-logica

Basic Architecture in Flutter: Separating UI and Data

  • 4 min

The architecture of an app is the way we organize responsibilities within the project.

When we start in Flutter, the temptation is strong:

“I have a button, and when I press it, I want to download a list of users. So I put the download code inside the button’s onPressed and that’s it.”

For a quick test it might work, but mixing everything together makes the screen grow quickly. UserScreen can coordinate its visual state, but it shouldn’t know how to connect to the server or interpret its JSON.

Today we are going to learn the single responsibility principle by separating the App into two layers: UI (Interface) and Logic (Services).

The Problem of Mixed Code

Look at this code. It’s what we should NOT do:

// ❌ BAD: Logic mixed with UI
class UserScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Text("Load"),
      onPressed: () async {
        // 😱 HORROR! HTTP requests inside the UI
        var url = Uri.parse('https://api.example.com/users');
        var response = await http.get(url);
        if (response.statusCode == 200) {
           // Parse JSON here... more dirty logic...
        }
      },
    );
  }
}
Copied!

Why is it bad?

  1. Not reusable: If you need to load users from another screen, you’ll have to copy and paste the code.
  2. Hard to read: The Widget file gets filled with technical network code.
  3. Hard to test: You need to replace or intercept the real calls to test the interface.

Separating Services and Repositories

The solution is to remove data access from the widgets and move it to specialized classes. A service usually wraps a specific API; a repository provides data to the rest of the application and can combine different sources. For this simple example, we’ll use a service.

Imagine a restaurant:

  • The Widget is the Waiter: Their job is to smile, show you the menu (UI), and take your order. They don’t cook.
  • The Service is the Chef: They are in the kitchen (invisible to the customer). They receive the order, get the ingredients (API), cook them (JSON Parsing), and deliver the finished dish.

Step 1: Create the Service Class

Let’s create a new file, for example lib/services/user_service.dart.

// ✅ GOOD: A dedicated class just for data
class UserService {
  // Simulated base URL
  final String _baseUrl = 'https://jsonplaceholder.typicode.com';

  // Clean method that returns what the UI needs
  Future<List<String>> getUsers() async {
    // All the "dirty" HTTP logic will go here as we'll see later
    // For now, we simulate waiting 2 seconds and returning data
    await Future.delayed(Duration(seconds: 2));
    
    return ["Luis", "Ana", "Carlos"];
  }
}
Copied!

Notice how clean it is. This class knows nothing about colors, buttons, or BuildContext. It only knows about data.

Step 2: Call the Service from the UI

Now, our Widget goes back to being a simple waiter. It just needs to call the service and wait.

class UserScreen extends StatefulWidget {
  @override
  _UserScreenState createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
  // 1. Instantiate the service (our chef)
  final UserService _userService = UserService();
  
  List<String> _users = [];
  bool _loading = false;

  Future<void> _loadData() async {
    // Activate the spinner
    setState(() => _loading = true);

    // 2. Request data from the service (Delegate the dirty work)
    final newUsers = await _userService.getUsers();

    // Update the UI
    if (!mounted) return;

    setState(() {
      _users = newUsers;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Clean Architecture")),
      body: _loading 
          ? Center(child: CircularProgressIndicator()) // Spinner
          : ListView.builder(
              itemCount: _users.length,
              itemBuilder: (ctx, i) => ListTile(title: Text(_users[i])),
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _loadData, // On press, call the function
        child: Icon(Icons.download),
      ),
    );
  }
}
Copied!

Suggested Folder Structure

To stay organized from now on, I recommend this folder structure under /lib:

  • /lib
  • /models (Data classes: User, Product…)
  • /screens (The screens: Home, Login…)
  • /widgets (Reusable widgets: Buttons, Cards…)
  • /services (Connection logic: ApiService, AuthService…)

Immediate Advantages

By making this small structural change, we have gained a lot:

  1. Clean Code: The build method only worries about whether it’s loading or showing the list. No URLs or HTTP status codes cluttering things up.
  2. Maintainability: If the API URL changes tomorrow, you only need to touch the user_service.dart file. You don’t have to search through all your screens.
  3. Scalability: We could add methods like createUser, deleteUser inside UserService and have it all organized.