<

アダプティブ アプリの構築

概要

Flutter は、次のようなアプリを構築する新しい機会を提供します。 単一のコードベースからモバイル、デスクトップ、Web 上で実行できます。 ただし、これらの機会には新たな課題も伴います。 アプリをユーザーにとって親しみのあるものにしたいと考えている場合、 使いやすさを最大限に高めて各プラットフォームに適応し、 快適でシームレスな体験を保証します。 つまり、単なるものではないアプリを構築する必要があります。 マルチプラットフォームですが、完全にプラットフォームに適応します。

プラットフォームに適応した開発には多くの考慮事項があります アプリですが、次の 3 つの主要なカテゴリに分類されます。

このページでは 3 つのカテゴリすべてを詳しく説明します コード スニペットを使用して概念を説明します。 これらの概念がどのように組み合わされるかを知りたい場合は、 をチェックしてくださいフロックとフォリオ例 ここで説明する概念を使用して構築されました。

アダプティブ アプリ開発テクニックのオリジナル デモ コードは、 flutter適応デモ。

アダプティブ レイアウトの構築

持ち込む際に最初に考慮しなければならないことの 1 つは、 アプリを複数のプラットフォームに適応させる方法は さまざまなサイズや形状のスクリーンに対応します。 それは実行されます。

レイアウトウィジェット

アプリやウェブサイトを構築している場合は、 おそらく、レスポンシブ インターフェイスの作成には精通しているでしょう。 Flutter 開発者にとって幸運なことに、 これを簡単にするためのウィジェットが多数用意されています。

Flutter の最も便利なレイアウト ウィジェットには次のようなものがあります。

一人っ子

  • Align—自分自身の中で子供を調整します。 -1 から 1 までの double 値を受け取ります。 垂直方向と水平方向の両方の配置に使用できます。

  • AspectRatio- サイズを調整しようとします。 子を特定のアスペクト比に調整します。

  • ConstrainedBox- 子にサイズ制約を課します。 最小または最大サイズを制御できます。

  • CustomSingleChildLayout—デリゲート関数を使用します 単一の子を配置します。代表者が決定できるのは、 子のレイアウト制約と配置。

  • ExpandedFlexible— の子供を許可しますRowまたColumn利用可能なスペースを埋めるために縮小または拡大します。

  • FractionallySizedBox- 子のサイズを分数に設定します 利用可能なスペースの。

  • LayoutBuilder- リフロー可能なウィジェットを構築します それ自体は親のサイズに基づいています。

  • SingleChildScrollView- 単一の子にスクロールを追加します。 と一緒によく使われますRowまたColumn

マルチチルド

  • ColumnRow、 とFlex—子供たちを配置します 単一の水平方向または垂直方向の実行で。 両方ColumnRow延長するFlexウィジェット。

  • CustomMultiChildLayout- デリゲート関数を使用して、 レイアウト段階で複数の子を配置します。

  • Flow-に似ているCustomMultiChildLayout、 ただし、実行中に実行されるため、より効率的です。 レイアウト段階ではなくペイント段階。

  • ListViewGridView、 とCustomScrollView- スクロール可能を提供します 子供のリスト。

  • Stack- 複数の子をレイヤー化して配置します のエッジに対してStack。 CSS の位​​置固定と同様に機能します。

  • Table- 古典的なテーブル レイアウト アルゴリズムを使用します。 複数の行と列を組み合わせたその子。

  • Wrap- 子を複数の水平方向に表示します または縦走。

利用可能なウィジェットとコード例をさらに確認するには、を参照してください。レイアウトウィジェット

視覚的な密度

入力デバイスが異なれば、さまざまなレベルの精度が得られます。 そのため、異なるサイズのヒット領域が必要になります。 flutterズVisualDensityクラスを使用すると調整が簡単になります アプリケーション全体にわたるビューの密度、 たとえば、ボタンを大きくすることで (したがってタップしやすくなります) タッチ デバイス上で。

変更するときは、VisualDensityあなたのためのMaterialAppMaterialComponentsそれをサポートするものは密度をアニメーション化します 合わせる。デフォルトでは、水平方向と垂直方向の両方の密度が は 0.0 に設定されていますが、密度は任意の負の値に設定できます。 または必要な正の値。異なるものを切り替えることで、 密度を変更すると、UI を簡単に調整できます。

Adaptive scaffold

カスタムの視覚密度を設定するには、密度を あなたのMaterialAppテーマ:

double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density =
    VisualDensity(horizontal: densityAmt, vertical: densityAmt);
return MaterialApp(
  theme: ThemeData(visualDensity: density),
  home: MainAppScaffold(),
  debugShowCheckedModeBanner: false,
);

使用するにはVisualDensity自分自身の視点の中で、 調べることができます:

VisualDensity density = Theme.of(context).visualDensity;

