Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

気をつけたい!Desktop対応で陥りやすい罠とその対策

GOTO-TSL
November 22, 2024

 気をつけたい!Desktop対応で陥りやすい罠とその対策

FlutterKaigi 2024 DAY2 13:30~

概要
Mobile向けに開発したものをWebや他のDesktopプラットフォームで表示すると、「なんか違う?」と感じたことはありませんか?
本セッションでは、MobileとDesktopをワンソースで開発する中で遭遇した実際の事例を基に、Desktopプラットフォームでの開発時に特に注意が必要なポイントを詳しく解説します。

•MobileとDesktopプラットフォームの違いについての概要
•プロジェクトで遭遇した具体的な事例の紹介
•注意が必要なWidgetと、それぞれの対応策
•プラットフォームごとの差分を考慮した開発手法の提案

想定視聴者
•MobileアプリをDesktopプラットフォームに展開しようと考えている方
•Desktopプラットフォームでの開発に興味がある方
•ワンソースでの複数プラットフォーム対応に挑戦している方
•Desktopプラットフォームの開発経験が少ない方

GOTO-TSL

November 22, 2024
Tweet

Other Decks in Programming

Transcript

  1. 目次 • 入力方法について ◦ 文字数制限をオーバーしても入力できてしまう ◦ 横スクロールができない • 画面レイアウトについて ◦

    BottomNavigationBarだと見栄えが悪い ◦ 左右の余白が足りずコンテンツが見づらい ◦ Gridのアイテムが大きく表示されてしまう • Web特有の課題 ◦ iOS/Safariブラウザでの問題 ▪ autofocusでキーボードが表示されない ▪ スワイプバック時に余分なページが表示される ◦ パッケージ周りの対応 ▪ GoogleSignIn ▪ FirebaseCrashlytics ▪ Drift ◦ 絵文字がうまく表示されない • その他 ◦ Widgetのデフォルト表示が違う • まとめ 9
  2. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 10
  3. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 13
  4. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 23
  5. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 31
  6. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 32
  7. 画面サイズに応じてNavigationを切り替える - M3のWindow class(width)を 基準に分岐 - NavigationBarと NavigationRailを出し分け - NavigationRailのextendedと

    labelTypeの変更 40 Scaffold( body: Row( children: [ if (screenSize != ScreenSize.compact) NavigationRail( extended: screenSize == ScreenSize.expanded labelType: (screenSize == ScreenSize.medium) ? NavigationRailLabelType.none : null, ), Expanded( child: child, ), ], ), bottomNavigationBar: screenSize == ScreenSize.compact ? NavigationBar(...) : null, ), https://m3.material.io/foundations/layout/applying-layout/window-size-classes#4ee6b7bb-864a-440c-9903-f738cc94fd58 BottomNavigationBarだと見栄えが悪い問題
  8. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 42 → デザイン要件周りの問題 - NavigationRail最下部への要素配置 - サイドメニューの実装
  9. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 43 → デザイン要件周りの問題 - NavigationRail最下部への要素配置 - サイドメニューの実装
  10. 思ってたのと違う 48 理想 現実 NavigationRail( selectedIndex: selectedIndex, destinations: [...], onDestinationSelected:

    (value) {...}, trailing: Row( children: [ IconButton( onPressed: () {}, icon: const Icon(Icons.account_circle), ), const SizedBox( width: 8, ), const Text('プロフィール '), ], ), ); NavigationRail最下部への要素配置問題
  11. 思ってたのと違う 49 理想 現実 NavigationRail( selectedIndex: selectedIndex, destinations: [...], onDestinationSelected:

    (value) {...}, trailing: Row( children: [ IconButton( onPressed: () {}, icon: const Icon(Icons.account_circle), ), const SizedBox( width: 8, ), const Text('プロフィール '), ], ), ); NavigationRail最下部への要素配置問題
  12. 最下部に配置するには... 50 NavigationRail最下部への要素配置問題 NavigationRail ( selectedIndex: selectedIndex , destinations: [...],

    onDestinationSelected: (value) {...}, trailing: Expanded( child: Container( color: Colors.orangeAccent, alignment: Alignment.bottomCenter, width: 256 - 16 * 2, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () {}, icon: const Icon(Icons.account_circle), ), const SizedBox( width: 16, ), const Text('プロフィール'), ], ), ), ), ); ←Expandedで伸ばして ←幅を広げて
  13. 最下部に配置するには... 51 NavigationRail最下部への要素配置問題 NavigationRail ( selectedIndex: selectedIndex , destinations: [...],

    onDestinationSelected: (value) {...}, trailing: Expanded( child: Container( color: Colors.orangeAccent, alignment: Alignment.bottomCenter, width: 256 - 16 * 2, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () {}, icon: const Icon(Icons.account_circle), ), const SizedBox( width: 16, ), const Text('プロフィール'), ], ), ), ), ); ←ここも可変にする必要がある
  14. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 54 → デザイン要件周りの問題 - NavigationRail最下部への要素配置 - サイドメニューの実装
  15. Drawerが出入りする際にNavigationRailに重なってしまう - Stackを使って無理やり解決できなくは無いが、 コードが複雑化してしまう 62 サイドメニューの実装問題 Stack( children: [ Row(

    children: [ switch (screenSize) { ScreenSize.compact => const SizedBox.shrink(), ScreenSize.medium => const SizedBox( width: 80, ), ScreenSize.expanded => const SizedBox( width: 256, ), }, Expanded( child: Scaffold( key: homeScaffoldKey, drawer: const HomeDrawer(), drawerEdgeDragWidth: 0, body: child, bottomNavigationBar: screenSize == ScreenSize.compact ? HomeNavigationBar( currentPage: currentPage, ) : null, ), ), ], ), if (screenSize != ScreenSize.compact) Row( children: [ HomeNavigationRail( currentPage: currentPage, ), const Spacer(), ], ), ], ) ←NavigationRailの幅分スペースを開ける必要
  16. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 66
  17. 左右の余白 69 左右の余白が足りずコンテンツが見づらい問題 class CreateStudyRecordScreen extends StatelessWidget { const CreateStudyRecordScreen({super.key});

    @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(...), body: Padding( padding: EdgeInsets.symmetric( horizontal: 16.0, ), child: ..., ), ); } }
  18. 左右の余白 70 左右の余白が足りずコンテンツが見づらい問題 class CreateStudyRecordScreen extends StatelessWidget { const CreateStudyRecordScreen({

    super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(...), body: Padding( padding: EdgeInsets.symmetric( horizontal: 16.0, ), child: ..., ), ); } }
  19. Material Designのbreakpointを元に値を分岐 74 左右の余白が足りずコンテンツが見づらい問題 enum BreakpointScreenSize { /// 0~599 extraSmall,

    /// 600~904 smallScaleBody , /// 905~1239 smallFixBody , /// 1240~1439 medium, /// 1440~ large, } static double margin(double width) { final size = screenSize(width); switch (size) { case BreakpointScreenSize. extraSmall: return 16; case BreakpointScreenSize. smallScaleBody : return 32; case BreakpointScreenSize. smallFixBody : return (width - 840) / 2; case BreakpointScreenSize. medium: return 200; case BreakpointScreenSize. large: return (width - 1040) / 2; } } https://github.com/koji-1009/breakpoints_mq/blob/2.1.2/lib/src/ext/breakpoints_enum_ext.dart ※弊社ではMaterial Design2のバージョンを利用
  20. breakpointMarginの設定 75 左右の余白が足りずコンテンツが見づらい問題 class CreateStudyRecordScreen extends StatelessWidget { const CreateStudyRecordScreen({

    super.key}); @override Widget build(BuildContext context) { final margin = MediaQuery.of(context).breakpointMargin; return Scaffold( appBar: AppBar(...), body: Padding( padding: EdgeInsets.symmetric( horizontal: margin, ), child: ..., ), ); } }
  21. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字やフォントがうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 79
  22. アイテムがグリッド状に並ぶ場合 82 GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, ), itemCount:

    bookshelfMaterials.length, itemBuilder: (context, index) { return BookshelfItem(...); } ) Gridのアイテムが大きく表示されてしまう問題
  23. 列数を画面幅に応じて可変に 85 const materialWidth = 150; extension BoxConstraintsExt on BoxConstraints

    { int get crossAxisCount { return maxWidth ~/ materialWidth; } } ... LayoutBuilder( builder: (context, constraints) { return GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: constraints.crossAxisCount, ), itemCount: bookshelfMaterials.length, itemBuilder: (context, index) { return BookshelfItem(...); } } Gridのアイテムが大きく表示されてしまう問題
  24. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字がうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 93
  25. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字がうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 94
  26. WebかつiOSの場合のみautofocusをfalseに設定する final autoFocus = !(kIsWeb && defaultTargetPlatform == TargetPlatform.iOS); …

    TextField( …, autofocus: autoFocus, ), 100 autofocus時にキーボードが表示されない問題
  27. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字がうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 107
  28. GoogleSignInボタン(Mobile) ボタンのUI - flutter_signin_buttonのSignInButton - https://pub.dev/packages/flutter_signin_button 109 パッケージ周りの対応 >GoogleSignIn SignInButton(

    Buttons.Google, text: text, onPressed: () async { final googleSignIn = GoogleSignIn( scopes: [ 'email', ], clientId: googleClientId , ); if (await googleSignIn.isSignedIn()) { await googleSignIn.signOut(); } final user = await googleSignIn.signIn(); if (user == null) { return; } final googleAuthentication = await user.authentication ; final idToken = googleAuthentication. idToken; ..., } )
  29. GoogleSignInボタン(Mobile) 110 パッケージ周りの対応 >GoogleSignIn SignInButton( Buttons.Google, text: text, onPressed: ()

    async { final googleSignIn = GoogleSignIn( scopes: [ 'email', ], clientId: googleClientId , ); if (await googleSignIn.isSignedIn()) { await googleSignIn.signOut(); } final user = await googleSignIn.signIn(); if (user == null) { return; } final googleAuthentication = await user.authentication ; final idToken = googleAuthentication. idToken; ..., } ) サインインフローの開始 - google_sign_inのsignIn()メソッド - https://pub.dev/packages/google_sign_in
  30. 代わりにrenderButton()を使う 113 パッケージ周りの対応 >GoogleSignIn class SignInWithGoogleButton extends HookConsumerWidget { const

    SignInWithGoogleButton({ super.key, ..., }); ... @override Widget build(BuildContext context, WidgetRef ref) { ... return (GoogleSignInPlatform.instance as GoogleSignInPlugin).renderButton(); } } renderButton(): https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart#L188
  31. サインインフロー完了後のユーザー情報取得 signIn()メソッドの場合 →戻り値として取得 114 final user = await googleSignIn.signIn(); if

    (user == null) { return; } final googleAuthentication = await user.authentication; final idToken = googleAuthentication.idToken; ..., パッケージ周りの対応 >GoogleSignIn
  32. サインインフロー完了後のユーザー情報取得 renderButton()の場合 →GoogleSignIn.onCurrentUserChangedのStreamを監視 115 ref .watch(googleSignInProvider(googleClientId: googleClientId)) .onCurrentUserChanged .listen( (GoogleSignInAccount?

    user) async { if (user == null) { return; } final googleAuthentication = await user.authentication; final idToken = googleAuthentication.idToken; ..., } パッケージ周りの対応 >GoogleSignIn ※googleSignInProviderはGoogleSingInクラスのインスタンスを公開
  33. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字がうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 125
  34. fallbackFontsを設定 - 表示に失敗した場合のフォントを定 義 - TextStyleのfontFamilyFallbackに 設定 final _fallbackFonts =

    [ GoogleFonts.notoColorEmoji().fontFamily ?? '', ]; ... // themeは任意のTextTheme theme.copyWith( displayLarge: theme.displayLarge?.copyWith( fontFamilyFallback: _fallbackFonts, ), displayMedium: theme.displayMedium?.copyWith( fontFamilyFallback: _fallbackFonts, ), ..., ); 129 絵文字がうまく表示されない問題
  35. 目次 - 入力方法について - 文字数制限をオーバーしても入力できてしまう - 横スクロールができない - 画面レイアウトについて -

    BottomNavigationBarだと見栄えが悪い - 左右の余白が足りずコンテンツが見づらい - Gridのアイテムが大きく表示されてしまう - Web特有の課題 - iOS/Safariブラウザでの問題 - autofocusでキーボードが表示されない - スワイプバック時に余分なページが表示される - パッケージ周りの対応 - GoogleSignIn - FirebaseCrashlytics - Drift - 絵文字がうまく表示されない - その他 - Widgetのデフォルト表示が違う - まとめ 132
  36. 該当箇所の実装 136 Widgetのデフォルト表示が違う問題 ReorderableListView .builder( itemBuilder: (context, index) { final

    category = categories. value[index]; return Column( key: Key(category.userCategoryId .toString()), children: [ ListTile( ..., title: Text(category.categoryName), trailing: const Icon( Icons.drag_handle, ), onTap: () {...} ), const Divider(), ], ); } ) drag_handle一個しか無いよなぁ...
  37. buildDefaultDragHandlesをfalseに設定 - ListTileのtrailingにIconを設定 ReorderableListView.builder( buildDefaultDragHandles: false, itemBuilder: (context, index) {

    return ListTile( ..., trailing: ReorderableDragStartListener( index: index, child: const Icon( Icons.drag_handle, ), ), ); } ) 139 Widgetのデフォルト表示が違う問題
  38. まとめ - Desktop対応時に起きた問題とその解決策を紹介しました - 入力方法、画面レイアウト、Web特有の問題などがあった - その対応は特別なことではない - Flutterリポジトリのissueを探しに行く -

    Widgetの各プロパティや実装について詳しくみる - デザイン面で難しければ相談する - 公式のドキュメントや MigrationGuideを読んでその通りに実装する 147