Hi! Today I want to introduce to you the fruit of my months of work at nights and weekends, designed to improve the content management experience and bring additional features to the world of Flutter application development - a new kind of CMS.
We're talking about Nanc - Not A Normal CMS. Why it's "not normal" and what you can do with it, you'll find out after reading this article.
The article will contain not only theory but "practice" as well - you will be able to play in the Nanc sandbox. In order to show the public the capabilities of Nanc, two demo applications were made: a client application that imitates any Flutter application and another one that gives an idea of what a Flutter-based Nanc application can do - prebuilt Nanc CMS. And the Nanc CMS application, which is a pre-configured CMS with an additional layer of logic implemented to synchronize the client application with the CMS.
At the end of most of the logic blocks in the text, you'll find a youtube video with an example of how to use / demonstrate some aspect of Nanc.
Intro
About CMS
Types of models
Editor
Fields
Dynamic Flutter apps
Nanc demo apps
What's next / Afterwords
So. Nanc is a backend-agnostic CMS that doesn't pull its own backend. How does it work? Nanc offers to implement network service interfaces, which right now have 6 methods but will have about 10 by the time of release. Is this a lot or a little? For example, it took 170 lines of code to implement the API for the demo application. These methods are responsible for all of Nanc's work with your existing backend. The implementation must be written in Dart (the development language for Flutter as well). In the future, Nanc will come with ready implementations of these interfaces for certain backend options - Firebase, Supabase, local / network SQLite and Generic implementations of REST and GraphQL (maybe something else?) and you will not have to think about this implementation at all or will have to, but just a little bit.
A model in Nanc is a structural description of an entity that you want to control with Nanc. A model contains an entity name, various visual parameters, and a description of the fields.
A collection is an entity that can have many instances. A list of users, books, and reviews are good examples of Collection-type models in Nanc.
If you are familiar with relational databases, an example of a Collection in Nanc would be almost any table in your database.
A solo (model) is an entity that exists in a single instance. For example - Feature Toggles or a configuration of something, or... "The main screen of a mobile application". Generally speaking, Solo-models are designed to increase usability, being just a projection of your database. And a Solo-model could easily be any table in your database with only one record. But at the moment, the implementation of this class of models requires that the record of this model (row in the database) has an id
equal to the id
of the model itself.
final Model landingPage = Model(
name: 'Landing page',
/// ? id in format [toSnakeCase(name)] will be set automatically, if omitted
// id: 'landing_page',
isCollection: false,
icon: IconPackNames.flu_document_page_break_regular,
fields: [
...
Nanc can be configured in several ways: by code, via the Nanc interface itself, and a combination of these options.
When I say "configuration", I mean, first of all, describing the structure of your models. Let's take a simple example, the Feature model, which is an entity describing the features of a product. This entity contains the following fields:
And the implementation as a code-first config will be as follows:
import 'package:fields/fields.dart';
import 'package:icons/icons.dart';
import 'package:model/model.dart';
final Model feature = Model(
name: 'Feature',
icon: IconPackNames.flu_ribbon_star_filled,
fields: [
[
IdField(width: 200),
StringField(name: 'Title', maxLines: 1, isRequired: true, width: 400),
],
[
IconField(name: 'Image', isRequired: true),
],
[
StringField(name: 'Description', isRequired: true, showInList: true),
],
],
);
By describing such a model and using it in Nanc CMS, all the CRUD operations of that model become available to you.
We could create the exact same Feature model (let's call it a Feature Variant) through the Nanc interface. And (considering that all the preparatory work for using Nanc is done) - when you create a model in Nanc, you will immediately create a table in the database, and the whole CRUD will also be available to you immediately.
Also, you can take the safer way of not creating anything in the database when you create a model through the Nanc interface. And independently create a table in your database, and then, under it, create a model in Nanc. By the way, this is what you have to do if you describe the configuration in code - new tables will not appear in your database from it.
This option becomes available to you when you have a Code-first config and decide to change it through the Nanc interface. In this case, all further changes to this model will only be possible through the interface, and changes made to the original code-model will be ignored. The only way to return to Code-first is to "reset" the model, in which case all changes to the model structure made through the interface will be wiped and Code-first config will be used again. No data is affected by this reset. It only affects the model structure.
Now let's look at what types of fields Nanc currently supports.
BoolField allows you to control the bool
data type and customize the default value.
ColorField provides you with a handy color-picker that lets you choose a color immediately in Nanc. It also gives you the ability to make changes manually, by editing the AHEX code. AHEX is a classic HEX-Color (for example, #10A0CF
), but with an additional transparency value specified first. In this case, this color would be similar to the color #FF10A0CF
(FF
is 100% opacity - the color is completely opaque). And this is what the same color would look like with 50% opacity: #7F10A0CF
.
DateField is responsible for controlling date and time (both values at once, separate ones for date and time will be implemented later). DateField contains two boolean parameters that allow you to modify the behavior of this field by making it an entity creation time timestamp and a change time timestamp.
DynamicField, on the one hand, is a very simple field, but on the other hand, it includes the full complexity of the other fields. Initially, you can configure only the appearance of this field (how it will look in the Nanc interface - the color and the accompanying icon). After that, this field can contain any values available in Nanc, including itself. What does this mean? Essentially, DynamicField is a typed JSON with the ability to position fields in order within itself.
EnumField is a field for selecting a value from multiple values. The rule to follow when selecting an EnumField is that if you have a final list of values to select that is not stored in a separate database table, choose Enum. Otherwise, choose SelectorField, which is discussed in more detail below. TODO: At the moment this field can only be configured via CodeConfig, configuration via the interface is not worked out.
HeaderField is not really a field, but a visual enhancer for your model in Nanc. You can use this nonfield to set a common header for a group of related fields, or to distinguish model fields from each other by using HeaderField as a delimiter.
IconField gives you the ability to select an icon (IconData
class) from a predefined set of icons. There are currently about 25,000 of them, and this set includes the following icons:
If necessary, other icons can be added to the basic delivery set, and in the not too distant future there will be an option to use your own icons as well.
One might wonder, "Icons are there, colors are there, but fonts?" If you did, you can find the answer in the documentation for the Text widget. The short answer is that all fonts from Google Fonts are available to you.
IdField is such a simple but so important field. Every model managed by Nanc must have at least one field of type Id. Currently, only the string ID type is supported (it can be any unique string within one entity). There are plans to add support for numeric type as well, which, however, can be implemented even now simply by casting the field data to the numeric type in the API implementation.
MultiSelectorField is a rather complex field that is responsible for implementing a relational many-to-many or many-to-one relationship. There are three modes of using this field. Let's walk through each of them in more detail. TODO: At the moment this field can only be configured through CodeConfig, the configuration through the interface is not worked out.
This mode gives you the ability to store the id
of related entities in the parent entity directly. For example - we have two models - Reader and Book. In this mode, the books taken by the reader will be accounted for as ids stored in the Reader model field. For example like this:
/// user table
{
id: 'UUID',
name: 'String',
books: [
'UUID',
'UUID'
// ...
]
}
Above is an example table structure expressed using JSON5 syntax.
The disadvantage of this mode is limited data integrity. If you don't implement automatic removal of obsolete (deleted) book IDs from the books
Reader field, you'll get errors.
The classic mode of providing relationships from the SQL world. When using this mode, you store entity relationships in a separate table and ensure 100% data integrity. The following structure is an example of this mode:
[
/// user table
{
id: 'UUID',
name: 'String'
},
/// book table
{
id: 'UUID',
title: 'String'
},
/// user_books_relations table
{
user_id: 'UUID',
book_id: 'UUID'
}
]
On the 7th second you can see a slight twitch and if you look closely you can notice that the page url has changed - this is how I tried to hide the bug: in third table mode the data is saved in the parent page only if it has already been saved 🤷🏼
Generally similar to Array of ids, except that the parent record does not store identifiers, but the entire object (as a flat structure, with no possible associated entities of the nested record). It has the same disadvantage as Array of ids, but has an additional one - increased use of storage. However there is (at least for the moment) an area of application of this mode, and it is very important. But we will talk about this a bit later.
I'm getting ahead of myself in the video, showing ScreenField, we'll come back to this
In general, there is an idea to make "non-canonical" modes virtual - so that they somehow work through Third table, and the necessary data is loaded when editing the page (if necessary).
NumberField stores numbers - simple as that.
SelectorField is similar to MultiSelectorField (as you might guess from their names), but a little simpler - the relationship here is one-to-one or one-to-many, and there are two modes. TODO: At the moment this field can only be configured via CodeConfig, configuration via the interface is not worked out.
A common form of SQL link provision, where the parent record field stores the ID of the linked record. Let's take the Reader as an example. Who is it? First of all it is a person, and what does a person have? That's right! The city of birth (which our Library, for some reason, also wanted to know).
/// user table
{
id: 'UUID',
name: 'String',
birth_place_id: 'UUID'
}
Very similar to the Array of objects from MultiSelectorField, but we will store a single associated value in the field of the parent record. The disadvantages are the same, the pluses will also be described just a little bit below.
StringField stores strings. This field has one personal setting responsible for the convenience of editing in Nanc - the parameter limiting the maximum height of the editable field. If your text will be large - it makes sense not to specify it at all, then the field will adjust to the height of the text. If limited to large - you can specify explicit field height (in lines) and then it will always be so. And finally, for short strings you can set it to one line, and then the field will not expand no matter how much you write into it afterwards.
StructureField allows you to store an array of typed structures in model records. You specify the type of data to be stored and can manage it easily and simply. The available types for structure fields are absolutely everything. So you can easily create a "Dynamic Structure Field Repeat". TODO: Only "flat" fields can be added to StructureField within the demo.
ScreenField is a field that allows you to write an entire application in Flutter, right in Nanc! With ScreenField you can describe the interface of a single...screen, update it however you want, and do it at any time in minutes - without the tedious and nerve-racking wait for reviews from Apple and Google.
Let's break down this type of field (and actually a whole separate functional offshoot of Nanc) in a little more detail.
With ScreenField, you can really create an interface of almost any complexity right in your browser (and your IDE), and then - without making a stock publication - update the corresponding screen in your application. If you need to check hypotheticals quickly, this is a great feature. This functionality is great for relatively simple (in terms of logic) pages in your app, which, however, need to change quite often. In the future, this functionality will be expanded to a state where you can really create anything you want without any restrictions.
Now let's walk through all aspects of creating dynamic screens with Nanc.
There are a lot of widgets in Flutter. A lot of them. What is a widget? It's a brick of functionality from which you assemble your application. It can be visual only, or it can be logical - with some behavior inside. Nanc provides an extensive list of implemented widgets that you can use to build your UI. But the more possibilities - the harder it is to learn about them... So the screen editor in Nanc gives you access to an interactive documentation where you can find out what widgets are currently implemented, what parameters and configurable properties they have, and, right in the documentation, see how they affect the appearance of the interface you create.
Nanc allows you to create an interface in real time, but most importantly - it allows you to do it very easily and quickly (it took a little more than 2 hours to interface a demo application). But the question arises - how to create the UI itself? There is no exotic syntax for describing UI in Nanc, nor "too" simple solutions, like long JSON, which will make you hate creating interfaces in Nanc.
The result of finding the best solution is simple and straightforward XML syntax. All standard Flutter widgets have exactly the same names, but in XML form. For example, the SizedBox
widget in Nanc would be <sizedBox>...</sizedBox>
, and this rule applies to all widgets without exception. If the widget has some complex property, it will have the same (or simpler) name as XML, with the prefix prop
. For example - the widget Container
has a complex property boxDecoration
, which has its own internal properties. So, this property in Nanc will have the following look: <prop:decoration>...</prop:decoration>
. This rule applies to all complex properties. And the last aspect is that arguments that are relatively simple are XML tag parameters. Let's take the same SizedBox
as an example:
<sizedBox width="50" height="50">
...
</sizedBox>
For some widgets, additional arguments are implemented to simplify writing code, and for SizedBox
it is the ize
argument which sets both width
and height
.
Everything written here is in the online documentation, so if you forget something or want to know something, refer to it and find answers to all your questions there.
Implement support for the new widget - a matter of 10 minutes to a few hours. At this point, almost all the basic widgets are implemented, of which you can create a complex interface with logic. Over time, all the widgets available in Flutter will be implemented in Nanc, and you can really do everything. But that's not all. You can easily and simply implement your own widgets and use them in Nanc with one or two lines of XML code. For example - there is no widget in the standard Flutter library that allows you to easily display the Carousel Slider with pictures. You will have to write an implementation yourself or use some open source solution like such. And, having implemented what you need - you can very easily integrate your widget into Nanc and use it.
Nanc provides more than just the ability to convert XML code into an interface in Flutter. Nanc provides templating and logic writing capabilities. Conditional element rendering, loop drawing, tap handling - this is already in the current 0.0.1
version of Nanc.
So far, the logic part is pretty straightforward - it supports interaction via taps and event handling in your `.dart' code written in advance - but eventually this part of Nanc will expand considerably, allowing you to write logic in Dart right in the browser and have it work in your application as well.
The approach to handling user clicks is as follows - you can define a list of "actions" that the user can do in your app. For example - open internal application screen, click on external link, display SnackBar, pop up modal window and many other things, and create handler for such actions in advance. And then use that action in any way you want in Nanc. For more information on event handling, see the documentation for the InkWell
widget in Nanc.
Nanc has a built-in XML editor, but it's not very handy. It's not searchable (yet), it's not very fast with a lot of code, and it has no auto-completion. How to live with it? For example - let the user use his favorite IDE and watch the changes in Nanc in real time. Let me show you how.
And this is the Web (which is what you have to play with):
In the future autocomplete support will be added, perhaps in the distant future...I tried to deep in XML Schema, spent several days, but so far could not 🤷🏼
Separately, I would like to mention the performance (drawing interface from XML on mobile devices). In short, it is identical to the performance of Flutter itself, without any overhead. At the moment, the "screen" is a lazily rendered list of widgets (SliverList), created asynchronously. Later, this implementation will be refined to start rendering widgets asynchronously, but in turn, so the time required to display the content will be equal to the time it takes to render the very first widget described in the XML.
To demonstrate the capabilities, a public set of demo apps has been created to show what can be achieved with Nanc right now. This is a client application on Android and the Web (the latter temporarily plays the role of an iOS application as well). As well as the Nanc CMS app. Read more about them below.
Client is a client demo application that uses a single library from the nanc ecosystem. This library allows you to convert XML into an application interface in Flutter. This application has just one screen, created entirely in Nanc, and it can be updated as desired and at any time without storerooms. On the bottom right there is a button with a connection icon - it is responsible for connecting to demo-CMS. More about what this "connection" will be below.
Admin is a Nanc-CMS demo application, with an additionally implemented layer of logic, which provides the ability to synchronize with clients (more about the connection below). In the Nanc-CMS demo application, the user's browser itself and its localStorage acts as the "backend". Everything you add or change is stored only in your browser. In Nanc-CMS you can modify / create / delete data related to the existing models (you will see them), and - you can create your own models through the interface and do the same with them.
As a SQL representation of the data models used in the creation of this demo, you can be guided by the following screenshot:
This section relates solely to the logic of the "demo" in the client and CMS applications. And it was implemented to simulate the experience of interacting with Nanc and the process of updating the client. But first things first.
In a real production project you could use Nanc in the following way: deploy a static Nanc CMS application somewhere, with API services implemented. It would communicate with your backend, and you would use Nanc to your liking. Your application contains one library from the nanc ecosystem that allows you to render the interface. You made an update - the app loaded new code from your backend, updated - everyone is happy and satisfied.
To show this model in action, the same thing is implemented, but in a simplified way:
Nanc CMS exists as a static, lying on the github pages and you can use it just like "in real life", but your browser acts as the backend. That is, the APIs have been implemented in such a way that they "go to the network" - in the browser localStorage. With this part we are done, but there is still a mobile application, which must somehow show you the process of "updating".
Well, that's where the "connection" comes in. In short - you can establish a direct connection between any Nanc client demo app and any Nanc CMS demo app. To do this you need to click on the bottom right button with the QR code icon in the CMS. In the modal window that appears, you will see the QR code. Next you have two chairs - you can scan this code with the mobile (or browser) client application by pressing the similar button at the bottom right, then the connection will be established automatically. Or you can click on the QR code and the data required for the connection will be copied to the clipboard. Then you will have to paste this data into the input field of the mobile application and connect by pressing the button. When the connection is established - you will understand yourself. After that, you can do whatever you want with the existing Landing Page, and see the changes in real time (after saving) - in the mobile app.
But you're not limited to the Landing Page. You can create any new models directly in the browser, fill them with content, and if your models will have a field for interface description (type Screen) - then when you save such entities, you will also see the result in the application - the screen from the new model will replace the existing application screen. The only point is that since the client application doesn't know what type of field your newly created record is, it has possible identifiers prescribed, which are expected to be ScreenFields.
So if you want to create a screen entirely from scratch and display it in the application, use something from the following list as the IdField value:
If you break something - just reset admin.nanc.io data (Chrome: F12 -> Application -> Application -> Storage -> Clear Site Data), well, when you reopen the client application it will always load the actual screen code created for this demo. (The code from your browser will be loaded only if you connect)
And finally, a small example of creating a new page of a new model containing a ScreenField and rendering it in the application:
The public demo is ready. The introductory article is written. Future plans for Nanc - to complete the functional integrity of the interface approach to model creation, making it possible to configure all the fields - Enum, Selector, and MultiSelector. Fix known bugs, like changing the position of elements in StructureField. Then "blah blah blah", and then "so-and-so". The backlog will be enough for the next half year at least, but the further model of expanding functionality will be based on customer needs, so if you have ideas/critiques / found bugs (and there are a lot of them) / anything else - please fill out the form, the link to which is available in the client demo application.
If you're interested in Nanc's features and you're interested in cooperation - fill out the form too, and we will definitely talk.