コンテナは変更に自動的に反応するだけではありません 密度が変化するとアニメーションも発生します。 これによりカスタム コンポーネントが結合され、 内蔵コンポーネントとともに、 アプリ全体でのスムーズな移行効果を実現します。

示されているように、VisualDensity単位のない、 したがって、見方によっては意味が異なる場合があります。 この例では、1 密度単位は 6 ピクセルに相当します。 しかし、これを決めるのは完全にあなたの見解次第です。 ユニットレスなので汎用性が高く、 ほとんどのコンテキストで機能するはずです。

マテリアルコンポーネントは一般的に、 それぞれに約 4 論理ピクセルの値を使用します。 視覚濃度の単位。詳細については、 サポートされているコンポーネントについては、を参照してください。VisualDensityAPI。 一般的な密度原理の詳細については、次を参照してください。 を参照してくださいマテリアル デザイン ガイド。

コンテキストに応じたレイアウト

密度の変更以上の必要があり、それが見つからない場合は、 必要なことを行うウィジェット。さらに追加することができます パラメータを調整し、サイズを計算し、 ウィジェットを交換するか、それに合わせて UI を完全に再構築します 特定のフォームファクター。

画面ベースのブレークポイント

手続き型レイアウトの最も単純な形式では、 画面ベースのブレークポイント。 flutterでは、 これはMediaQueryAPI。 使用するサイズに厳格なルールはありません ここでは、これらは一般的な値です。

class FormFactor {
  static double desktop = 900;
  static double tablet = 600;
  static double handset = 300;
}

ブレークポイントを使用すると、簡単なシステムをセットアップできます デバイスのタイプを確認するには:

ScreenType getFormFactor(BuildContext context) {
  // Use .shortestSide to detect device type regardless of orientation
  double deviceWidth = MediaQuery.of(context).size.shortestSide;
  if (deviceWidth > FormFactor.desktop) return ScreenType.Desktop;
  if (deviceWidth > FormFactor.tablet) return ScreenType.Tablet;
  if (deviceWidth > FormFactor.handset) return ScreenType.Handset;
  return ScreenType.Watch;
}

代わりに、さらに抽象化することもできます そしてそれを小規模から大規模まで定義します。

enum ScreenSize { Small, Normal, Large, ExtraLarge }

ScreenSize getSize(BuildContext context) {
  double deviceWidth = MediaQuery.of(context).size.shortestSide;
  if (deviceWidth > 900) return ScreenSize.ExtraLarge;
  if (deviceWidth > 600) return ScreenSize.Large;
  if (deviceWidth > 300) return ScreenSize.Normal;
  return ScreenSize.Small;
}

画面ベースのブレークポイントは、 アプリ内のトップレベルの決定。のようなものを変更する 視覚的な密度、パディング、またはフォント サイズが最適な場合は、 グローバルベースで定義されています。

画面ベースのブレークポイントを使用して、 最上位のウィジェット ツリー。たとえば、次のように切り替えることができます ユーザーがハンドセットを使用していないときは、垂直レイアウトから水平レイアウトに変更できます。

bool isHandset = MediaQuery.of(context).size.width < 600;
return Flex(
    children: [Text('Foo'), Text('Bar'), Text('Baz')],
    direction: isHandset ? Axis.vertical : Axis.horizontal);

別のウィジェットでは、 一部の子を完全に交換することもできます。

Widget foo = Row(
  children: [
    ...isHandset ? _getHandsetChildren() : _getNormalChildren(),
  ],
);

LayoutBuilder を使用して柔軟性をさらに高める

合計画面サイズを確認することは有益ですが、 全画面ページまたは全体的なレイアウトの決定、 多くの場合、ネストされたサブビューには理想的ではありません。 多くの場合、サブビューには独自の内部ブレークポイントがあります。 レンダリングに使用できるスペースのみを考慮します。

Flutter でこれを処理する最も簡単な方法は、LayoutBuilderクラス。LayoutBuilderを許可します 受信したローカル サイズ制約に応答するウィジェット、 これにより、ウィジェットの汎用性が高まります。 グローバル値に依存します。

前の例は次のように書き直すことができます。LayoutBuilder:

Widget foo = LayoutBuilder(
    builder: (context, constraints) {
  bool useVerticalLayout = constraints.maxWidth < 400;
  return Flex(
    children: [
      Text('Hello'),
      Text('World'),
    ],
    direction: useVerticalLayout ? Axis.vertical : Axis.horizontal,
  );
});

このウィジェットはサイド パネル内で構成できるようになりました。 ダイアログ、または全画面表示でも、 提供されるスペースに合わせてレイアウトを調整します。

デバイスのセグメンテーション

