How to Create a Server Driven UI Engine for Flutter
2024-6-13 17:30:56 Author: hackernoon.com(查看原文) 阅读量:4 收藏

Hello!

Today I will show you how to create a super-duper engine for Server-Driven UI in Flutter, which is an integral part of a super-duper CMS (that's how its creator, that is, I, position it). You, of course, may have a different opinion, and I will be happy to discuss it in the comments.

This article is the first of two (already three) in the series. In this one, we will look directly at Nui, and in the next one - how deeply Nui is integrated with Nanc CMS, and between this and the next article there will be another one with a huge amount of information about Nui performance.

In this article will be a lot of interesting things about Server-Driven UI, Nui (Nanc Server-Driven UI) capabilities, project history, selfish interests, and Doctor Strange. Oh yes, there will also be links to GitHub and pub.dev, so if you like it and don't mind spending 1-2 minutes of your time - I will be glad to have your star and like.


Table of Contents

  1. Intro
  2. Reasons for Development
  3. Proof of Concept
  4. Syntax
  5. IDE Editor
  6. Performance
  7. Components and UI Creation
  8. Playground
  9. Interactivity and Logic
  10. Data Transfer
  11. Documentation
  12. Future Plans

A Little Intro

I have already written an article about Nanc, but since then, more than a year has passed since and the project has significantly progressed in terms of capabilities and "completeness", and most importantly - it was released with finished documentation, and under the MIT license.

So what is Nanc?

It is a general-purpose CMS that does not drag its backend with it. At the same time, it is not something like React Admin, where to create something, you need to write tons of code.

To start using Nanc, it is enough to:

  1. Describe the data structures that you want to manage through the CMS using Dart DSL
  2. Write an API layer that implements the communication between the CMS and your backend

Moreover, the first can be done completely through the interface of the CMS itself - that is, you can manage data structures through the UI. The second can be skipped if:

  1. You are using Firebase
  2. Or you are using Supabase
  3. Or you want to play around and run Nanc without tying it to a real backend - with a local database (for now, this role is played by a JSON file or LocalStorage)

Thus, in some scenarios, you will not have to write a single line of code to get a CMS for managing any of your content and data. In the future, the number of these scenarios will increase, let's say - plus GraphQL and RestAPI. If you have ideas for what else it would be possible to implement an SDK for - I will be happy to read the suggestions in the comments.

Nanc operates with entities - aka models, which at the data storage layer level can be represented as a table (SQL) or a document (No-SQL). Each entity has fields - a representation of columns from SQL, or the same "fields" from No-SQL.

One of the possible field types is the so-called "Screen" type. That is, this entire article is the text of just one field from the CMS. At the same time, architecturally it looks like this - there is a completely separate library (actually several libraries), which together implement the Server-Driven UI Engine called Nui. This functionality is integrated into the CMS, on top of which a lot of additional features are rolled.

With this, I conclude the introductory part dedicated directly to Nanc and begin the story about Nui.

How It All Began

Disclaimer: All coincidences are accidental. This story is fictional. I dreamed it.

I worked in one large company on several applications at once. They were largely similar but also had many differences.

But what was completely identical in them was what I can call the article engine. It consisted of several (5-10-15, I don't remember exactly anymore) thousand lines of rather crumpled code that processed JSON from the backend. These JSONs eventually had to turn into UI, or rather, into an article to be read in a mobile application.

The articles were created and edited using the admin panel, and the process of adding new elements was very, incredibly, extremely painful and long. Seeing this horror, I decided to propose the first optimization - to have mercy on the poor content managers and implement for them the functionality of previewing articles in real-time right in the browser, in the admin panel.

Said and done. After some time, a scrawny piece of the application was spinning in the admin panel, saving content managers a lot of time on previewing changes. If, earlier, they had to create a deep link, and then for each change open the dev build, follow this link, wait for downloads, and then repeat everything, now they could simply create articles and see them right away.

But my thought did not stop there - I was too annoyed by this engine, and by other developers as it was possible to determine whether they needed to add something to it or just clean the Augean stables.

If it was the latter, the developer was always in a good mood at meetings—though the smell... the camera can't capture that.

If it was the former, the developer was often sick, lived through earthquakes, had a broken computer, headaches, meteorite impacts, terminal stage depression, or an overdose of apathy.

Expanding the engine's functionality also required adding numerous new fields to the admin panel so content managers could utilize the new features.

Looking at all this, I was struck by an incredible thought: why not create a general solution to this problem? A solution that would prevent us from constantly tweaking and expanding the admin panel and the application for each new element. A solution that would solve the issue once and for all! And here comes the...

Sneaky greedy little plan

I thought - "I can solve this problem. I can save the company many tens, if not hundreds of thousands; but the idea may be too valuable for the company to just give it away as a gift."

By gift, I mean that the ratio of the potential value for the company differs from what the company will pay me in the form of a salary by orders of magnitude. It's like if you went to work at a startup at an early stage but for a salary less than what you are offered in some big company, and without a share in the company. And then the startup becomes a unicorn, and they tell you - "Well, dude, we paid you a salary." And they would be right!

I love analogies, but I am often told that they are not my strong suit. It's like you are a fish likes to swim in the ocean, but you're a freshwater fish.

And then - I decided to make a proof of concept (POC), in my free time, so as not to screw up by offering some ideas that may not even be possible to implement.

Proof of Concept

The original plan was to use an existing ready-made library for rendering markdown but expand its capabilities so that it could render not only the standard elements from the markdown list but also something much more complex. The articles were not just text with pictures. There was also a beautiful visual design, built-in audio players, and much more.

I spent 40 hours, counting from Friday evening to Monday morning, to test this hypothesis - how extensible this library is for new features, how well everything works in general, and most importantly - whether this solution can overthrow the notorious engine from the throne. The hypothesis was confirmed - after disassembling the library to the bones and a little patching, it became possible to register any UI elements by keywords or special syntax constructions, all this could be easily expanded, and most importantly - it really could replace the article engine. I came somewhere in 15 hours. The remaining 25 I spent finalizing the POC.

The idea was not only to replace one engine with another - no. The idea was to replace the entire process! The admin panel not only allows you to create articles but also manages content that is visible in the application. The original idea was to create a complete replacement that would not be tied to a specific project but would allow for managing it. Most importantly - this replacement should also provide a convenient editor for these very articles so that they can be created and immediately see the result.

For the POC, I thought it would be enough to just make an editor. It looked something like this:

UI Editor

After 40 hours, I had a working code editor consisting of a turbulent mixture of markdown and a bunch of custom XML tags (for example, <container>), a preview displaying the UI from this code in real-time, and also the biggest eye bags that this world has ever seen. It is also worth noting that the used "code editor" is another library capable of syntax highlighting, but the trouble is that it can highlight markdown, it can also highlight XML, but the highlighting of a hodgepodge constantly breaks. So for the 40 hours, you can add a couple more for the monkey-coding of a chimera that will provide highlighting of both in one bottle. It's time to ask - what happened next?

First Demo

Next was the demo. I gathered a couple of senior managers, explained to them my vision for solving the problem, the fact that I confirmed this vision in practice, and showed what works and how, and what possibilities it has.

The guys liked the work. And there was a desire to use it. But there was also a gnawing greed. My greed. Couldn't I just give it to the company like that? Of course not. But I didn't plan to either. The demo was part of a daring plan where I shocked them with my craft, they simply couldn’t resist and were ready to meet any conditions, just to use this incredible, exclusive, and amazing development. I will not disclose all the details of this fictional (!) story, but I will only say that I wanted money. Money and a vacation. A paid one-month vacation, as well as money. How much money is not so important, it is only important that the amount correlates with my salary and the number 6.

But I was not a completely reckless daredevil.

Dormammu, I've come to bargain. And the deal was as follows - I work two full weeks in my mode (sleep 4 hours, work 20 hours), finishing the POC to a state of "can be used for our app purposes", and in parallel with this, I implement a new feature in the application - a whole screen, using this ultra-thing (for which these two weeks were originally allotted). And at the end of two weeks, we hold another demo. Only this time we gather more people, even the company's top management, and if what they see impresses them, and they want to use it - the deal is done, I get my desires, and the company gets a super gun. If they don't want any of this - I'm ready to accept the fact that I worked for free these two weeks.

Pedra Furada (near Urubici)

Well, the trip to Urubici, which I had already planned for my month-long vacation, unfortunately, never happened. The manager guys did not dare to agree to such an audacity. And I, lowering my gaze to the ground, went to whittle a new screen in the "classic way". But there is no such story in which the main character, defeated by fate, does not get up from his knees and try to tame his beast again.

Although no... it seems there are: 1, 2, 3, 4, 5.

Having watched all these movies, I decided that this was a sign! And that it's even better this way - it's a pity to sell such a promising development for some goodies there (who am I kidding???), and I will continue to develop my project further. And I continued. But not 40 hours on weekends anymore, only 15-20 hours a week, at a relatively calm pace.

To code or not to code?

Breaking the 4th wall is not an easy task. Just like trying to come up with interesting headlines that will make the reader continue reading and wait for how the story with the company will end. I will finish the story in the second article. And now, it seems, it's time to switch to the implementation, functional capabilities, and all that, which, in theory, should make this article technical and HackerNoon greater!

Syntax

The first thing we will talk about is syntax. The original hodgepodge-idea was suitable for POC, but as practice has shown, markdown is not that simple. Plus, combining some native markdown elements with purely Flutter ones is not always consistent.

The very first question is - will the image be ![Description](Link) or <image>?

If the first one - where do I shove a bunch of parameters?

If the second - why, then, do we have the first?

The second question is texts. The possibilities of Flutter for styling texts are limitless. The possibilities of markdown are “so-so”. Yes, you can mark the text in bold or italics, and there were even thoughts of using these constructions ** / __ for styling. Then there were thoughts of shoving <color="red"> text </color> tags in the middle, but this is such a curve and creep that blood flows from the eyes. Getting some kind of HTML, with its own marginal syntax, was not desirable at all. Plus, the idea was that this code could be written even by managers without technical knowledge.

Step by step, I removed the part of the chimera and got a markdown super-mutant. That is, we got a patched library for rendering markdown, but stuffed with custom tags and without markdown support. That is, as if we got XML.

I sat down to think and experiment with what other simple syntaxes are there. JSON is slag. Making a person write JSON in a crooked Flutter editor is getting a maniac who will want to kill you. And it's not just about that, it doesn't seem to me that JSON is suitable for typing by a person in general, especially for UI - it is constantly growing to the right, a bunch of mandatory "", there are no comments. YAML? Well, maybe. But the code will also crawl sideways. There are interesting links, but you can't achieve much with their help alone. TOML? Pf-f-f.

Okay, I settled on XML after all. It seemed to me, and still seems now, that this is a rather "dense" syntax, very well suited for UI. After all, HTML layout designers still exist, and here everything will be even simpler than on the web (probably).

Next, the question arose - it would be nice to get the possibility of some highlighting/code completion. As well as logical constructions, kinda {{ user.name }}. Then I started experimenting with Twig, Liquid, looked at some other template engines that I don't remember anymore. But I ran into another problem - it is quite possible to implement part of what was planned on a standard engine, say, Twig, but it definitely won't work to implement everything. And yes, it's good that there will be auto-completion and highlighting, but they will only interfere if you roll your own new features on top of the standard Twig syntax, which will be needed for Flutter. As a result, with XML, everything turned out very well, experiments with Twig / Liquid did not give any outstanding results, and at certain points, I even ran into the impossibility of implementing some features. Therefore, the choice still remained with XML. We will talk more about the features, but for now, let's focus on auto-completion and highlighting, which were so tempting in Twig/Liquid.

IDE

The next thing I want to say is that Flutter has crooked text inputs. They work well in mobile format. Also good in desktop format when it comes to something, well, a maximum of 5-10 lines in height. But when it comes to a full-fledged code editor, where this editor is implemented in Flutter - you can't look at it without tears. In Trello, where I keep track of all tasks, and write notes and ideas, there is such a "task":

Task to change the UI code editor

In fact, almost from the very beginning of working on the project, I kept in mind the idea of replacing the Nui code editor with something more adequate. Let's say - embed a web view with the Open Source part from VS Code. But so far, my hands have not reached this, besides, a crutchy but still-working solution to the problem of the curvature of this editor came to my mind - to use your own development environment instead.

This is achieved as follows - create a file with UI-code (XML), ideally with the extension .html / .twig, open the same file through the CMS - Web / Desktop / Local / Deployed - it does not matter. And open the same file through any IDE, even through the web version of VS Code. And voila - you can edit this file in your favorite tool, and have a real-time preview right in the browser or anywhere.

Nanc + IDE Sync

In such a scenario, you can even screw on full-fledged auto-completion. In VS Code, there is the possibility of implementing it through custom HTML tags. However, I don't use VS Code, my choice is IntelliJ IDEA and for this IDE there is no such simple solution anymore (well, at least there wasn't, or at least I didn't find it). But there is a more general solution that will work both there and there - XML Schema Definition (XSD). I spent about 3 evenings trying to figure out this monster, but success never came, and in the end, I abandoned this matter, leaving it for better times.

It is also interesting that in the end, after many iterations of experiments, updates, let's say, the engine responsible for converting XML into widgets, we got such a solution for which the language is not particularly important. Just as a carrier of information about the structure of your UI, the choice eventually fell on XML, but at the same time, you can safely feed it JSON, and even a binary form - compiled Protobuf. And this brings us to the next topic.

Performance

In this sentence, the size of this article will be 3218 words. When I started writing this section, to do everything qualitatively - it was necessary to write a lot of test cases comparing the performance of rendering Nui and regular Flutter. Since I already had a demo screen implemented, completely created on Nui:

Nalmart Screen Demo

it was necessary to create an exact match of the screen natively (in the context of Flutter, of course). As a result, it took more than 3 weeks, a lot of rewriting the same thing, improving the testing process, and obtaining more and more interesting numbers. And the size of this section alone exceeded 3500 words. Therefore, I came to the idea that it makes sense to write a separate article that will be entirely and completely devoted to the performance of Nui, as a particular case, and to the additional price that you will have to pay if you decide to use Server-Driven UI as an approach.

But I will make a small spoiler: there were two main scenarios for evaluating performance that I considered - the time of initial rendering. It is important if you decide to implement the whole screen on Server-Driven UI, and this screen will open somewhere in your application.

So if this screen is very heavy, then even a native Flutter screen will take a long time to render, so when switching to such a screen, especially if this transition is accompanied by an animation, lags will be visible. The second scenario is frame time (FPS) with dynamic UI changes. The data has changed - you need to redraw some component. The question is how much this will affect the rendering time, whether it will affect so much that when the screen is updated, the user will see lags. And here's another spoiler - in most cases, you won't be able to tell that the screen you see is completely implemented on Nui. If you embed a Nui widget into a regular, native Flutter screen (say, some area of the screen that should change very dynamically in the application) - you are guaranteed not to be able to recognize this. There are, of course, drops in performance. But they are such that they do not affect the FPS even at a frame rate of 120FPS - that is, the time of one frame will almost never exceed 8ms. This is true for the second scenario. As for the first one - it all depends on the level of complexity of the screen. But even here, the difference will be such that it will not affect the perception and will not make your application a benchmark for user smartphones.

Below are three screen recordings from Pixel 7a (Tensor G2, screen refresh rate was set to 90 frames (maximum for this device), video recording rate of 60 frames per second (maximum for recording settings). Every 500ms, the position of elements in the list is randomized, from the data of which the first 3 cards are built, and after another 500ms, the order status is switched to the next one. Will you be able to guess which of these screens is implemented entirely on Nui?

P.S. The loading time of images does not depend on the implementation since on this screen, with any implementation, there are a lot of Svg images - all icons, as well as brand logos. All svg (as well as regular pictures) are stored on GitHub, as a hosting, so they can load quite slowly, which is observed in some experiments.

YouTube:

Available Components - How to Create UI

When creating Nui, I adhered to the following concept - it is necessary to create such a tool that, first of all, Flutter developers will find it as easy to use as creating regular Flutter applications. Therefore, the approach to naming all components was simple - to name them in the same way as they are named in Flutter.

The same applies to widget parameters - the scalars, like String, int, double, enum, etc., which, as a parameter, are not configured themselves. These types of parameters within Nui are called arguments. And to complex class parameters, like decoration in the Container widget, called property. This rule is not absolute since some properties are too verbose, so their names have been simplified. Also, for some widgets, the list of available parameters has been extended. For example - to make a square SizedBox or Container, you can pass only one custom arguments size, instead of two identical width + height.

I will not give a complete list of implemented widgets, as there are quite a few of them (53 at the moment). In short - you can implement almost any UI for which it would make sense to use Server-Driven UI as an approach in principle. Including complex scrolling effects associated with Slivers.

Implemented widgets

Also, regarding the components, it is worth noting the entry point or widget to which you will have to pass the cloud XML-code. At the moment there are two such widgets - NuiListWidget and NuiStackWidget.

The first one, by design, should be used if you need to implement the whole screen. Under the hood, it is a CustomScrollView containing all the widgets that will be parsed from the original markup code. Moreover, the parsing, one might say, is "intelligent": since the content of CustomScrollView should be slivers, then a possible solution would be to wrap each of the widgets in the stream in a SliverToBoxAdapter, but this would have an extremely negative impact on performance. Therefore, the widgets are embedded in their parent as follows - starting from the very first one, we go down the list until we meet a real sliver. As soon as we meet a sliver - we add all the previous widgets to SliverList, and add it to the parent CustomScrollView. Thus, the performance of rendering the entire UI will be as high as possible, since the number of slivers will be minimal. Why is it bad to have a lot of slivers in CustomScrollView? The answer is here.

The second widget - NuiStackWidget can also be used as a full screen - in this case, it is worth keeping in mind that everything you create will be embedded in the Stack in the same order. And it will also be necessary to explicitly use slivers - that is, if you want a list of slivers - you will have to add CustomScrollView and already implemented the list inside it.

The second scenario is the implementation of a small widget that can be embedded into native components. Let's say - to make a product card that will be completely customizable at the initiative of the server. It seems a very interesting scenario in which you can implement all the components in the component library using Nui, and use them as regular widgets. At the same time, there will always be the opportunity to completely change them without updating the application.

It is worth noting that NuiListWidget can also be used as a local widget, and not the whole screen, but for this widget, you will need to apply appropriate restrictions, such as setting an explicit height for the parent widget.

Here's what a counter app would look like if it were created using Flutter:

import 'package:flutter/material.dart';
import 'package:nui/nui.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Nui App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Nui Demo App'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    required this.title,
    super.key,
  });

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: NuiStackWidget(
          renderers: const [],
          imageErrorBuilder: null,
          imageFrameBuilder: null,
          imageLoadingBuilder: null,
          binary: null,
          nodes: null,
          xmlContent: '''
<center>
  <column mainAxisSize="min">
    <text size="18" align="center">
      You have pushed the button\nthis many times:
    </text>
    <text size="32">
      {{ page.counter }}
    </text>
  </column>
</center>
''',
          pageData: {
            'counter': _counter,
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

And here is another example, only completely on Nui (including logic):

import 'package:flutter/material.dart';
import 'package:nui/nui.dart';

void main() {
  runApp(const MyApp());
}

final DataStorage globalDataStorage = DataStorage(data: {'counter': 0});

final EventHandler counterHandler = EventHandler(
  test: (BuildContext context, Event event) => event.event == 'increment',
  handler: (BuildContext context, Event event) => globalDataStorage.updateValue(
    'counter',
    (globalDataStorage.getTypedValue<int>(query: 'counter') ?? 0) + 1,
  ),
);

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return DataStorageProvider(
      dataStorage: globalDataStorage,
      child: EventDelegate(
        handlers: [
          counterHandler,
        ],
        child: MaterialApp(
          title: 'Nui App',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(title: 'Nui Counter'),
        ),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    required this.title,
    super.key,
  });

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: NuiStackWidget(
          renderers: const [],
          imageErrorBuilder: null,
          imageFrameBuilder: null,
          imageLoadingBuilder: null,
          binary: null,
          nodes: null,
          xmlContent: '''
<center>
  <column mainAxisSize="min">
    <text size="18" align="center">
      You have pushed the button\nthis many times:
    </text>
    <dataBuilder buildWhen="counter">
      <text size="32">
        {{ data.counter }}
      </text>
    </dataBuilder>
  </column>
</center>

<positioned right="16" bottom="16">
  <physicalModel elevation="8" shadowColor="FF000000" clip="antiAliasWithSaveLayer">
    <prop:borderRadius all="16"/>
    <material type="button" color="EBDEFF">
      <prop:borderRadius all="16"/>
      <inkWell onPressed="increment">
        <prop:borderRadius all="16"/>
        <tooltip text="Increment">
          <sizedBox size="56">
            <center>
              <icon icon="mdi_plus" color="21103E"/>
            </center>
          </sizedBox>
        </tooltip>
      </inkWell>
    </material>
  </physicalModel>
</positioned>
''',
          pageData: {},
        ),
      ),
    );
  }
}

