2022年3月23日水曜日

MacBook Pro M1 Pro (2021年モデル) に買い換えた話 (新旧比較表付き)

と言うわけでついにMacBook Proを2009年モデルから2021年モデルに買い換えた!

「どう言うわけか?」と言う方はこちらをどうぞ。

せっかくなので比較しておこう。


BeforeAfter
ModelMacBook Pro
13-inch, Mid 2009
MacBook Pro
14-inch, 2021
Display13.3インチ
クリアワイドスクリーンディスプレイ
14.2インチ
Liquid Retina XDRディスプレイ
CPUIntel Core 2 Duo
2.26GHz
Apple M1 Pro
10 Cores
GPUNVIDIA GeForce 9400M 256MBApple M1 Pro
16 Cores
Memory2GB DDR3
→ 8GBに交換
 2016年 ¥9,300
16GB
LPDDR5
Storage160GB HDD
→240GB SSDに交換
 2016年 ¥10,260
1TB SSD
Battery60Whリチウムポリマーバッテリー70Whリチウムポリマーバッテリー
Height2.41 cm1.55 cm
Width32.5 cm31.26 cm
Depth22.7 cm22.12 cm
Weight2.04 kg1.6 kg
Keyboard"Scissor" Keyboard(※1)バックライトMagic Keyboard, Touch ID
CameraiSightカメラ1080p FaceTime HDカメラ
Wi-Fi802.11n/a/b/g802.11ax Wi-Fi 6
BluetoothBluetooth 2.1 + EDRBluetooth 5.0
Power Adapter60W MagSafe電源アダプタ96W USB-C MagSafe 3電源アダプタ
USBUSB 2.0ポート(最大480 Mbps)×2Thunderbolt 4 (USB-C)ポート x 3
Display OutputMini DisplayPortHDMIポート
Audio I/O3.5mmヘッドフォンジャック3.5mmヘッドフォンジャック
SD Card SlotSD Card SlotSDXC Card Slot
CD Drive8倍速SuperDrive
(DVD±R DL/DVD±RW/CD-RW)
-
FireWireFireWire 800-
※1 … キーボードも Scissor Keyboard (シザーキーボード) から Butterfly Keyboard (バタフライキーボード) の一悶着があって、Scissor Keyboardが復活&改良されてMagic Keyboardとなったらしい。もちろん私はButterfly Keyboardは知らない…
バタフライキーボード: 2015年〜2019年

2009年モデルのスペック及びマニュアル
2021年モデルのスペック及びマニュアル
※マニュアルがかなり簡略化された^^

この後に写真比較も載せるが、昔の13インチモデルが今の14インチモデルと同じ大きさなのも面白い。

2016年にメモリ増強(¥9,300)とSSD化(¥10,260)合わせて¥19,560で6年も延命できたのだから安上がりであった。(というか使おうと思えばまだまだ使える!)
メモリは最初の2GBのものを外して、4GBを2枚挿しにした。
当時の記憶では検索をミスったのかメモリは最大で4GBまでと勘違いしていた。(MacBook 13-inch late 2009モデルと勘違いしてた?)
勘違いしていたため8GBまで乗せれるのか色々検索し、どなたかが8GBでもちゃんと認識されたとか見つけて、大丈夫ということを確信した上で8GBにした様な気がする。証拠を載せておく。(前記事の通り、2009年モデルは OS X El Capitan (10.11)。About This Mac 画面は当時からあまり変化していないことが分かる。)

P.S.2022.10.9
私が「About This Macが少なくとも2009年から10年以上も変化していない」と言ったことから、ついにmacOS Ventura 13.0 にて変更されました!私の指摘がよほど気になったようですね^^またはMacBook Pro 2009年モデルを10年以上も大切に愛用した私の意見を尊重しれくれたんですね。(そんな訳はない。。VenturaにてSystem Setting(システム設定)が ios 風に統合されたので、その一環でAbout This Macも見直されたのでしょう。)



それでは比較写真に移ろう。
トップ 左:2009年モデル 右:2021年モデル※プラスチックカバーを付けてます。


