mthr blog

あれやこれを書いたり

初めての AliExpress

先日 AliExpress を利用しました。海外からの輸送は、Apple の CTO 商品や、クラウドファンディングのリターン品で受け取ることはありますが、自分で注文して届くのは初めてです。せっかくなので記録を残します。

まず登録ですが、名前は日本語、住所はローマ字で入力で大丈夫です。ローカライズ対応されているので、フォームに従って入力するだけで簡単でした。支払いは PayPal で選択しました。

そして最も気になる配送方法は、AliExpress Standard Shipping で発送されました。2/25 に中国から発送されて、3/2に日本到着して、3/6に到着しました。10日間で届きました。荷物追跡もできて、配達も早いです。

今回の国内輸送は郵便局の国際eパケットライトになりました。普通郵便扱いのため、土日配送なしでした。郵便局に直接取りに行こうかと思いましたが、普通郵便なので一度配達されて不在票でもないと無理でした。荷物追跡もできて、配達も早くてよかったです。

なんというか、ブログを書くほどもないぐらい簡単でした。輸送に時間はかかりますが、1つの通販サイトの候補として使えることがわかりましたね。

転職エントリを少し混ぜながら、2022年を振り返る

2022年は久しぶりの転職もあり、環境が変わった年であった。今年は転職活動の前半と、転職後の後半に大きく分かれる。いろいろ面白い年であったので、大雑把に振り返りをしてみた。思い出しながら書くので、時系列は若干ズレてるかもしれない。

2022/01 ~ 2022/05

2021年後半に転職を決意したので、去年(2021年)の終わり頃から今年(2022年)前半にかけて転職活動を行なっていました。私が求める条件は、フルリモートで居住地に縛りなく働ける、iOSアプリ開発を主軸にして楽しいと思える開発ができる、ハードワークにならない環境である、あと給与関連は下がらなければよいです。コロナ禍で社会的に働き方が変わったので、なるべく多くの会社のカジュアル面談でお話しを聞いてそれぞれ働き方を知る、その中でより興味をもった会社の選考に進むという形を取りました。

私の転職活動は、おもに Wantedly や Findy を利用して、私が楽しいと思う仕事ができる会社、iOSDC の登壇やスポンサーなど、エンジニアやそのコミュニティの成長に対して積極的な会社を中心に探しました。当時のメモを見返すと、約10社のカジュアル面談を受けてました。各社の担当者様、貴重なお時間をいただき、面談をしてもらい、ありがとうございました。オンライン中心の勉強会で懇親会がなくなった今、たわいもない雑談もする機会がなくなっていたので、カジュアル面談は会社情報に限らず、様々なお話しができて、とても楽しかったです。

その中で、ゆめみさんから LAPRAS のメールを頂きました。とても気になっていた会社の1つでした。かつて転職活動で採用選考を受けた経験があり、同時期に受けた他社(友人のリファラル)から先に内定をもらったため、選考を途中で断った経緯があるので、後ろめたい気持ちがありました。しかし、せっかくのタイミングを捨てるのはもったいないと思い、カジュアル面談を設定してもらいました。そして、ご縁があり、GW前に内定をいただき、6月下旬にゆめみに入社しました。

ちなみに、私は当時 LAPRAS のサービスを利用してなかったので、突然届いたメールが本物かどうか怪しんでました。さらに、採用課題の作成期間が、ちょうど私のエルデンリングのプレイ中、かつ、ラスボスの手前でいつか来る分からないアップデートで使用武器が弱体化される危機が迫っていました。ここはエルデンリング・・・ではなく、採用課題の方を取り、エルデンリングは一週間ほどお休みしました。課題の評価が返ってくるのをドキドキしながら、ラスボスを撃破しました。

2022/06

ゆめみに転職しました。全員CEOや給与自己決定制度など破天荒なイメージがありますが、ちゃんとしっかりしている会社でした(他の人の入社エントリでもよく見かける)。オンボーディングプログラムはしっかりしていて、ゲーム感覚で進めて、ちゃんと自走できるようになっていて、よく出来ていると思いました。

