flutter StreamBuilderから遷移した画面のStreamBuilder内のTextFieldを初期化する

困った。StreamBuilderで作ったList画面から、これまたStremBuilderで作ったEdit画面へ画面遷移。

Edit画面のStreamBuilder内にTextFieldを置いて、Streamから取得した値で初期化しようとすると、キーボードを表示する度にStreamが流れてしまい、編集しようとも初期化しようとする値に戻ってしまう。

EditScreenのStremが流れるたびに呼び出し元のListScreenにもStreamが流れてしまい、想定した動きにならない。

仕方ないのでEditScreenに初期化済み判定用のフラグを用意してみた。

class ListScreen extends StatelessWidget {

  // APIからデータを取得してストリームに流し込むことを想定
  Stream<List<String>> _stream = Stream<List<String>>.value(
      ["Title1", "Title2", "Title3", "Title4", "Title5"]);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text("First Screen")),
        body: StreamBuilder(
          stream: _stream,
          builder: (context, AsyncSnapshot<List<String>> snapshot) {
            if (!snapshot.hasData) {
              return Container();
            }

            return ListView.builder(
                itemCount: snapshot.data.length,
                itemBuilder: (context, index) {
                  return InkWell(
                    onTap: () {
                      Navigator.of(context).push(MaterialPageRoute(
                        builder: (context) => DetailScreen(),
                      ));
                    },
                    child: Card(
                      child: Container(
                          margin: EdgeInsets.all(10.0),
                          child: Text(snapshot.data[index])),
                    ),
                  );
                });
          },
        ));
  }
}

class DetailScreen extends StatefulWidget {
  DetailScreen();

  @override
  State<StatefulWidget> createState() => DetailScreenState();
}

class DetailScreenState extends State<DetailScreen> {
  // APIから取得してデータをストリームに流し込むことを想定
  Stream<String> _stream = Stream<String>.value("this is id='s data");

  // TextFieldを初期化済みかどうかを管理するためのフラグ
  bool _initialize = false;

  final _titleTextController = TextEditingController();

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Second Screen")),
      body: StreamBuilder(
          stream: _stream,
          builder: (context, AsyncSnapshot<String> snapshot) {
            if (!snapshot.hasData) {
              return Container();
            } else if (!_initialize) {
              // ストリームから流れてきたデータをTextFieldの初期値にセット
              // ただしSoftwareKeyboardの表示・非表示が切り替わるたびにStreamが
              // 流れてくるため、初期化済みか判定用のフラグ管理する
              _titleTextController.text = snapshot.data;
              _initialize = true;
            }
            return Container(
              child: Column(
                children: <Widget>[
                  TextFormField(
                    controller: _titleTextController,
                    decoration: InputDecoration(labelText: "Title"),
                  ),
                ],
              ),
            );
          }),
    );
  }
}

これはもうちょっと使い方調べないとダメやな。
provider packageのStreamProvider使って同じことしてみる。

dart catchErrorのチェーン

catchErrorをチェーンしたときの理解があやしかったので試した

void main() async {

  // catchErrorで例外を投げる
  _future(true)                    // o:実行される x:されない
      .catchError((e) => throw e)  // x
      .then((v) => _future(v))     // o
      .catchError((e) => throw e)  // o
      .then((v) => _future(v))     // x
      .catchError((e) => throw e); // o

  // catchErrorで何もしない
  _future(true)
      .catchError((e) => {})       // x
      .then((v) => _future(v))     // o
      .catchError((e) => {})       // o
      .then((v) => _future(v))     // o
      .catchError((e) => {});      // o
}

Future<bool> _future(bool b) async {
  if (b) {
    return false;
  } else {
    throw Exception();
  }
}

catchErrorで例外を投げると次のcatchErrorは実行されてしまう。例外投げたら処理中断したい場合が多いと思うのでcatchErrorを複数チェーンするのはやめたほうが良さそう。素直にこう書く

try {
  _future(true)
      .then((v) => _future(v))
      .then((v) => _future(v));
} catch(e) {
}

以上です

