パターン別にProviderがキャッシュを使うかどうかを調査した

目的

Riverpodが提供するProviderは、値をキャッシュしてbuildのたびに処理が動かないようにしてくれている。 FutureProviderでWebAPIをコールする処理がbuildの度に実行されないのは便利だ。

しかし、キャッシュを使いたくないと思っていたのに使われていたということも起こりがちなので、どういう場合にキャッシュが使われるのかを把握しておくことは重要だ。

今回は、下記のパターンでキャッシュが使われるかどうかを確認した。

Providerのパターン

  1. 基本のProvider
  2. 内部にref.watch(値が変化する)を持つProvider
  3. 引数ありで引数が毎回変化する (Provider.family)
  4. 引数ありで引数は常に一定の値 (Provider.family)
  5. 自動破棄 (Provider.autoDispose)

状況のパターン

WidgetにてそれぞれのProviderをref.watchして、下記の状況でキャッシュが返却されるかどうかを確認する。

  1. buildする
  2. build後、再度buildする(リビルド)
  3. build後、画面をdisposeして再度build

結果

パターンbで結果が分かれる理由

Riverpodの仕様で、下記の場合は再計算を行うから。

  • 内部のref.watchの値が変化した場合
  • 引数の値が変化した場合

パターンcで結果で分かれる理由

autoDisposeを指定している場合、画面がdisposeされProviderをwatchするWidgetがなくなると、Providerは破棄される。 その後、画面を再度生成すると、パターンaと同様の状況になり値が新規に計算される。

一方、1~4のProvoderは画面の破棄だけでは影響をうけないので、更新や破棄などが発生せずキャッシュが返却される。(画面の破棄によって影響を受けるように作れば当然再計算されます。)

実装で気を付けること

画面と同じ寿命を持つProvoderにはautoDisposeをつける

例えば、ポップアップ画面を表示する際に最新のデータをProviderで取得して表示する。という機能があるとする。

この場合、一度画面を閉じて再度開いた時には最新の値が表示されることが期待される(キャッシュをだしてほしくない)ので、autoDisposeを指定しておくことが望ましい。

そすれば、画面が破棄されるのと同時にProviderは破棄され、再度画面を開いた際にProviderが再び値を再計算してくれる。

FutureProviderでWebAPIをたたく場合

現在時刻を返却する、引数のないWebAPIがあるとする。このAPIにFutureProviderでアクセスして値を取得し、画面表示する機能を考える。

今回の結果から言うと、このFutureProviderはリビルドを行っても毎回キャッシュを返却してしまう。

ビルドのたびに常に最新の値を取得したいというニーズがある場合は、FutureProviderの内部にref.watchを配置するなどの工夫を行う必要がある。

How To Mock a Node Module Which Provides Function using Jest

//app.js (This is a target of testing.)
const moduleA = require("./moduleA.js")();

module.exports = (()=>{
    console.log(moduleA.showValue());
    moduleA.add(10);
    console.log(moduleA.showValue());
})();
//moduleA.js (External library like a web API and database module)
const moduleA = require("./moduleA.js")();

module.exports = (()=>{
    console.log(moduleA.showValue());
    moduleA.add(10);
    console.log(moduleA.showValue());
})();
//app.test.js (BAD CASE)

const moduleA = require('../moduleA.js')();
jest.mock('../moduleA.js', () => () => {
    return  {
        add: jest.fn(),
        showValue: jest.fn().mockImplementation(() => 1000)
    };
});

describe("app", () => {
    test("", () => {
        require("../app.js");
        expect(moduleA.add).toHaveBeenCalled(); //----->Failed. jest says add has never been called.
        expect(moduleA.showValue).toHaveBeenCalled();
    });
});
//app.test.js (GOOD CASE)

const moduleA = require('../moduleA.js')();
jest.mock('../moduleA.js', () => () => {
    this.instance = this.instance || {//---> This mock should be singleton.
        add: jest.fn(),
        showValue: jest.fn().mockImplementation(() => 1000)
    };
    return this.instance;
});

describe("app", () => {
    test("", () => {
        require("../app.js");
        expect(moduleA.add).toHaveBeenCalled(); //-----> Pass!
        expect(moduleA.showValue).toHaveBeenCalled();//-----> Pass!
    });
});

Here is all source code.
github.com