Separate UI code so that there is highlighting:

<center>
  <column mainAxisSize="min">
    <text size="18" align="center">
      You have pushed the button\nthis many times:
    </text>
    <dataBuilder buildWhen="counter">
      <text size="32">
        {{ data.counter }}
      </text>
    </dataBuilder>
  </column>
</center>

<positioned right="16" bottom="16">
  <physicalModel elevation="8" shadowColor="black" clip="antiAliasWithSaveLayer">
    <prop:borderRadius all="16"/>
    <material type="button" color="EBDEFF">
      <prop:borderRadius all="16"/>
      <inkWell onPressed="increment">
	    <prop:borderRadius all="16"/>
        <tooltip text="Increment">
          <sizedBox size="56">
            <center>
              <icon icon="mdi_plus" color="21103E"/>
            </center>
          </sizedBox>
        </tooltip>
      </inkWell>
    </material>
  </physicalModel>
</positioned>

Nui Counter App with Nui logic

There is also interactive and comprehensive documentation that shows detailed information about what arguments and properties each widget has, as well as all their possible values. For each of the properties, which can also have both arguments and other properties, there is also documentation, with a full demonstration of all available values. In addition to this, each of the components contains an interactive example in which you can see the implementation of this widget live and play with it by changing it as you like.

Nanc Playground