レイアウトを決定したい場合があります 実行している実際のプラットフォームに基づいて、 サイズに関係なく。たとえば、 カスタム タイトル バー。動作を確認する必要がある場合があります。 システム タイプを選択し、タイトル バーのレイアウトを微調整します。 ネイティブのウィンドウ ボタンではカバーされません。

使用しているプラ​​ットフォームの組み合わせを確認するには、 を使用できますPlatformAPI とkIsWeb価値:

bool get isMobileDevice => !kIsWeb && (Platform.isIOS || Platform.isAndroid);
bool get isDesktopDevice =>
    !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
bool get isMobileDeviceOrWeb => kIsWeb || isMobileDevice;
bool get isDesktopDeviceOrWeb => kIsWeb || isDesktopDevice;

PlatformAPI は、Web ビルドからアクセスすることはできません。 例外をスローします。dart.ioパッケージはありません Web ターゲットでサポートされています。結果として、このコードは次のことをチェックします。 Web ファーストの場合、短絡のため、Dart は 決して電話しないでくださいPlatformウェブターゲットについて。

スタイリングのための唯一の信頼できる情報源

おそらく自分の意見を維持するのが容易になるでしょう 値をスタイル設定するための単一の信頼できるソースを作成する場合 パディング、間隔、角の形状、フォント サイズなど。 これは、いくつかのヘルパー クラスを使用して簡単に実行できます。

class Insets {
  static const double xsmall = 3;
  static const double small = 4;
  static const double medium = 5;
  static const double large = 10;
  static const double extraLarge = 20;
  // etc
}

class Fonts {
  static const String raleway = 'Raleway';
  // etc
}

class TextStyles {
  static const TextStyle raleway = const TextStyle(
    fontFamily: Fonts.raleway,
  );
  static TextStyle buttonText1 =
      TextStyle(fontWeight: FontWeight.bold, fontSize: 14);
  static TextStyle buttonText2 =
      TextStyle(fontWeight: FontWeight.normal, fontSize: 11);
  static TextStyle h1 = TextStyle(fontWeight: FontWeight.bold, fontSize: 22);
  static TextStyle h2 = TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
  static late TextStyle body1 = raleway.copyWith(color: Color(0xFF42A5F5));
  // etc
}

これらの定数は、ハードコードされた数値の代わりに使用できます。

return Padding(
  padding: EdgeInsets.all(Insets.small),
  child: Text('Hello!', style: TextStyles.body1),
);

すべてのビューが同じ共有設計システム ルールを参照しているため、 見た目が良くなり、一貫性が増す傾向があります。 特定のプラットフォームの値を変更または調整する エラーが発生しやすいメソッドを使用する代わりに、1 か所で実行できます。 検索して置き換えます。共有ルールを使用すると追加の利点があります 設計面での一貫性の強化を支援します。

表現できるいくつかの一般的なデザイン システム カテゴリ この方法は次のとおりです。

  • アニメーションのタイミング
  • サイズとブレークポイント
  • インセットとパディング
  • コーナー半径
  • ストローク
  • フォントファミリー、サイズ、スタイル

ほとんどのルールと同様に、例外もあります。 アプリ内の他の場所では使用されない 1 回限りの値。 スタイリングルールを乱雑にしてもあまり意味がありません これらの値を使用しますが、次の場合には検討する価値があります。 既存の値から導出する必要があります (たとえば、padding + 1.0)。再利用や重複にも注意する必要があります 同じ意味値の。それらの値はおそらく次のようになります。 グローバル スタイル ルールセットに追加されました。

各フォームファクターの長所に合わせた設計

画面サイズ以外にも時間をかける必要があります 特有の長所と短所を考慮する さまざまなフォームファクタの。必ずしも理想的とは限りません マルチプラットフォーム アプリで同じものを提供できるようにする どこにでも機能性を。それができるかどうかを検討してください 特定の機能に焦点を当てる感覚、 一部のデバイス カテゴリでは、特定の機能を削除することもあります。

たとえば、モバイル デバイスは持ち運びが可能で、カメラが付いています。 ただし、細かいクリエイティブな作業にはあまり適していません。 これを念頭に置くと、コンテンツのキャプチャに重点を置くことができます。 モバイル UI の位置データでタグ付けします。 ただし、コンテンツの整理や操作に重点を置く タブレットまたはデスクトップ UI の場合。

もう 1 つの例は、Web の非常に低い障壁を活用することです。 共有するため。 Web アプリをデプロイしている場合は、 どのディープリンクをサポートするかを決定し、 それらを念頭に置いてナビゲーション ルートを設計します。

ここでの重要なポイントは、それぞれが何を意味するのかを考えることです。 プラットフォームが最大限のパフォーマンスを発揮し、独自の機能があるかどうかを確認する 活用することができます。

デスクトップ ビルド ターゲットを使用して迅速なテストを行う

アダプティブをテストする最も効果的な方法の 1 つ インターフェイスは、デスクトップ ビルド ターゲットを利用することを目的としています。

