Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

Friendlyで「出来たらいいな」2つ

WPFアプリのE2Eテストを書く必要があり、Friendlyを触ってみた。

ishikawa-tatsuya.hatenablog.com

2つほど出来たらいいな、があった。

1. ViewModelのオブジェクトを取りたい

//WindowControl w 
var vm = w.AppVar.Dynamic().DataContext();
if(vm is HogeViewModel) {
   //falseになる
}

ってやると、dynamicで対象のビューのVMが取れるし、メソッドを叩くこともできる。ただ、VMの型にキャストすることが出来ない。VMの型にキャストできれば、Assertするのが楽になるので、やりたい。

2. HierarchicalTemplate

こちらに書いたが、メニューの子メニューをVisualTreeで辿れない。

qiita.com

とりあえず、書き散らかしただけですいません。。。

DeployGateのコマンドラインツールのインストールでコケた

こんなログが出た

"xcrun clang -o conftest -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/universal-darwin19 -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/backward -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0 -I. -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT    -g -Os -pipe -DHAVE_GCC_ATOMIC_BUILTINS conftest.c  -L. -L/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib -L. -L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.Internal.sdk/usr/local/lib   -arch x86_64   -lruby.2.6   "
In file included from conftest.c:1:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby.h:33:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/ruby.h:24:10: fatal error: 'ruby/config.h' file not found
#include "ruby/config.h"
         ^~~~~~~~~~~~~~~
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/ruby.h:24:10: note: did not find header 'config.h' in framework 'ruby' (loaded from '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks')
1 error generated.
checked program was:
/* begin */
1: #include "ruby.h"
2: 
3: int main(int argc, char **argv)
4: {
5:   return 0;
6: }
/* end */

Xcodeコマンドラインツールのパスが違ってたので直したらインストールできた

$ sudo xcode-select -switch /Library/Developer/CommandLineTools

これだけ。

あらざ〜す

MacのhomebrewでOpenSSLがビルドエラーになる場合の対処方法|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

RiverPodをやってみた

Flutterの設計で一番悩ましいのが、ViewとModelの連携。状態遷移だと思われる。

2020年の初頭からFlutterやりはじめて、「Provider + ChangeNotifier」でやってた。これでも全然動く。

ただ、Providerは必ずcontextを経由する必要があるので、ウィジェットの階層構造に気をつけないといけないとか、データを変更した後にnotifyListenersを自分で呼ばないといけないとか、そういう面倒さがあった。

値を再代入するだけで、当該UIのウィジェットが再ビルドされたらそれで良いのだけども、GUI(デスクトップアプリやスマホアプリ)を作る場合にそこが一番面倒。

RIverpodの便利なところは、提供されるProviderがimmutableに設計されているため、グローバルに定義しても良い点。BuildContextに依存しない点の2点がでかい。

riverpod版

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:state_notifier/state_notifier.dart';

final counterProvider = StateNotifierProvider((_) => CounterNotifier());

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier(): super(0);
  void increment () => state++;
}

class CounterPage extends HookWidget {