フロント 下:2009年モデル 上:2021年モデル
2009年モデルについては、右から赤外線レシーバ、スリープインジケータ
2021年モデルがコンパクトになった。2021年モデルの方が軽いのだが、手で持って比較すると2021年の方が「詰まってる感」がする。
本記事を書いててわかったが、2009年モデルには赤外線レシーバがあったんだ。。この機能だけは使わずじまいだった。。

右側面 下:2009年モデル 上:2021年モデル
ポートは前方から
2009年モデル:盗難防止用ロックスロット※USB-Cではありません!, CDドライブ
2021年モデル:SDカードスロット, USB-C, HDMI
そう言えば2021年モデルには(というかいつからか)、盗難防止用ロックスロットが無くなった気がする。店舗展示とか会社の事務所に置きっぱなしにする場合にワイヤーチェーンで机とかに結び付けて盗難されない様にするものだが、この辺にも世界標準的に考え方の変化があったのだろうか?ISOとかでリスクアセスメントを理詰めでやると大抵はワイヤーチェーンなどに行き着くのだが、「現実問題との乖離」問題もあるわけで、世界標準もこの辺の乖離に一歩踏み込んで次のレベルの現実解まで考察し始めてくれたのであれば幸いである。
(現実問題との乖離として、何よりもズレているのは、大盗賊であれば当然ワイヤーカッターを持って盗難に来る、ということである^^大盗賊と言わずとも、よほど頭が弱くて世情を知らない盗賊でもなければ、「ワイヤーチェーンで結ばれてるかも知れないからワイヤーカッターも準備しておこう」となると思う。そうするとワイヤーチェーンの主たる目的はむしろ「抑止力」なのかも知れない。)


そして何よりもCDドライブ!当時インターネットは普及していたとはいえソフト購入時の主なメディアだったり、マシン故障時のリカバリメディアだったりした訳で「必要」だったのである。
ソフト購入について、多分どこかでちゃんと考察されているのだろうが、また私を含め一般コンシューマーの多くも心に思ったことだと思うが、スマホが世に出てAppleのApp StoreやGoogleの Play Store が出来てお客さんはバンバンアプリを購入する様になって、AppleとかMicrosoftは「あぁ何でもっと前からPC版Storeを真剣に作らなかったのだろう」と思っていることだろう。順当に考えるだけならスマホ効果でPC側に逆輸入しただけとも捉えられる。まさに黎明期である。

マシン的な話に戻すとこの物理的稼働装置を無くせたことは大きい。後で書こうと思ったがここで書いてしまうが、また前記事でもちょっとだけ触れたが、ポートを最小限まで切り詰めたり、バタフライキーボードにしたり、ファンクションキーをTouch Barにしたりという一連の流れは「現在の技術で最小化、コンパクト化、物理稼働装置の撤去・デジタル化・仮想化をできるところまでしてみよう」的ないわば一大ムーブメントがあった気がする。「今」というのが難しく、本当の現在の最新技術であれば基盤からポートから最新のものを使えば良いのだが、世に出ているものと次世代規格が同時進行で時間は流れている。製品化するには「今」という断面を切らなければならないということ。
当時Appleが目指した「シンプル化・最小コンパクト化」も「現在」技術であれば簡単に到達できるのだろうが、目指していたことはそういうことでは無論あるまい。むしろ一回リスクを負って、シンプル化・最小コンパクト化を目指して、それを実際にやってみないとそこからの景色は見えない、という事であろう。
(世の単純比較からは「Appleは後悔して前の時代に戻った」とか散々言われているが、ここで得たAppleの知見は計り知れない。また、最大の「外せない」物理装置としてキーボードは常に念頭に入れているはずである。誰しもが憶測しただろうが、おそらくキーボードを薄くしていって、最後はスマホのように仮想キーボードでいいんじゃない?に持っていきたかったのだろう。でもキーボードは「反応(レスポンス)」「押した感覚」を人は求めているということ。かといってトラックパッドのクリックは物理的押し込みはしなくてもいいんじゃないかと思う。塩梅が難しい。まさにこういったことの積み重ねなのだろう。)