デスクトップで実行している場合、ウィンドウのサイズを簡単に変更できます アプリの実行中にさまざまな画面サイズをプレビューできます。 これをホット リロードと組み合わせると、 レスポンシブUIの開発。

Adaptive scaffold 2

まずタッチを解決する

優れたタッチ UI の構築は、多くの場合より困難になる場合があります 従来のデスクトップ UI よりも、部分的には、 右クリックなどの入力アクセラレータがないため、 スクロール ホイール、またはキーボード ショートカット。

この課題に取り組む 1 つの方法は、最初に集中することです 優れたタッチ指向の UI 上で。ほとんどのことはまだ実行できます テストでは、反復速度の点でデスクトップ ターゲットを使用します。 ただし、モバイル デバイスに頻繁に切り替えることを忘れないでください。 すべてが正しいと感じられることを確認します。

タッチインターフェースを磨き上げたら、次の調整を行うことができます。 マウス ユーザーの視覚的な密度を調整し、すべてのレイヤーに重ね​​ます。 追加の入力。これらの他の入力に次のようにアプローチします。 アクセラレータ - タスクを高速化する代替手段。 考慮すべき重要なことはユーザーが何を期待しているかです 特定の入力デバイスを使用する場合、 そしてそれをアプリに反映するように努めます。

入力

もちろん、アプリの外観を変えるだけでは十分ではありません。 さまざまなユーザー入力もサポートする必要があります。 マウスとキーボードでは、それらを超えた入力タイプが導入されています スクロール ホイール、右クリック、 ホバー操作、タブの移動、キーボード ショートカット。

スクロールホイール

スクロールウィジェットのようなScrollViewまたListViewデフォルトでスクロール ホイールをサポートしているため、 ほぼすべてのスクロール可能なカスタム ウィジェットが構築されています これらのいずれかを使用すると、それらと同様に機能します。

カスタムのスクロール動作を実装する必要がある場合は、 を使用できますListenerウィジェット。 UI がスクロール ホイールにどのように反応するかをカスタマイズします。

return Listener(
    onPointerSignal: (event) {
      if (event is PointerScrollEvent) print(event.scrollDelta.dy);
    },
    child: ListView());

タブトラバーサルとフォーカスのインタラクション

物理キーボードを持つユーザーは、次の操作ができることを期待しています。 Tab キーを使用してアプリケーションをすばやく移動します。 運動能力や視覚に違いがあるユーザーは、多くの場合、 完全にキーボードナビゲーションです。

タブの操作については、次の 2 つの考慮事項があります。 フォーカスがウィジェットからウィジェットに移動する方法 (トラバーサルと呼ばれます) ウィジェットがフォーカスされているときに表示される視覚的なハイライト。

ボタンやテキストフィールドなどのほとんどの組み込みコンポーネントは、 デフォルトでトラバースとハイライトをサポートします。 含めたい独自のウィジェットがある場合 トラバーサルでは、FocusableActionDetectorウィジェット 独自のコントロールを作成します。機能性を兼ね備えています のActionsShortcutsMouseRegion、 とFocusアクションを定義する検出器を作成するウィジェット とキー バインディング、およびフォーカスを処理するためのコールバックを提供します そしてホバーハイライト。

class _BasicActionDetectorState extends State<BasicActionDetector> {
  bool _hasFocus = false;
  @override
  Widget build(BuildContext context) {
    return FocusableActionDetector(
      onFocusChange: (value) => setState(() => _hasFocus = value),
      actions: <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<Intent>(onInvoke: (intent) {
          print('Enter or Space was pressed!');
          return null;
        }),
      },
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          FlutterLogo(size: 100),
          // Position focus in the negative margin for a cool effect
          if (_hasFocus)
            Positioned(
                left: -4,
                top: -4,
                bottom: -4,
                right: -4,
                child: _roundedBorder())
        ],
      ),
    );
  }
}

走査順序の制御

順序をより詳細に制御するには、 ウィジェットはユーザーがタブを押したときにフォーカスされます。 使用できますFocusTraversalGroupセクションを定義するには タブ移動時にグループとして扱われるツリーの。

たとえば、次のすべてのフィールドをタブで移動することができます。 送信ボタンにタブ移動する前のフォーム:

return Column(children: [
  FocusTraversalGroup(
    child: MyFormWithMultipleColumnsAndRows(),
  ),
  SubmitButton(),
]);

Flutter には、ウィジェットとグループを横断するためのいくつかの組み込み方法が備わっています。 デフォルトでReadingOrderTraversalPolicyクラス。 このクラスは通常はうまく機能しますが、これを変更することも可能です 別の事前定義されたものを使用するTraversalPolicyクラスまたは作成によって カスタムポリシー。

キーボードアクセラレータ