  Widget build(BuildContext context) {

    final v = useProvider(counterProvider.state);
    final counter = useProvider(counterProvider);

    return Scaffold(
      body: Center(
        child: Text(v.toString(), style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

StatefulWidget + State + setState

import 'package:flutter/material.dart';

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

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: Center(
        child:  Text('$_counter', style: TextStyle(fontSize: 24))
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        child: new Icon(Icons.add),
      ), 
    );
  }
}

この2つを見比べると、パッと見るだけでも以下のことがわかる。

  • ユーザーの操作に伴いsetStateを書きまくるコードはだいぶ辛い。
  • StateNotifierではstateを再代入するだけに徹する。公開される値がstateしか無いのがミソ。
    • 再代入するとはいえ、StateTの持ってる色んなプロパティの状態をキープしつつイミュータブルにしたいので、freezedパッケージを使うのよね、と。

個人的には、notifyListener()を意識しなくて良いのが好きなので、StateNotifierを推していきたい。

  • スクロールの管理
  • テキストボックスの値の代入
  • 引数ありの画面遷移

この辺がクリア(自分の中で噛み砕ければ)できれば、Riverpodいける。がんばろう。

SendGridでTo毎にメールを送る(SMTP API) をPythonで書いた

SendGridのWebAPIではto_emailsというパラメータがあるが、これはToを複数追加するだけで、メールがTo単位で1件ずつ送信されるわけではない。

To毎にメールをバラして送信したい場合は、smtpapiを利用するようだ。Web API経由では出来なかった。

smtpapiを入れる

github.com

このライブラリはSendGridのメールサーバに送るためのヘッダー文字列を作るだけのライブラリ。

実際にメールを送る時は、PythonSMTPメール送信をするのにおなじみsmtplibを使う。

サンプルコード

    import os
    import smtplib
    from email.mime.text import MIMEText

    # SMTP認証情報
    host = 'smtp.sendgrid.net'
    port = 587
    user = 'apikey'
    passwd = os.environ.get( 'SENDGRID_API_KEY' )
    server = smtplib.SMTP( host, port )
    server.starttls()
    server.login( user, passwd )
    # HEADER
    from smtpapi import SMTPAPIHeader
    header = SMTPAPIHeader()
    header.set_tos(to)

    # メール本文
    message = MIMEText(content, 'html')
    message['From'] = _from
    message['To'] = ",".join(to)
    message['Subject'] = subject
    message['X-SMTPAPI'] = header.json_string()
    # メール送信
    server.sendmail(_from,
       to, 
       message.as_string().encode('utf-8')
    )
    server.quit()
  • HTMLメールの場合は、MIMETextの第2引数にhtmlを入れ、message.as_string().encode('utf-8')にあるように文字列をUTF8なりでエンコードする。この指定がないとasciiエンコードするため。
  • message['To']は文字列である必要があるので、リストをカンマ区切りの文字列に変換する。
  • message['X-SMTPAPI']にSendGridに送るための情報をセットする

FlutterでTextFieldに非同期で取得した結果を初期値に入れたい

色々探してやっとわかった..

flutter.institute

initStateに直接awaitって書くことは出来ない。そりゃそうだな。ビルドが終わってから状態を変え、再ビルドするという方法を探していた。

class MyWidget extends StatefulWidget {
    @override
    State createState() => new MyWidgetState();
}

class MyWidgetState extends State<MyWidget> {
    var _result;

    @override
    void initState() {
        loadAsyncData().then((result) {
            setState(() {
                _result = result;
            });
        });
    }
    
    @override
    Widget build(BuildContext context) {
        if (_result == null) {
            return new Container();
        }    
       return new TextField(initialValue :result);
    }
}

要約するとこうなる。

  • initStateの中にsetStateを用意して、非同期の結果をメンバ変数にいれる
  • setStateもいつ呼ばれるかわからんが、呼ばれるとbuildがもっかい走るので、resultの状態に応じてビルドするWidgetを変える。

うーん、今回はStateFulWidgetにしたけど、ProviderとStatelessWidgetでも同じことができる気がする... あとでやってみよう。

PrismのDialogServiceでDIしたViewModelが毎回初期化される

PrismのDialogServiceでDIしたViewModelに限って、毎回ViewModelのコンストラクタが呼ばれるという現象がありました。

prism:ViewModelLocator.AutoWireViewModel="True"にしているのにね。この設定にした場合、ViewModelのインスタンスはSingletonになってくれるが、DialogServiceで呼び出す小画面に限りなぜか・・・

  • バージョン: Prism.Wpf 7.2.0.1422

結論から言えば、このコードを書けばSingletonになってくれた。

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
      containerRegistry.RegisterSingleton<OrdeSearchPageViewModel>();
}

RegisterSingleton で、型に対象のVMのクラスを指定するだけだった。Prismの沼かもしれん。

Flutterでカメラで画像を撮って保存したい

まー、ありますわね。そーゆーのがね。

要件としては、写真撮ってメモを追加して何件かリストにしてPOSTだから、取った写真をリスト表示できることが必要。これが意外と難しくてワロタ。

最終的に、こういう風にするしかなさげ。

  • image_pickerプラグインで画像をカメラでとって、
  • path_providerにて、application内のディレクトリに保存してパスを持っておく
  • ギャラリーに移す場合は、gallary_saverでファイルをコピーする。
  • image_gallay_saverはバグがあって動かないようだ。

まとめ

ギャラリーに直接保存してしまうと、保存先のファイルパスが取れない。戻り値がboolになるのは、gallary_saverの仕様。

  Future<bool> takePhoto() async {
    final ImagePicker picker = ImagePicker();
    final PickedFile _image = await picker.getImage(source: ImageSource.camera);
    return GallerySaver.saveImage(_image.path)
  }

true/falseより、保存先のパスが一番欲しいよねw issueに上がってるけど、できないのかな。FlutterのMethodChannel#invokeMethod型のFuture返すから、頑張れば取れる可能性も微レ存。flutterの仕様的に。

github.com

あ、あとこのライブラリでやると、画像がギャラリーに保存できなかった。issueも上がってる。

pub.dev

こんだけのコードなんだけどね。多分MethodChannelの使い方が間違っているのだろう。

import 'dart:async';
import 'dart:typed_data';

import 'package:flutter/services.dart';

class ImageGallerySaver {
  static const MethodChannel _channel =
      const MethodChannel('image_gallery_saver');

  /// save image to Gallery
  /// imageBytes can't null
  static Future saveImage(Uint8List imageBytes, {int quality = 80, String name}) async {
    assert(imageBytes != null);
    final result =
    await _channel.invokeMethod('saveImageToGallery', <String, dynamic> {
      'imageBytes': imageBytes,
      'quality': quality,
      'name': name
    });
    return result;
  }

  /// Save the PNG,JPG,JPEG image or video located at [file] to the local device media gallery.
  static Future saveFile(String file) async {
    assert(file != null);
    final result =
    await _channel.invokeMethod('saveFileToGallery', file);
    return result;
  }

}

image_gallery_saver/image_gallery_saver.dart at master · hui-z/image_gallery_saver · GitHub

そんなかんじ。