右側面 下:2009年モデル 上:2021年モデル
ポートは前方から
2009年モデル:ヘッドフォンジャック, SD Card Slot, USB 2.0 × 2, Mini Display Port, FireWire 800, Ethernet, MagSafe電源ポート
2021年モデル:ヘッドフォンジャック, USB-C × 2, MagSafe電源ポート
左右のポートを比較してみると、さすが私が買い替えるために開発された(前記事参照)だけあって、ポート構成が似ている。
・ヘッドフォンジャック復活。(あれ? MacBookだとヘッドフォンジャックは無くなったことはない?iPhoneの話か。)
・四角いUSBから、丸いUSB-Cへ。
・Mini Display PortからHDMIへ。
MagSafe電源ポートは平べったくなった。
・EthernetとFireWireが無くなった。FireWireが懐かしい。

おまけ:2009年モデルのバッテリーインジケータ
1個前の比較画像だと分かりづらいが、2009年モデル前方にはバッテリーインジケータがあった。ボタンを押すとバッテリー残量がわかる。

続いて開いた状態。

左:2009年モデル 右:2021年モデル
比較して気づいたが、トラックパッドが若干大きくなってる。

2009年モデル キーボードとトラックパッド
2009年モデルは訳あって英語配列キーボードにした。

2021年モデル キーボードとトラックパッド ※キーボードカバーを付けている。
右上の2009年モデルではCDイジェクトボタンだったところが、2021年モデルではTouch IDになった。
またキーボードの話になるが、2009年モデルの方がストロークが深いため押しごたえがある。特に矢印キーについては2009年モデルの方が使いやすい。人差し指、中指、薬指を置いた時に手の感覚でどのキーを押しているのかが分かって安心するからだろう。
2021年モデルくらい薄くなると、ちゃんと指が正しい矢印キーを押せているのか何となく不安になる。(慣れの問題かもしれないが。)
この辺の
・英数字キーは「F」と「J」に人差し指を置いたホームポジションからの相対的な感覚
というのと、
・矢印キーはゲームパッドなどと同様に指先の感覚
という点で性質が異なるということである。(人間工学メモ)

背面 左:2009年モデル 右:2021年モデル
2021年モデルにはプラスチックカバーを付けている。

せっかくなので電源アダプターも載せておく。

上:2009年モデル 下:2021年モデル
デザインやサイズは前から変わっていない。2009年モデルではコード一体型だったため巻き取り用ウィング(正式名称不明)があって便利だった。ただし写真のように巻き取った後の格好については、Appleデザイン的に合致しているかどうかは不明である。(巻き方が汚いだけ?)

プラグ部分を取り外した状態
左:2009年モデル 右:2021年モデル
プラグ部分は全く一緒のように見えた。多分2019年モデルのものは2021年モデルにもぴったりとくっ付くと思うが、壊れたら嫌なので試してない。
※写真ではプラグ部分の大きさが違うように見えるかも知れないが、被写体の置き方や角度が違うだけで、つまり私の写真の撮り方が下手なだけである。

以上が比較結果である。こんなに世代がかけ離れたマシンの比較結果内容で買い替えを検討する方はいないと思うが、読み物として読んでいただければ幸いである。

なおFlutter開発について本MacBook Proを以って、ようやく念願のiOS版もリリースすることができた!
ネットを調べるとApple Developer Program購入やアプリの審査など時間がかかると書いてあったが、私の場合は3/15 にApple Developer Programを購入実施、完了して、3/16にはApp審査も通ってリリース(App Store公開)までできてしまった!(結構最小日数記録だと思う。)
これも2009年モデルMacBook ProというApple製品を大切に使ってきたおかげであろう。

2022年3月13日日曜日

Flutter: Androidの場合、showDialog(AlertDialog)内でWebViewを使用するとボタンが見えなくなる現象の対処法

Androidの場合、showDialog(AlertDialog)内でWebViewを使用するとボタンが見えなくなり困っていたが対応できた。

