Flutter, React Native(Expo), Xamarin.Formsでハンバーガーメニュー(DrawerMenu)の実装を比べる

Flutter, React Native(Expo), Xamarin.Formsで、振る舞いが同じアプリを作っているんですが、その時にそれぞれのフレームワークでハンバーガーメニューはどのような実装になるのか調べました。今回はハンバーガーメニューから別画面に遷移できるようにしました。
調べた時に「要点となる実装部分だけの記載」という記事が多くて、初心者にとっては実装する時に「このコードはどこに追加(置き換え)するんだろうか？」って思ったので、FlutterとReact Native(expo)はコピペで動作するように記載しました。Xamarin.Formsはコピペの後でnamespaceを書き換えてください。

# 留意事項
 - その１
    - 「こう実装すればこう動く、ってことが分かればそれでOK！」を方針に実装を進めた個人開発の内容を記載しましたので、「なぜその実装が必要なのか？」や「なぜその実装で動くのか？」は分かってない部分多数です。。。 :bow:

 - その２
    - 筆者は実戦経験が乏しい（というより、アプリ開発は個人開発を除いて、現場での開発が未経験。。）、なので、ご指摘は「ここは、こうした方が良いよ:wink:」くらいのノリでお願いします。:pray::pray::pray:

 - その３
    - 「HelloWorldはできたけど、他にはこんなことできたら嬉しいな〜」って思っていた時のことを思い出して書きました。そのため、強々エンジニアや超人エンジニアの方々はこのページを参考にしない方が良いと思います。:expressionless::sleepy: :cold_sweat:

※iOSとAndroidのバージョン
![iOS-13.3](https://img.shields.io/badge/iOS-13.3-brightgreen)![Android-29(API Level)](https://img.shields.io/badge/Android-29(API Level)-brightgreen)

# フレームワークごとの相違点&要点
画面遷移は[この記事(自分の記事)](https://qiita.com/ito-kenichi/items/4bdaa6baeede5dad2a0b)を参考にしてます。

 - Flutter
    - Drawer(ハンバーガーメニュー) + ListView（メニュー内をList表示）で実装

 - React Native (Expo)
    - StackとDrawerをインストール&import
    - Drawer Navigator(ハンバーガーメニュー＋その要素)を定義

 - Xamarin.Forms
    - メニュー部分をContentPageとする（ → メニューを1つの画面として扱う）
    - メニュー部分のContentPageをMasterDetailPageのMaster(ハンバーガーメニューの土台)に実装する

# Flutter

以下の記事を参考に、Flutterでハンバーガーメニューを実装しました。
参照記事
https://flutter.keicode.com/basics/drawer.php
https://dev.classmethod.jp/articles/intro_flutter_widget_drawer/

## 概要

特にライブラリのインポートは不要で、Flutterの機能のみでハンバーガーメニューと画面遷移を実装できます。
DrawerとListViewをWidgetに追加して、ListViewのListTitleにメニューに表示する項目名を定義します。

![画面収録-2021-01-03-16.20.00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456945/581862f1-6a5e-3ac7-5f15-a28b52e5d20a.gif)![画面収録-2021-01-03-16.21.00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456945/ce0f0784-231f-c3f9-b25e-e03662b73f45.gif)

## 実装
![Flutter-1.20.3](https://img.shields.io/badge/Flutter-1.20.3-brightgreen)

```dart:main.dart
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
      routes: {
        '/first': (BuildContext context) => MyHomePage(),
        '/second': (BuildContext context) => MySecondPage(),
      },
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('TestProject'),
      ),
      drawer: new Drawer(
          child: new ListView(
            children: <Widget> [
              new DrawerHeader(child: new Text('Header'),),
              new ListTile(
                title: new Text('First Menu Item'),
                onTap: () {
                  Navigator.of(context).pushNamed('/first');
                },
              ),
              new ListTile(
                title: new Text('Second Menu Item'),
                onTap: () {
                  Navigator.of(context).pushNamed('/second');
                },
              ),
            ],
          )
      ),
      body: new Center(
        child: new Text(
          'Center',
        ),
      ),
    );
  }
}

class MySecondPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('2ページ目'),
      ),
      drawer: new Drawer(
          child: new ListView(
            children: <Widget> [
              new DrawerHeader(child: new Text('Header'),),
              new ListTile(
                title: new Text('First Menu Item'),
                onTap: () {
                  Navigator.of(context).pushNamed('/first');
                },
              ),
              new ListTile(
                title: new Text('Second Menu Item'),
                onTap: () {
                  Navigator.of(context).pushNamed('/second');
                },
              ),
            ],
          )
      ),
      body: Center(
      ),
    );
  }
}
```

# React Native(Expo)
以下の記事を参考に、React Native(Expo)でハンバーガーメニューを実装しました。
参照記事
https://qiita.com/zaburo/items/aaf198c79520dbaa48e2
https://react.keicode.com/native/navigation-drawernavigator.php
https://aboutreact.com/react-native-navigation-drawer/

## 概要

まず、react-navigation/drawer, react-navigation/stackをプロジェクトにインストールします。
NavigationContainerにDrawerNavigatorを実装し、そこへDrawer.Screeenをメニューの要素として実装します。
プロジェクトフォルダの直下に「screen」フォルダを新規作成し、以下のファイルを「screen」フォルダに新規作成します。

- Screen1.js
- Screen2.js

![画面収録-2021-01-03-16.26.03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456945/1431d3d5-4d40-cdde-6625-cf2b7b431a09.gif)![画面収録-2021-01-03-16.26.54.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456945/e95420af-0af8-53ea-56fc-766ca82eaaf3.gif)

## 実装
![React Native cli-2.0.1](https://img.shields.io/badge/React Native cli-2.0.1-brightgreen)![Expo-3.23.3](https://img.shields.io/badge/Expo-3.23.3-brightgreen)
※その他のバージョンは以下の通りです。

```
    "@react-navigation/drawer": "^5.9.0",
    "@react-navigation/stack": "^5.9.0",
```

```javascript:App.js
import 'react-native-gesture-handler';

import * as React from 'react';
import { View, TouchableOpacity, Image } from 'react-native';

import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createDrawerNavigator } from '@react-navigation/drawer';

import FirstPage from './pages/Screen1';
import SecondPage from './pages/Screen2';

const Stack = createStackNavigator();
const Drawer = createDrawerNavigator();

const NavigationDrawerStructure = (props)=> {
  const toggleDrawer = () => {
    props.navigationProps.toggleDrawer();
  };

  return (
    <View style={{ flexDirection: 'row' }}>
      <TouchableOpacity onPress={()=> toggleDrawer()}>
        {/*Donute Button Image */}
        <Image
          source={{uri: 'https://raw.githubusercontent.com/AboutReact/sampleresource/master/drawerWhite.png'}}
          style={{ width: 25, height: 25, marginLeft: 5 }}
        />
      </TouchableOpacity>
    </View>
  );
}

function firstScreenStack({ navigation }) {
  return (
      <Stack.Navigator initialRouteName="FirstPage">
        <Stack.Screen
          name="FirstPage"
          component={FirstPage}
          options={{
            title: 'First Page', //Set Header Title
            headerLeft: ()=> <NavigationDrawerStructure navigationProps={navigation} />,
            headerStyle: {
              backgroundColor: '#f4511e', //Set Header color
            },
            headerTintColor: '#fff', //Set Header text color
            headerTitleStyle: {
              fontWeight: 'bold', //Set Header text style
            },
          }}
        />
      </Stack.Navigator>
  );
}

function secondScreenStack({ navigation }) {
  return (
    <Stack.Navigator
      initialRouteName="SecondPage"
      screenOptions={{
        headerLeft: ()=> <NavigationDrawerStructure navigationProps={navigation} />,
        headerStyle: {
          backgroundColor: '#0460fe', //Set Header color
        },
        headerTintColor: '#fff', //Set Header text color
        headerTitleStyle: {
          fontWeight: 'bold', //Set Header text style
        }
      }}>
      <Stack.Screen
        name="SecondPage"
        component={SecondPage}
        options={{
          title: 'Second Page', //Set Header Title
          
        }}/>
    </Stack.Navigator>
  );
}

function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator
        drawerContentOptions={{
          activeTintColor: '#e91e63',
          itemStyle: { marginVertical: 5 },
        }}>
        <Drawer.Screen
          name="FirstPage"
          options={{ drawerLabel: 'First page Option' }}
          component={firstScreenStack} />
        <Drawer.Screen
          name="SecondPage"
          options={{ drawerLabel: 'Second page Option' }}
          component={secondScreenStack} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

export default App;
```

```javascript:Screen1.js
import * as React from 'react';
import { Button, View, Text, SafeAreaView } from 'react-native';

const FirstPage = ({ navigation }) => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <View style={{ flex: 1 , padding: 16}}>
        <View
          style={{
            flex: 1,
            alignItems: 'center',
            justifyContent: 'center',
          }}>
          <Text
            style={{
              fontSize: 25,
              textAlign: 'center',
              marginBottom: 16
            }}>
            This is the First Page under First Page Option
          </Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

export default FirstPage;
```

```javascript:Screen2.js
import * as React from 'react';
import { Button, View, Text, SafeAreaView } from 'react-native';

const SecondPage = ({ navigation }) => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <View style={{ flex: 1, padding: 16 }}>
        <View
          style={{
            flex: 1,
            alignItems: 'center',
            justifyContent: 'center',
          }}>
          <Text
            style={{
              fontSize: 25,
              textAlign: 'center',
              marginBottom: 16
            }}>
            This is Second Page under Second Page Option
          </Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

export default SecondPage;
```

# Xamarin.Forms
以下の記事を参考に、Xamarin.Formsでハンバーガーメニューを実装しました。
参照記事：https://www.atmarkit.co.jp/ait/articles/1611/02/news025_4.html

## 概要

XAMLのClassでMasterDetailPageを作ります。MasterDetailPageのMasterにメニュー画面を「ContentPageとして」追加します。メニュー画面(ContentPageの方)にListViewで要素を追加します。

![_2021-01-03 16.33.11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456945/c3a953c2-607f-0561-c29c-3390280df185.gif)![_2021-01-03 16.33.59.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456945/07091c13-3bb1-2b2a-831d-068763be14ea.gif)

## 実装
![Xamarin.Forms-4.6.0.1141](https://img.shields.io/badge/Xamarin.Forms-4.6.0.1141-brightgreen)

```C#:App.xaml.cs
using Xamarin.Forms;

namespace xamarin.forms_project
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            MainPage = new MyMasterDetailPage();
        }

        protected override void OnStart()
        {
        }

        protected override void OnSleep()
        {
        }

        protected override void OnResume()
        {
        }
    }
}
```

```xml:MyMasterDetailPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  xmlns:local="clr-namespace:xamarin.forms_project;assembly=xamarin.forms_project"
                  x:Class="xamarin.forms_project.MyMasterDetailPage">
    <MasterDetailPage.Master>
        <local:MyMasterPage x:Name="masterPage" />
    </MasterDetailPage.Master>
    <MasterDetailPage.Detail>
        <NavigationPage>
            <x:Arguments>
                <local:FirstPage />
            </x:Arguments>
        </NavigationPage>
    </MasterDetailPage.Detail>
</MasterDetailPage>
```

```C#:MyMasterDetailPage.cs
using System;
using Xamarin.Forms;

namespace xamarin.forms_project
{
    public partial class MyMasterDetailPage : MasterDetailPage
    {
        public MyMasterDetailPage()
        {
            InitializeComponent();
            masterPage.ListView.ItemSelected += OnItemSelected;
        }

        void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
        {
            var item = e.SelectedItem as MyMasterPageItem;
            if (item != null)
            {
                Detail = new NavigationPage((Page)Activator.CreateInstance(item.TargetType));
                masterPage.ListView.SelectedItem = null;
                IsPresented = false;
            }
        }
    }
}
```

```xml:MyMasterPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="xamarin.forms_project.MyMasterPage"
             Padding="0,40,0,0"
             Icon="hanburger.png"
             Title="メニュー">
    <StackLayout VerticalOptions="FillAndExpand">
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Menu" HorizontalTextAlignment="Center" TextColor="White" FontSize="16" />
        </Frame>
        <ListView x:Name="listView" VerticalOptions="FillAndExpand" SeparatorVisibility="None">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ImageCell Text="{Binding Title}" ImageSource="{Binding IconSource}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>
```

```C#:MyMasterPage.xaml.cs
using System.Collections.Generic;
using Xamarin.Forms;

namespace xamarin.forms_project
{
    public partial class MyMasterPage : ContentPage
    {
        public MyMasterPage()
        {
            InitializeComponent();

            var myMasterPageItems = new List<MyMasterPageItem>();
            myMasterPageItems.Add(new MyMasterPageItem
            {
                Title = "１枚目",
                IconSource = "icon.png",
                TargetType = typeof(FirstPage)
            });
            myMasterPageItems.Add(new MyMasterPageItem
            {
                Title = "２枚目",
                IconSource = "icon.png",
                TargetType = typeof(SecondPage)
            });
            listView.ItemsSource = myMasterPageItems;
        }
        public ListView ListView { get { return listView; } }
    }
}
```

```xml:SecondPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="xamarin.forms_project.SecondPage">
    <StackLayout x:Name="Layout">
        <Image Source="https://picsum.photos/250?image=9" HeightRequest="500" WidthRequest="500" />
    </StackLayout>
</ContentPage>
```

```C#:SecondPage.xaml.cs
using Xamarin.Forms;

namespace xamarin.forms_project
{
    public partial class SecondPage : ContentPage
    {
        public SecondPage()
        {
            InitializeComponent();
        }
    }
}
```
```C#:MyMasterPageItem.cs
using System;

namespace xamarin.forms_project
{
    public class MyMasterPageItem
    {
        public string Title { get; set; }
        public string IconSource { get; set; }
        public Type TargetType { get; set; }
    }
}
```

# 感想
[画面遷移の記事](https://qiita.com/ito-kenichi/items/4bdaa6baeede5dad2a0b)と同じく、個人的にはXamarin.Formsがわかりやすかったかな〜と。