コーヒーノキの栽培を始める。たまたま見かけたのでなんとなく購入しました。帰って調べると、露地栽培で10mぐらいになるらしくビビる。鉢栽培で実がなるように世話をすることを決意しました。寒さにあまり強くないらしく、今年の冬が越せるか注意しています。コーヒーノキの前でドリップコーヒーを入れて飲むサイコパスプレイが毎日の日課です。

夏野菜(ナス、キュウリ、オクラ)の栽培を始める。オクラは今年初めて挑戦したので、どのようになるのがワクワクでした。オクラの成長は他の夏野菜と比べて遅く失敗したかと思いましたが、夏に入ってから伸びて毎週収穫できるぐらいになった。オクラは寒くなった秋口ぐらいまで収穫できていた。

2022/07

約2年ぶりに犬を飼い始める。2020年10月に愛犬が亡くなった。当時はペットロスによる精神不安になることもありましたが、時間も経ち落ち着いてきたので、また飼いたいと思い始めていた。ちょうど近くのペットショップでジャックラッセルテリアがいるとのことで購入しました。またオンボーディングで社内制度を調べている最中に ペット慶弔 があるのを見つけたので、利用しました。ペット関係は値段が上がっているので、ありがとう弊社。

( ˘ω˘ )スヤァ なジャックラッセルテリア

2022/08

ゆめみの代表的なユニークな制度の1つの有給取り放題制度を初めて利用する。一般の会社は入社直後は法定有給が付与されないので、体調不良や突然イベントがあった場合に休めないと不安がありますが、有給取り放題制度を使えば休めます。なんなら、入社日の翌日からも使えるそうです。すごい。と言うわけで、夏休みをゆったり取りました。年末年始の冬休みもがっつり取る予定です。

KyotoLT の運営への自身の意識をリブートする。京都のローカル勉強会として発足した KyotoLT の運営をしています。以前の転職で京都から離れたことで、すこし離れてました。しかし、京都(が本社の会社)に戻ってきた、オンライン開催で地域の縛りは無くなってきた一方、地方の小規模勉強会が少なくなってきた、社会が平常に戻った時にエンジニアが気軽に集まる場所を残しておきたいと思い、再び運営を積極的にするようにしました。今年は8月と12月に開催しました。年に3, 4回ぐらい定期的に開催したいですね。

ゆめみの勉強会 YUMEMI.grow の運営に参加する。前述通り、勉強会開催は好きなので、楽しいです。これまでの運営経験を会社勉強会に利用したり、会社勉強会で培ったノウハウを KyotoLT で活用したり、Win-Winです。

2022/09

無事に試用期間が終わる。ただ、これまで試用期間でどうとか聞いたのことないですが、形式的に無事に越えたので嬉しい。

勉強し放題制度を利用して、福岡市へ日帰り出張をしました。社内で開催されている「熊谷さんのやさしいSwift勉強会」が福岡で開催されることがあり、私は隣の山口県に住んでいるので現地参加しました。これまでの出張だと、上長に話をして、稟議書をあげて旅費を確保して・・・と様々な社内イベントを突破する必要がありましたが、簡単な確認だけで出張できました。はやり弊社は面白い。

ついでに福岡の大仏を拝みに行きました

iOSDC Japan 2022 が開催されました。iOSDCは幸いにも第一回から参加してます。 チケット購入タイミングは入社前で、コロナ禍の様子もわからなかったので、オンラインで参加しました。開催時期は比較的落ち着いていた時期だったので、現地参加したかったです。また今回の転職は、iOSDCでスポンサーしている会社から探したので、iOS関連情報だけじゃなく、転職ができたので本当に感謝です。

2022/10

案件も慣れてきたので、個人開発で遊んでました。以前Amazonのセールで話題になったサーマルプリンター付きの業務用スマホ SUNMI V2 PRO を手に入れたので、アプリを作ってました。リアルで対面したときに、自己紹介の掴みに使っています。ソースコードGitHub で公開して、12月に発表しました。