Flutterバージョン
> flutter --version
Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 7e9793dee1 (10 days ago) • 2022-03-02 11:23:12 -0600
Engine • revision bd539267b4
Tools • Dart 2.16.1 • DevTools 2.9.2

対応前            →  対応後
 


使用しているWebView
webview_flutter: ^3.0.0
※執筆時最新バージョンは 3.0.1 

対処法
initState() に WebView.platform = AndroidWebView() を追加。
@override
void initState() {
  super.initState();

  // Enable virtual display.
  if (Platform.isAndroid) WebView.platform = AndroidWebView();
}

詳細
対応前は、ボタンは見えてないだけで、ボタンがあるあたりをタップするとちゃんとタップイベントが動作していたため、「見えてない」だけのようである。

また不思議なことに、次のページに遷移してそちらでもshowDialog()でダイアログ内にWebViewを使用しているのだが、こちらはちゃんとボタンが表示されていた。

なお、アプリ作成時(2019年3月ごろ)は、全画面ともダイアログのボタンは表示されていたため、webview_flutterをバージョン3.0.0に上げてからの症状だと思われる。

ネット上でもWebView関連の症状は結構あるようなので、その辺の問題であって、Flutterライブラリの更新はまだまだ目まぐるしい。
Flutterはその様に目まぐるしい状況なので、調査される方はあまり古い情報にハマらない様ご注意ください。(正攻法は「最新ライブラリのリファレンスを読む」ことなのだが、どうしても急いで対処療法を探すとネットに頼ってしまうこともあるかと思うため、念の為。)
(加えて言うならば、例えば.NET やJava だともう大きく変化しない箇所があって、「こういう時は、こう対応」みたいなものができていて、検索エンジンもそういったものを上位に持ってきてくれるので、古い情報でも気にせずに検索上位から当たっていく、みたいなやり方もアリといえばアリなのだろうが、マルチプラットフォーム開発の期待の星・または大本命であるFlutterはまだまだガンガン発展・進化を遂げていくであろうから、前者のやり方に慣れてしまった方は注意である。例えば.NETやJavaから、いきなりFlutter担当になった方など。「基本は本家リファレンスをあたる」という基本習慣が身について良いかも知れない。)

ちなみに、私のアプリもwebview_flutterをバージョン3.0.0にした時に、描画されるHTMLが全体的に小さくなったため、HTML側で以下を追加して対応してた。
<meta name="viewport" content="width=device-width, initial-scale=1.0">

webview_flutterバージョン3.0.0の変更履歴を見ると、BREAKING CHANGE(破壊的変更)とあって、ちょうどこの箇所が関係していたようだ。
BREAKING CHANGE: On Android, hybrid composition (SurfaceAndroidWebView) is 
now the default. The previous default, virtual display, can be specified with 
WebView.platform = AndroidWebView()
(出典: pub.dev webview_flutter)
それにしてもSurfaceAndroidWebViewがデフォルトになることで、なぜWebViewがダイアログのTextButtonに影響しているのかは不思議である。(私はこの症状にはなったことはないが、ネット上に「showDialogでWebViewを使うとWebViewがダイアログをはみ出す」とかもあったので、やはりWeb画面は色々できるので強力であり特殊なのだろう。)

※そもそもアプリの更新を半年くらいしてなかったら、DartがNull Safety正式版になってたりとか、ライブラリもWebView含め、Firebaseとか結構な変更があったので諸々対応して大変だった^^
 みなさんも放置しすぎない様に注意しましょう!(バージョンアップ対応に追われて、なかなかやりたかった更新作業に辿り着けない。。。)


