State management in ClojureDart

Introduction

Today we will focus on state management in CLJD. In the past article, we made a quick application with a global state with defonce function. But we can make things better thanks to re-dash a package for CLJD to make state management clearer and more efficient.

Re-Dash an introduction

Re-dash is based on re-frame state management for ClojureScript. It is developed by Werner Kok.

It will allow us to make some handling events and react in the view with subscribe.

If you want to know more about what I'm talking about re-frame has nice documentation about the concept: data loop.

Local vs Global state

Re-dash can be compared to Riverpod in Flutter. Because it allows us to create some global state and manage it. For all local states as we do in Flutter with flutter_hook for example prefer use atom.

But how can we use it for CLJD? Let's take a look at some code.

The model

Before using re-dash you will need to import it in the deps like so:

{:paths ["src"] ; where your cljd files are
 :deps {net.clojars.htihospitality/re-dash {:mvn/version "0.1.1"} ; Here re-dash import
        tensegritics/clojuredart
        {:git/url "https://github.com/tensegritics/ClojureDart.git"
         :sha "b3a3399f3a0425a97ef9d6c5a5f31aeee2ee3feb" #_"8d5916c0dc87146dc2e8921aaa7fd5dc3c6c3401"}}
 :aliases {:cljd {:main-opts ["-m" "cljd.build"]}}
 :cljd/opts {:kind :flutter
             :main acme.main}}
deps.edn

Now let's make a file named albums_model.cljd in a models folder, it will contain all the business logic of the app, paste this code into the file:

(ns models.albums-model
  (:require
   [hti.re-dash :as rd]
   [api.albums :as api]))

(defn register! []

  (rd/reg-event-fx
   ::refresh-albums
   (fn [_ _]
     {::fetch-albums nil}))

  (rd/reg-fx
   ::fetch-albums
   (fn [_]
     (let [result (await (api.albums/get-albums))]
       (rd/dispatch [::populate-albums result]))))

  (rd/reg-event-db
   ::populate-albums
   (fn [db [_ albums]]
     (assoc db :albums albums)))

  (rd/reg-sub
   ::get-albums
   (fn [db _]
     (:albums db nil))))
models/albums_model.cljd

As we can see we first make a function named register! we will use it later. But the important part here is that we use a function named rd/reg-event-fx, rd/reg-fx , rd/reg-event-db, and rd/reg-sub . Let's take a look at each function.

rd/reg-event-fx & rd/reg-event-db: this is the act of registering an event handler that can be dispatched from.

rd/reg-fx: this is the act of registering an effect handler, here to handle the side effect of an HTTP call.

rd/reg-sub: this is the act of registering a subscription function that can be subscribed to by any widget to query the derived state value (when it actually changes)

::refresh-albums : this function has the purpose to call the fetch-albums. It's well useful when we need to refresh the list.

::fetch-albums: will fetch from the API function the albums list. And dispatch (call another function in re-dash),  ::populate-albums and past some result of the call.

::populate-albums: this function will change the state  db by using the albums param. And thanks to the assoc function we can add a key/value to db, (assoc db :albums albums).

::get-albums: with two params we can see the first param is db it's our state. It contains all the states we have made like the :albums ones we have made with the ::populate-albums.

If you want to know more about each re-dash capability take a look at this documentation.

Now we have our model in place we can use it in the view.

The view

Let's change the main to be more concise:

(ns acme.main
  (:require
   [pages.albums-list :as albums-list]
   ["package:flutter/material.dart" :as m]
   [cljd.flutter :as f]
   [models.albums-model :as albums-model]))

(defn main []
  (albums-model/register!)
  (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)
   (albums-list/view)))
acme/main.cljd
The important part here is the (albums-model/register!) function, we need to put this before the run function. To be sure the model will be instantiated only one time.

And create a new file named albums_list.cljd:

(ns pages.albums-list
  (:require
   [pages.album_detail :as album-detail]
   ["package:flutter/material.dart" :as m]
   [hti.re-dash :as rd]
   [cljd.flutter :as f]
   [models.albums-model :as albums-model]
   ["list_view_refreshable.dart" :as ext-refresh]))

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

(defn- list-view-albums [albums 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- build-list-items [albums]
  (f/widget
   :get [m/Navigator]
   (-> (list-view-albums albums navigator)
       ext-refresh/ListViewRefreshable
       (.refreshable
        #(rd/dispatch [::albums-model/refresh-albums]))))) ; Here dispatch to refresh the list

(defn view []
  (rd/dispatch [::albums-model/refresh-albums])
  (f/widget
   :watch [albums (rd/subscribe [::albums-model/get-albums])] ; Here subscribe to `get-albums`
   (if-some [{} albums]
     (build-list-items albums)
     (m/CircularProgressIndicator))))
pages/albums_list.cljd

First, we dispatch  (rd/dispatch [::albums-model/refresh-albums]) to fetch the list when the view is rendered for the first time, then we can watch a re-dash function we have made like so:
:watch [albums (rd/subscribe [::albums-model/get-albums])]

This function will refresh the tree when the albums in db change.

When we pull to refresh the list, we call it with an anonymous function the dispatch event from re-dash #(rd/dispatch [::albums-model/refresh-albums])

This will call the refresh-albums in the model and fetch the list again.

Conclusion

And it's done, we have separated the business logic to our view thanks to re-dash package! Enjoy coding! I would like to thank Werner Kok, for this amazing state management package, and the review of this article!

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

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