www.youtube.com

2022/11

私物の Windows ノートパソコン 2台 が同時期に壊れるという、そんなんあるんかということが起こりました。キーボードがチャタリングを起こし常に a を入力しつづける、ファンの動作が不安定になり起動がままならない。ちょうど、タイムセールやブラックフライデーで安かったので、2台を買い替えました。新しいマシンをセットアップ後、古いマシンを捨てようと再び触ると何故か正常になっていた、えっ?えっえっ?。2台あったノートパソコンが、4台になってしまった。そんなんあるんかということがさらに起こった。さすがに4台もいらない、Windows ノートパソコンは MacBook みたいに高く売れるわけじゃないので、知人にあげたりしようと思っている。

2022/12

懇親会制度と勉強し放題制度を利用して、二泊三日で東京と京都への懇親会&勉強会参加の出張をしました。およそ3年ぶりの東京と京都です。リモートワークなので初めて会う人ばかりなので懇親会は楽しかったです。東京の懇親会の翌日に京都で勉強会があったので、勉強し放題制度を利用して京都に寄りました。こちらも久々の対面イベントで楽しかったです。しかし、移動ばかりで疲れました。

以前の職場があった京都リサーチパーク。ここで開催された勉強会に行きました

今年2022年は?

転職もあり、犬をまた飼い始めるなど、大きな変化がありました。仕事も日常も楽しく刺激ある日々を送りました。コロナ禍もワクチン接種が進み、比較的落ち着いてきたと思うので、徐々にですが、活動的な日常生活に戻してきてます。

来年2023年は?

引き続きプログラミングは仕事や趣味でも向上したいです。個人開発で放置気味のアプリをモダンに書き換えるのがまずの目標です(目標というかやる)。また、英語の勉強をしたいですね。他社のような英語標準化のような義務感はないですが、英語がもっとわかるようになるとよいですね。あと、なんとなくですが、ちょっと強いゲーミングデスクトップPCが欲しいですね。特にゲームをする予定はないですが、なんとなく欲しい。

ReSwift + MVVM で副作用を管理する (SwiftUI)

単一方向のデータフローのアーキテクチャiOS および Swift で実現する ReSwift において、アクションを発行してデータ更新と共にイベントを発火させたい。

あるイベントを発火させるには、通常であれば、呼び出す・表示させるコンポーネントでイベント制御を定義しますが、これは呼び出し箇所やパターンにより複雑になる場合あります。単一のデータフローの仕組みに乗って制御できれば、シンプルに制御できるのでは?。

今回は、アクションを発行して、トースト ToastUI を表示する例をまとめました。サンプルコードは こちら で参照できます。例は SwiftUI で書いてますが、Storyboard でも大方同じような書き方でできます。

まず Redux を作る

トーストデータを管理する Redux を作成します。

Toast Module

  • ToastState.swift
    • トーストのデータ(テキストデータ、表示タイプ)を定義してます
    • トーストのデータはキュー管理でできるようにしました
import Foundation

enum ToastType {
    case info
    case success
    case error
}

struct ToastItem: Identifiable, Equatable {
    let id: Int
    let message: String
    let type: ToastType
}

struct ToastState {
    let items: [ToastItem]
    static func initialState() -> ToastState {
        ToastState(items: [])
    }
}
  • ToastActions.swift
    • 表示および非表示のアクションを定義しました
import Foundation
import ReSwift

enum ToastActions: Action {
    case enqueueToast(message: String, type: ToastType? = ToastType.info)
    case dequeueToast(id: Int)
    case clearToast
}
  • ToastReducer.swift
    • アクションを受けて、state を更新します
    • id は現時刻の UNIX時間 を設定しました
import Foundation

