Using generative code in ClojureDart

Introduction

One thing I want to experiment in ClojureDart is to use generative code from Dart.

One example of generative code could be retrofit a package in Dart to configure a client http easily. We will make an example how to use this in ClojureDart during this article.

If you want a quick example of ClojureDart take a look of my previous article.

Adding Retrofit package

Take a look of Retrofit documentation where we can find how to install it, for quick install follow:

flutter pub add retrofit
flutter pub add json_annotation
flutter pub add dio
flutter pub add -d retrofit_generator
flutter pub add -d build_runner

Create the RestApi client

Create a file named api_client.dart inside lib/

import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';

part 'api_client.g.dart';

@RestApi()
abstract class RestClient {
  factory RestClient(Dio dio) = _RestClient;

  @GET("/albums")
  Future<List<Album>> getAlbums();
}

@JsonSerializable()
class Album {
  int? id;
  String? title;
  int? userId;

  Album({this.id, this.title, this.userId});

  factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json);
  Map<String, dynamic> toJson() => _$AlbumToJson(this);
}
api_client.dart

Generate the code

Now we will use the build_runner we just install to generate the code for the client api, run this command:

dart pub run build_runner build

It's generate api_client.g.dart , we now have everything we want in the dart part. Let's see how to use it in ClojureDart now.

Subscribe to Etienne Théodore

Don’t miss out on the latest articles. Only one email by week about Flutter/Dart (no-spam)
your@email.com
Subscribe

Use RestClient into ClojureDart

We will use previous example code to modify it and implement the retrofit part.

First the api/albums.cljd:

(ns api.albums
  (:require
   ["api_client.dart" :as api]
   ["package:dio/dio.dart" :as dio]))

(def ^:private base-url "https://jsonplaceholder.typicode.com/")

(def ^:private dio-client (dio/Dio (dio/BaseOptions .baseUrl base-url)))

(def ^:private api-client (api/RestClient dio-client))

(defn get-albums
  "Call http for /albums or /albums/{page}
     The argument page should be a number.
     **Usage**
     ```clojure
     :watch [response (get-album)];; You get a list of Album
     ```
"
  ([] (-> api-client
          (.getAlbums))))
api/albums.cljd

The important part here is the require, as you can see, to import local Dart file we need to ["api_client.dart" :as api] by default ClojureDart looking in lib/ so we don't have to add it.

Then we create a def for our base url. Then a def to configure dio, it will be use by retrofit. Finally we create the api-client with the api/RestClient we created before.

Then get-albums just call the (-> api-client(.getAlbums)) from the RestClient dart. The documentation for the  -> macro can be find here .

Now we have a list of Album with a proper type. So to use it we can change some code in the main.cljd:

(ns acme.main
  (:require
   [api.albums :as api]
   [pages.album_detail :as album-detail]
   ["package:flutter/material.dart" :as m]
   [cljd.flutter :as f]))

(defn- navigate [navigator page name]
  (.push
   navigator (#/(m/MaterialPageRoute Object)
              .settings (m/RouteSettings .name name)
              .builder
              (f/build page))))

(defn- build-list-items [albums]
  (f/widget
   :get [m/Navigator]
   (m/ListView.builder
    .itemCount (count albums)
    .itemBuilder
    (f/build
     #_{:clj-kondo/ignore [:unresolved-symbol]}
     [idx]
     (let [album (get-in albums #_{:clj-kondo/ignore [:unresolved-symbol]} [idx])]
       (m/ListTile
        .onTap (fn [] (navigate navigator
                                (album-detail/view album)
                                (str "/album-detail/" (.-id album))))
        .title (m/Text (.-title album))))))))

(defn main []
  (f/run
   (m/MaterialApp
    .title "Fetch Data List Example"
    .theme (m/ThemeData .primarySwatch m/Colors.blue))
   .home
   (m/Scaffold .appBar (m/AppBar .title (m/Text "Fetch Data List Example")))
   .body
   (m/Center)
   :watch [response (api.albums/get-albums)]
   (if-some [{} response]
     (build-list-items response)
     (m/CircularProgressIndicator))))
acme/main.cljd

We change the call to :watch [response (api.albums/get-albums)] then we use it in the builder list (.-title album) to get the title from the model Album for example.

In the detail view we change some code too:

(ns pages.album_detail
  (:require
   ["package:flutter/material.dart" :as m]
   [cljd.flutter :as f]))

(defn view [album]
  (m/Scaffold
   .appBar (m/AppBar .title (m/Text (str (.-id album))))
   .body
   (f/widget
    :get [m/Navigator]
    :get {{{name .-name} .-settings} m/ModalRoute}
    m/Center
    (m/Column
     .mainAxisAlignment m/MainAxisAlignment.center
     .children
     [(m/Column
       .crossAxisAlignment m/CrossAxisAlignment.start
       .children [(m/Text name)
                  (m/Text (str "Title:" (.-title album)))
                  (m/Text (str "Id:" (.-id album)))
                  (m/Text (str "UserId:" (.-userId album)))])
      (m/ElevatedButton .onPressed #(.pop navigator) .child (m/Text "Go back!"))]))))
pages/album_detail.cljd

Here we can see (.-userId album) to get the userId from the Album class.

Conclusion

And it's done, we have changed the api part to be more typed, and safe. And use local generated code thanks to retrofit. We can still change the baseUrl inside ClojureDart or other BaseOptions settings. Well done !

Like always feel free to fork/clone/reuse the code I use for this article:

GitHub - Kiruel/fetch-data-list at retrofit-impl
ClojureDart example to fetch a simple list. Contribute to Kiruel/fetch-data-list development by creating an account on GitHub.