Working with Retrofit and XML on Flutter
Not long ago I was looking for the answer to this problem. Coming from an iOS background, I haven’t really been too close to libraries such as Retrofit and other code generator type of libraries.
In any case, it was a bit difficult to find the information I was looking for and that’s why I decided to post about it, hopefully it will save time to the next person searching for the answer.
What is what?
This is not a post about Retrofit or dio, I will explain the concepts briefly but there are already pretty good post out there explaining this libraries.
So what are…?
Dio: It is a very powerful http client for Dart and includes, among very cool stuff, something called Interceptors.
Retrofit: It is a wrapper on top of dio that can perform a type conversion of your requests via source generator.
If you want to know more about the dio and its interceptors, I recommend taking a look at this post from Gonçalo Palma. But as far as for this post, you only need to keep in mind that an interceptor is an operation that will sit between your http request and your result. They will basically intercept requests and responses so we can perform any operation on them.
What are we doing?
We will build a small sample app where we are going hit the Smart Dublin 🇮🇪API to fetch the next buses arriving to the stop 7602. In an actual product, we would build the app so we can query any stop, but for our sample, we will only get the info for this stop number.
This is the endpoint we’ll be fetching is here.
You will find the full project at the end of the post, but if you would like to follow along, start by creating a new flutter app.
Project structure
First we are going to create the file structure that we will be using, so create the stop.dart, arrival.dart and smart_dublin.dart files as in the screenshot.
Dependencies
We will need to add a few dependencies and dev_dependencies, but in general lines, we will be using:
Retrofit
XML
xml2json
build_runner
Just add the dependencies as shown below:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.3
retrofit: ^1.3.4
xml: ^3.5.0
xml2json: ^4.2.0
dev_dependencies:
flutter_test:
sdk: flutter
retrofit_generator: ^1.3.7
build_runner: ^1.10.0
Just in case, at the moment of writing this, I am using Flutter 1.21.0–10.0.pre.18 and Dart 2.10.
Our first retrofit network request
Let’s start by opening smart_dublin.dart and writing down our retrofit skeleton:
*import *'package:retrofit_xml_sample_app/models/stop.dart';
*import *'package:dio/dio.dart';
*import *'package:retrofit/retrofit.dart';
*part *'smart_dublin.g.dart';
@RestApi(baseUrl: "https://data.smartdublin.ie/cgi-bin/rtpi/")
*abstract class *SmartDublinClient {
*factory *SmartDublinClient(Dio dio, {String baseUrl}) {
*return *_SmartDublinClient(dio, baseUrl: baseUrl);
}
@GET("/realtimebusinformation?format=xml")
@DioResponseType(ResponseType.plain)
Future<Stop> getStopInformation(@Query("stopid") String stopId);
}
There are a few things happening on the code above:
We are telling the file that there will be another part of it with part 'smart_dublin.g.dart'. This other part is the autogenerated by build_runner. You will be able to figure out a file is generated as they all end in .g.dart .
We are using annotations such as @RestApi, @GET and @DioResponseType. This annotations contains some information that will be used by retrofit to generate the second part of the file.
And we are we requesting to get back a xml format! 😱 Well, that part is my fault too. I couldn’t come up with a simpler sample that will return xml to us. The truth is that most of the current endpoints will have the chance to return json; however this is not true in legacy systems or outdated APIs. I decided to force the return on XML of this simple API just for illustration purposes.
And in case you are wondering about it, you have most likely used annotations before, think about @override for instance.
In any case, you will find many examples on how to use Retrofit that will look similar to this one. You will also find the use of something called interceptors. As we mentioned at the beginning of the post, interceptors capture requests and responses so we can perform any operation on them.
Something we can do with interceptors is, for instance, to print all the responses by console while we are on debug mode (very useful for quickly debugging network operations). For something as such, you just need to add the following to the factory body, right above the return statement.
dio.interceptors.add(LogInterceptor(requestBody: *true*, responseBody: *true*));
Interceptors are particularly interesting in our use case, because when we are performing a network request that returns a xml, we can intercept the response and transform it into a json file to keep operating with it. You can do that with the following interceptor:
dio.interceptors.add(InterceptorsWrapper(
onResponse: (Response response) *async *{
response.data = Utils.*decodeXmlResponseIntoJson*(response.data);
*return *response;
},
));
There’s an additional step here and it is a decodeXmlResponseIntoJson method that I created in an Utils file, for that just create an utils/main.dart file and paste the following:
*import *'dart:convert';
*import *'package:xml2json/xml2json.dart';
*const *kQuoteReplacer = "¿*¿*¿*¿*";
*class *Utils {
*static dynamic decodeXmlResponseIntoJson*(String data) {
String cleanDataString = data.replaceAll(""", kQuoteReplacer);
*final *Xml2Json transformer = Xml2Json();
transformer.parse(cleanDataString);
*final *json = transformer.toGData();
*return *jsonDecode(json);
}
}
You can use different json transformers from the xml2json library. I found GData to be the one that better suits me for this transformations, but that may not be the case for your use cases, so make sure to check some of the other transformers as well.
Our models
You may have noticed that our retrofit code is using a model called Stop. This is because it is converting the response of our network request straight away into an object that is our stop model, pretty handy!
This models will only need a factory “fromJson” method to work with retrofit.
Let’s copy the stop model into stop.dart :
*import *'package:retrofit_xml_sample_app/models/arrival.dart';
*class *Stop {
*final *List<Arrival> arrivals;
*final *String stopId;
Stop({
*this*.arrivals = *const *[],
*this*.stopId,
});
*factory *Stop.fromJson(Map<String, *dynamic*> jsonMap) {
*final *stopId = jsonMap['realtimeinformation']['stopid']['\$t'];
*final *List<Arrival> arrivalsList = [];
*final *results = jsonMap['realtimeinformation']['results']['result'];
*if *(results != *null*) {
*for *(*var *result *in *results) {
arrivalsList.add(Arrival.fromJson(result));
}
}
*return *Stop(
arrivals: arrivalsList,
stopId: stopId,
);
}
@override
String toString() {
*return *'Stop { stopId: $stopId, arrivals: ${arrivals.length} }';
}
}
The factory method is the one in charge of filling up the object with the values from the response. It basically parses the response into the model.
The way I modelled the classes for this example is that a stop have a list of arrivals . The code for the arrival model looks as follows:
*class *Arrival {
*final *String route;
*final *String origin;
*final *String destination;
*final *String dueTime;
Arrival({
*this*.route,
*this*.origin,
*this*.destination,
*this*.dueTime,
});
*factory *Arrival.fromJson(Map<String, *dynamic*> jsonMap) {
*final *route = jsonMap['route']['\$t'];
*final *origin = jsonMap['origin']['\$t'];
*final *destination = jsonMap['destination']['\$t'];
*final *dueTime = jsonMap['duetime']['\$t'];
*return *Arrival(
route: route,
origin: origin,
destination: destination,
dueTime: dueTime,
);
}
@override
String toString() {
*return *'Arrival { route: $route }';
}
}
Autogenerating
Now we can autogenerate that g.dart file we saw before.
For doing so, just run this on the terminal within your project folder:
flutter pub run build_runner build
After doing so, you should see the new autogenerated file smart_dublin.g.dart with the implementation of our method.
If you have already run this file and need to overwrite the previously autogenerated file, you can add the — delete-conflicting-outputs flag to the build_runner command:
flutter pub run build_runner build --delete-conflicting-outputs
Putting everything together
It is time to actually use our new retrofit method. For that first we need to build a UI. Just to keep this simple, I have tied all the UI together in the main.dart file.
We will need a MyApp stateful widget:
*import *'package:flutter/material.dart';
*import *'package:retrofit_xml_sample_app/api/smart_dublin.dart';
*import *'package:retrofit_xml_sample_app/models/stop.dart';
*import *'package:dio/dio.dart';
*const *kStopId = '7602';
*void *main() {
runApp(MyApp());
}
*class *MyApp *extends *StatelessWidget {
@override
Widget build(BuildContext context) {
*return *MaterialApp(
title: 'Bus stop info',
theme: ThemeData(
primarySwatch: Colors.*blue*,
visualDensity: VisualDensity.*adaptivePlatformDensity*,
),
home: MyHomePage(),
);
}
}
*class *MyHomePage *extends *StatefulWidget {
MyHomePage({Key key}) : *super*(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
*class *_MyHomePageState *extends *State<MyHomePage> {
//To complete next
}
Next we will fill our _MyHomePageState class. First our variables, one to hold the stop and another one to hold our new api client.
Stop _stop = Stop(arrivals: [], stopId: kStopId);
*final *_apiClient = SmartDublinClient(Dio());
We are using an empty list of arrivals for our stop model as a default value.
The init method of our SmartDublinClient class will need for you to send a new Dio instance. If you were using multiple api clients for any reason, you can reuse the same dio instance (at least I haven’t encounter any side effect on this practice at the moment).
We will create a new method to request the stop info from our api client, and use State as our state management solution for this sample:
*void *loadStopInformation() *async *{
Stop stop = *await *_apiClient.getStopInformation(kStopId);
setState(() {
_stop = stop;
});
}
And our override methods for initState and build:
@override
*void *initState() {
*super*.initState();
loadStopInformation();
}
@override
Widget build(BuildContext context) {
*return *Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
Padding(
padding: *const *EdgeInsets.all(16.0),
child: Text(
' 🚌 Stop 7602',
style: TextStyle(
fontWeight: FontWeight.*bold*,
fontSize: 32,
),
),
),
Flexible(
child: buildList(),
)
],
),
),
);
}
You may have noticed we are calling buildList within the build method, let’s copy that one as well. I kept them separated for clarity in the sample:
Widget buildList() {
*if *(_stop.arrivals.isEmpty) {
*return *Text("Sorry, there are no buses anytime soon 🤷🏽♀️");
}
*return *ListView.builder(
itemCount: _stop.arrivals.length,
itemBuilder: (BuildContext context, int index) {
*return *Padding(
padding: *const *EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Container(
height: 40,
decoration: BoxDecoration(
color: Colors.*white*,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.*grey*,
spreadRadius: 1,
offset: Offset(0, 0),
)
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text("Arriving in ${_stop.arrivals[index].dueTime} mins"),
Text(
"${_stop.arrivals[index].route} to ${_stop.arrivals[index].destination}"),
],
),
),
);
},
);
}
In the build list method we simply show a message if the stop arrivals are empty and a list when they are not.
After putting everything together you should see something like this 👇🏼
Congrats! 🥳
Glad you made it through! Hope you have enjoyed the post and learn something on the way.
If you just want to dive straight into the code, you will find it here 👇🏼
sergiofraile/retrofit_xml_sample_app
Let me know your thoughts and what you would like to hear next! See you next post! 👋
Top comments (0)