Nui is very tightly integrated into Nanc CMS. You don't have to use Nanc to use Nui, but using Nanc can give you advantages, namely - the same interactive documentation, as well as Playground, where you can see the results of the layout in real-time, play with the data that will be used in it. Moreover, it is not necessary to create your own local build of the CMS, you can quite manage with the published demo, in which you can do everything you need.

You can do this by following the link, and then clicking on the Page Interface / Screen field. The opened screen can be used as a Playground, and by clicking the Sync button, you can synchronize Nanc with your IDE through a file with sources, and all the documentation is available by clicking the Help button.

P.S. These complexities exist because I never found the time to make an explicit separate page with documentation on the components in Nanc, as well as the inability to insert a direct link to this page.

Interactivity and Logic

It would be too pointless to create an ordinary mapper from XML to widgets. This, of course, can also be useful, but there will be much fewer use cases. Not the same thing - completely interactive components and screens that you can interact with, which you can granularly update (that is, not all at once - but in parts that need updating). Also, this UI needs data. Which, taking into account the presence of the letter S in the phrase Server-Driven UI, can be substituted directly into the layout on the server, but you can also do it more beautifully. And not to drag a new portion of the layout from the backend for every change in the UI (Nui is not a time machine that brings the best practices of jQuery to flutter).