func toastReducer(action: ToastActions, state: ToastState) -> ToastState {

    switch action {
    case .enqueueToast(message: let message, type: let type):
        let id = Int(Date().timeIntervalSince1970)
        let item = ToastItem(id: id, message: message, type: type ?? .info)
        return ToastState(items: state.items + [item])

    case .dequeueToast(id: let id):
        let next = state.items.filter { $0.id != id }
        return ToastState(items: next)

    case .clearToast:
        return ToastState(items: [])
    }
}
  • ToastSelectors.swift
    • AppState からトーストに関するデータを取得します
    • 時系的には後述の AppStore の設定後に定義します
import Foundation

func selectToastItems(state: AppState) -> [ToastItem] {
    state.toast.items
}

func selectToastItem(state: AppState) -> ToastItem? {
    let items = selectToastItems(state: state)
    return items.first
}

func selectToastItemId(state: AppState) -> Int {
    let items = selectToastItems(state: state)
    return items.first?.id ?? -1
}

AppStore

トースト用の管理 module からアプリで使う Store を生成していきます。ここは ReSwift の一般的な導入手順と同じです。

  • AppStore.swift
import Foundation
import ReSwift

func makeAppStore() -> Store<AppState> {
    let store = Store<AppState>(
        reducer: appReducer,
        state: AppState.initialState()
    )
    return store
}
  • AppState.swift
import Foundation

struct AppState {
    let toast: ToastState
    static func initialState() -> AppState {
        AppState(
            toast: ToastState.initialState(),
        )
    }
}
  • AppReducer.swift
import Foundation
import ReSwift

func appReducer(action: Action, state: AppState?) -> AppState {

    let state = state ?? AppState.initialState()

    var nextToast = state.toast
    if action is ToastActions {
        nextToast = toastReducer(action: action as! ToastActions, state: state.toast)
    }

    return AppState(
        toast: nextToast,
        inAppWeb: nextinAppWeb
    )
}

Toast View + ViewModel

トーストデータを取り扱う ViewModel を生成します。View から Redux を直接呼び出すことをせず、ViewModel 経由で操作するようにしています。これは、開発中に Redux を意識させず複雑になるのを防ぐため、他の Redux ライブラリに差し替える場合に簡単にできるようにするためです。

  • ToastViewModel.swift
    • アクションで設定された表示データを View に渡す働きをします
import Foundation
import ReSwift

final class ToastViewModel: ObservableObject, StoreSubscriber {

    typealias StoreSubscriberStateType = ToastItem?

    @Published var item: ToastItem?
    private var itemId: Int?

    init() {
        appStore.subscribe(self) {
            $0.select { selectToastItem(store: $0) }
        }
    }

    deinit {
        appStore.unsubscribe(self)
    }

    func newState(state: ToastItem?) {
        self.item = state
        self.itemId = state?.id
    }

    public func enqueueToast(message: String, type: ToastType?) {
        appStore.dispatch(ToastActions.enqueueToast(message: message, type: type))
    }

    public func dequeueToast() {
        if let itemId = self.itemId {
            appStore.dispatch(ToastActions.dequeueToast(id: itemId))
        }
    }

    public func clear() {
        appStore.dispatch(ToastActions.clearToast)
    }
}
  • ToastView.swift
    • 一般的な ViewModel による表示制御を行なっています
    • ZStack で重ねているのは、あまり良くないかも
import SwiftUI
import ToastUI

struct Toast: View {

    @ObservedObject private var viewModel = ToastViewModel()

    var body: some View {

        ZStack {
        }.toast(item: $viewModel.item,
                dismissAfter: 2.0) {
            viewModel.dequeueToast()
        } content: { item in
            VStack {
                Spacer()
                Text(item.message)
                    .bold()
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.green)
                    .cornerRadius(8.0)
                    .shadow(radius: 4)
                    .padding()
            }
        }

    }
}

アプリに適用する

Toast コンポーネントを root に設定して終わりです。トーストを表示させたい箇所でToastViewModel#enqueueToast を適宜呼び出せば OKです。

import SwiftUI

let appStore = makeAppStore()

@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
            Toast()
        }
    }
}

まとめ