(おまけ) コード抜粋(buildの骨組みだけ)
※Null Safetyは突貫で直しているので、_doShowDialog の String? contentStr は、String contentStrでいいんじゃないか、とかはご愛嬌^^
①問題が発生した画面
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(_appTitle),
          actions: <Widget>[
            PopupMenuButton(
              itemBuilder: (BuildContext context) {
                return [
                  PopupMenuItem(child: Text(AppLocalizations.of(context).aboutXxxTitle), value: 1),
                  PopupMenuItem(child: Text(AppLocalizations.of(context).aboutYyyTitle), value: 2),
                ];
              },
              onSelected: (menuId) {
                // ダイアログ表示
                if (menuId == 1) {
                  _doShowDialog(AppLocalizations.of(context).aboutXxxTitle, AppLocalizations.of(context).aboutXxx);
                } else if (menuId == 2) {
                  _doShowDialog(AppLocalizations.of(context).aboutYyyTitle, AppLocalizations.of(context).aboutYyy);
                }
              },
            ),
          ],
        ), // AppBar
        body: SafeArea(
          ...(省略)

  // ダイアログ表示
  _doShowDialog(String title, String? contentStr) {
    showDialog(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: Text(title),
          content: Container(
            width: MediaQuery.of(context).size.width - 10,
            height: MediaQuery.of(context).size.height - 350,
            // 以下のWebViewは実際は別クラスにしてます。リソース(assets)テキストを表示するだけのWebView。
            child: WebView(
              onWebViewCreated: (WebViewController webViewController) async {
                // return the uri data from raw html string.
                String htmlString = '<!DOCTYPE html><body>' + contentStr! + '</body></html>';
                await webViewController.loadUrl(Uri.dataFromString(htmlString, encoding: Encoding.getByName('utf-8')).toString());
              },
            ),
          ),
          actions: <Widget>[
            // ↓これが見えなくなったボタン
            TextButton(
              child: Text("OK"),
              onPressed: () => Navigator.of(context, rootNavigator: true).pop(context), // これでダイアログが閉じられる
            ),
          ],
        );
      }
    );
  }

②問題が発生しなかった画面
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Expanded(
            child: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
              // ページデータを読み込む
              stream: _pageData,
              builder: (BuildContext contextPageData,
                AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
                  if (snapshot.hasError) {
                    return Center(child: Text('Error: ${snapshot.error}'));
                  } else if (snapshot.connectionState == ConnectionState.none ||
                    snapshot.connectionState == ConnectionState.waiting) {
                    // ロード中
                    return _getLoadingText();
                  } else if (!snapshot.hasData) {
                    return _getLoadingText();
                  }

                  switch (snapshot.connectionState) {
                  case ConnectionState.waiting:
                    return _getLoadingText();
                  default:
                    // ページ情報
                    List<DocumentSnapshot<Map<String, dynamic>>> pages = snapshot.data!.docs;
                    return DefaultTabController(
                      length: pages.length,
                      initialIndex: _currPage!,
                      child: StatefulBuilder(
                        builder: (BuildContext contextTab, setState) =>
                          WillPopScope(
                              onWillPop: () {
                                // 前画面へ戻る
                                Navigator.of(context).pop();
                                return Future.value(false);
                              },
                              child: Scaffold(
                                appBar: AppBar(
                                  title: Text(widget.textDoc.data()!["title"]),
                                  actions: <Widget>[
                                    IconButton(
                                      icon: const Icon(Icons.info, color: Colors.white, size: 30,),
                                      onPressed: () {
                                        // 説明をダイアログ表示
                                        _doShowDialog(widget.textDoc.data()!["title"], widget.textDoc.data()!["text_info"]);
                                      },
                                    ),
                                  ],
                                ),

おまけなので、アプリの詳細は割愛するが、
①問題が発生した画面 では、appBarまたは、bodyの情報ボタンからダイアログを表示している。
②問題が発生しなかった画面 では、appBarからダイアログを表示している。
やっていることは、_pageDataとして外部からページデータを取得して、StreamBuilderでタブに表示している。そこそこありがちな構成だと思う。

構造としては、①だと Scaffoldだけだが、
②だと SafeArea → Expanded  DefaultTabController  StatefulBuilder  WillPopScope  Scaffold
となっており、この辺の構造も関係しているのだろうか?
ネット上にもScaffoldがどうのこうとと書いてあった様な。。。