Let's start with the logic: variables and computed expressions can be substituted into the layout. Let's say a widget is defined as <container color="{{ page.background }}"> will extract its color directly from the data passed to the "parent context" stored in the background variable. And <aspectRatio ratio="{{ 3 / 4}}"> will set the corresponding aspect ratio value for its descendants. There are built-in functions, comparisons, and much more that can be used to build UI with some logic.

The second point is templating. You can define your own widget directly in the UI code using the <template id="your_component_name"/> tag. At the same time, all internal components of this template will have access to the arguments passed to this template, which will allow flexible parameterization of custom components and then reuse them using the <component id="your_component_name"/> tag. Inside templates, you can pass not only attributes but also other tags/widgets, which makes it possible to create reusable components of any complexity.

Point three - "for loops". In Nui, there is a built-in <for> tag that allows you to use iterations to render the same (or multiple) components multiple times. This is convenient when there is a set of data from which you need to create a list/row/column of widgets.

Fourth - conditional rendering. At the layout level, the <show> tag is implemented (there was an idea to call it <if>), which allows you to draw nested components, or not embed them into the tree at all under various conditions.

Point five - actions. Some components that the user can interact with can send events. Which you can completely control as you like. Let's say, <inkWell onPressed="something"> - with such a declaration, this widget becomes clickable, and your application, or rather, some EventHandler, will be able to handle this event and do something. The idea is that everything related to logic should be implemented directly in the application, but you can implement anything. Make some generic handlers that can handle groups of actions, like "go to screen" / "call method" / "send analytics event". There are plans to implement dynamic code as well, but there are nuances here. For Dart, there are ways to execute arbitrary code, but this affects performance, and besides, the interoperability of this code with the application code is hardly 100%. That is, by creating logic in this dynamic code, you will constantly encounter some limitations. Therefore, this mechanism needs to be very carefully worked out in order to be really applicable and useful.