dart Future型を戻り値とする関数(メソッド)

Futureを返すメソッドの理解があやしかったので実験してみた
バージョン : Dart 2.6.0 (build 2.6.0-dev.1.0 d6c6d12ebf)

Future<int>

Future<int> _future() {
  // OK
  // return Future.value(1);

  // Warning : info: This function has a return type of 'Future<int>'
  // 何も返さない

  // NG : error: The return type 'int' isn't a 'Future<int>'
  // return 1;
}

Future<int> async

Future<int> _future() async {
  // OK
  // return 1;
  // return Future.value(1);

  // Warning : info: This function has a return type of 'Future<int>'
  // 何も返さない
}

Future<void>

Future<void> _future() {
  // OK
  // return Future.value(null);

  // Warning : info: This function has a return type of 'Future<void>'
  // 何も返さない

  // NG : error: Missing return value after 'return'
  // return;
}

Future<void> async

Future<void> _future() async {
  // OK
  // return;
  // 何も返さない
  // return Future.value(null);
}

・結論
async つければ Futureかえしても値をそのまま返しても良いみたい。Futureつけるときはasyncつけて簡潔に書く。以上です

flutter ビルドできないときに確認すること

Android Studioでflutterアプリを実機にインストールできないときの備忘録

xcodeのSigning Teamが選択されていない

・現象
iOSの実機にインストールできない

エラー

2019-10-17 13:16:41.773 ios-deploy[39315:228336] [ !! ] Error 0xe80000be: This application's application-identifier entitlement does not match that of the installed application. These values must match for an upgrade to be allowed. AMDeviceSecureInstallApplication(0, device, url, options, install_callback, 0)
Could not install build/ios/iphoneos/Runner.app on 53d01d7b3b0eb6a24f2adb9d9f99c212573a7d16.
Try launching Xcode and selecting "Product > Run" to fix the problem:
  open ios/Runner.xcworkspace

Error launching application on xxxxxのiPhone.

・解決策
xcodeの「Signing & Capabilities」より「Team」を選択する

flutter が違う証明書を使用している

・現象
xcodeから直接ビルドはできるが、AndroidStudioからビルドできない

エラー

Error output from Xcode build:
↳
    2019-10-17 13:21:23.638 xcodebuild[40625:235490]  DVTProvisioningProfileManager: Failed to load profile "/Users/xxxxx/Library/MobileDevice/Provisioning Profiles/xxxxx.mobileprovision" (Error Domain=DVTProvisioningProfileProviderErrorDomain Code=1 "Failed to load profile." UserInfo={NSLocalizedDescription=Failed to load profile., NSLocalizedRecoverySuggestion=Profile is missing the required UUID property.})
    2019-10-17 13:21:29.655 xcodebuild[40625:235483]  DVTPortal: Service '<DVTPortalAppIDService: 0x7f9fc2ef4680; action='addAppId'>' encountered an unexpected result code from the portal ('9401')
    2019-10-17 13:21:29.655 xcodebuild[40625:235483]  DVTPortal: Error:
    Error Domain=DVTPortalServiceErrorDomain Code=9401 "An App ID with Identifier 'xxx.yyy.zzz' is not available. Please enter a different string." UserInfo={payload=<CFBasicHash 0x7f9fc2ef5260 [0x7fff9505d8e0]>{type = mutable dict, count = 10,
    entries =>
    	0 : <CFString 0x7f9fc2ef5f70 [0x7fff9505d8e0]>{contents = "requestId"} = <CFString 0x7f9fc2ef53f0 [0x7fff9505d8e0]>{contents = "C94C1E95-4601-46B6-919C-09E769FAA274"}
    	1 : responseId = <CFString 0x7f9fc2ef5670 [0x7fff9505d8e0]>{contents = "c0e66a1e-a862-4052-9988-5a8c0c8d2165"}
    	2 : <CFString 0x7fff950ba5b8 [0x7fff9505d8e0]>{contents = "protocolVersion"} = QH65B2
    	3 : <CFString 0x7f9fc2ef5550 [0x7fff9505d8e0]>{contents = "requestUrl"} = <CFString 0x7f9fc2ef5590 [0x7fff9505d8e0]>{contents = "https://developerservices2.apple.com/services/QH65B2/ios/addAppId.action"}
    	6 : <CFString 0x7f9fc2ef54f0 [0x7fff9505d8e0]>{contents = "userLocale"} = en_US
    	8 : resultCode = <CFNumber 0x160cc8f9a965e017 [0x7fff9505d8e0]>{value = +9401, type = kCFNumberSInt64Type}
    	9 : userString = <CFString 0x7f9fc2ef3d80 [0x7fff9505d8e0]>{contents = "An App ID with Identifier 'xxx.yyy.zzz' is not available. Please enter a different string."}
    	10 : <CFString 0x7f9fc2ef2a80 [0x7fff9505d8e0]>{contents = "resultString"} = <CFString 0x7f9fc2ef3d80 [0x7fff9505d8e0]>{contents = "An App ID with Identifier 'xxx.yyy.zzz' is not available. Please enter a different string."}
    	11 : httpCode = <CFNumber 0x160cc8f9a9419117 [0x7fff9505d8e0]>{value = +200, type = kCFNumberSInt64Type}
    	12 : <CFString 0x7f9fc2ef5eb0 [0x7fff9505d8e0]>{contents = "creationTimestamp"} = <CFString 0x7f9fc2ef5f10 [0x7fff9505d8e0]>{contents = "2019-10-17T04:21:29Z"}
    }
    , NSLocalizedDescription=An App ID with Identifier 'xxx.yyy.zzz' is not available. Please enter a different string.}
    ** BUILD FAILED **