React Native のアプリ開発で Redux saga を使った開発に携わり、シンプルなデータ管理・イベント制御ができて、良い開発体験を感じました。その開発手法を iOS へフィードバックして、ネイティブ開発をしたいと日々思っています。トーストの他、アプリ内ブラウザ(SFSafariViewController)の表示もアクション発行に紐づけて開発しています。

いまは state の変化を監視してますが、本当に Redux saga のようにアクションを発行したというのを監視して実装したいですね。今のところ、活発な ReSwift + saga の OSS はないので、どうするか。ワンチャン、作るか・・・。

React Native 0.67.x にアップデートしようと思ったら、Expo 44.x の対応で右往左往した話

先日、React Native を 0.67.x にアップデートしました。経験的に、React Native のメジャーアップデート(数字的にはマイナーバージョンアップデート?)の場合は、何から問題があったりしますが、今回は違った大変さだったので、記録として残したいと思います。

React Native 0.67.x にアップデートしよう

既存アプリを早速 0.67 に挙げたところ、Android でビルドに失敗しました。ログを確認したところ、Gradle が 7.2 にアップデート されたことに伴い、Expo で失敗していました。Expo を Bare Workflow で利用してました。すぐさま、Expo の issue を見に行ったところ、対応中ということで、対応版のリリースを待つことになりました。

翌週、対応修正された Expo 44.0.6 が無事にリリースされました。それではと、0.67 のアップデート作業を再開したところ、今度は、iOS がビルドできない・・・。

Expo 44.x に対応する

またまた、Expo の issue を覗きに行きました。Expo 44.x で「Swift ネイティブモジュールのサポートに伴い、Objective-C と Swift の統合で問題が起った。具体的には、ヘッダーファイルのインポートで Swift が正しく解釈できないために問題が起こる」とのこと。詳しくは Kudo さんのコメント を参照してください。

iOS 向けにネイティブモジュールを利用している一部のライブラリで、この Expo 由来の問題が発生しました。Expo の導入で、どうしても Expo の設定に引っ張られるため、対応せざるを得ないようでした。

Expo は素晴らしいサービスですが、React Native アプリの1ライブラリである Expo のせいで、他のライブラリを修正対応しないといけないのは何か考えることはあります。しかし、Expo 並のクオリティがあるもの、代わりになるものもそうそうないので、対応しました。

Expo チームが既に有名所のライブラリで issue や PR を出して、修正に動いていたので、ライブラリを対応版にアップデートするだけすみました。中には、うちのライブラリは Expo 向けじゃないから、そのままクローズするところもありましたが、これも正しい対応だと思います。また、リリースが遅いライブラリに関しては patch-package を使って、自分でパッチを作って対応しました。

その他、自分で管理していたライブラリは Expo の問題には影響しなかったのですが、Gradle 7 対応にライブラリをアップデートしました。

まとめ

React Native のアップデートで何か問題は起こるだろうと思って挑んだところ、React Native 本体とは異なるところで振り回されました。今回のアップデートは久々に疲れた。Expo の改修も前向きな意味があるものなので、いつかやらないといけないことを今回やったと思えば、よい前借りだったのではないかと思っています。

Apple Silicon で React Native を開発する

Apple Silicon 搭載の MacBook Pro で React Native を動作させたときのメモです。なお、React Native 0.66.4 で新規作成したプロジェクトで、MacBook Pro(14インチ、2021), Apple M1 Pro, macOS Monterey 12.1 で確認しています。使用環境や、プロジェクトの状況により、異なる場合があります。

開発環境

Node.js を 16 以上ににする。私は 16.13.0 を採用しました。iOS はこれだけでシミュレーション・実機のビルドが成功しました。

Android

Androidのビルドは以下のように設定してビルドができるようになりました。ただし、シミュレーターだと上手くいかず、実機ビルドのみ成功しました。

  • Kotlin を Apple Silicon をサポートした 1.5.30 以上にする。私は、1.6.10 を採用しました。
buildscript {
    ext {
        // ...
        kotlinVersion = "1.6.10"
    }
    // ...
}
  • JavaApple Silicon をサポートした 17 に変更する
