tag:crieit.net,2005:https://crieit.net/users/uniyeh/feed uniyehの投稿 - Crieit Crieitでユーザーuniyehによる最近の投稿 2022-01-20T19:00:13+09:00 https://crieit.net/users/uniyeh/feed tag:crieit.net,2005:PublicArticle/17948 2022-01-20T19:00:13+09:00 2022-01-20T19:00:13+09:00 https://crieit.net/posts/Dart-61e932ada423e Dartを使った大規模なアプリ開発 <p>こんにちは、Quireです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>は、Dartで開発された初めてのWebアプリケーションではありませんが、クライアントとサーバーの両方でDartをふんだんに使ったものとしては、初めてかもしれません。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/1a086b990119-20220120.png" alt="" /></p> <p>動作の軽い、徹底して階層構造のタスク管理ツールです。このプロジェクトはDartコードで合計53992行、1620 KB。コミュニティーからのオープンソース ライブラリも使っています。</p> <h1 id="私たちについて"><a href="#%E7%A7%81%E3%81%9F%E3%81%A1%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">私たちについて</a></h1> <p>このプロジェクトを開始するまで、私たちはRikuloというDartファンの集まりでした。Rikuloはこれまで、UIフレームワークやUIライブラリ、Webサーバー、メッセージサーバー、DBクライアントなどのDartライブラリをリリースしています。<br /> Dartが2011年に初めてリリースされたとき、すぐに将来性を感じ、さっそく小規模のプロジェクトに着手。その後、ほとんどDartで書かれた大規模アプリの開発へと進みました。今回はそのときの経験をお話しします。Dartによる開発に興味を持っていただければ幸いです。</p> <h1 id="なぜDartか"><a href="#%E3%81%AA%E3%81%9CDart%E3%81%8B">なぜDartか</a></h1> <p>私たちがDartを選んだ理由はたくさんありますが、以下に大きなものを挙げてみます。<br /> - 強く型付けされていて、無数の小さなミスを防ぐことができる。公式IDEのDart Editorは、タイプミスへの即時フィードバックやオートコンプリート機能を備えており、トラッキングも容易。<br /> - 継承がクラスベースで、プロトタイプベースよりも直感的に使える。<br /> - クライアントとサーバーの両方で同じ言語が使えて、データモデルやコードベースを共有できる。<br /> - JavaScriptの問題点をほぼ解消。完全ではないが、以前にあった問題の99%には対応している。<br /> - JavaScript Harmonyで人気の特徴をいくつか備えている。例えばFuture(Promise)パターン、矢印関数など。<br /> - 強力なチームの後ろ盾があり、ハイクオリティーな公式ライブラリや使いやすいAPIを利用できる。まだ開発途上ながら、仕様はすでにかなり安定している。<br /> - サーバーサイドではマルチスレッドではなくイベントループ方式で、好みだった。<br /> - JavaScriptへのコンパイル時にTree Shakingできる(下記参照)。</p> <h5 id="もちろんデメリットもあります。以下に挙げてみます。"><a href="#%E3%82%82%E3%81%A1%E3%82%8D%E3%82%93%E3%83%87%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%E3%82%82%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82%E4%BB%A5%E4%B8%8B%E3%81%AB%E6%8C%99%E3%81%92%E3%81%A6%E3%81%BF%E3%81%BE%E3%81%99%E3%80%82">もちろんデメリットもあります。以下に挙げてみます。</a></h5> <ul> <li>コミュニティーがJavaScriptよりも圧倒的に小さい。</li> <li>DartとJavaScript間の通信が自明でない。</li> <li>強い型付けなので、APIのPolyfillが面倒。</li> <li>Dart Editorの性能が(今のところ)大規模プロジェクトに適さない。</li> </ul> <p>強い型のサポートにより、書くプロセスがJavaScriptよりもずっとロバストです。また、Javaよりもシンプルで、Vanilla JavaScriptと比べてもシンプルなことがあります(関数の書き方など)。全体的にDartは扱いやすい言語ですが、以下のような例外はあります。</p> <ul> <li>コンストラクタのボディよりも先にイニシャライザでFinalフィールドを初期化する必要がある。</li> <li>ミックスインの仕様が使いにくく、2.0まで改善されそうにない。</li> <li>ジェネリック型パラメータがクラスにしかなく、関数宣言にない。(そのためコンパイラの負荷が増す)。</li> </ul> <h2 id="クライアントサイド"><a href="#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%82%B5%E3%82%A4%E3%83%89">クライアントサイド</a></h2> <p>Dart VMがChromeに統合される日まで、JavaScriptにコンパイルして作成することになります。JavaScriptにコンパイルする言語はたくさんありますが、Dartには以下のような独自のメリットがあります。</p> <ul> <li>私たちは開発時、Dart VMを内蔵したChromiumベースのブラウザー、DartiumでDartをネイティブに実行している。このイテレーションではコンパイルが不要で、スムーズに進めることができる。</li> <li>テストとプロダクションの段階でJavaScriptにコンパイルし、すべてのメジャーなブラウザーで実行する。このとき使用するコンパイラがTree Shakingを行い、不要なコードを除去するため、JavaScriptのコードサイズを小さくできる。</li> </ul> <h3 id="サーバーサイド"><a href="#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%B5%E3%82%A4%E3%83%89">サーバーサイド</a></h3> <p>サーバーサイドについてはあまりコミュニティーで話題になりませんが、私たちは以下の理由から、Dartはサーバーサイドのプログラミング言語に入ると考えています。</p> <ul> <li>Webサービスは本質的に非同期なため、イベントループ方式と相性がいい(マルチスレッドと比べて)。</li> <li>クライアントサイドよりもサーバーサイドのほうが、よりロバストで安全なコードが必要。強い型付けの言語はこの点において有利。</li> </ul> <h1 id="サポートライブラリ"><a href="#%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA">サポートライブラリ</a></h1> <p>Quireは約30のライブラリをインポートしています。そのうち10ライブラリはコミュニティーからで、残りはDartからリリースされたものです。Dartに詳しい方ならAngularDARTとPolymer.dartがあると思うかもしれませんが、実はどちらも使っていません。</p> <h5><a target="_blank" rel="nofollow noopener" href="https://angulardart.org/">AngularDART</a>を使わない理由は、以下のとおりです。</h5> <ul> <li>DOMを細かく制御したい。</li> <li>私たちはクライアント構造の構築に独自のアーキテクチャガイドラインを使用しており、そのパラダイムがAngularのロジックと異なる。</li> <li>AngularDARTについて調べたとき、コンパイルされたJavaScriptのコードサイズにかなりのオーバーヘッドが生じた。ただし、これはその後かなり改善された。</li> </ul> <h5 id="私たちは以下の理由から、Polymer.dartも使っていません。"><a href="#%E7%A7%81%E3%81%9F%E3%81%A1%E3%81%AF%E4%BB%A5%E4%B8%8B%E3%81%AE%E7%90%86%E7%94%B1%E3%81%8B%E3%82%89%E3%80%81Polymer.dart%E3%82%82%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82">私たちは以下の理由から、Polymer.dartも使っていません。</a></h5> <ul> <li>カプセル化とイベント リターゲティングのため、ShadowDOMはBootstrapのようなセレクター指向のフレームワークと併用できない。</li> <li>ShadowDOMの外部からスタイルを適用できない。サードパーティーのコンポーネントセットがPolymerで作成されている場合、ユーザーが見た目や感じを変えることは不可能に近い。(更新:2013年12月時点で、内部のスタイルを外部から変更可能。<a target="_blank" rel="nofollow noopener" href="http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201/#toc-style-cat-hat">Shadow DOM 201</a>を参照。)</li> </ul> <h3 id="DQuery&Bootjack"><a href="#DQuery%EF%BC%86Bootjack">DQuery&Bootjack</a></h3> <p>私たちのクライアントサイド スタックの基盤は、Rikuloリリースのオープンソース プロジェクト、DQueryとBootjackです。<br /> - <a target="_blank" rel="nofollow noopener" href="https://github.com/rikulo/dquery">DQuery</a>はjQueryの一部、特にイベントデリゲートの仕組みの部分をDartに移植したものです。<br /> - <a target="_blank" rel="nofollow noopener" href="https://github.com/rikulo/bootjack">Bootjack</a>はBootstrap 3の完全な移植で、CSS およびAPIとほぼ同じです。</p> <p>このようにアプリケーション スタックを構築して、JavaScriptについて知っていること、できることを活用しています。</p> <h3 id="Stream"><a href="#Stream">Stream</a></h3> <p>私たちのWebサーバーは、Dartのみで書かれ、ルーティング、フィルター、サーバーサイドMVCなどを備えたStreamです。イベントループ方式とシームレスに連携して動作します。リクエストハンドラーも、ノンブロッキングのルーチンをつなげるだけで書くことができ、従来のマルチスレッド方式よりも生産的かつ快適に仕事ができます。私たちは、Webサービスをスケールし、nginxでHTTPSを処理し、Streamにリクエストをデリゲートしたりもしています。このアーキテクチャによりDart VMを個別にSpawn/Despawnでき、サーバーのアップグレードもユーザーの使用を妨げずに実施できます。</p> <h1 id="終わりに"><a href="#%E7%B5%82%E3%82%8F%E3%82%8A%E3%81%AB">終わりに</a></h1> <p>Dartは使い勝手の良い言語です。この記事がDartコミュニティー成長の一助となることを願っています。最後に、Dartで何ができるか興味のある方は、ぜひ<a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>を試してみてください。</p> uniyeh tag:crieit.net,2005:PublicArticle/17924 2022-01-07T16:19:06+09:00 2022-01-11T17:18:59+09:00 https://crieit.net/posts/Flutter-61d7e96a4daa2 Flutterベースのモバイル向けタイムラインの作成 <h1><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>タイムライン(モバイル向け)の構造を初公開</h1> <p>2018年に初めてFlutterベースのアプリを作ったときは、とても楽しく興奮しました。それから3年経ち、<a target="_blank" rel="nofollow noopener" href="https://quire.io">Quireアプリ</a>もかなり充実して、従来のモバイル向けプロジェクト管理アプリの域を超えるまでになりました。Quireモバイルアプリの現行バージョンは、階層表示、ボード表示だけでなく、タイムライン表示にも対応しています。</p> <p>モバイルアプリ向けのタイムライン表示の作成を決めたときは、簡単にできるとは思いませんでした。当時は類似の既成コンポーネントもなかったためですが、驚いたのは、インターネットでタイムライン表示の構造についての情報も見当たらなかったことです。そこで、いちかばちか、自分たちで作ってみることにしました。</p> <h5><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>モバイルアプリ用のタイムラインでは、以下を計画していました。</h5> <ol> <li>横方向への無限日付スクロール</li> <li>レンダリングオンデマンド(ROD)。ビューポートにあるときのみ実行されるWidgetのState</li> <li>任意の位置に素早く配置</li> <li>操作がかんたんで使いやすいインターフェースと、スムーズなユーザーエクスペリエンス</li> </ol> <p>数週間で初期開発が完了し、以下のような構造になりました。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/52c3e5779da7-20220105.png" alt="" /></p> <ol> <li>タイムラインペインのコアベース(週、週末のセクションなど)</li> <li>タスクリスト(階層構造のタスクリスト)</li> <li>タイムラインペインのビューポートベース双方向リスト</li> <li>1ペインのみのとき、2ペインにまたがるときの両方に対応した期間の横棒</li> <li>期間の横棒上の固定ラベル</li> </ol> <p>上図のように、タスクごとにタイムラインペインが割り当てられ、すべてのタイムラインのスクロール位置は互いに同期されます。</p> <h2 id="インデックスベースのスクロールビュー"><a href="#%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%93%E3%83%A5%E3%83%BC">インデックスベースのスクロールビュー</a></h2> <p>Google Flutter Widgetに似たインデックスベースのスクロールビューを作るために、Centerに引数のあるカスタムスクロールビューを使用します。実装すると、任意の位置まで素早くスクロールできるようになります。スクロール中のどの時点でも、各位置とインデックスを表示できます。</p> <p>イメージ的には、少しスクロールした時点で、新しいCenterの引数でタイムラインをリロードしてビューポートの外に移し、またスクロールするとビューポート内に配置される、という感じです。</p> <h2 id="タイムラインペイン"><a href="#%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%A9%E3%82%A4%E3%83%B3%E3%83%9A%E3%82%A4%E3%83%B3">タイムラインペイン</a></h2> <p>タイムラインをスムーズに使えるように、インデックスベースのスクロール表示と似た発想で、横方向にスクロールできるカスタム「無限双方向スクロールビュー」を実現しました。実装すると、タイムラインをなめらかにスクロールできます。</p> <p>無限双方向スクロールビューには、Flutterの強力なViewportの考え方を活用しました。そして、Backwardリストのインデックスを-1から始まる負の数に変更しました。Index 0に当たる日付が分かるようにフラグも設定して、任意の日付まで素早くスクロールできるようにしました。</p> <pre><code class="js">Widget forwardList = SliverList( delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return cellBuilder(context, _getIndex(forward: true, index: index)); }) ); Widget backwardList = SliverList( delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return cellBuilder(context, _getIndex(forward: false, index: index)); }), ); Scrollable( viewportBuilder: (BuildContext context, ViewportOffset offset) { return Viewport( offset: offset, center: forwardListKey, slivers: [ backwardList, forwardList, ] ); }, ) </code></pre> <h2 id="2ペインにまたがるときの問題と解決方法"><a href="#2%E3%83%9A%E3%82%A4%E3%83%B3%E3%81%AB%E3%81%BE%E3%81%9F%E3%81%8C%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E5%95%8F%E9%A1%8C%E3%81%A8%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95">2ペインにまたがるときの問題と解決方法</a></h2> <p>ビューポートには無限のリスト2つがスクロールされるため、期間の横棒が2つのリストにまたがることもあります。そこで、どちらのリストにも完全に同じ期間の横棒を作成し、ぴったり重ねて、リストがビューポートの外に移動してもリスト内のアンカーが壊れないようにしました。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/a56ed0583195-20220105.gif" alt="" /></p> <h2 id="固定ラベルで解決"><a href="#%E5%9B%BA%E5%AE%9A%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A7%E8%A7%A3%E6%B1%BA">固定ラベルで解決</a></h2> <p>モバイル機器の小さい画面ではプロジェクトのどこを見ているかが分かりにくく、使っていると混乱してきます。この問題は、できるだけ多くの情報を提供することで軽減できます。そこで便利なのが固定ラベルです。</p> <p>最初はとにかくシンプルにするため、スクロールビューのスクロール通知に従って、位置を取得してから配置されたラベルに設定していました。固定ラベルを各タイムラインペインの開始位置に表示するには、期間の横棒の現在位置の計算がベースとなります。</p> <p>しかし、新しく配置されたラベルは次のフレームまでしか更新されず、スクロールビューと同じ時間枠で同期されないため、ずれて見えてしまいました。</p> <p>幸いFlutterコミュニティーが、レンダリングレイヤー固定ヘッダーというすばらしい解決方法を教えてくれました。つまりレイアウトのタイミングによる方法です。レンダリングレイヤーにすべてのWidgetをサイズとともに入れるだけでなく、そのピクセルすべてを計算する必要があります。最後にlocalToGlobal関数を、スクロール位置、および2ペインにまたがるときのペイン切り替えに基づいた演算操作と置き換えて、パフォーマンスを向上させました。</p> <h2 id="始まりはこれから"><a href="#%E5%A7%8B%E3%81%BE%E3%82%8A%E3%81%AF%E3%81%93%E3%82%8C%E3%81%8B%E3%82%89">始まりはこれから</a></h2> <p>今は大変な時代ですが、だからこそテクノロジーの分野で貢献したいと考えています。タイムライン表示の作成でまず考えたのは、どうやってFlutterの強力なフレームワークを活用して、ビューコンポーネントを一から作り直すことなく、軽く安定したゴージャスなUIを実現するか、ということでした。</p> <p>各日付単位はインデックスとして、FlutterのSliverに組み込まれています。ほとんどのものはWidgetレイヤーの高レベルの開発概念に留まり、固定ビューのときのみレンダリングレベルに移動します。</p> <p><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quireアプリ</a>をインストールして、Flutterベースのモバイルアプリを使ってみませんか。Quireタイムラインについて気になることは、コメントを投稿するか、<a target="_blank" rel="nofollow noopener" href="https://twitter.com/quire_io">@quire_io</a>でツイートしてお知らせください!</p> <p>※転載許可済み: https://zenn.dev/quireteam/articles/2b2c44c3e49fac</p> uniyeh