タブトラバーサルに加えて、デスクトップユーザーとWebユーザーは慣れています 特定のアクションにバインドされたさまざまなキーボード ショートカットがあること。 それがDeleteキーを押して簡単に削除するか、Control+N新しいドキュメントの場合は、異なる点を考慮してください。 ユーザーが期待するアクセラレータ。キーボードは強力です 入力ツールを最大限に活用して効率を高めるようにしてください。 ユーザーはきっと喜んでくれるはずです。

キーボード アクセラレータは、Flutter のいくつかの方法で実現できます。 あなたの目標に応じて。

次のような単一のウィジェットがある場合、TextFieldまたはButtonそれか すでにフォーカス ノードがある場合は、それをラップすることができます。RawKeyboardListenerそしてキーボード イベントをリッスンします。

  @override
  Widget build(BuildContext context) {
    return Focus(
      onKey: (node, event) {
        if (event is RawKeyDownEvent) {
          print(event.logicalKey);
        }
        return KeyEventResult.ignored;
      },
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: 400),
        child: TextField(
          decoration: InputDecoration(
            border: OutlineInputBorder(),
          ),
        ),
      ),
    );
  }
}

キーボード ショートカットのセットを適用したい場合は、 ツリーの大部分を使用できます。Shortcutsウィジェット:

// Define a class for each type of shortcut action you want
class CreateNewItemIntent extends Intent {
  const CreateNewItemIntent();
}

Widget build(BuildContext context) {
  return Shortcuts(
    // Bind intents to key combinations
    shortcuts: <ShortcutActivator, Intent>{
      SingleActivator(LogicalKeyboardKey.keyN, control: true):
          CreateNewItemIntent(),
    },
    child: Actions(
      // Bind intents to an actual method in your code
      actions: <Type, Action<Intent>>{
        CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
            onInvoke: (intent) => _createNewItem()),
      },
      // Your sub-tree must be wrapped in a focusNode, so it can take focus.
      child: Focus(
        autofocus: true,
        child: Container(),
      ),
    ),
  );
}

Shortcutsウィジェットが便利なのは、 このウィジェット ツリーの作成時にショートカットを起動できるようにします または、その子のいずれかにフォーカスがあり、表示されます。

最後のオプションはグローバル リスナーです。このリスナー 常時オンのアプリ全体のショートカットまたは 表示されているときはいつでもショートカットを受け入れることができるパネル (フォーカス状態に関係なく)。グローバルリスナーの追加 で簡単ですRawKeyboard:

void initState() {
  super.initState();
  RawKeyboard.instance.addListener(_handleKey);
}

@override
void dispose() {
  RawKeyboard.instance.removeListener(_handleKey);
  super.dispose();
}

グローバル リスナーでキーの組み合わせを確認するには、 を使用できますRawKeyboard.instance.keysPressed地図。 たとえば、次のようなメソッドは、 提供されたキーのうちのいくつかが押されています:

static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
  return keys.intersection(RawKeyboard.instance.keysPressed).isNotEmpty;
}

この 2 つのことをまとめると、 アクションを起動できるのは次の場合ですShift+Nが押されています:

void _handleKey(event) {
  if (event is RawKeyDownEvent) {
    bool isShiftDown = isKeyDown({
      LogicalKeyboardKey.shiftLeft,
      LogicalKeyboardKey.shiftRight,
    });
    if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
      _createNewItem();
    }
  }
}

静的リスナーを使用する場合の注意点が 1 つあります。 ユーザーが次の場合にこれを無効にする必要があることがよくあります。 フィールドに入力しているとき、または関連付けられているウィジェットを入力しているとき 視界から隠されています。 と違ってShortcutsまたRawKeyboardListener、 これを管理するのはあなたの責任です。これは特に可能性があります 削除/バックスペース アクセラレータをバインドする場合に重要です。Delete、しかしその後子供を産みますTextFieldsユーザーが 入力しているかもしれません。

マウスの入力、終了、ホバー

デスクトップではマウス カーソルを変更するのが一般的です コンテンツに関する機能を示すため、 マウスがホバリングしています。たとえば、よく見るのは、 ボタンの上にマウスを置くとハンド カーソルが表示されます。 またはIテキストの上にマウスを置くとカーソルが表示されます。

マテリアル コンポーネント セットにはサポートが組み込まれています 標準のボタンとテキスト カーソル用。 独自のウィジェット内からカーソルを変更するには、 使用MouseRegion:

// Show hand cursor
return MouseRegion(
  cursor: SystemMouseCursors.click,
  // Request focus when clicked
  child: GestureDetector(
    onTap: () {
      Focus.of(context).requestFocus();
      _submit();
    },
    child: Logo(showBorder: hasFocus),
  ),
);

MouseRegionカスタムの作成にも役立ちます ロールオーバーおよびホバー効果:

return MouseRegion(
  onEnter: (_) => setState(() => _isMouseOver = true),
  onExit: (_) => setState(() => _isMouseOver = false),
  onHover: (e) => print(e.localPosition),
  child: Container(
    height: 500,
    color: _isMouseOver ? Colors.blue : Colors.black,
  ),
);

イディオムと規範

アダプティブ アプリに関して考慮すべき最後の領域は、プラットフォーム標準です。 各プラットフォームには独自の慣用句や規範があります。 これらの名目または事実上の標準は、ユーザーの期待を伝えるものです アプリケーションがどのように動作すべきかについて。ウェブのおかげもあって、 ユーザーはよりカスタマイズされたエクスペリエンスに慣れており、 しかし、これらのプラットフォーム標準を反映しても、 大きなメリット:

  • 認知的負荷を軽減する—ユーザーのニーズに合わせて 既存のメンタルモデルにより、タスクの達成が直感的になり、 考える必要が少なくて済むので、 生産性が向上し、フラストレーションが軽減されます。

  • 信頼を築く—ユーザーは警戒したり疑念を抱いたりする可能性があります アプリケーションが期待どおりに動作しないとき。 逆に、親しみやすい UI はユーザーの信頼を築くことができます。 品質に対する認識を向上させることができます。 これには多くの場合、アプリ ストアが向上するという追加の利点もあります 評価 - 誰もが評価できるものです。

各プラットフォームで予想される動作を考慮する

最初のステップは、時間をかけて何を考えるかです。 期待される外観、プレゼンテーション、または動作はこのプラットフォーム上にあります。 現在の実装の制限を忘れるようにしてください。 理想的なユーザー エクスペリエンスを思い描いてください。 そこから逆算して作業します。

これについて考える別の方法は、次のように尋ねることです。 「このプラットフォームのユーザーは、この目標をどのように達成すると予想しますか?」 次に、それがアプリでどのように機能するかを想像してみてください。 一切の妥協をせずに。

プラットフォームを定期的に利用していない場合、これは難しいかもしれません。 特定のイディオムを意識していないため、簡単に見逃してしまう可能性があります それらは完全に。たとえば、生涯 Android ユーザーは、 iOS のプラットフォーム規約を知らない可能性が高く、 同じことが macOS、Linux、Windows にも当てはまります。 これらの違いはあなたにとっては微妙かもしれませんが、 しかし、経験豊富なユーザーにとっては痛いほど明らかです。

プラットフォームの擁護者を探す

可能であれば、各プラットフォームの支持者として誰かを割り当てます。 理想的には、支持者はプラットフォームをプライマリ デバイスとして使用します。 そして、自分の意見をしっかり持ったユーザーの視点を提供することができます。 人数を減らすには、役割を組み合わせます。 Windows と Android の支持者を 1 人抱え、 1 つは Linux と Web 用、もう 1 つは Mac と iOS 用です。

目標は、情報に基づいたフィードバックを継続的に得ることです。 どのプラットフォームでも快適です。擁護者は奨励されるべきである かなりうるさく、違うと感じたものは何でも指摘する デバイス上の典型的なアプリケーション。簡単な例は次のとおりです ダイアログのデフォルトのボタンは通常、Mac では左側にあります と Linux ですが、Windows では右側にあります。 プラットフォームを使用していない場合、このような詳細は見逃しがちです 定期的に。

ユニークであり続ける

アプリが期待される動作に準拠しているという意味ではありません デフォルトのコンポーネントまたはスタイルを使用する必要があります。 最も人気のあるマルチプラットフォーム アプリの多くは、非常に特徴的な機能を持っています。 カスタム ボタン、コンテキスト メニュー、 そしてタイトルバー。

プラットフォーム間でスタイルと動作を統合できるほど、 開発とテストが容易になります。 秘訣は、ユニークな体験を生み出すことと、 各プラットフォームの規範を尊重しながら、強力なアイデンティティを実現します。

考慮すべき一般的な慣用句と規範

いくつかの具体的な規範と慣用句を簡単に見てみましょう 検討してみてはいかがでしょうか、どのようにアプローチできるでしょうか Flutterでそれらを実行します。

スクロールバーの外観と動作

デスクトップとモバイルのユーザーはスクロールバーを期待していますが、 しかし、プラットフォームが異なれば動作も異なることが予想されます。 モバイル ユーザーは、表示されるだけの小さなスクロールバーを期待しています。 デスクトップユーザーは一般にスクロール中を期待していますが、 クリックまたはドラッグできる大きなスクロールバーが遍在します。

Flutter が組み込まれていますScrollbarすでにあるウィジェット に応じて適応可能な色とサイズをサポートしています。 現在のプラットフォーム。微調整を 1 つ行うとよいでしょう。 トグルalwaysShownデスクトップ プラットフォームの場合:

return Scrollbar(
  thumbVisibility: DeviceType.isDesktop,
  controller: _scrollController,
  child: GridView.count(
      controller: _scrollController,
      padding: EdgeInsets.all(Insets.extraLarge),
      childAspectRatio: 1,
      crossAxisCount: colCount,
      children: listChildren),
);

この細部への微妙な配慮により、アプリの雰囲気がさらに良くなります。 特定のプラットフォーム上で快適です。

複数選択

リスト内の複数選択の処理は別の領域です プラットフォームごとに微妙な違いがあります。

static bool get isSpanSelectModifierDown =>
    isKeyDown({LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight});

コントロールまたはコマンドのプラットフォーム対応チェックを実行するには、 次のように書くことができます:

static bool get isMultiSelectModifierDown {
  bool isDown = false;
  if (Platform.isMacOS) {
    isDown = isKeyDown(
        {LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight});
  } else {
    isDown = isKeyDown(
        {LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight});
  }
  return isDown;
}

キーボード ユーザーにとっての最後の考慮事項は、すべて選択アクション。 選択可能な項目のリストが多数ある場合は、 キーボード ユーザーの多くは、次のことができることを期待するでしょう。Control+Aすべての項目を選択します。

タッチデバイス

タッチ デバイスでは、通常、複数選択が簡素化されます。 期待される動作は、isMultiSelectModifierデスクトップ上にあります。 シングルタップで項目を選択または選択解除できます。 通常はボタンがありますすべて選択またクリア現在の選択内容。

さまざまなデバイスで複数選択をどのように処理するかによって異なります 特定の使用例に基づいて説明しますが、重要なことは、 各プラットフォームに最適なものを提供していることを確認する インタラクションモデルが可能です。

選択可能なテキスト

Web (および程度は低いですがデスクトップ) での一般的な期待 それは、最も目に見えるテキストをマウス カーソルで選択できることです。 文字が選択できない場合は、 ウェブ上のユーザーは拒否反応を示す傾向があります。

幸いなことに、これは次の方法で簡単にサポートできます。SelectableTextウィジェット:

return SelectableText('Select me!');

リッチテキストをサポートするには、次を使用しますTextSpan:

return SelectableText.rich(
  TextSpan(
    children: [
      TextSpan(text: 'Hello'),
      TextSpan(text: 'Bold', style: TextStyle(fontWeight: FontWeight.bold)),
    ],
  ),
);

タイトルバー

最新のデスクトップ アプリケーションでは、カスタマイズするのが一般的です アプリ ウィンドウのタイトル バーにロゴを追加する より強力なブランディングや状況に応じたコントロールでコストを節約 メイン UI の垂直方向のスペース。

Samples of title bars

これは Flutter では直接サポートされていませんが、bits_dojoネイティブのタイトルバーを無効にするパッケージ、 それらを自分のものに置き換えてください。

このパッケージを使用すると、必要なウィジェットをTitleBar内部では純粋な Flutter ウィジェットを使用しているためです。 これにより、ナビゲーション中にタイトル バーを簡単に調整できるようになります。 アプリのさまざまなセクションに移動します。

コンテキスト メニューとツールチップ

デスクトップでは、いくつかのインタラクションがあります。 オーバーレイに表示されるウィジェットとしてマニフェストします。 ただし、トリガーの仕方、解除の仕方が異なります。 そして配置:

  • コンテキストメニュー- 通常は右クリックによってトリガーされます。 コンテキスト メニューがマウスの近くに配置され、 どこかをクリックすると閉じられますが、 メニューからオプションを選択するか、メニューの外側をクリックします。

  • ツールチップ- 通常、ホバリングによってトリガーされます。 インタラクティブ要素で 200 ~ 400 ミリ秒、 ツールチップは通常ウィジェットに固定されています (マウスの位置とは対照的に) 閉じられます マウス カーソルがそのウィジェットから離れたとき。

  • ポップアップ パネル (フライアウトとも呼ばれます)—ツールチップと同様に、 ポップアップ パネルは通常、ウィジェットに固定されています。 主な違いは、パネルがほとんどの場合、 タップイベントで表示され、通常は非表示になりません カーソルが離れると、それら自体が表示されます。 代わりに、パネルは通常、 をクリックすると閉じられます。 パネルの外側、またはaを押す近いまた送信ボタン。

Flutter で基本的なツールチップを表示するには、 内蔵のものを使用するTooltipウィジェット:

return const Tooltip(
  message: 'I am a Tooltip',
  child: Text('Hover over the text to show a tooltip.'),
);

Flutter は編集時に組み込みのコンテキスト メニューも提供します またはテキストを選択します。

より高度なツールチップ、ポップアップ パネルを表示するには、 またはカスタム コンテキスト メニューを作成します。 利用可能なパッケージのいずれかを使用するか、 または、StackまたOverlay