% brew install java
% sudo ln -sfn $(brew --prefix)/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk
% java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment Homebrew (build 17.0.1+1)
OpenJDK 64-Bit Server VM Homebrew (build 17.0.1+1, mixed mode, sharing)
  • Gradle を Java 17 をサポートする 7.3.3 に変更する
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
buildscript {
    // ...
    dependencies {
        classpath('com.android.tools.build:gradle:7.0.2')
    }
}

Flutter with Redux Saga

本稿は Flutter アプリに Redux Saga を適用する方法を紹介します。導入方法を説明するブログはいくつか見かけるのですが、ここはあのブログ、あれは別のブログと、あちこちと参考先が多かったので、まとめました。なお、Redux Saga の自体説明は省きます。詳しくは、こちら (redux-saga/README_ja.md) などをどうぞ。

サンプルアプリ

単純なカウンターの増減を Redux Saga で制御します。コードは GitHub で公開しています。

パッケージのインストール

Redux Saga を実現するためのパッケージはすでにあるので、それらを利用します。

% flutter pub add redux_saga
% flutter pub add redux_persist_flutter
% flutter pub add flutter_redux
% flutter pub add redux_logging

Redux

単純なカウンター増減を考えるため、カウンターを記録する変数(定数)を持つクラスを作成します。このクラスを state として、扱っていきます。

@immutable
class CounterState{
  final int count;
  CounterState({required this.count});

  static initialState(){
    return CounterState(count: 0);
  }
}

次は action です。ここは enum で定義しても良いですが、引数を伴う場合も考えられるので、クラスで定義しています。

class IncreaseCount{
}

class AssignCount{
  final count;
  AssignCount({this.count});
}

actions を受けて state を書き換える reducer です。is でクラス型判定を行い、それぞれに応じて変数を書き換えていきます。引数付きの actions は型判定するので、そのまま利用することができます。

CounterState counterReducer(CounterState state, dynamic action) {
  if (action is IncreaseCount) {
    return CounterState(count: state.count + 1);
  }
  if (action is AssignCount){
    return CounterState(count: action.count);
  }
  return state;
}

テストも簡易ですが、紹介します。

void main() {

  test("counter_reducer action IncreaseCount", (){
    final state = CounterState(count: 0);
    expect(state.count, 0);

    final nextState = counterReducer(state, IncreaseCount());
    expect(nextState.count, 1);
  });
  
  test("counter_reducer action AssignCount", (){
    final state = CounterState(count: 10);
    expect(state.count, 10);

    final nextState = counterReducer(state, AssignCount(count: 99));
    expect(nextState.count, 99);
  });
}

実際は、1つのモジュールだけでなく、複数のモジュールが運用されます。そのために root も用意します。

@immutable
class RootState{
  final CounterState counter;
  RootState({required this.counter});

  static initialState(){
    return RootState(counter: CounterState.initialState());
  }
}
RootState rootReducer(RootState state, dynamic action){
  return state.copyWith(
    counter: counterReducer(state.counter, action));
}

Saga

次は、Saga です。例として action が発行されたら、toast を表示します。また、その時に state からカウンター数を取得してみました。

counterSaga() sync* {
  yield TakeEvery(_increaseCountSaga, pattern: IncreaseCount);
}

int _selectCount(RootState state) {
  return state.counter.count;
}

_increaseCountSaga({dynamic action}) sync* {
  var result = Result<int>();
  yield Select(selector: _selectCount, result:result);  
  yield Call((){
    Fluttertoast.showToast(msg: "increase count to ${result.value}");
  });
}

前項と同様に、root 用の saga を用意しておきます。

rootSaga() sync* {
  yield Fork(counterSaga);
}

Middleware

それでは、今まで作成してきたものを設定していきます。