The sixth point is the local UI update. This is possible thanks to the <dataBuilder> tag. This tag (Bloc under the hood) can "look" at a specific field, and when it changes, it will redraw its subtree.

Data

Initially, I followed the path of two stores for data - the "parent context" mentioned above. As well as "data" - data that can be defined directly in the UI, using the <data> tag. To be honest, I can't remember now the argumentation why it was necessary to implement two ways of storing and transferring data to the UI, but I can't somehow harshly criticize myself for such a decision.

They work as follows - the "parent context" is an object of type Map<String, dynamic>, passed directly to the NuiListWidget / NuiStackWidget widgets. Access to this data is possible by the prefix page:

<someWidget value="{{ page.your.field }}"/>

You can refer to anything, to any depth, including arrays - {{ page.some.array.0.users.35.age }}. If there is no such key/value, you will get null. Lists can be iterated over using <for>.

The second way - "data" is a global data store. In practice, this is a certain Bloc located higher in the tree than NuiListWidget / NuiStackWidget. At the same time, nothing prevents organizing their use in a local style, passing your own instance of DataStorage through DataStorageProvider.

At the same time, the first method is not reactive - that is, when the data in page changes, no UI will update itself. Since this is, in fact, just the arguments of your StatelessWidget. If the data source for page is, say, your own Bloc, which will give a set of values to Nui...Widget - then, as with a regular StatelessWidget, it will be completely redrawn with newdata.

