Flutter + GetX. Connect your app to API

Yuriy NovikovYuriy Novikov
7 min read

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:

  1. Dependency injection

  2. 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

  1. Http, Dio, and GetConnect all do their work well. I chose Dio due to additional research and the high number of likes.

  2. Glass_kit turns no design into an outstanding design.

  3. Flutter_spinkit provides several dozens of custom spinners.

Full project on github.

Thanks for reading and happy coding! 😊

0
Subscribe to my newsletter

Read articles from Yuriy Novikov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yuriy Novikov
Yuriy Novikov