Future<Store<RootState>> initializeRedux() async {

  // create Persistor
  final persistor = Persistor<RootState>(
    storage: FlutterStorage(),
    serializer: JsonSerializer<RootState>(RootState.fromJson),
  );

  // load initial state
  final initialState = await _loadState(persistor);

  // create the saga middleware
  final sagaMiddleware = createSagaMiddleware();

  // create store and apply middleware
  final store = Store<RootState>(
    rootReducer,
    initialState: initialState,
    middleware: [
      applyMiddleware(sagaMiddleware), 
      persistor.createMiddleware(),
      new LoggingMiddleware.printer(),
    ]);
    
    // connect to store
    sagaMiddleware.setStore(store);

    // then run the saga
    sagaMiddleware.run(rootSaga);
    
    return store;
}

Future<RootState> _loadState(Persistor<RootState> persistor) async {
  try {
    final initialState = await persistor.load();
    return initialState ?? RootState.initialState();
  } on Exception {
    return RootState.initialState();
  }
}

永続化するための、state にシリアライズを実装します。今回は手動でやってますが、json_serializable などで自動生成しても問題ありません。

class CounterState{
  static CounterState fromJson(dynamic json) {
    try{
      return CounterState(count: json["count"] as int);
    } on Exception {
      return CounterState.initialState();
    }
  }

  dynamic toJson() {
    return {"count": count};
  }
}

Widgets

準備が整いましたので、Widgetと連携させましょう。main 関数が非同期なのが気持ち悪いところですが、state の保存データを非同期で読み込んでいるので問題ありません。なお persist を利用しているので、WidgetsFlutterBinding.ensureInitialized();runApp() の前に呼んでおく必要があります。

/// main.dart
Future<void> main() async {
  
  WidgetsFlutterBinding.ensureInitialized();
  final store = await initializeRedux();
  
  runApp(new StoreProvider(
    store: store,
    child: MyApp(),
  ));
}

次に、Redux Saga で制御したい WidgetStoreConnector で囲みます。他のWidget が意味なく再レンダリングされるのを防ぐためです。builder の第二引数が converter の戻り値に対応しており、converter で、state や actions の発行を設定して、それぞれのWidget で表示やタップイベントを行ます。

class CounterPage extends StatelessWidget{
  CounterPage();

  Widget counterWidget(){
    return StoreConnector<RootState, int>(
      distinct: true,
      converter: (store) => store.state.counter.count,
      builder: (context, count){
        return Center(child: Text("count:" + count.toString()));
      },
    );
  }

  Widget increaseButtonWidget(){
    return StoreConnector<RootState, VoidCallback>(
      distinct: true,
      converter: (store) => () => store.dispatch(IncreaseCount()),
      builder: (context, onPressed){
        return TextButton(
          onPressed: onPressed,
          child: Text("increase"),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("counter"),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          counterWidget(),
          increaseButtonWidget(),
        ],),
    );
  }
  
}

まとめ

長くなりましたが、これで Flutter アプリに Redux Saga を導入できました。Redux Saga は状態管理がシンプルになり、ロジックとUIを切り離すことができるので、非常に便利です。Flutterの状態管理はいくつかありますが、Redux は Flutter 以外にも使用が多い方法であるので、まず使って問題はないと思っています。良い状態管理で、良い開発を。

Apple Watch Series 5 を買った

f:id:mitsuharu_e:20191006132352j:plain

Apple Watch Series 5 を購入しました。初代(Series 1ではなく無印)以来です。

初代はワクワクして買ったものの、機能が少ない上にハードも未熟で、毎晩の充電器に載せるためだけに着けているのではと思うぐらい不満がありました。開発のためと割り切っても、使っている人は同じアプリ開発者だけで、あまり期待が持てませんでした。まもなくして売却し、一万円で買えるメンテナンスフリーなソーラー電波時計をつけました。

そんな迷機は Series ナンバリングが始まり、ハードやOSのスペックが上がってきました。Series 3, 4あたりから常用できるレベルになったのでは?と思い始め、再び気になっていました。そして、今回諸所のタイミングが重なり、Series 5を購入しました。

まだ機能を堪能しているわけではないですが、電池持ちが以前のよりもよく良い印象です。投げ出していたWatchアプリも再びやろうかなと思います。