Humble Opinion About Getx
GetX is the first package on pub.dev when sorted by most likes. It has more than 14,000 likes and more than 10,000 stars on GitHub. However, most of the Flutter community will say that it’s a very bad package and it shouldn’t be used.
I was wondering if it was true or not. Maybe all the community is wrong. It’s the reason I cloned the GetX repo to understand the code and find out what’s bad.
A short reminder about GetX. It’s a package that does everything: state management, navigation, internationalization, dependency injection, responsiveness, and a bunch of other utilities.
The package is divided into 9 folders:
get_animations
get_common
get_connect
get_core
get_instance
get_navigation
get_rx
get_state_manager
get_utils
I’m going to go through each of them. I will check the quality of the code and some other things.
Disclaimer! I’m reading the source code of the last commit. It’s slightly different from the code used by the latest version of the package that people are likely using.
Disclaimer: I’ve started to read the code on October 8 at 19:00. It’s 21:00, and the more I read the code, the more I have things to say.
Almost no tests?
First thing I tried when I cloned the repo was to run the tests. A simple flutter test
, and hop! 217 tests passed! That’s nice! Very nice! But only 217 tests that gather the features of Riverpod, get_id, and Hive? I have written more tests for smaller projects.
The log of the tests triggered me. One of the test files is a benchmark. It’s comparing some native Dart solutions with the ones from GetX.
============================================
PERCENTAGE TEST
referenceValue is 100% more than requestedValue
00:02 +15: /home/clement/tmp/getx/test/benchmarks/benckmark_test.dart: run benchmarks from ValueNotifier
============================================
VALUE_NOTIFIER X GETX_VALUE TEST
-----------
30 listeners notified | [GETX_VALUE] time: 1182ms
30 listeners notified | [VALUE_NOTIFIER] time: 1023ms
-----------
30000 listeners notified | [GETX_VALUE] time: 12318ms
30000 listeners notified | [VALUE_NOTIFIER] time: 5790ms
-----------
ValueNotifier delay 5790 ms to made 30000 requests
GetValue delay 12318 ms to made 30000 requests
-----------
GetValue is -53% faster than Default ValueNotifier with 30000 requests
00:02 +16: /home/clement/tmp/getx/test/benchmarks/benckmark_test.dart: run benchmarks from Streams
============================================
DART STREAM X GET_STREAM X GET_MINI_STREAM TEST
-----------
30 listeners notified | [MINI_STREAM] time: 1123ms
30 listeners notified | [STREAM] time: 722ms
-----------
GetStream is -36% faster than Default Stream with 30 requests
-----------
00:03 +20: /home/clement/tmp/getx/test/benchmarks/benckmark_test.dart: run benchmarks from Streams
30000 listeners notified | [STREAM] time: 196003ms
30000 listeners notified | [MINI_STREAM] time: 3475ms
00:03 +21: /home/clement/tmp/getx/test/benchmarks/benckmark_test.dart: run benchmarks from Streams
60000 listeners notified | [STREAM] time: 369240ms
60000 listeners notified | [MINI_STREAM] time: 3126ms
-----------
dart_stream delay 369240 ms to made 60000 requests
getx_mini_stream delay 3126 ms to made 60000 requests
-----------
GetStream is 11712% faster than Default Stream with 60000 requests
00:10 +217: All tests passed!
Can you see what’s wrong? I’m not talking about the conjugation; I’m also bad at English. It’s these lines:
dart_stream delay 369240 ms to make 60000 requests
getx_mini_stream delay 3126 ms to make 60000 requests
-----------
GetStream is 11712% faster than Default Stream with 60000 requests
11712% faster??? That means that 1 second in the GetX stream reference is equal to 118 seconds in the Dart reference! Also, Dart streams are so slow that they can bend spacetime. Look, they spent 369 seconds during the 10 seconds of the test suite.
I wasn’t expecting this result. It smells like bad testing, so I’m going to go through the tests and check if they are good.
I’m going to start with the internationalization test. It’s easy; there’s just one! It’s called Get.defaultDialog smoke test
, so you can quickly grasp the meaning of this internationalization test.
testWidgets("Get.defaultDialog smoke test", (tester) async {
await tester.pumpWidget(
Wrapper(child: Container()),
);
await tester.pumpAndSettle();
expect('covid'.tr, 'Corona Virus');
expect('total_confirmed'.tr, 'Total Confirmed');
expect('total_deaths'.tr, 'Total Deaths');
...
});
It’s hard to understand what the tests are doing. The code is not complicated at all, but there’s no documentation or very little.
Nothing is very well tested. At least 84 tests are dedicated to the extensions (for example, 1.days == Duration(day:1)
).
I have the feeling that the tests are not very exhaustive. There are no edge cases. I would expect more tests for the library’s code. According to the docs, these are the three pillars of GetX with their number of tests:
- State management: 11 (~5%)
- Route management: 71 (~33%)
- Dependency management: 6 (~3%)
So people are using a library where its main functionalities make up only 41% of the test cases. That’s very weird. It should be way more.
Out of curiosity, I ran a test coverage analysis. It’s not a metric I like to use because it can be easily manipulated. For those who want to learn how to do it, check Andrea’s post here.
The result is very bad. Only 43.5% of the code is tested. Only get_animations
is fully tested. That’s normal; it’s the easiest thing to test.
The sub-package GetConnect
, “an easy way to communicate from your back end to your front end with HTTP or WebSockets,” is not tested at all.
State management is tested at ~26%, route/navigation at ~58%, and dependency injection at ~77%.
In conclusion, GetX is poorly tested. No one would accept this kind of testing in any serious company. So why would you use this package to build your app?
Just a moment to understand that weird benchmark where the GetX custom stream is 11712% faster and where the stream run time is 360 seconds. First, the maintainer likely doesn’t understand the difference between microseconds and milliseconds. Let’s say it’s a simple mistake made on a Friday afternoon. It’s still much faster, but the test is incorrect. The GetX MiniStream
and Dart’s stream have different implementations. If we set one parameter correctly, the native stream is only about twice as slow and does more.
get_utils
(or get_wrong
)
As shown with the tests, get_utils
is the most important library of GetX. It contains extensions and a ton of other utilities.
First, the abundant use of dynamic
and var
hits me. A ton of variables could be final
, and the dynamic
type is used, likely mimicking JavaScript or Python. The package could greatly benefit from generics.
Another issue is that everything is static and a singleton. Let’s take an example of a function that probably takes inspiration from popular JavaScript packages:
/// Checks if data is null.
static bool isNull(dynamic value) => value == null;
Okay, that one made me laugh. Let’s look at this one instead:
/// Checks if data is null or blank (empty or only contains whitespace).
static bool? isBlank(dynamic value) {
return _isEmpty(value);
}
What should be passed? Anything? It’s stupid. We could pass an int
, a random class, whatever… We lose information, don’t take advantage of the linter, and it’s a sign of bad design.
That’s why it has useless code, and ultimately, the code will be slower:
static bool isBool(String value) {
if (isNull(value)) {
return false;
}
return (value == 'true' || value == 'false');
}
Really? You test if a String
is a null value? The linter would have detected this monstrosity if dynamic
hadn’t been used.
Another example of bad code:
static bool isVideo(String filePath) {
var ext = filePath.toLowerCase();
return ext.endsWith(".mp4") ||
ext.endsWith(".avi") ||
ext.endsWith(".wmv") ||
ext.endsWith(".rmvb") ||
ext.endsWith(".mpg") ||
ext.endsWith(".mpeg") ||
ext.endsWith(".3gp");
}
First, I’m very sorry if you have to check a .mov
file. Second, a Set
could have been used instead of seven conditions. It would be much easier to maintain, and performance would be better.
I could continue like this for hours. It’s full of useless functions that any developer should be able to write in 30 seconds.
static bool isPhoneNumber(String s) {
if (s.length > 16 || s.length < 9) return false;
return hasMatch(s, r'^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$');
}
If you are from Sweden, Iceland, Djibouti, or Denmark, your phone number is considered incorrect.
static bool isPalindrome(String string) {...}
Really? Who needs that?
static bool isLengthGreaterThan(dynamic value, int maxLength) {
final length = _obtainDynamicLength(value);
if (length == null) {
return false;
}
return length > maxLength;
}
This is much worse than writing myTypedValue.length >= maxLength
.
static bool isLowerThan(num a, num b) => a < b;
static bool isGreaterThan(num a, num b) => a > b;
static bool isEqual(num a, num b) => a == b;
Again, JavaScript inspiration. Also, isEqual
is wrong. That’s not how to compare doubles. (0.1 + 0.2) == 0.3
will be false
.
Extensions, Hard-coded Values
The extensions aren’t inherently “bad”; they’re mainly syntactic sugar. The main problem is that they use many hard-coded values.
Consider context_extension.dart
:
bool get showNavbar => (width > 800);
Why? Who are you to decide this for me?
bool get isDesktopOrLess => width <= 1200;
/// True if the width is higher than 1200p
bool get isDesktopOrWider => width >= 1200;
/// same as [isDesktopOrLess]
bool get isDesktop => isDesktopOrLess;
What does this even mean? It’s always desktop.
Another incredible function:
T responsiveValue<T>({
T? watch,
T? mobile,
T? tablet,
T? desktop,
}) {
assert(
watch != null || mobile != null || tablet != null || desktop != null);
var deviceWidth = mediaQuerySize.width;
//big screen width can display smaller sizes
final strictValues = [
if (deviceWidth >= 1200) desktop, //desktop is allowed
if (deviceWidth >= 600) tablet, //tablet is allowed
if (deviceWidth >= 300) mobile, //mobile is allowed
watch, //watch is allowed
].whereType<T>();
final looseValues = [
watch,
mobile,
tablet,
desktop,
].whereType<T>();
return strictValues.firstOrNull ?? looseValues.first;
}
The assertion is removed when building in release mode. In my opinion, this is a strange way of doing it. A switch
statement with guard clauses would be better for readability. Also, if all function arguments are null, the function returns watch
, which is null. What’s the use of looseValues
?
String trPlural([
String? pluralKey,
int? i,
List<String> args = const [],
]) {
return i == 1 ? trArgs(args) : pluralKey!.trArgs(args);
}
So trPlural(null, 2)
throws an exception?
get_animation
(Actually Not Bad?)
The code is actually okay. It could be simplified, and there are some strange things like 1.0 as dynamic
, but it seems to do the job. It lacks documentation, however. It’s unclear how users should use the API.
It’s a simple package that provides several pre-made animation widgets. Let’s move on to the next.
get_connect
First, let’s laugh together at this comment:
// TODO(39783): Document this.
get_connect
is a package that allows HTTP and GraphQL requests and communication via WebSockets. It doesn’t use package:http
but dart:io
. The latter has an HTTPClient
, so get_connect
rewrites its own system from scratch.
It works, yes, but what’s the point of using a poorly maintained and completely untested package over http
? None. Useless package. At least, it doesn’t overuse static methods.
I don’t have much to say. The code is hard to understand because it lacks documentation. It doesn’t make sense to mix HTTP requests with GraphQL, especially since so few people will use the GraphQL functionality.
get_instance
Yes, it looks like magic.
This package registers objects in a store. If you have a database instance, you store it with this package and later use it anywhere in your code.
It’s a large singleton storing everything in a Map
.
The State Management, My Friends
The documentation explains how to use GetX’s state management:
To make it observable, you just need to add ".obs" to the end of it:
var name = 'Jonatas Borges'.obs;
And in the UI, when you want to show that value and update the screen whenever the values change, simply do this:
Obx(() => Text("${controller.name}"));
It’s not clear at all. Can name
have a type, or does it use dynamic
? Where is name
stored? Inside the widget, or is it another global variable? What is Obx()
? When does it rebuild? And what the heck is controller.name
? Where does it come from?
I don’t feel it’s as simple as the README claims:
That’s all. It’s that simple.
There’s more documentation. It’s a huge text with tons of claims about why GetX state management is better than all other current solutions. The claims seem aggressive, as if the other solutions are bad in comparison.
10 minutes later… I’ve read the examples. They’re trash. They don’t explain anything.
So I go back to the documentation. It’s painful, and I’m starting to get tired of reading about GetX. I tried to write some code, but it doesn’t work. I feel very stupid because it’s supposed to be dead easy. I start to wonder if I’m just a bad developer.
Wow! I got something working!
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CountController extends GetxController {
int count = 0;
int uselessVariable = 2;
void increment() {
count++;
update();
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
GetBuilder<CountController>(
init: CountController(),
builder: (controller) {
print("build 1");
return GestureDetector(
onTap: () {
controller.increment();
},
child: Text(controller.count.toString()),
);
},
),
GetBuilder<CountController>(
init: CountController(),
builder: (controller) {
print("build 2");
return GestureDetector(
child: Text(controller.count.toString()),
);
},
),
],
),
);
}
}
The GetBuilder
is “something”. The documentation mentions the init
parameter, so I used it. I have a controller containing a state and methods to update it.
I have two GetBuilder
widgets, each with a different init
, so I expect two different states. When I tap the text, it updates the state and rebuilds. I added print
statements to track the rebuilds.
When I tap the first text, I get this output:
flutter: build # first tap
flutter: build 2
flutter: build # second tap
flutter: build 2
flutter: build # third tap
flutter: build 2
Why does the second controller update? Why do the GetBuilder
widgets have the same state? Why do I see the same value on my screen?
I’m too tired to understand and keep going. The documentation is definitively trash. It doesn’t explain anything. It only makes weird claims that are probably untrue. It says it uses less RAM; I wonder how. It claims that StatefulWidget
is no longer necessary. We shouldn’t replace a very easy and straightforward solution like StatefulWidget
with a bad state management solution.
It feels like this package is trying to be flutter_bloc
, but everything it copied is bad. There are many ways to handle states in the package, and they are all confusing. Since the last update (2021, come on!), the author hasn’t bothered to write basic documentation for the APIs. Even ChatGPT could improve the documentation…
All this management with the god class Get
and global variables is confusing. I really don’t recommend it, and you shouldn’t use it. The package has very bad design and shows a lack of skill.
get_navigation
The last package I will read before exploding. It helps with navigation by removing all calls to the context.
The navigation API is normal. It’s the same as go_router
, but instead of writing context.pop()
, you write Get.back()
.
What triggers me is the snackbars, dialogs, and bottom sheets. GetX provides, via the great god class Get
, a way to open any of them.
Example from the documentation:
final snackBar = SnackBar(
content: Text('Hi!'),
action: SnackBarAction(
label: 'I am a old and ugly snackbar :(',
onPressed: (){}
),
);
// Find the Scaffold in the widget tree and use
// it to show a SnackBar.
Scaffold.of(context).showSnackBar(snackBar);
//With Get:
Get.snackbar('Hi', 'i am a modern snackbar');
This is bad. You depend on this package to update its special function if, one day, showSnackBar
changes (remember, last update in 2021). It only accepts text, so goodbye to snackbars with little icons or custom widgets.
The package maintainer claims that the context is bad when it’s not. His packages reduce the field of possibilities.
Conclusion
I strongly recommend you never use GetX. It’s a package that attempts to do everything, but does nothing well.
There are so few tests for such an “important” project in the Flutter community that it should be ashamed. Student projects at university often have more thorough code and documentation. Do you want to base your future product on a package that probably doesn’t work very well? Up to you.
I’ll say it again: the nonexistent API documentation is a nightmare. Usually, you can hover over a function and understand its behavior. With GetX, this is not possible. Even the small bits of API documentation are unclear. The public documentation is very bad. It’s hard to understand how to use the library. It mixes many ways of doing the same thing, incorporating many bad practices that complicate code testing.
The state management is one of the most important aspects of this package, and it’s not documented well. I spent time trying to understand how to use it, and the first thing I did had strange behavior. It claims to be a better solution; I’m still waiting for proof that it uses less RAM, and frankly, who cares? State management solutions don’t use a ton of RAM; it’s not a good selling point.
Navigation seems alright at first glance, but the API is not documented, so I don’t understand most of the parameters. It may seem convenient at first, but you lose a lot of freedom.
You base your entire app on GetApp
. You cannot easily change the navigation to use something with more features, like go_router
. You lock yourself into an ecosystem.
The rest of the code is useless. Why would you need so many bad extensions that are specific to very niche business cases? I don’t want an extension to check the social security numbers of USA citizens.
Most of what GetX provides can be done yourself. Write your own extensions. Use the default Navigator
with a custom function to build special routes if needed. Learn Dart and Flutter!
The package is not maintained anymore. The author says a new version is coming soon. Again, the last release was in 2021. A pre-release version is available, but it’s still bad. I was reading the last commit of the project and was surprised by the bad complexity of this project. I don’t believe a new version will change anything.
I really think GetX should be abandoned. It’s not a good tool to build a professional app, and if you’re saying it is, then you should reconsider. People will learn bad practices about Flutter and programming in general.
That’s it. I hope this is clear enough.