Make Flutter app with Clojure ?

Introduction

We need to talk about ClojureDart, this allows us to write Flutter app and Dart program with Clojure language. This can be a real game changer for all Flutter developers out there.

We will make a quick example to show the potential of ClojureDart for Flutter.

Before starting you will need to install a few things to be able to start a project. Take a look at the quick start for the Flutter app here.

After the installation you should be able to start the project with the command clj -M:cljd flutter.

Building an example app

We will create a simple list of items fetched from an API, and a second view with some details to it.

0:00
/

After the installation you should have a file src/acme/main.cljd with inside:

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

(defn main []
 (f/run
  (m/MaterialApp
   .title "Welcome to Flutter"
   .theme (m/ThemeData .primarySwatch m.Colors/pink))
  .home
  (m/Scaffold
   .appBar (m/AppBar
        .title (m/Text "Welcome to ClojureDart")))
  .body
  m/Center
  (m/Text "Let's get coding!"
    .style (m/TextStyle
        .color m.Colors/red
        .fontSize 32.0))))
src/acme/main.cljd

As we can see, we can use ["package:flutter/material.dart" :as m] to import a Dart package and use it like m/Scaffold or every widget in material package.

The syntax can be hard to understand because we are on a functional paradigm here, if you are having some trouble take a look at the official documentation of Clojure. And this book (in free access), to see more about Clojure.

First, we will make a file to handle all the API calls. Create the src/api/albums.cljd inside it paste this:

(ns api.albums
 (:require
  ["dart:core" :as dc]
  ["package:http/http.dart" :as http]))

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

(defn get-album
 "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
   ```
   or
   ```clojure
   :watch [response (get-album 1)];; You get one album
   ```
"
 ([] (http/get (dc/Uri.parse (str base-url "albums"))))
 ([page] (http/get (dc/Uri.parse (str base-url "albums/" page)))))
src/api/albums.cljd

There are a few things to understand here.

First, the ns (for namespace), will tell "where" is the file api/albums.cljd  so that after it we can see api.albums in the folder where the file is. This will allow us to call the functions outside this file (will see later).

We can see some required packages like http from Dart. To use it you will need to flutter pub add http before building the app.

The second thing is the first function called base-url is a def function to define an object, here a simple string to get the base URL of the API. You can see this function is private via ^:private, meaning you can use it only in this file.

Now let’s talk about the get-album function, the function can take 0 arguments [] or one argument [page]. This will fetch a list of albums from this API https://jsonplaceholder.typicode.com/albums.

And if you pass a number argument this will get only one album, https://jsonplaceholder.typicode.com/albums/1 for example.

We will see later how to use it but we have done the API part.

Now back to the main.cljd, remove everything, and paste this code:

(ns acme.main
 (:require
  [api.albums :as api]
  ["package:flutter/material.dart" :as m]
  ["package:http/http.dart" :as http]
  ["dart:convert" :as c]
  [cljd.flutter :as f]))
src/acme/main.cljd

As you can see to import the API file we just created we require [api.albums :as api] . The api.albums is the namespace we define before.

Now after this paste:

(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-album)]
  (if-some [{sc .-statusCode body .-body} ^http/Response response]
   (case sc
    200 (build-list-items (c/json.decode body))
    (m/Text (str "Something wrong happened, status code: " sc)))
   (m/CircularProgressIndicator))))
src/acme/main.cljd

This code will create a simple material app with Scaffold and call the /albums with the function (api.albums/get-album) like you can see without parameters. We :watch it because (api.albums/get-album) return a Future. More information about :watch and others ClojureDart here .

Then we use the response with [{sc .-statusCode body .-body} ^http/Response response] to get the http code and body.

With the case function we can check the http code and create a list of items with (build-list-items (c/json.decode body)) when the code is 200 or if not a (m/Text (str "Something wrong happened, status code: " sc)) function to return an error message widget.

And return a (m/CircularProgressIndicator) function when loading.

Now we will define the build-list-items function before the main:

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

This private function defn-  took an argument [body], the list of albums. With the ListView.builder we create the list and a simple ListTile with an onTap callback to navigate to a detail view (album-detail/view album) and we pass a second argument to the navigate function (str "/album-detail/" (get album "id”)) to name our view.

Now the navigator part, paste this before the build-list-items:

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

Simple navigation push with the widget builder page and the name the route name.

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


And finally, we will create the album_detail.cljd file in the folder src/pages/:

(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 (get album "id"))))
  .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:" (get album "title")))
         (m/Text (str "Id:" (get album "id")))
         (m/Text (str "UserId:" (get album "userId")))])
   (m/ElevatedButton .onPressed #(.pop navigator) .child (m/Text "Go back!"))]))))
src/pages/album_detail.cljd

To use it in the main you will need to import it via the required like so:

(ns acme.main
 (:require
  [api.albums :as api]
  [pages.album_detail :as album-detail] ; here the detail view
  ["package:flutter/material.dart" :as m]
  ["package:http/http.dart" :as http]
  ["dart:convert" :as c]
  [cljd.flutter :as f]))
src/acme/main.cljd

Now we can run the application via clj -M:cljd flutter.

Well done the example is completed!

Conclusion

We have made a quick example application to fetch from an API a list of items and made a List to scroll these items. Then a detailed view to see more about the item.

I enjoyed making some Clojure, it’s a new language and paradigm for me. But using it to make a Flutter app was fun! I think ClojureDart could be nice for production apps, but we still need to talk about unit testing, and architecture maybe in a future article :) !

Like always take a look at the code here => https://github.com/Kiruel/fetch-data-list. Don’t forget to join the mailing list if you want to not miss any articles.

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

Usefull links:

- ClojureDart repo

- Slack ClojureDart channel #clojuredart

- Clojure Books

- Clojure documentation