こんにちは!
今日は、超優れた CMS に不可欠な要素であるFlutterのサーバー駆動型 UI 用の超優れたエンジンを作成する方法を紹介します (これが、作成者、つまり私が位置づける点です)。もちろん、皆さんには異なる意見があるかもしれません。その場合は、コメントで喜んで議論させていただきます。
この記事は、シリーズの2 つの記事 (すでに 3 つ) のうちの最初の記事です。今回は Nui を直接取り上げ、次の記事では Nui が Nanc CMS とどの程度深く統合されているかについて説明します。また、この記事と次の記事の間には、Nui のパフォーマンスに関する膨大な情報を含む別の記事が掲載されます。
この記事には、Server-Driven UI、Nui (Nanc Server-Driven UI) の機能、プロジェクトの履歴、利己的な関心、Doctor Strange など、興味深い内容が多数記載されています。そうそう、GitHub と pub.dev へのリンクもありますので、気に入っていただき、1 ~ 2 分ほどお時間を割いていただける場合は、ぜひ星を付けて「いいね」していただければ幸いです。
私はすでに Nanc についての 記事を書いていますが、それから 1 年以上経ち、プロジェクトは機能と「完全性」の点で大幅に進歩し、最も重要なことに、完成したドキュメントとともに MIT ライセンスの下でリリースされました。
これは、バックエンドを引きずらない汎用 CMS です。同時に、何かを作成するために大量のコードを書く必要がある React Admin のようなものではありません。
Nanc を使い始めるには、次の手順で十分です。
さらに、最初のステップは CMS 自体のインターフェースを通じて完全に実行できます。つまり、UI を通じてデータ構造を管理できます。2番目のステップは、次の場合にスキップできます。
したがって、シナリオによっては、コンテンツやデータを管理するための CMS を取得するために 1 行もコードを記述する必要がありません。将来的には、GraphQL や RestAPI など、こうしたシナリオの数が増えるでしょう。SDK を実装できる他の用途についてアイデアがあれば、コメントで提案をいただければ幸いです。
Nanc はエンティティ (別名モデル) を操作します。エンティティは、データ ストレージ レイヤー レベルでテーブル (SQL) またはドキュメント (No-SQL) として表すことができます。各エンティティにはフィールドがあります。フィールドは、SQL の列の表現、または No-SQL の同じ「フィールド」です。
使用可能なフィールド タイプの 1 つは、いわゆる「画面」タイプです。つまり、この記事全体は、CMS の 1 つのフィールドのテキストです。同時に、アーキテクチャ的には、完全に別のライブラリ (実際には複数のライブラリ)があり、これらが一緒に Nui と呼ばれるサーバー駆動型 UI エンジンを実装しているように見えます。この機能は CMS に統合されており、その上に多くの追加機能が展開されています。
これで、ナンクに直接捧げられた導入部を終え、ヌイについての物語を始めます。
免責事項: すべての偶然は偶発的です。この物語はフィクションです。夢で見たものです。
私はある大企業で、一度に複数のアプリケーションに取り組んでいました。それらは大体似ていましたが、多くの違いもありました。
しかし、それらに完全に共通していたのは、私が記事エンジンと呼べるものだった。それは、バックエンドからのJSONを処理する数千行 (5、10、15 行、正確にはもう覚えていない) のかなりしわくちゃのコードで構成されていた。これらの JSON は最終的に UI、つまりモバイル アプリケーションで読み取られる記事に変換する必要があった。
記事は管理パネルを使用して作成および編集されており、新しい要素を追加するプロセスは非常に、信じられないほど、非常に苦痛で長いものでした。この恐ろしい状況を見て、私は最初の最適化を提案することにしました。つまり、かわいそうなコンテンツ管理者に慈悲を与え、彼らのために、ブラウザーの管理パネルで記事をリアルタイムでプレビューする機能を実装するというものでした。
言ってみれば、完了です。しばらくすると、アプリケーションの小さな部分が管理パネルで回転するようになり、コンテンツ マネージャーは変更をプレビューする時間を大幅に節約できるようになりました。以前は、ディープ リンクを作成し、変更ごとに開発ビルドを開いてこのリンクをたどり、ダウンロードを待ってからすべてを繰り返す必要がありましたが、今では記事を作成してすぐに確認するだけで済みます。
しかし、私の考えはそこで止まりませんでした。私はこのエンジンに、そして他の開発者たちにも非常にイライラしていました。何かを追加する必要があるのか、それともアウゲイアスの厩舎を掃除するだけでよいのかを判断することができたからです。
後者の場合、開発者は会議では常に上機嫌だったが、匂いは...カメラでは捉えられない。
前者の場合、開発者は病気にかかったり、地震を経験したり、コンピューターが壊れたり、頭痛や隕石の衝突、末期のうつ病、あるいは過剰な無関心を経験したりすることがよくありました。
エンジンの機能を拡張するには、コンテンツ マネージャーが新しい機能を利用できるように、管理パネルに多数の新しいフィールドを追加する必要もありました。
これらすべてを見て、私は驚くべき考えに衝撃を受けました。この問題に対する一般的な解決策を作ればよいのでは?新しい要素ごとに管理パネルとアプリケーションを常に調整したり拡張したりしなくて済むような解決策。問題を一気に解決する解決策!そして、ここに...
私はこう思いました。「私はこの問題を解決できる。会社に何万、いや何十万ものコストを節約できる。しかし、このアイデアは会社にとってあまりにも価値が高いので、ただ贈り物として配ることはできないかもしれない。」
贈り物とは、会社にとっての潜在的価値の比率が、会社が私に給料という形で支払う金額と桁違いに異なることを意味します。これは、初期段階のスタートアップ企業で働き始めたものの、大企業で提示される給与よりも低く、会社の株式も与えられなかったようなものです。その後、スタートアップ企業がユニコーン企業になり、「まあ、給料は払ったよ」と言われるのです。そして、その言葉は正しいのです。
私は例え話が大好きですが、得意分野ではないとよく言われます。それは、あなたが海で泳ぐのが好きな魚なのに、淡水魚であるようなものです。
そして、実装不可能なアイデアを提案して失敗しないように、空き時間に概念実証 (POC) を行うことにしました。
当初の計画では、マークダウンをレンダリングするために既存の既製のライブラリを使用する予定でしたが、その機能を拡張して、マークダウン リストの標準要素だけでなく、より複雑なものもレンダリングできるようにしました。記事は単なる画像付きのテキストではありませんでした。美しいビジュアル デザイン、組み込みのオーディオ プレーヤーなどもありました。
金曜日の夜から月曜日の朝まで、この仮説をテストするために 40 時間を費やしました。このライブラリは新しい機能に対してどの程度拡張可能か、全体的にすべてがどの程度うまく機能するか、そして最も重要なのは、このソリューションが悪名高いエンジンを王座から引きずり下ろせるかどうかです。仮説は確認されました。ライブラリを骨組みまで分解し、少しパッチを当てると、キーワードまたは特殊な構文構造で任意の UI 要素を登録できるようになり、これらはすべて簡単に拡張でき、最も重要なのは、実際に記事エンジンを置き換えることができることです。私は 15 時間で何とか到達しました。残りの 25 時間は POC の完成に費やしました。
アイデアは、エンジンを別のエンジンに置き換えるだけではありませんでした。プロセス全体を置き換えるというものでした。管理パネルでは、記事を作成できるだけでなく、アプリケーションに表示されるコンテンツを管理することもできます。当初のアイデアは、特定のプロジェクトに縛られず、それを管理できる完全な代替品を作成することでした。最も重要なのは、この代替品では、これらの記事を作成するための便利なエディターも提供され、記事を作成してすぐに結果を確認できるようにすることです。
POC の場合、エディターを作成するだけで十分だと思いました。次のような感じでした。
40 時間後、マークダウンと一連のカスタム XML タグ ( <container>
など) の乱雑な組み合わせで構成される、機能するコード エディター、このコードからリアルタイムで UI を表示するプレビュー、そしてこの世界で今までに見たことのない最大の目の下のたるみができました。また、使用した「コード エディター」は構文の強調表示が可能な別のライブラリであることも注目に値しますが、問題は、マークダウンを強調表示でき、XML も強調表示できるものの、寄せ集めの強調表示が常に壊れてしまうことです。そのため、40 時間の間に、キメラのモンキー コーディングをさらに 2、3 時間追加して、1 つのボトルで両方の強調表示を提供することができます。では、次に何が起こったのかを尋ねてみましょう。
次はデモです。私は上級管理職を数人集め、問題を解決するための私のビジョンと、このビジョンを実際に確認した事実を説明し、何がどのように機能するか、そしてどのような可能性があるかを示しました。
みんなは作品を気に入ってくれました。そして、それを使いたいという欲求もありました。しかし、心の中には、むしばむような欲望もありました。私の欲望です。会社にそのまま渡せないでしょうか? もちろん、そうはしませんでした。でも、そのつもりもありませんでした。デモは、私の技術で彼らを驚かせるという大胆な計画の一部でした。彼らは、この信じられないほどの独占的で素晴らしい開発を使うためなら、抵抗できず、どんな条件でも受け入れる覚悟でした。この架空の (!)物語の詳細をすべて明かすつもりはありませんが、私が欲しかったのはお金だということだけは言えます。お金と休暇。お金だけでなく、1 か月の有給休暇。お金の額はそれほど重要ではなく、金額が私の給料と 6 という数字に相関しているかどうかだけが重要です。
しかし、私はまったく無謀な命知らずというわけではありませんでした。
ドルマムゥ、交渉に来た。そして、契約内容はこうだ。私は自分のモードで丸々2週間働き(睡眠4時間、労働20時間)、POCを「アプリの用途に使える」状態に仕上げ、これと並行して、この超高性能なものを使って、アプリに新しい機能、つまり全画面を実装する(この2週間はもともとこのために割り当てられた)。そして、2週間が経つと、またデモを開催する。ただし今回はもっと多くの人、会社の経営陣も集め、彼らが見たものに感銘を受け、使いたくなったら、契約は成立し、私は望みを叶え、会社はスーパーガンを手に入れる。もし彼らがこれを一切望まないなら、私はこの2週間無償で働いたという事実を受け入れる覚悟だ。
さて、私が 1 か月の休暇に計画していたウルビシへの旅行は、残念ながら実現しませんでした。マネージャーたちは、そのような大胆なことに同意する勇気がありませんでした。そして、私は視線を地面に下げて、「古典的な方法」で新しいスクリーンを削り始めました。しかし、運命に打ち負かされた主人公が膝から立ち上がって再び獣を飼いならそうとしないような物語はありません。
これらすべての映画を見て、これは兆候だと判断しました。そして、この方法のほうがさらに良いと思いました。このような有望な開発を、そこでの特典のために売るのは残念です (冗談じゃない)。そして、私はプロジェクトをさらに開発し続けるつもりです。そして、続けました。しかし、週末に 40 時間働くことはもうなく、週に 15 ~ 20 時間、比較的穏やかなペースで働きました。
第 4 の壁を破るのは簡単なことではありません。読者が読み続け、会社とのストーリーの結末を待ちたくなるような興味深い見出しを考え出すのと同じです。そのストーリーは 2 番目の記事で終わります。そして、どうやら、実装、機能、その他すべてに移る時が来たようです。理論的には、これによりこの記事は技術的になり、 HackerNoon はより優れたものになるはずです。
まず最初に、構文について説明します。元々の寄せ集めのアイデアは POC には適していましたが、実践してみると、マークダウンはそれほど単純ではありません。さらに、ネイティブのマークダウン要素と純粋なFlutter要素を組み合わせると、必ずしも一貫性が保たれるわけではありません。
最初の質問は、画像![Description](Link)
なるのか、それとも<image>
なるのかということです。
最初の場合、たくさんのパラメータをどこに押し込めばいいのでしょうか?
2 番目なら、なぜ 1 番目が存在するのでしょうか?
2 番目の質問はテキストです。Flutter のテキスト スタイル設定の可能性は無限です。マークダウンの可能性は「まあまあ」です。はい、テキストを太字や斜体でマークできますし、 **
/ __
構造を使用してスタイル設定することも考えられました。次に、 <color="red">
text </color>
タグを真ん中に押し込むという考えもありましたが、これは目から血が流れるほどの曲がりくねった不気味なものです。独自の限界構文を持つ何らかの HTML を取得することは、まったく望ましいものではありませんでした。さらに、このコードは技術的な知識のないマネージャーでも記述できるという考えもありました。
段階的にキメラの部分を削除し、Markdown のスーパーミュータントを作成しました。つまり、Markdown をレンダリングするためのパッチを当てたライブラリを作成しましたが、カスタム タグが詰め込まれており、Markdown はサポートされていません。つまり、XML を作成した場合と同じことになります。
他にどんなシンプルな構文があるのか考え、実験してみました。JSON はゴミです。歪んだ Flutter エディターで JSON を書かせるのは、あなたを殺そうとする狂人を作るようなものです。それだけではありません。JSON は、一般的に人が入力するのに適していないように思われます。特に UI の場合です。常に右に伸び、必須の""
がたくさんあり、コメントはありません。YAML でしょうか。まあ、そうかもしれません。しかし、コードは横に這うようにもなります。興味深いリンクもありますが、それだけではあまり効果がありません。TOML でしょうか。ふーん。
わかりました。結局、XML に落ち着きました。これはかなり「密度の高い」構文で、UI に非常に適しているように私には思えましたし、今でもそう思います。結局のところ、HTML レイアウト デザイナーはまだ存在しており、ここではすべてが Web 上よりもさらにシンプルになります (おそらく)。
次に、疑問が生じました。ハイライト/コード補完の機能があればいいのに、と。論理構造だけでなく、 {{ user.name }}
ようなものも。その後、Twig、Liquid を試し始め、もう覚えていない他のテンプレート エンジンもいくつか見てみました。しかし、別の問題に遭遇しました。計画されていた機能の一部を標準エンジン (たとえば Twig) に実装することは可能ですが、すべてを実装することは絶対にできないということです。そして、自動補完とハイライト機能があることは良いことですが、Flutter に必要な標準の Twig 構文の上に独自の新機能をロールする場合にのみ、それらは干渉します。結果として、XML ではすべてが非常にうまくいきましたが、Twig / Liquid での実験では目立った結果は得られず、ある時点では、一部の機能を実装できないことにさえ遭遇しました。そのため、選択肢は依然として XML のままでした。機能については後ほど詳しく説明しますが、今は、Twig/Liquid で非常に魅力的だった自動補完と強調表示に焦点を当てましょう。
次に言いたいのは、Flutter には歪んだテキスト入力があるということです。モバイル形式ではうまく機能します。デスクトップ形式でも、高さが最大 5 ~ 10 行であればうまく機能します。しかし、このエディターが Flutter で実装されている本格的なコード エディターになると、涙なしでは見ることができません。すべてのタスクを追跡し、メモやアイデアを書き込むTrelloには、次のような「タスク」があります。
実際、プロジェクトに取り組み始めた当初から、Nui コード エディターをより適切なものに置き換えるというアイデアを念頭に置いていました。たとえば、VS Code のオープン ソース部分を含む Web ビューを埋め込むなどです。しかし、これまでのところ、私の手はこれに達していません。また、このエディターの湾曲の問題に対する、頼りないながらもまだ機能する解決策が思い浮かびました。それは、代わりに独自の開発環境を使用することです。
これは次のようにして実現されます。UI コード (XML) を含むファイル (拡張子は.html
/ .twig
が理想的) を作成し、同じファイルを CMS (Web / デスクトップ / ローカル / デプロイ済み) から開きます (どこであってもかまいません)。次に、同じファイルを任意の IDE (VS Code の Web バージョンからでも) から開きます。すると、このファイルをお気に入りのツールで編集し、ブラウザー内またはどこからでもリアルタイムでプレビューできるようになります。
このようなシナリオでは、本格的な自動補完を組み込むこともできます。VS Code では、カスタム HTML タグを使用して実装できます。ただし、私は VS Code を使用しておらず、IntelliJ IDEA を選択していますが、この IDE にはそのような簡単なソリューションはもうありません (少なくとも、ありませんでした。少なくとも私は見つけられませんでした)。ただし、どちらにも機能するより一般的なソリューションがあります。それは、XML スキーマ定義 (XSD) です。このモンスターを解明しようと 3 晩ほど費やしましたが、成功することはなく、結局、この問題を放棄して、より良い時期まで残しました。
また、多くの実験、更新、つまり XML をウィジェットに変換するエンジンの繰り返しを経て、最終的に言語が特に重要ではないソリューションが得られたことも興味深いことです。UI の構造に関する情報のキャリアとして、最終的に XML が選択されましたが、同時に、JSON やバイナリ形式 (コンパイルされた Protobuf) も安全に入力できます。これが次のトピックにつながります。
この文章で、この記事のサイズは 3218 語になります。このセクションを書き始めたとき、すべてを定性的に行うには、Nui と通常の Flutter のレンダリングのパフォーマンスを比較するテスト ケースをたくさん書く必要がありました。Nui で完全に作成されたデモ画面がすでに実装されていたため、次のようになりました。
画面の正確な一致をネイティブに作成する必要がありました (もちろん、Flutter のコンテキストで)。その結果、3 週間以上かかり、同じことを何度も書き直し、テスト プロセスを改善し、ますます興味深い数値を取得しました。そして、このセクションのサイズだけで 3500 語を超えました。そのため、特定のケースとしての Nui のパフォーマンスと、アプローチとしてサーバー駆動型 UI を使用することにした場合に支払う必要がある追加コストに完全に専念する別の記事を書くことが理にかなっているという考えに至りました。
ただし、少しネタバレします。パフォーマンスを評価するために検討した主なシナリオは 2 つあります。1つは初期レンダリングの時間です。画面全体をサーバー駆動型 UI に実装することにした場合、この画面がアプリケーションのどこかで開かれることになります。
したがって、この画面が非常に重い場合、ネイティブの Flutter 画面でもレンダリングに長い時間がかかるため、このような画面に切り替えると、特にこの遷移にアニメーションが伴う場合は、遅延が目立ちます。 2 番目のシナリオは、動的な UI の変更によるフレーム時間 (FPS)です。 データが変更されたため、一部のコンポーネントを再描画する必要があります。 問題は、これがレンダリング時間にどの程度影響するか、画面が更新されたときにユーザーが遅延を感じるほど影響するかどうかです。 そして、ここでもう 1 つネタバレですが、ほとんどの場合、表示されている画面が完全に Nui で実装されているとはわかりません。 Nui ウィジェットを通常のネイティブ Flutter 画面 (アプリケーションで非常に動的に変化する必要がある画面の一部) に埋め込むと、これを認識できないことが保証されます。 もちろん、パフォーマンスは低下します。 しかし、それらは 120FPS のフレーム レートでも FPS に影響を与えないほどです。つまり、1 フレームの時間が8ms
を超えることはほとんどありません。 これは 2 番目のシナリオに当てはまります。最初の点については、すべて画面の複雑さのレベルに依存します。しかし、ここでも、違いは認識に影響を与えず、アプリケーションがユーザーのスマートフォンのベンチマークになることはありません。
以下は、Pixel 7a (Tensor G2、画面リフレッシュ レートは 90 フレーム (このデバイスの最大)、ビデオ録画レートは 60 フレーム/秒 (録画設定の最大) に設定されています。500 ミリ秒ごとに、リスト内の要素の位置がランダム化され、そのデータから最初の 3 枚のカードが構築され、さらに 500 ミリ秒後に順序ステータスが次のものに切り替わります。これらの画面のうち、完全に Nui で実装されているものを推測できますか?
PS この画面では、どの実装でも、アイコンやブランド ロゴなど、多数の SVG 画像が表示されるため、画像の読み込み時間は実装に依存しません。すべての SVG (および通常の画像) は、ホスティングとして GitHub に保存されるため、読み込みが非常に遅くなる可能性があります。これは、いくつかの実験で確認されています。
ユーチューブ:
Nui を作成する際、私は次のコンセプトに従いました。まず第一に、Flutter 開発者が通常の Flutter アプリケーションを作成するのと同じくらい簡単に使用できるツールを作成する必要があるということです。したがって、すべてのコンポーネントに名前を付ける方法はシンプルで、Flutter での名前と同じ方法で名前を付けるというものでした。
同じことがウィジェット パラメータにも当てはまります。これは、パラメータとしてそれ自体が構成されないString
、 int
、 double
、 enum
などのスカラーです。Nui 内では、これらのタイプのパラメータはargumentsと呼ばれます。また、 Container
ウィジェットのdecoration
などの複雑なクラス パラメータはpropertyと呼ばれます。一部のプロパティは冗長すぎるため、名前が簡略化されているため、このルールは絶対的なものではありません。また、一部のウィジェットでは、使用可能なパラメータのリストが拡張されています。たとえば、正方形のSizedBox
またはContainer
を作成するには、2 つの同一のwidth
+ height
ではなく、1 つのカスタム引数size
のみを渡すことができます。
実装されているウィジェットの完全なリストは提供しません。ウィジェットの数はかなり多いためです (現時点で 53 個)。つまり、原則としてサーバー駆動型 UI Slivers
アプローチとして使用することが理にかなっているほぼすべての UI を実装できます。Slivers に関連付けられた複雑なスクロール効果も含まれます。
また、コンポーネントに関しては、クラウド XML コードを渡す必要があるエントリ ポイントまたはウィジェットに注目する価値があります。現時点では、そのようなウィジェットはNuiListWidget
とNuiStackWidget
2 つあります。
最初のものは、設計上、画面全体を実装する必要がある場合に使用する必要があります。内部的には、元のマークアップ コードから解析されるすべてのウィジェットを含むCustomScrollView
です。さらに、解析は「インテリジェント」であると言えるかもしれません。 CustomScrollView
のコンテンツはslivers
である必要があるため、可能な解決策は、ストリーム内の各ウィジェットをSliverToBoxAdapter
でラップすることですが、これはパフォーマンスに非常に悪影響を及ぼします。したがって、ウィジェットは次のように親に埋め込まれます。最初のウィジェットから始めて、実際のsliver
に出会うまでリストを下っていきます。 sliver
に出会うとすぐに、以前のすべてのウィジェットをSliverList
に追加し、それを親CustomScrollView
に追加します。したがって、 slivers
の数が最小限になるため、UI 全体のレンダリングのパフォーマンスが可能な限り高くなります。 CustomScrollView
にslivers
がたくさんあると悪いのはなぜでしょうか。答えはここにあります。
2 番目のウィジェット ( NuiStackWidget
はフルスクリーンとしても使用できます。この場合、作成したものはすべて同じ順序でStack
に埋め込まれることに注意してください。また、 slivers
明示的に使用する必要があります。つまり、 slivers
のリストが必要な場合は、 CustomScrollView
を追加して、その中にリストを実装する必要があります。
2 番目のシナリオは、ネイティブ コンポーネントに埋め込むことができる小さなウィジェットの実装です。たとえば、サーバーの主導で完全にカスタマイズ可能な製品カードを作成するとします。これは、Nui を使用してコンポーネント ライブラリ内のすべてのコンポーネントを実装し、通常のウィジェットとして使用できる非常に興味深いシナリオです。同時に、アプリケーションを更新せずに完全に変更する機会が常にあります。
NuiListWidget
画面全体ではなくローカル ウィジェットとしても使用できることに注意してください。ただし、このウィジェットの場合は、親ウィジェットの高さを明示的に設定するなど、適切な制限を適用する必要があります。
Flutter を使用して作成されたcounter app
次のようになります。
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), ), ); } }
次に、Nui のみを完全に使用した別の例を示します (ロジックを含む)。
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: {}, ), ), ); } }
強調表示されるように UI コードを分離します。
<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 は Nanc CMS に非常に緊密に統合されています。Nui を使用するために Nanc を使用する必要はありませんが、Nanc を使用すると、同じインタラクティブなドキュメントや、レイアウトの結果をリアルタイムで確認したり、レイアウトで使用されるデータを操作したりできる Playground などの利点が得られます。さらに、CMS の独自のローカル ビルドを作成する必要はなく、公開されているデモで十分に管理でき、必要なことはすべて実行できます。
これを行うには、リンク をたどり、 Page Interface / Screen
フィールドをクリックします。開いた画面はプレイグラウンドとして使用でき、同期ボタンをクリックすると、ソースを含むファイルを介して Nanc を IDE と同期できます。また、ヘルプボタンをクリックすると、すべてのドキュメントを参照できます。
PS これらの複雑さは、Nanc のコンポーネントに関するドキュメントを明示的に別のページを作成する時間がなかったことと、このページへの直接リンクを挿入できなかったために生じています。
XML からウィジェットへの通常のマッパーを作成するのはあまりにも無意味です。もちろん、これも便利ですが、使用例ははるかに少なくなります。同じことではありません - 対話できる完全にインタラクティブなコンポーネントと画面、細かく更新できる (つまり、一度にすべてではなく、更新が必要な部分)。また、この UI にはデータが必要です。これは、Server-Driven UI というフレーズに文字Sが含まれていることを考慮すると、サーバー上のレイアウトに直接置き換えることができますが、より美しく行うこともできます。また、UI が変更されるたびに、バックエンドからレイアウトの新しい部分をドラッグする必要はありません (Nui は、jQuery のベスト プラクティスを flutter にもたらすタイム マシンではありません)。
ロジックから始めましょう。変数と計算式をレイアウトに代入できます。ウィジェットが<container color="{{ page.background }}">
として定義されているとします。ウィジェットは、 background
変数に格納されている「親コンテキスト」に渡されたデータから直接色を抽出します。また、 <aspectRatio ratio="{{ 3 / 4}}">
子孫に対応するアスペクト比の値を設定します。組み込み関数、比較などがあり、それらを使用してロジックを使用して UI を構築できます。
2 つ目のポイントはテンプレートです。 <template id="your_component_name"/>
タグを使用して、UI コード内で直接独自のウィジェットを定義できます。同時に、このテンプレートのすべての内部コンポーネントは、このテンプレートに渡された引数にアクセスできるようになり、カスタム コンポーネントを柔軟にパラメーター化して、 <component id="your_component_name"/>
タグを使用して再利用できるようになります。テンプレート内では、属性だけでなく他のタグ/ウィジェットも渡すことができるため、あらゆる複雑さの再利用可能なコンポーネントを作成できます。
ポイント 3 - 「for ループ」。Nuiには、反復処理を使用して同じ (または複数の) コンポーネントを複数回レンダリングできる組み込みの<for>
タグがあります。これは、ウィジェットのリスト/行/列を作成する必要があるデータのセットがある場合に便利です。
4 番目は条件付きレンダリングです。レイアウト レベルでは、 <show>
タグが実装されています ( <if>
と呼ぶアイデアもありました)。これにより、ネストされたコンポーネントを描画したり、さまざまな条件下でツリーにまったく埋め込まないようにしたりできます。
ポイント 5 - アクション。ユーザーが操作できる一部のコンポーネントは、イベントを送信できます。これは、完全に自由に制御できます。たとえば、 <inkWell onPressed="something">
と宣言すると、このウィジェットはクリック可能になり、アプリケーション、またはEventHandler
がこのイベントを処理して何かを実行できるようになります。ロジックに関連するすべてのものをアプリケーションに直接実装するという考えですが、実装できるものは何でもかまいません。「画面に移動」/「メソッドの呼び出し」/「分析イベントの送信」などのアクションのグループを処理できる汎用ハンドラーをいくつか作成します。動的コードを実装する計画もありますが、微妙な違いがあります。Dart では、任意のコードを実行する方法がありますが、これはパフォーマンスに影響し、さらに、このコードとアプリケーション コードの相互運用性は 100% ではありません。つまり、この動的コードでロジックを作成すると、常にいくつかの制限に遭遇することになります。したがって、このメカニズムを実際に適用可能で便利なものにするには、非常に慎重に検討する必要があります。
6 番目のポイントは、ローカル UI の更新です。これは、 <dataBuilder>
タグのおかげで可能になりました。このタグ (内部の Bloc) は特定のフィールドを「確認」でき、フィールドが変更されると、そのサブツリーを再描画します。
当初、私はデータの 2 つの保存方法 (前述の「親コンテキスト」) を採用しました。また、「データ」も採用しました。これは、 <data>
タグを使用して UI で直接定義できるデータです。正直なところ、データを保存して UI に転送する 2 つの方法を実装する必要があった理由を今では思い出せませんが、そのような決定をしたことを自分自身で厳しく批判することはできません。
これらは次のように動作します - 「親コンテキスト」はMap<String, dynamic>
型のオブジェクトであり、 NuiListWidget
/ NuiStackWidget
ウィジェットに直接渡されます。このデータにはプレフィックスpage
によってアクセスできます。
<someWidget value="{{ page.your.field }}"/>
配列( {{ page.some.array.0.users.35.age }}
を含め、任意のものを任意の深さまで参照できます。そのようなキー/値が存在しない場合は、 null
が返されます。リストは<for>
を使用して反復処理できます。
2 番目の方法 - 「データ」はグローバル データ ストアです。実際には、これはNuiListWidget
/ NuiStackWidget
よりもツリーの上位にある特定のBloc
です。同時に、 DataStorageProvider
を介して独自のDataStorage
インスタンスを渡すことで、ローカル スタイルでの使用を整理することを妨げるものは何もありません。
同時に、最初の方法はリアクティブではありません。つまり、 page
内のデータが変更されても、UI は更新されません。これは、実際にはStatelessWidget
の引数にすぎないためです。 page
のデータ ソースが、たとえばNui...Widget
に値のセットを提供する独自の Bloc である場合、通常のStatelessWidget
と同様に、新しいデータで完全に再描画されます。
データを操作する 2 番目の方法は、リアクティブです。このクラスの API ( updateValue
メソッド) を使用してDataStorage
のデータを変更すると、Bloc クラスのemit
メソッドが呼び出され、UI ( <dataBuilder>
タグ) にこのデータのアクティブなリスナーがある場合は、それに応じてそのコンテンツが変更されますが、UI の残りの部分は変更されません。
したがって、非常にシンプルなpage
と反応性の高いdata
という 2 つのデータ ソースが考えられます。これらのソースでデータを更新するロジックと、これらの更新に対する UI の反応を除けば、これらのデータ ソースの間に違いはありません。
既存のドキュメントのコピーになってしまうため、意図的に作業のニュアンスや側面をすべて説明しませんでした。したがって、試してみたい、またはもっと知りたい場合は、ぜひこちらに来てください。作業の側面が不明瞭だったり、ドキュメントで説明されていないことがあれば、その問題を指摘していただけると嬉しいです。
この記事では取り上げていないが、利用可能な機能のいくつかを簡単に紹介します。
独自のタグ/コンポーネントを作成し、ライブ プレビュー付きの引数やプロパティと同様に、それらに対してまったく同じインタラクティブなドキュメントを作成する機能。たとえば、 SVG 画像をレンダリングするためのコンポーネントはこのように実装されます。すべての人に必要なわけではないので、これをエンジンのコアに押し込む意味はありませんが、1 つの変数を渡すだけで使用できる拡張機能として、簡単でシンプルに使用できます。直接 -実装の例。
独自のアイコンを追加することで拡張できる、組み込みの巨大なアイコン ライブラリ (ここで私は一貫性がなく、「押し付け」になってしまいました。ロジックは、できるだけ多くのアイコンをすぐに使用できるようにし、新しいアイコンを使用するためにアプリケーションを更新する必要がないようにすることでした)。すぐに使用できるアイコンは、 fluentui_system_icons 、 material_design_icons_flutter 、およびremixiconです。NancのPage Interface / Screen -> Icons
を使用して、使用可能なすべてのアイコンを表示できます。
Google Fontsを含むカスタムフォント
XML を JSON/protobuf に変換し、UI の「ソース」として使用する
これらすべてとさらに多くのことがドキュメントで学習できます。
主なことは、ロジックを使用してコードを動的に実行する可能性を実現することです。これは非常にクールな機能であり、Nui の機能を非常に真剣に拡張できます。また、残りのめったに使用されないが、時には非常に重要なウィジェットを標準の Flutter ライブラリから追加できます (追加する必要があります)。XSD をマスターして、すべてのタグの自動補完が IDE に表示されるようにします (このスキームをタグ ドキュメントから直接生成するというアイデアがあります。そうすれば、カスタム ウィジェット用に簡単に作成でき、常に最新の状態になります。また、生成された DSL を Dart で作成して、XML / Json / Protobuf に変換するというアイデアもあります)。さて、追加のパフォーマンス最適化 - 現時点では悪くありませんが、非常に悪くはありませんが、さらに改善して、ネイティブ Flutter にさらに近づけることができます。
私がお伝えしたいのはこれだけです。次の記事では、Nui のパフォーマンス、テスト ケースの作成方法、このプロセスで実行したレーキの数、どのシナリオでどのような数値が得られるかについて詳しく説明します。
Nui を試してみたり、もっと詳しく知りたいと思ったら、ドキュメントデスクにアクセスしてください。また、難しくなければ、 GitHubでスターを付け、 pub.devでいいねを押してください。あなたにとっては難しくないかもしれませんが、この巨大な船で孤独に漕いでいる私にとっては、非常に役立ちます。