Flutter: Remove ifs from the widget tree with Visibility and without

Flutter: Remove ifs from the widget tree with Visibility and without

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

I chose to be very strict in how I write my widgets. I want no logic in them at all—full Separation of Concerns. View only displays data on…

I chose to be very strict in how I write my widgets. I want no logic in them at all—full Separation of Concerns. View only displays data on the screen. ViewModel contains all presentation logic, including navigation.

That's why I stuck with GetX — I like navigation without context triggered from the ViewModel, among other things.

If you are a member, please continue,otherwise, read the full story here.

Let me show you what I mean. Say you're calling some API to get user data. Most people write something like this:

GetBuilder<UserViewModel>(
builder: (controller) {
if (controller.isLoading) {
return CircularProgressIndicator();
} else if (controller.error != null) {
return Text('Error: ${controller.error}');
} else {
return Text('User: ${controller.user?.name}');
}
},
)

It works fine, but I want to avoidifs whenever possible, especially within the widget tree.

The better way with Visibility

Here's what I try to do instead — use Visibilitywidgets. Each state gets its own widget, and you control visibility with flags.

Let me show you the whole setup.

The model

Nothing fancy here, just your data:

class User {
final String id;
final String name;
final String email;

User({
required this.id,
required this.name,
required this.email,
});
}

The service

This is where we pretend to hit the API. Just a simple example.

class UserService {
Future<User> fetchUser(String userId) async {
// Simulate network delay
await Future.delayed(Duration(seconds: 2));

// Simulate random success/failure
if (DateTime.now().second % 3 == 0) {
throw Exception('Failed to load user');
}

return User(
id: userId,
name: 'John Doe',
email: 'john@example.com',
);
}
}

The ViewModel

We have clear boolean flags for each state:

class UserViewModel extends GetxController {
final UserService _service = UserService();

bool isLoading = false;
User? user;
String? error;

bool get hasData => user != null;
bool get hasError => error != null;
String get username => user != null ? user!.name : '';
String get errorMessage => error != null ? error! : '';

@override
void onInit() {
super.onInit();
loadUser('123');
}

Future<void> loadUser(String userId) async {
isLoading = true;
error = null;
user = null;
update();

try {
user = await _service.fetchUser(userId);
} catch (e) {
error = e.toString();
} finally {
isLoading = false;
update();
}
}
}

The view

And here's where it all comes together. Look at this — no ifs in sight inside GetBuilder. Each state has its own Visibilitywidget with a clear condition:

class UserView extends GetView<UserViewModel> {
const UserView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Details')),
body: Padding(
padding: const EdgeInsets.all(18.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 50,
children: [
GetBuilder<VisibilityExampleController>(
builder: (controller) {
return Stack(
children: [
//loading state
Visibility(
visible: controller.isLoading,
child: Center(
child: CircularProgressIndicator(),
),
),

// Success state
Visibility(
visible: controller.hasData,
child: Text(
controller.username,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),

// Error state
Visibility(
visible: controller.hasError,
child: Text(
controller.errorMessage,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Get.theme.colorScheme.error,
),
),
),
],
);
},
),
ElevatedButton(
onPressed: () {
controller.loadUser('234');
},
child: Text('Load again'))
],
),
),
),
);
}
}

Here is the resulting screen, by the way:

The approach has two problems, though:

  1. The View became a bit verbose.
  2. We render 7 widgets instead of one. (Instead of one CircularProgressIndicator, we render a Stack, three Visibilitywidgets, two SizedBoxs, and one CircularProgressIndicator).

Is it an adequate price to pay for better separation of concerns? I don't know. Probably. I recently learned that Flutter is able to paint 250 thousand bunny images, so rendering 6 more simple widgets is nothing.

Anyway, let's try another approach.

Without Visibility

Model and Service did not change.

ViewModel:

class UserViewModelextends GetxController {
final UserService _service = UserService();

Widget _widget = CircularProgressIndicator();
Widget get widget => _widget;

@override
void onInit() {
super.onInit();
loadUser('123');
}

Future<void> loadUser(String userId) async {
_widget = CircularProgressIndicator();
update();

try {
var user = await _service.fetchUser(userId);
_widget = Text(
user.name,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
} catch (e) {
var error = e.toString();
_widget = Text(error,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Get.theme.colorScheme.error,
));
} finally {
update();
}
}
}

View:

class UserView extends GetView<UserViewModel> {
const UserView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Details')),
body: Padding(
padding: const EdgeInsets.all(18.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 50,
children: [
GetBuilder<UserViewModel>(
builder: (controller) {
return controller.widget;
},
),
ElevatedButton(
onPressed: () {
controller.loadUser('234');
},
child: Text('Load again'))
],
),
),
),
);
}
}

The View became extremely concise. We get rid of some class variables in the ViewModel. But we construct widgets in the ViewModel. I don't think that it is philosophically wrong. The ViewModel is part of the presentation, and having some widgets there is OK. GetX users are used to calling navigation methods and showing dialogs from there.

Summary

We have three not-perfect approaches:

  1. Having logic (ifs) inside the View. That is what everybody is doing, but that doesn't mean the approach is right.
  2. Using Visibilitywidgets. Brings extra verbosity and extra rendering.
  3. Constructing widgets inside the ViewModel. Feels a bit awkward. (Update: This approach can be mixed with StateWidgetFactoryfrom Part 2, which will remove the UI code from the ViewModel.)

I am leaning toward the third one, but I will try to do it in a real project to get more feelings.

Thank you for reading!

Report Page