Flutter + GetX. Connect your app to API
Table of contents
- 1. Application folder structure
- 2. Make HTTP requests using the HTTP package
- 3. Make HTTP requests using the Dio package
- 4. Make HTTP requests using the GetConnect from the GetX package
- 5. Comparing three packages
- 6. Decoupling controller from service
- 7. Displaying joke on the View
- 8. Decoupling View from Controller
- 9. The glassmorphic design
- 10. Custom loading spinner
- 11. Conclusion
Originally published on Medium
We will compare the usage of three packages by making a simple Joke app.
The app will use the public Joke API.
We will also use glass_kit for the glassmorphic design and flutter_spinkit for the custom spinning loader.
DEMO (Please be patient, it can take 2–3 sec to load)
If you are a member, please continue, otherwise, read the full story here.
1. Application folder structure
As usual, I have generated the folder structure with get_cli.
As you can see, we have three JokeService implementations: using the Http package, the Dio package, and GetConnect from GetX.
Looking ahead, I should say that they all work as expected. I did not have any difficulty in implementing the solution using any of them.
But let’s look at the code.
2. Make HTTP requests using the HTTP package
joke_service_http.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
class JokeServiceHttp implements JokeServiceInterface{
String url = 'https://v2.jokeapi.dev/joke/Programming?format=json&type=single';
@override
Future<JokeModel> fetchJoke() async {
var jokeModel = JokeModel();
try {
var response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
var jsonResponse = json.decode(response.body);
jokeModel.joke = jsonResponse['joke'];
} else {
jokeModel.error = 'Error: Failed to load joke';
}
} catch (e) {
jokeModel.error = 'Error: $e';
}
return jokeModel;
}
}
JokeServiceHttp (as well as other implementations) implements JokeServiceInterface.
abstract class JokeServiceInterface {
Future<JokeModel> fetchJoke();
}
All of them use the JokeModel class to transfer data to the Controller.
class JokeModel {
String? joke;
String? error;
JokeModel({this.joke, this.error});
}
This model is mutable. We can make it immutable but it is really doesn’t worth the effort. Immutable objects offer maintainability in exchange for performance. We did not expect any maintainability problems with this simple app. 😊
Let’s check the code of the other two implementations:
3. Make HTTP requests using the Dio package
joke_service_dio.dart
class JokeServiceDio implements JokeServiceInterface {
final String url = 'https://v2.jokeapi.dev/joke/Programming?format=json&type=single';
final Dio _dio = Dio();
@override
Future<JokeModel> fetchJoke() async {
try {
final response = await _dio.get(url);
if (response.statusCode == 200) {
return JokeModel(joke: response.data['joke']);
} else {
return JokeModel(error: 'Error: Failed to load joke');
}
} on DioException catch (e) {
return JokeModel(error: 'DioError: ${e.message}');
} catch (e) {
return JokeModel(error: 'Error: $e');
}
}
}
Dio implementation is a bit more concise. Dio implicitly parses the URL and decodes JSON.
4. Make HTTP requests using the GetConnect from the GetX package
joke_service_getconnect.dart
class JokeServiceGetConnect extends GetConnect implements JokeServiceInterface {
final String url = 'https://v2.jokeapi.dev/joke/Programming?format=json&type=single';
@override
Future<JokeModel> fetchJoke() async {
try {
final response = await get(url);
if (response.status.hasError) {
return JokeModel(error: 'Error: ${response.statusText}');
} else {
return JokeModel(joke: response.body['joke']);
}
} catch (e) {
return JokeModel(error: 'Error: $e');
}
}
}
5. Comparing three packages
From this simple example, we cannot see the difference between those packages. Each one works well. According to likes***** and what I have read on the internet Dio is considered to be more advanced, while Http is more basic.
Http is an official HTTP package made by the Dart team. I have an anti-official mind, so for me, it is not an important argument.
GetConnect can help in converting model classes to/from JSON. In this app, this feature is not required since our JokeModel is very simple.
Also, the argument for GetX is that it is already here, we don’t need to add any package to the project.
Personally, I would go with Dio.
*****Dio has almost the same number of likes as Http. But since Dio is a third-party package those likes weigh more. IMHO.
6. Decoupling controller from service
According to “D” in SOLID we should decouple the controller and service. Practically it means that the controller should not know about service implementation but work with the interface.
This is why we have JokeServiceInterface
in the first place.
There are two techniques also called design patterns that are commonly used for decoupling:
Dependency injection
Service Locator
I always point out that there is confusion in the Flutter community. Most developers think that Dependency Injection and Service Locator are the same. They are not. They are two completely different design patterns.
Currently, our code uses Dependency Injection:
joke_controller.dart
class JokeController extends GetxController {
JokeServiceInterface jokeService;
JokeController({required this.jokeService});
var joke = ''.obs;
var isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchJoke();
}
Future<void> fetchJoke() async {
isLoading.value = true;
JokeModel jokeModel = await jokeService.fetchJoke();
if (jokeModel.joke != null) {
joke.value = jokeModel.joke!;
} else {
Get.snackbar(
'Oops',
jokeModel.error!,
colorText: Colors.white,
backgroundColor: Colors.blue.shade800,
icon: const Icon(Icons.add_alert),
);
}
isLoading.value = false;
}
}
JokeController only knows about JokeServiceInterface.
joke_binding.dart
class JokeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JokeController>(
() => JokeController(jokeService: JokeServiceGetConnect()),
);
}
}
We have injected JokeServiceGetConnect through the constructor.
Let’s rewrite it with Service Locator.
joke_controller.dart
class JokeController extends GetxController {
JokeServiceInterface jokeService = Get.find<JokeServiceInterface>();
// JokeController({required this.jokeService});
...
joke_binding.dart
class JokeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JokeController>(
() => JokeController(),
);
Get.lazyPut<JokeServiceInterface>(() => JokeServiceGetConnect());
}
}
We do not inject JokeServiceGetConnect through the constructor anymore. Instead, we put it inside the GetX Service Locator, and then, in the controller, we just take it from there.
Dependency Injection is considered a better solution. I also prefer it, but if you used to Service Locator you have my permission to use it. 😏 Just be consistent in your code.
7. Displaying joke on the View
joke_view.dart
class JokeView extends GetView<JokeController> {
const JokeView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.black, Colors.blue.shade700, Colors.purple.shade700, Colors.black],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GlassContainer(
height: 400,
width: 400,
gradient: LinearGradient(
colors: [Colors.white.withOpacity(0.40), Colors.white.withOpacity(0.10)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderGradient: LinearGradient(
colors: [Colors.white.withOpacity(0.60), Colors.white.withOpacity(0.10), Colors.lightBlueAccent.withOpacity(0.05), Colors.lightBlueAccent.withOpacity(0.6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.39, 0.40, 1.0],
),
blur: 15.0,
borderWidth: 1.5,
elevation: 3.0,
isFrostedGlass: true,
shadowColor: Colors.black.withOpacity(0.20),
alignment: Alignment.center,
frostedOpacity: 0.12,
margin: EdgeInsets.all(8.0),
padding: EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Obx(() {
if (controller.isLoading.value) {
//return CircularProgressIndicator();
return SpinKitSpinningLines(color: Colors.white);
} else {
return SingleChildScrollView(
child: Text(
controller.joke.value,
style: TextStyle(fontSize: 24, color: Colors.white),
textAlign: TextAlign.left,
),
);
}
}),
),
),
),
SizedBox(height: 20),
GlassContainer(
height: 60,
width: 400,
gradient: LinearGradient(
colors: [Colors.white.withOpacity(0.40), Colors.white.withOpacity(0.10)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderGradient: LinearGradient(
colors: [Colors.white.withOpacity(0.60), Colors.white.withOpacity(0.10), Colors.lightBlueAccent.withOpacity(0.05), Colors.lightBlueAccent.withOpacity(0.6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.39, 0.40, 1.0],
),
blur: 15.0,
borderWidth: 1.5,
elevation: 3.0,
isFrostedGlass: true,
shadowColor: Colors.black.withOpacity(0.20),
alignment: Alignment.center,
frostedOpacity: 0.12,
margin: EdgeInsets.all(8.0),
padding: EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: controller.fetchJoke,
child: Text('Next Joke', style: TextStyle(fontSize: 24)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
minimumSize: Size(double.infinity, double.infinity),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
),
),
),
],
),
),
),
);
}
}
The code is long and mostly related to GlassContainers. We will check them a bit later.
Here are snippets that show Obx
and ElevatedButton
widgets:
child: Obx(() {
if (controller.isLoading.value) {
//return CircularProgressIndicator();
return SpinKitSpinningLines(color: Colors.white);
} else {
return SingleChildScrollView(
child: Text(
controller.joke.value,
style: TextStyle(fontSize: 24, color: Colors.white),
textAlign: TextAlign.left,
),
);
}
...
child: ElevatedButton(
onPressed: controller.fetchJoke
We have inherited the controller
property from GetView<JokeController>
.
8. Decoupling View from Controller
The View can be decoupled from the Controller using the same two techniques.
It makes sense to use Service Locator since we already use Get.lazyPut<JokeController>
. We just need to change our code a bit: create an interface for JokeController
and use Get.lazyPut
according to the Service Locator pattern:
Get.lazyPut<JokeControllerInterface>(
() => JokeController(),
);
Then change a View to be JokeView<JokeControllerInterface>.
Not a big trouble but I am not doing it in my apps. Just don’t see any practical gain since we always have one-to-one relationships between View and Controller.
9. The glassmorphic design
After discovering the glass_kit package my self-perception dramatically changed. Before I thought that I was a bad designer. Now I think that I am a very good one.
Here is the code that creates a gradient background:
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.black, Colors.blue.shade700, Colors.purple.shade700, Colors.black],
),
),
And here is a GlassContainer from the glass_kit package:
GlassContainer(
height: 400,
width: 400,
gradient: LinearGradient(
colors: [Colors.white.withOpacity(0.40), Colors.white.withOpacity(0.10)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderGradient: LinearGradient(
colors: [Colors.white.withOpacity(0.60), Colors.white.withOpacity(0.10), Colors.lightBlueAccent.withOpacity(0.05), Colors.lightBlueAccent.withOpacity(0.6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.39, 0.40, 1.0],
),
blur: 15.0,
borderWidth: 1.5,
elevation: 3.0,
isFrostedGlass: true,
shadowColor: Colors.black.withOpacity(0.20),
alignment: Alignment.center,
frostedOpacity: 0.12,
margin: EdgeInsets.all(8.0),
padding: EdgeInsets.all(8.0),
child: Padding(
It is highly customizable, with a lot of properties. It makes code look long but the result is worth it.
10. Custom loading spinner
In the same way, we should not use the default theme we should not use the default loading spinner.
flutter_spinkit package has several dozens of custom spinners which are super easy to use:
if (controller.isLoading.value) {
//return CircularProgressIndicator();
return SpinKitSpinningLines(color: Colors.white);
}
Obviosly, we should add the package to the project first. 😊
11. Conclusion
Http, Dio, and GetConnect all do their work well. I chose Dio due to additional research and the high number of likes.
Glass_kit turns no design into an outstanding design.
Flutter_spinkit provides several dozens of custom spinners.
Thanks for reading and happy coding! 😊
Subscribe to my newsletter
Read articles from Yuriy Novikov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by