利用可能なパッケージには次のものがあります。

  • context_menus
  • anchored_popups
  • flutter_portal
  • super_tooltip
  • custom_pop_up_menu

これらのコントロールはタッチ ユーザーにとってアクセラレータとして役立ちますが、 これらはマウスユーザーにとって不可欠です。これらのユーザーが期待しているのは、 右クリックしてコンテンツをその場で編集し、 カーソルを合わせると詳細情報が表示されます。その期待に応えられない ユーザーを失望させる可能性があります。少なくとも、 何かが正しくないという感覚。

横方向のボタンの順序

Windows では、ボタンの列を表示するときに、 確認ボタンは最初に配置されます 列(左側)。他のすべてのプラットフォームでは、 それは逆です。確認ボタンは、 列の最後(右側)に配置されます。

これは、Flutter で次を使用して簡単に処理できます。TextDirectionのプロパティRow:

TextDirection btnDirection =
    DeviceType.isWindows ? TextDirection.rtl : TextDirection.ltr;
return Row(
  children: [
    Spacer(),
    Row(
      textDirection: btnDirection,
      children: [
        DialogButton(
            label: 'Cancel',
            onPressed: () => Navigator.pop(context, false)),
        DialogButton(
            label: 'Ok', onPressed: () => Navigator.pop(context, true)),
      ],
    ),
  ],
);

Sample of embedded image

Sample of embedded image

デスクトップ アプリのもう 1 つの一般的なパターンはメニュー バーです。 Windows と Linux では、このメニューは Chrome のタイトル バーの一部として表示されます。 一方、macOS では、主画面の上部に沿って配置されます。

現在、次を使用してカスタム メニュー バー エントリを指定できます。 プロトタイプのプラグインですが、この機能は 最終的にはメイン SDK に統合される予定です。

Windows と Linux では、 カスタム タイトル バーとメニュー バーを組み合わせることはできません。 カスタム タイトル バーを作成すると、 ネイティブのものを完全に置き換えることになります。 つまり、統合されたネイティブ メニュー バーも失われます。

カスタム タイトル バーとメニュー バーの両方が必要な場合は、 Flutter で実装することでそれを実現できます。 カスタム コンテキスト メニューに似ています。

ドラッグアンドドロップ

タッチベースとタッチベースの両方の中核となるインタラクションの 1 つ ポインターベースの入力はドラッグ アンド ドロップです。これですが 両方のタイプの入力で相互作用が期待されます。 考慮すべき重要な違いがある場合 ドラッグ可能な項目のリストをスクロールすることになります。

一般的に、タッチ ユーザーはドラッグ ハンドルが表示されることを期待しています。 ドラッグ可能な領域とスクロール可能な領域を区別するため、 または、長いボタンを使用してドラッグを開始します。 ジェスチャーを押します。これはスクロールとドラッグによるものです 両方とも入力用に 1 本の指を共有しています。

マウス ユーザーには、より多くの入力オプションがあります。彼らは車輪を使うことができる またはスクロールバーを使用してスクロールします。これにより、通常は必要がなくなります 専用のドラッグハンドル用。 macOSを見てみると Finder または Windows Explorer が機能することがわかります。 この方法では、項目を選択してドラッグを開始するだけです。

Flutter では、ドラッグ アンド ドロップを多数実装できます。 方法。特定の実装についての議論は外部です この記事の範囲ですが、いくつかの高レベルのオプション それは:

  • 使用DraggableDragTargetAPI カスタムのルックアンドフィールを直接実現します。

  • 引っ掛けるonPanジェスチャーイベント、 親内でオブジェクトを自分で移動しますStack

  • いずれかを使用してください既製のリストパッケージpub.dev で。

ユーザビリティの基本原則を理解する

もちろん、このページは完全なリストを構成するものではありません あなたが検討するかもしれない事柄のうち。オペレーティング システムが増えるほど、 サポートするフォーム ファクターと入力デバイス、 設計におけるすべての順列を特定することがより困難になります。

時間をかけて基本的なユーザビリティ原則を学び、 開発者はより良い意思決定を行えるようにします。 生産中に設計を行ったり来たりする繰り返しを削減し、 その結果、生産性が向上し、より良い成果が得られます。

開始するためのリソースをいくつか紹介します。

  • レイアウト適用に関するマテリアルのガイドライン
  • 大画面向けのマテリアルデザイン
  • 正規レイアウトに関するマテリアルのガイドライン
  • 高品質のアプリを構築する (Android)
  • UI デザインですべきこと、してはいけないこと (Apple)
  • ヒューマンインターフェースガイドライン(Apple)
  • レスポンシブ デザイン手法 (Microsoft)
  • マシンのサイズとブレークポイント (Microsoft)