Xcode's output:
↳
    note: Using new build systemnote: Planning buildnote: Constructing build descriptionerror: Failed to register bundle identifier. The app identifier "xxx.yyy.zzz" cannot be registered to your development team. Change your bundle identifier to a unique string to try again. (in target 'Runner' from project 'Runner')error: Provisioning profile "iOS Team Provisioning Profile: *" doesn't support the Associated Domains and Push Notifications capability. (in target 'Runner' from project 'Runner')error: Provisioning profile "iOS Team Provisioning Profile: *" doesn't include the aps-environment and com.apple.developer.associated-domains entitlements. (in target 'Runner' from project 'Runner')warning: The use of Swift 3 @objc inference in Swift 4 mode is deprecated. Please address deprecated @objc inference warnings, test your code with “Use of deprecated Swift 3 @objc inference” logging enabled, and then disable inference by changing the "Swift 3 @objc Inference" build setting to "Default" for the "Runner" target. (in target 'Runner' from project 'Runner')

Could not build the precompiled application for the device.

Error launching application on xxxxxのiPhone.

・解決策
flutter run コマンドで証明書が選択できる

$ flutter run -t lib/main.dart

Launching lib/main.dart on xxxxxのiPhone in debug mode...
Multiple valid development certificates available (your choice will be saved):
  1) iPhone Developer: xxxxx (xxxxx)
  2) iPhone Developer: xxxxx (xxxxx)
  3) iPhone Developer: xxxxx (xxxxx)
  a) Abort
Please select a certificate for code signing [1|2|3|a]: 

設定されている証明書を変更したい場合は1度クリアしてからflutter runする

$ flutter config --clear-ios-signing-cert

AndroidStudioのRunボタンが押せない

・現象
Runボタン(右向三角の再生ぽいやつ)が押せない
Run/Debug Configurationsが設定されていないから

・ここで設定
f:id:yoppy0066:20191018120225p:plain

Dart Endpointを設定
f:id:yoppy0066:20191018120246p:plain

以上です

flutter android studioでdebug printが表示されない

iPhoneにビルドしてdebug printを確認したかったのだがAndroidStudioのコンソールになかなか表示されず躓いた

うまくいかないやつ

print("表示されない");
debugPrint("これも表示されない");

うまくいったやつ

import 'dart:developer';
・・・
log("やっと表示された");