Lambda関数からLambda関数を呼び出す方法

やりたいこと

とあるLambda関数Aから別のLambda関数Bを呼び出して、
関数Bの処理が終わったら関数Aの処理にもどるような仕組みをつくりたい。

環境

ソースコード

Lambda関数 test_method_A
この関数には、事前に「lambda:InvokeFunction」の権限を与えておくこと。

import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; 


export const handler = async (event) => {
    const client = new LambdaClient();
    const input = {
        FunctionName: "test_method_B"
    };

    const command = new InvokeCommand(input);
    const res = await client.send(command);
    
    //Uint8Arrayで返却されるためデコードする。
    const decoder = new TextDecoder();
    const text = decoder.decode(res.Payload);
    console.log(text);


    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

Lambda関数 test_method_B

export const handler = async(event) => {
    const response = {
        statusCode: 200,
        body: JSON.stringify('I\'m method B!'),
    };
    return response;
};

 

実行結果

test_method_Bのデータを取得できた。

INFO	{"statusCode":200,"body":"\"I'm method B!\""}

参考リンク

docs.aws.amazon.com

Watch OSアプリと連携するFlutterアプリの作り方

Flutterアプリ側でボタンを押すと、Watch OSアプリ側のカウンターが増加するアプリを作成します。

ソースコードはこちら。
github.com


<----------------------宣伝------------------------->
Flutterアプリ制作・バグ修正承ります!
coconala.com <----------------------宣伝------------------------->




実行用シミュレーターの準備

Xcode->Window->Devices and Simulatorsを選択し、左下の+ボタンをクリックしてシミュレーターの新規作成ダイアログを表示する。

上記のように設定してNext

ペア設定するウォッチの設定をしてCreateする。

Flutterアプリを新規作成

flutter pub add flutter_watch_os_connectivity

を実行してflutter_watch_os_connectivityをプロジェクトに追加する。
下記のコードを入力してビルドする。(Build->Flutter->Build iOS)

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_watch_os_connectivity/flutter_watch_os_connectivity.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;
  final FlutterWatchOsConnectivity _flutterWatchOsConnectivity =
      FlutterWatchOsConnectivity();

  void incrementCounter() {
    counter++;
    sendMessage("$counter");
  }

  Future<void> sendMessage(String txt) async {
    bool isReachable = await _flutterWatchOsConnectivity.getReachability();
    if (isReachable) {
      await _flutterWatchOsConnectivity.sendMessage({"COUNTER": txt});
    } else {
      if (kDebugMode) {
        print("No reachable watches.");
      }
    }
  }

  @override
  void initState() {
    super.initState();
    _flutterWatchOsConnectivity.configureAndActivateSession();
    _flutterWatchOsConnectivity.activationStateChanged
        .listen((activationState) {
      if (activationState == ActivationState.activated) {
        if (kDebugMode) {
          print("activationDidCompleteWith state= ${activationState.name}");
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text(
              'Sample App',
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          incrementCounter();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

ビルドは失敗するが、iOS用のコードは生成されているはずなので次へ進む。

ウォッチアプリの作成

Xcodeを開きFlutterから生成されたiOSのプロジェクトを開く。
Flutterプロジェクト->ios->Runner.xcworkspace


Xcode上でFile->New->Targetとしてダイアログを開き、watchOSタブのAppを選択してnext

任意のProduct Nameを入力して、Watch App for Existing iOS Appを選択してfinish

新しくディレクトリが作成されるので、ContentViewファィルにウォッチ側のコードを記入する。

//
//  ContentView.swift
//


import SwiftUI
import WatchConnectivity
import AVFoundation


struct ContentView: View {
    
    @ObservedObject var connector = PhoneConnector()
    
    var body: some View {
        VStack {
            VStack {
                
                Text(String(connector.counter))
                    .font(.largeTitle)
                    .foregroundColor(Color.gray)
                
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class PhoneConnector: NSObject, ObservableObject, WCSessionDelegate {
    @Published var counter = 0
    
    override init() {
        super.init()
        if WCSession.isSupported() {
            WCSession.default.delegate = self
            
            WCSession.default.activate()
        }
    }
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("activationDidCompleteWith state= \(activationState.rawValue)")
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        print("didReceiveMessage: \(message)")
        
        //受け取ったメッセージから解析結果を取り出す。
        let result = message["COUNTER"] as! String
        
        if let c = Int(result) {
            //結果を画面に反映
            debugPrint(c)
            DispatchQueue.main.async {
                self.counter = c
            }
        }
        
    }
}

ビルドの設定

Embedded Contentの設定

ファイルツリーのRunnerをダブルクリックして設定を表示する。

左側のツリーでRunnerを選択した状態でGeneralのFrameworks,Libraries and Embedded Contentの追加ボタンをクリック

作成済みのウォッチアプリが表示されるので選択してAdd

WKCompanionBundleIdentifierの設定

続いてCounting Watch Appを選択し、InfoタブのWKCompanionBundleIdentifierにRunnerのBundleIdentierを入力する。
RunnerのBundleIdentierは、Runner->General->Identifierから確認できる。

ウォッチアプリのBundleIDを修正する

そのままSigning & Capabilitiesタブへ移動し、Bundle Identifierを修正する。

アプリの実行

ターゲットにWatchCountingSampleを選択する。

実行環境に先ほど作成したシミュレーターを指定する。

ビルドが成功すれば、シミュレーターが起動する。

その後、ターゲットにRunnerを指定し同じシミュレーターを指定してこちらも実行すると、こちらのように両方のアプリが起動する。

シングルページWebアプリの挙動を図で解説

シングルページアプリケーションは下図のような構成をとることが多い。

この構成の場合は、各サーバーの役割は、こうなる。

Webサーバー

 静的なファイル置き場

APIサーバー

 HTTPのアクセスを受け付ける
 リクエストを分析して適切な処理を動かす
 レスポンスを返す

DBサーバー

 データを保持する。

本稿では、各サーバー間をデータがどのようにながれていくのかを図で説明する。

画面の挙動とデータの流れ

1.ページへのアクセス


2.WebAPIの呼び出し


3.結果の返却と表示


ソースコードイメージ

APIサーバー

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.New()
	r.GET("/book", func(c *gin.Context) {
		//パラメータの確認 (2-②の処理)
                //データベースへのクエリ発行と結果取得 (2-③、3-①の処理)
                //クエリ結果をjsonデータへ加工してレスポンス作成 (3-②の処理)
	})

	r.Run()
}

クライアント(main.js)

function onSearchButtonClicked(keyword) {
    const response = await fetch("https://aaa.com/book?n=" + keyword);(2-①、3-③の処理)

    //結果を加工して画面表示 (3-③の処理)
}

テンプレアプリでFlutter開発をスムーズに

Flutterでアプリを作成する際、アプリが持つべき機能や構造を何度も実装するのはとても無駄なことですね。

AndroidStudioでFlutterプロジェクトを作成すると、カウンターアプリが生成されますが、欲しいコードが全然実装されてません

そこで、よく使うコードを含んだテンプレートを作成しました。
カウンターの値と、最終更新日をローカルに保持するアプリです。

github.com

テンプレアプリが持つ機能

  • ローカルデータ保存

SharedPreerenceでローカルにデータを保存する機能
データベースに置き換えて使ってもよき

言語化の予定が無くても、文字列リソース置き場として使った方がいいと思う

  • レスポンシブ

LayoutBuilderとbreakpointで、スマホタブレット、デスクトップに場合分け

  • アプリ全体へ反映する設定

ダークモードや言語設定などのアプリ全体へ反映したい設定を行うクラスあり

テンプレアプリの構造

Androidアプリ開発で私が良く使っている構造です。
同じモバイルアプリなので、当然Flutterにも適用可能です。

  • View

Widgetの集まりです。
なるべく状態はViewModelにもたせて、StatelessWidgetで書けるとよいと思います。
状態はProviderを使って取得します。

  • ViewModel

UIの状態を保持し、変更をViewに通知します。
また、複雑なロジックを処理するためにUseCaseをコールしたり、Repositoryを通じてデータの読み書きを行います。

  • UseCase

複雑なロジックを実行します。
シンプルな場合は、クラスを作らずにViewModelに書いたりもします。

  • Repository

データの読み書きを行います。
データの読み書きに関しては様々なライブラリがあると思いますが、Repositoryより下位に隠蔽しておくことで、ライブラリを変更したときの影響範囲を小さくできます。

  • Data

実際にデータへのアクセスを実装しています。