The second way of working with data is reactive. If you change the data in DataStorage, using the API of this class - the updateValue method, then this will call the emit method of the Bloc class, and if there are active listeners of this data in your UI - <dataBuilder> tags, then their content will be changed accordingly, but the rest of the UI will not be touched.

Thus, we get two potential data sources - a very simple page, and a reactive data. Except for the logic of updating data in these sources and the UI's reaction to these updates, there is no difference between them.

Documentation

I deliberately did not describe all the nuances and aspects of the work, since it would turn out to be a copy of the already existing documentation. Therefore, if you become interested in trying or just learning more - please welcome here. If any aspects of the work are not clear or the documentation does not cover something, then I will be flattered by your message indicating the problem:

I will briefly list some of the features that are not covered in this article, but are available to you:

  • Creating your own tags/components, with the ability to create exactly the same interactive documentation for them, just like for their arguments and properties with live preview. This is how, for example, the component for rendering SVG images is implemented. There is no point in pushing it into the core of the engine, because not everyone needs it, but as an extension available for use by passing just one variable - easy and simple. Directly - an example of implementation.

  • A huge built-in library of icons that can be expanded by adding your own (here I turned out to be inconsistent, and "shoved", the logic was to make as many icons as possible available for use immediately and there was no need to update the application to use new icons). Out of the box are available: fluentui_system_icons, material_design_icons_flutter and remixicon. You can view all available icons using Nanc, Page Interface / Screen -> Icons

  • Custom fonts, including Google Fonts out of the box

  • Converting XML to JSON/protobuf and using them as "sources" for UI

All this and much more can be studied in the documentation.

What's next?

The main thing is to work out the possibility of dynamically executing code with logic. This is a very cool feature that will allow you to very seriously expand the capabilities of Nui. Also, you can (and should) add the remaining rarely used, but sometimes very important widgets from the standard Flutter library. To master XSD, so that auto-completion for all tags appears in the IDE (there is an idea to generate this scheme directly from the tag documentation, then it will be easy to create it for custom widgets and it will always be up-to-date, and there is also an idea to make a generated DSL in Dart, which can then be converted into XML / Json / Protobuf). Well, and additional performance optimization - it's not bad right now, very not bad, but it can be even better, even closer to native Flutter.

That's all I have. In the next article, I will tell in great detail about the performance of Nui, how I created test cases, how many dozens of rakes I walked through in this process, and what numbers can be obtained in which scenarios.

If you become interested in trying Nui or getting to know it better - please go to the documentation desk. Also, if it's not difficult, then please put a star on GitHub and a like on pub.dev - it's not difficult for you, but for me, a lonely rower on this huge boat - it's incredibly useful.


文章来源: https://hackernoon.com/how-to-create-a-server-driven-ui-engine-for-flutter?source=rss
如有侵权请联系:admin#unsafe.sh