ちなみに上の記述ではRunでもDebugでのビルドでもダメだった。あと、細かい挙動としてRunでビルドした場合はコンソールの「Run」に表示されて。Debugでビルドした場合はコンソールの「Logcat」に表示された

以上です

ionic(cordova)でsignin with appleを実装するプラグイン

iOS13のリリースも近いがcordovaアプリでも当然実装しないとならなくなりそう
ionicで標準装備してくれるとありがたいが、まだなさそうなのでサードパーティプラグインを使うしかなさそう

github.com
こちらで

以上です

ios SideMenu ライブラリの使い方

アプリの左から開くサイドメニューを作りたくライブラリを探したらけっこう出てきた。こちらの使い方を調べたので簡単にまとめておく
github.com

今回やるながれはこんな感じ
1. メニュー化するViewControllerの作成
2. AppDelegateで1.で作成したViewControllerをメニューとしてセットアップ
3. メニューを開く
4. メニューを閉じて値を返す

cocoapodsまたはcarthageで他のライブラリとおなじように入れられるのでそれは省略

1. メニュー化するViewControllerの作成

今回はMenuViewControllerとかにする

MainViewController.swift

import UIKit
import SideMenu

class LeftMenuViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

	// 背景色は白にする
	view.backgroundColor = .white

	// メニューとするViewControllerはナビゲーションをもつので、見た目とか調整
	self.navigationController?.navigationBar.tintColor = .clear
	self.navigationController?.navigationBar.barStyle = UIBarStyle.black
	self.navigationController?.navigationBar.isTranslucent = true
	self.navigationController?.navigationBar.backgroundColor = .green
    }
}
2. AppDelegateで1.で作成したViewControllerをメニューとしてセットアップ

AppDelegate.swift

import UIKit
import SideMenu

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
	// アプリ起動時に表示するViewControllerを設定
	let viewController = ViewController()
	let navigationController = UINavigationController(rootViewController: viewController)
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.backgroundColor = UIColor.white
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()

        // さきほど作ったメニュー用のMenuViewControllerをSideMenuにセットする
        let menuViewController = MenuViewController()
        let menuNavigationController = SideMenuNavigationController(rootViewController: menuViewController)
        SideMenuManager.default.leftMenuNavigationController = menuNavigationController
        SideMenuManager.default.addPanGestureToPresent(toView: navigationController.navigationBar)
        SideMenuManager.default.addScreenEdgePanGesturesToPresent(toView: navigationController.view)

        // Readmeに書かれているオプションもここで指定できる
        leftMenuNavigationController.statusBarEndAlpha = 0
        leftMenuNavigationController.menuWidth = 300
        leftMenuNavigationController.enableSwipeToDismissGesture = false
        return true
    }

    ・・・
}
3. メニューを開く

ViewController.swift

import UIKit
import SideMenu

class ViewController: UIViewController {

   ・・・

   @objc func showMenu(_ sender: UITapGestureRecognizer){
     // ここでメニューが開く
     let menu = SideMenuManager.default.leftMenuNavigationController!
     present(menu, animated: true, completion: nil)
   }
}
4. メニューを閉じて値を返す

メニュー側の画面にメニューを閉じる処理と呼び出し元に値を返してやる処理を追加

class MenuViewController: UIViewController {

   ・・・

    @objc func onTap(_ sender: UITapGestureRecognizer){
        dismiss(animated: true, completion: nil)

        NotificationCenter.default.post(
          name: Notification.Name("SelectMenuNotification"),
          object: nil,
          userInfo: ["hoge":"fuga"] // 返したいデータをセットするよ
        )
    }

呼び出し側でメニューから値を受け取る

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // メニューからの通知を受け取れるようにするよ
        NotificationCenter.default.addObserver(
          self,
          selector: #selector(catchSelectMenuNotification(notification:)),
          name: Notification.Name("SelectMenuNotification"),
          object: nil
        )

        ・・・
    }

    @objc func catchSelectMenuNotification(notification: Notification) -> Void {
	// notification.userInfo にメニューからの返り値を取得できるよ
    }
}

SideMenuのライブラリは色々あるけど今回試したのはこんな感じでした。以上です