2021/09/04

[js]JSONっぽいデータをJavaScriptで読みたい

JSONのデータはこんな感じだ。

{
  "name": "Yoshio",
  "age": 92
}

 

JSONっぽいデータと書いたが、今回はこういうデータのことである。

{
  name: "Yoshio",
  age: 92
}

「key: value」と呼ぶとしたら、keyの部分にダブルクォーテーションがないタイプのデータである。
JavaScriptのコード中に書くようなデータと言えばよいか。

 

何でか知らんが、サーバがこういうデータを返してくるようなのだ。
それをJavaScriptで読んで処理をしたい。


一応取り込めたのだが、この方法はあまりよろしくないとさんざん書かれている。

const VALUE = "{name: 'Yoshio', age: 92}";
const result = eval(`(${VALUE})`);
console.log(JSON.stringify(result));

$ node ev.js
{"name":"Yoshio","age":92}

 

よろしくない理由は eval() を使うと危険なためである。

置き換えの例としてFunctionを使うよう書かれていた。

const VALUE = "{name: 'Yoshio', age: 92}";
const result = Function(`return ${VALUE}`)();
console.log(JSON.stringify(result));

$ node ev.js
{"name":"Yoshio","age":92}

 

このシンプルなデータだとFunctionで置き換えられたのだが、複雑になるとそうもいかないようなのだが、どうしたらよいかわからん。。。

2021/07/04

[js]import * は普通

前回、namespace にして export default して import させていた。
namespace は無しにして import * でも同じことはできるのだが、わざわざ namespace にしてみたのだ。

その理由は「ワイルドカードよりも特定の namespace を使った方がよいのではないか」という気持ちがあったからだ。ほら、不特定なものよりも特定した方が好まれるじゃないか。

しかし。。。

TypeScript: Documentation - Namespaces and Modules
https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html

太字で

we recommended modules over namespaces in modern code.

最新のコードでは、名前空間よりもモジュールを推奨しました。
(Google翻訳)

と書いてある。
「最新のコードでは」だから将来変わるのかもしれんが、2021/07/04 19:23 ではそうなっていた。

 

なので、ワイルドカードで指定しているとかそういうのはあんまあり考えなくて良いんだろうね。変に気を回して時間を掛けてしまったよ。

[js]ファイルを分けたい

大きいプログラムを作ると、ファイルを分けたくなるだろう。そうじゃなくても、機能ごとにファイルを分けたいだろうし。
まあ、私が分けたいのだ。


index.ts

01: import * as greet from './greet';
02: 
03: const hello: greet.Greet = greet.hello();
04: if (hello != null) {
05:     console.log(`hello: ${JSON.stringify(hello)}`);
06: }

greet/index.ts

01: export type Greet = {
02:     greet: string;
03:     time?: string;
04: };
05: 
06: export function hello(): Greet {
07:     return {greet: 'hello', time: 'allday'};
08: }
09: 
10: export default {}
  • greet ディレクトリにファイルがあるので、 import の from はそこを指定する。
    • ファイル名が index.ts なのでディレクトリの指定だけで済んでいる。
  • greet/index.ts は export default {} としたので、ファイルの中にある export 指定したものは同じ名前で export される。

という理解で正しいだろうか?

 

import の from より前の部分があまりわかってない。
こうか?

  • 個別に import するなら、 import {個別にコンマ区切り} from '~~'
  • 全部 import するなら、 import 名前 from '~~'

 

greet/index.ts から個別に importすると、こうなる。

01: import {Greet,hello} from './greet';
02: 
03: const msg: Greet = hello();
04: if (msg!= null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

列挙するのが面倒ではあるが、使用しているものがはっきりするので場合によってはわかりやすい。


じゃあ全部 import するので import Hello from './greet' でよいかというと、これはダメそうだ。
Hello.Greet や Hello.hello という指定ができなかったのだ。
「Cannot find namespace 'Hello'」というエラーになる。

namespace を使う場合はこういう書き方になるようだ。
import するときに「import Hello from './greet'」としたが、別に namespace名と同じにする必要はなさそうだった。

あと、namespace も export を指定しないと個別に import できない(個別の話。export default指定しているなら関係ない)。namespace を export しても、namespace の中身が全部 export されたことになるわけではなく、その中でも export を付けたものだけが外部から見えるようだ。

index.ts

01: import Hello from './greet';
02: 
03: const msg: Hello.Greet = Hello.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

greet/index.ts

01: namespace Hello {
02:     export type Greet = {
03:         greet: string;
04:         time?: string;
05:     };
06: 
07:     export function hello(): Greet {
08:         return {greet: 'hello', time: 'allday'};
09:     }
10: }
11: export default Hello;

 

この greet/index.ts を import * した場合は namespace 名を使わなくても済むようだ。

index.ts

01: import * as Pochi from './greet';
02: 
03: const msg: Pochi.default.Greet = Pochi.default.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

 

 

namespace を付けるなら、 export default ではなく個別にした方がよいのかな?と思ったが、そうすると import側でも個別にしないといけなかった。
1ファイルで複数の namespace を入れ込むことは(私の実装だと)やらないので、これは default のままの方が使い勝手がよいか。

index.ts

01: import {Hello} from './greet';
02: 
03: const msg: Hello.Greet = Hello.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

greet/index.ts

01: namespace Hello {
02:     export type Greet = {
03:         greet: string;
04:         time?: string;
05:     };
06: 
07:     export function hello(): Greet {
08:         return {greet: 'hello', time: 'allday'};
09:     }
10: }
11: export {Hello};

 


import の後ろは、 * as xx だったり name だったり {name} だったりといろいろあるが、いったいなんなんだ。

ECMAScript 2015 Language Specification – ECMA-262 6th Edition
https://262.ecma-international.org/6.0/#sec-imports

たくさんあるので from を使うパターンだけ見ていこう。
以下、イタリック&下線の部分はさらに展開されるという意味である。

import ImportClause FromClause;

FromClause の方は種類がないので、先にそちらを。

from StringLiteral

順番に展開すると ModuleSpecifier → StringLiteral なのだが、まあよかろう。
つまり文字列だ。

ImportClause は5種類に分かれる。

  • ImportedDefaultBinding
  • * as BindingIdentifier
  • { ImportsList }
  • ImportedDefaultBinding , * as BindingIdentifier
  • ImportedDefaultBinding , { ImportsList }

たぶん BindingIdentifier は「勝手に割り当てて良い名称」だと思う。bind だから紐付ける名称になるのかな。

上3つが import でよく見る形式で、残り2つはその組み合わせだ。
使えるものを展開してるのはこちら。

import - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import

まあ、こっちの方がわかりやすいな。日本語だし。


これで基本的なところはできたんじゃなかろうか。

もう1つやりたいのが、export するファイルとそうじゃないファイルを分ける作業だ。
実装を見られて困るとかではなく、C言語で言うところのヘッダファイルとソースファイルに分けたいというだけである。

できるのだろうか?
なお、tsconfig.js は isolatedModules:true を指定しているものとする。

 

re-export というのでやってみた。

 

greet/index.d.ts

01: export type Greet = {
02:     greet: string;
03:     time?: string;
04: };

greet/hello.ts

01: import {Greet} from './index.d';
02: 
03: export function hello(): Greet {
04:     return {greet: 'hello', time: 'allday'};
05: }

 

greet/index.ts

01: export {hello} from './hello';
02: export type {Greet} from './index.d';

 

index.ts

01: import {hello,Greet} from './greet';
02: 
03: const msg: Greet = hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

うーん、無理やりやりましたという感じが拭えない。
それに呼び出し元の index.ts の方に修正が入るのはダメだ。

 

これは書き方としてダメだった。namespace 内ではダメだそうだ。

01: export namespace Hello {
02:     export {hello} from './hello';
03:     export type {Greet} from './index.d';
04: };
05: export default Hello;

 

これならいけるようだ。

01: import {hello as myHello} from './hello';
02: import {Greet as myGreet} from './index.d';
03: 
04: export namespace Hello {
05:     export const hello = myHello;
06:     export type Greet = myGreet;
07: };
08: export default Hello;

うーん・・・。もっとよいやり方がありそうな気がする。

 

というわけで、あきらめて本を買うことにした。

O'Reilly Japan - プログラミングTypeScript
https://www.oreilly.co.jp/books/9784873119045/

本の中身は書かないのだが、私が import あたりをうまく理解できない理由が分かってきた気がする。

「JavaScript」とか「TypeScript」とかで検索しながらやっていたけど、import というかモジュールの扱いについて過去の経緯がいろいろあり、ネットで見つけた情報はどの時点の JavaScript について書いているかによって変わってくるからだと思う。そういうのを知らずにネットで調べていたので、あっちの情報とこっちの情報をまぜこぜにしてうまくいかなかったとか、そんな感じになったんじゃなかろうか。

本の内容で勉強することの利点は、よくも悪くも情報が混ざらないことだと思うので、あやふやな知識でふらふら調べるよりはましなんじゃなかろうか。せっかくお金出したんだから、ましであってほしい。

2021/07/03

[js]nullチェックとtruthy

JavaScript で null チェックする際、私はこう書くことが多い。

let val;

...

if (val) {
  // not null
  ...
} else {
  // null
  ...
}

C言語だと 0 以外の値なら真なのでアドレス値が入っているかどうかを判定するし、 Java だとそういう書き方は興られたような気がする。

で、JavaScript の場合はどういう仕様なのか?

 

Truthy - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
https://developer.mozilla.org/ja/docs/Glossary/Truthy

falsy として定義された値以外は truthy 扱いで、 truthy は boolean 判定するところに出てきたら true として扱うという仕様だった。

falsy はこれら。

  • false
  • 0
  • -0
  • 0n
  • ''(空文字列)
  • null
  • undefined
  • NaN

オブジェクトの場合は null か undefined かそれ以外になるはずなので、 if (オブジェクトの変数) だけで事足りるはずだ。


あとは、その書き方が一般的なのかどうか、だ。
私は if (変数) や if (!変数) の書き方で見慣れているのだが、一般的にそうでないなら使いどころは絞った方がよいと思うのだ。

nullとundefined - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined

うーん「== null」って書いてあるな。

確かに、 != null だと null か undefined だけだが、変数名だけだと幅が広すぎるのか。
x && x.length > 0 とか x && x !== 0 だったら変数名だけでよいかもしれないが、やっぱりちゃんと書いた方が無難だろう。

 

というわけで、 null / undefined のチェックにはきちんと == null のように条件を書くことにしよう。


ついでに読んでいてわからなかったのがこちら。

ルートレベル(root level)のundefinedのチェック
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined#rtoreberuroot-levelnoundefinednochekku

null だろうと undefined だろうと、 x.y を参照するのに x が どこも指していないのだったらダメだろう。

undefined かどうかをチェックするには確かに typeof するしかないのだが、 null は別によいってことなんだろうか。

うーん、わからん。。。

 

ごにょごにょ考えたが、あれは x.y != null みたいなチェックをいきなりするのではなく、先に x をチェックしろということを言いたいだけなんだろうか。
それならわかるけど、やっぱり x != null だけでよいと思うのだ。

Checking for root level undefined
https://basarat.gitbook.io/typescript/recap/null-undefined#checking-for-root-level-undefined

原文も同じ。

まあいいや。
ルートレベルより下のプロパティやメソッドにアクセスする前にはルートレベルのチェックをするか try-catch を考慮するようにしておけば良かろう。

TypeScriptとnull/undefinedチェック

JavaScript のことを書いているサイトであればほぼ書かれていると思われる null/undefined チェック問題。問題というよりは FAQ なんだろうけど、私は何度やっても覚えられない。


まず、 null と undefined の型だが、これはオブジェクトではなくプリミティブ型とのこと。昔ちょっと JavaScript を書いたときは 'undefined' と文字列になっていたような気がするが、そう書いてある()からそうなんだろう。

にもかかわらず、 typeof null は "object" だ。 typeof undefined は "undefined" である。
nullは「プリミティブ値」と表現されていたし、undefined も同様だ

Primitive (プリミティブ) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
https://developer.mozilla.org/ja/docs/Glossary/Primitive

undefinedはプリミティブデータ型なので typeof で "undefined" になるが、 null は「プリミティブに見える」と微妙な表現をされている特殊なオブジェクトだそうな。あらゆるオブジェクトは nullから派生するらしい。

 

等値演算子(==, !=)を使う場合、null と undefined は同類と見なされる。

01: let val = null;
02: 
03: if (val) {
04:   console.log('defined');
05: } else {
06:   console.log('undefined');
07: }

これはどっちを通ったかというと、"undefined" の方を通った。 1行目が undefined でも同じだ。

null と undefined を区別したいなら同値演算子(===, !==)を使う。

A user name of null?
https://developer.mozilla.org/ja/docs/Learn/Getting_started_with_the_web/JavaScript_basics#a_user_name_of_null

この例は (!myName || myName === null) となっているのだが、 !myName だけでよいのでは?
もし && だったら myName === null だけでよいだろうし。

 

  • undefined はプリミティブデータ型でありプリミティブ値である
  • null はプリミティブ値のようでありオブジェクトのようであり、特殊な扱い
  • undefined と null を同じ扱いにして良いなら等値演算子(==, !=)で扱える
    • x == null とか x == undefined とか
    • !x でもよい
  • undefined かどうかを調べたいなら typeof === 'undefined' にする
    • null の typeof は 'object' になる

 

ちなみに、変数に undefined を与えて console.log したのが上、 'undefined' を与えて console.log したのが下。

image

null だとどっちで与えても白い文字で出力された。 うーん。

console.log なのか JavaScript 全体なのか分からないけど、人間寄りになるように動いてしまうのが苦手意識を持ってしまう原因かもしれん(私がね)。


TypeScript は型定義があるため、もうちょっと複雑になりそうだ。

nullとundefined - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined

等値演算子で null チェックすればよいよ、ということだ。

といっても、ここは TypeScript の仕様説明ではないので、もうちょっと確認しよう。

 

まず、 null が型の方として扱われるようだ。

let val: number = null;

これはコンパイルエラーだ。どうしても代入したければ、私は null も OKですよ、ということにするしかないのか。

let val: number | null = null;

ちなみに、これもダメだ。

let val: number = null as number;

 

null は型扱いなので、変数を null型で指定できるようだ。

let val: null = null;
val = 'hello';

こんなのは代入する行でコンパイルエラーになる。 undefined もそうだった。

let val: undefined;
val = null;

使い道はないけどね。

 

だから、 null や undefined を代入したいのなら型のところで | null のような形にすることになる。
なるのだが、 undefined はデフォルト値になるので、ちょっと特殊な扱いかもしれない。その代わり、代入しないまま使おうとしたら、 | undefined になっていない限りはエラーになる。型と異なる変数が入っている、という扱いかもしれないが、TS2454 コンパイルエラーは before being assigned なので別エラーにはなっている。


C言語をよく使う人からすると、使い終わった変数に null を代入したいという気持ちに駆られることがある。

開いたファイルを扱うオブジェクトの変数があって、その変数が null だったら先にオープンしてから処理を続ける、みたいな。それをやろうとすると、最初は undefined だからよいとして、一度オープンして、クローズしたら null にしたくなる。しかし null を代入させるなら | null で許容させる必要がある。

私の期分の問題かもしれないが、 | null というか、型を複数割り当てるというのがどうも苦手だ。 string | number とかされたら「どっちが入ってんだよ!」ってなるからやらないけど、似たような気持ちになってしまう。

なら指定しなければ良いのだが、そうじゃなかったら boolean の変数を用意して判断するみたいなことになるだろう。でも、そこまでするのはなんか大げさな気がしてしまう。

なので、あまり選びたくない2択のどちらかをせねばならぬ。
あるいは、そういう設計の仕方自体が JavaScript らしくないということかもしれない。

 

そういう、言語仕様ではなく、センスというかお作法というかがよくわかっていないというのが最大の悩みであった。

[js]Arrayと配列と型付き配列 (2)

こんな話を2回に分ける必要もないのだが、日記だから良いのだ。

Arrayクラスと型付き配列は別物っぽいという話だ。

JavaScript 型付き配列 - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Typed_arrays

型付き配列というのは、いわゆる C言語などでの配列に近いもののようである。ただ、C言語では単に型のデータを配列にするのだが、型付き配列は「xxx型の配列のクラス」であり、その型ごとにクラスが用意されているところが異なる。

なので、定義されたクラス以外は(デフォルトでは)存在しない。

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
  • BigInt64Array
  • BitUint64Array

まあ、こんだけあれば十分だね。

どうやら、JavaScript では最初に Arrayクラスがあって、その後で型付き配列ができたようだ。データがメモリとして連続しているという前提がないと高速化できないというのはよくわかる。

つまり、Arrayクラスとは別物であるし、成り立ちからしても Arrayクラスの派生ではないだろう。 ArrayBufferクラスというものがあるようなので、むしろそちらが基底クラスになっているだろうか? しかし分類としては Array側になっていた。まあ配列だしね。

索引付きコレクション
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects#indexed_collections

なんかこう、Java のドキュメントのように継承関係の図はないんだろうか。

2021/07/02

[js]Arrayと配列と型付き配列 (1)

JavaScript というか TypeScript というか Node.js というか、とにかくその辺で実装をしている。

慣れない・・・。
同じことをするのに書き方がいくつかあるというのが混乱する。

特にアローね。

function x(val: number): number {
  return val + 2;
}

const x = (val) => val + 2;

アロー演算子でしか書けない場合があるのは良いとして、どっちでもよい場合が困るのだ。
function とか return とか、そこまで省略したいの??

ということを訴えてみたのだが、そういう言語だからしょうがないよな。
人間、あきらめも肝心だ。


今日は JavaScript で他のコードを読んでいて、知らない関数が出てきたので調べてみると、Arrayクラスの関数だったということがあった。
クラスだからメソッドだろうけど、そういうことではなく、import しないと使えないやつではない標準で提供されているものくらいは把握しておかないとつらいかろうということに気付いたのだ。

 

Array - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array

最初に書かれている解説で気になった言葉を箇条書きにする。

  • Arrayクラス
  • グローバルオブジェクト
  • 配列の構築に使用される
  • 型付き配列

まず、Arrayクラスはいいだろう。
「Arrayクラスはグローバルオブジェクト」と書いてあるので、importしなくても使えるのだろう。これもよい。
配列の構築に使用される、ということは、static関数みたいなのを持っていて、それで作ってあげたりするのだろう。

で、型付き配列とは挙動が異なるように読み取ることができる。

JavaScript 型付き配列 - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Typed_arrays

混同してはいけません、という文章があるので、ここに答があるに違いない!

が、今日は眠たいのでここまで。

2021/06/20

[js]PouchDB

前回の RxDB は、PouchDB のアダプタが使えますよ、といっていた。
今回はその Pouch DB だ。

PouchDB, the JavaScript Database that Syncs!
https://pouchdb.com/

簡単に使えるならなんでもいいんだけどね。


find() は別になっているのでインストールがいるのだった。

npm i pouchdb pouchdb-find

01: const PouchDB = require('pouchdb');
02: PouchDB.plugin(require('pouchdb-find'));
03: 
04: const db = new PouchDB('kittens');
05: 
06: const fn = async () => {
07:     db.bulkDocs([
08:         {
09:             name: "yoshida",
10:             color: "green",
11:             healthpoints: 30,
12:         },
13:         {
14:             name: "yoshio",
15:             color: "blown",
16:             healthpoints: 33,
17:         },
18:         {
19:             name: "momo",
20:             color: "pink",
21:             healthpoints: 9,
22:         },
23:     ]);
24: 
25:     await db.createIndex({
26:         index: {fields: ['healthpoints']}
27:     });
28:     const resHealth = await db.find({
29:         selector: {healthpoints: {$exists: true}},
30:         sort: ['healthpoints'],
31:     });
32:     for (let lp = 0; lp < resHealth.docs.length; lp++) {
33:         console.log(resHealth.docs[lp].name);
34:     }
35:     console.log();
36: 
37:     await db.createIndex({
38:         index: {fields: ['color']}
39:     });
40:     const resColor = await db.find({
41:         selector: {color: {$exists: true}},
42:         sort: ['color'],
43:     });
44:     for (let lp = 0; lp < resColor.docs.length; lp++) {
45:         console.log(resColor.docs[lp].name);
46:     }
47: 
48:     await db.destroy();
49: };
50: fn();

createIndex()が、fields を列挙したらその分作られるのかと思ったのだが、それぞれ作ることになるようだ。戻り値で"created" か "exists" が取ってこれるが、存在するなら作らないと書いてあるので、そんなに気にしなくてよいのか。

また、createIndex() した後に put() したら再度 createIndex() がいるのかと思ったが、そうでもないようだ。今のコレクションに対してインデックスを作るという意味ではないのか。

 

TypeScriptの場合はこんな書き方で良いのかな?
find() したドキュメントがうまいことプロパティにアクセスできなかったので as を使ってキャストしてしまった。よいのかどうかわからん。

01: import * as PouchDB from 'pouchdb';
02: 
03: PouchDB.plugin(require('pouchdb-find'));
04: 
05: const db = new PouchDB.default('kittens');
06: 
07: type dbType = {
08:     _id: string;
09:     _rev: string;
10:     name: string;
11:     color: string;
12:     healthpoints: number;
13: };
14: const fn = async () => {
15:     db.bulkDocs([
16:         {
17:             name: "yoshida",
18:             color: "green",
19:             healthpoints: 30,
20:         },
21:         {
22:             name: "yoshio",
23:             color: "blown",
24:             healthpoints: 33,
25:         },
26:         {
27:             name: "momo",
28:             color: "pink",
29:             healthpoints: 9,
30:         },
31:     ]);
32: 
33:     let idxRes = await db.createIndex({
34:         index: { fields: ['healthpoints'] }
35:     });
36:     const resHealth = await db.find({
37:         selector: { healthpoints: { $exists: true } },
38:         sort: ['healthpoints'],
39:     });
40:     for (let lp = 0; lp < resHealth.docs.length; lp++) {
41:         const r = resHealth.docs[lp] as dbType;
42:         console.log(r.name);
43:     }
44:     console.log();
45: 
46:     idxRes = await db.createIndex({
47:         index: { fields: ['color'] }
48:     });
49:     const resColor = await db.find({
50:         selector: { color: { $exists: true } },
51:         sort: ['color'],
52:     });
53:     for (let lp = 0; lp < resColor.docs.length; lp++) {
54:         const r = resColor.docs[lp] as dbType;
55:         console.log(r.name);
56:     }
57: 
58:     await db.destroy();
59: };
60: fn();

[js]RxDB

JavaScript でデータベースを使う必要性が出てきてしまった。
練習してみよう。


Introduction · RxDB - Documentation
https://rxdb.info/

NoSQLデータベースだそうな。SQLite か LMDB かでいえば、LMDB 側ということになろう。
私はそんなにデータベースを使ったことがないので、比較できるのはその2つくらいなのだ。

npm i でインストールすれば使えるのだろうと思ったが、RxDB 以外にもインストールしないと使えないらしい。
adapter というものもインストールして設定しないと使えないっぽい。
今回は Node.js なので、adapter のページにあった leveldown というものを使ってみよう。 Level DBらしい。

npm i rxdb rxjs leveldown pouchdb-adapter-leveldb

 

メニューの順番からすると、 RxDatabase, RxSchema, RxCollection, RxDocument, RxQuery の順にやっていくのだろう。

SQL ではないからか、テーブルではなくスキーマと呼ぶのか? そこはまだよいのだが、コレクションとかドキュメントとかあるのだが、何なのかが分からん。。。

開発者が知っておくべき、ドキュメント・データベースの基礎:特集:MongoDBで理解する「ドキュメント・データベース」の世界(前編)(1/3 ページ) - @IT
https://www.atmarkit.co.jp/ait/articles/1211/09/news056.html

ドキュメントはデータベースに突っ込むデータで、コレクションはその集合・・・?

  • createRxDatabase()でデータベースを作る
  • データベースに突っ込みたいデータの構造を決めて addCollections() で追加する
  • insert() でデータを突っ込む
  • find() でデータを取り出す
01: import {
02:   createRxDatabase,
03:   addRxPlugin,
04: } from 'rxdb';
05: 
06: addRxPlugin(require('pouchdb-adapter-leveldb'));
07: const leveldown = require('leveldown');
08: 
09: const fn = async () => {
10:   const db = await createRxDatabase({
11:     name: 'heroesdb',
12:     adapter: leveldown,
13:   });
14: 
15:   const myHeroSchema = {
16:     "title": "hero schema",
17:     "version": 0,
18:     "description": "describes a simple hero",
19:     "type": "object",
20:     "properties": {
21:       "name": {
22:         "type": "string",
23:         "primary": true
24:       },
25:       "color": {
26:         "type": "string"
27:       },
28:       "healthpoints": {
29:         "type": "number",
30:         "minimum": 0,
31:         "maximum": 100
32:       },
33:     },
34:     "required": ["color"],
35:     "attachments": {
36:       "encrypted": false
37:     },
38:     indexes: ['healthpoints', 'color']
39:   }
40:   await db.addCollections({
41:     heroes: {
42:       schema: myHeroSchema
43:     }
44:   });
45: 
46:   await db.heroes.bulkInsert([
47:     {
48:       name: "yoshida",
49:       color: "green",
50:       healthpoints: 30,
51:     },
52:     {
53:       name: "yoshio",
54:       color: "blown",
55:       healthpoints: 33,
56:     },
57:     {
58:       name: "momo",
59:       color: "pink",
60:       healthpoints: 9,
61:     },
62:   ]);
63: 
64:   const query = await db.heroes.find();
65:   const resHealth = await query.sort('healthpoints').exec();
66:   for (let lp = 0; lp < resHealth.length; lp++) {
67:     console.dir(resHealth[lp].get('name'));
68:   }
69:   console.log();
70:   const resColor = await query.sort('color').exec();
71:   for (let lp = 0; lp < resColor.length; lp++) {
72:     console.dir(resColor[lp].get('name'));
73:   }
74: 
75:   await query.remove();
76: }
77: fn();


難しすぎでは!?と思ったが、SQL を使うタイプだと SQL文というテキストになっているだけであんまり変わらんか。

addCollections() の戻り値はコレクションではあるのだが、コレクションの集合のようなものが返ってくるようだ。
これに find() したかったら、今回だと .heroes.find() のように名前を挟まないといかん。
そうなると db.heroes.find() と変わらん気がする。

 

それとは別に気になったのは、RxDB はその下で動いているデータベースを隠蔽するような形で動いていると思うのだが、トランザクションがあるデータベースだとパフォーマンスが悪くならないだろうか。 bulkInsert() で全部放り込めるなら良いけれども、取得しては保存、取得しては保存、みたいな使い方をしたい場合もあるだろうし。

2021/06/19

[js]非同期とコールバックとPromise

JavaScript で検索するとたくさん出てくる「コールバック Promise」。
この記事もその1つになるのだろう。自分の考えを整理するのに、どうしても文章に書かざるを得ない人たちの一人なので仕方がない。
ちなみに、ブラウザでの動作は考えておらず、Node.js から使うものとして書いている。


JavaScript はシングルスレッドだと言われている。仕様ではここになるのかな?

8.6 Agents
https://262.ecma-international.org/11.0/#sec-agents

An agent comprises a set of ECMAScript execution contexts,
an execution context stack,
a running execution context,
an Agent Record,
and an executing thread.
Except for the executing thread, the constituents of an agent belong exclusively to that agent.

【DeepL翻訳】
エージェントは、ECMAScript の実行コンテキストのセット、実行コンテキストスタック、実行中の実行コンテキスト、エージェントレコード、および実行スレッドから構成されます。実行スレッドを除き、エージェントの構成要素はそのエージェントにのみ属します。

実行しているコンテキストではスタックとかスレッドとかで構成されるけど、全部 a や an が付いているので1つしかないということなんじゃなかろうか。
「single thread」という単語は見つけられなかったのだよ。

解釈は間違ったかもしれんが、とりあえずシングルスレッドであることは信用しよう。


シングルスレッドなのに非同期ってどういうことじゃ、と思ったが、ここは割込みができるシステムと考えればよいと思う。

 

組み込み開発ではよくあることだが、CPU は実行状態として「通常モード」と「特権モード」のようなモードというか状態というかを持つことがある。電源を入れて起動直後は特権モードで、その状態じゃないとできない設定をいろいろやってから通常モードになって動き始める、みたいな感じだ。

通常モードで動いているけれども、例えば外部のセンサからINPUTピンに対して信号が来た場合には割り込んで良いようにする、という設定にしておくと、信号が来たら現在の状態をスタックに保存してから特権モードに切り替わり、特権モードで処理をして終わったらまた通常モードに戻る、というようなことができる。

こういう特権モードになるときは CPUとしてまるまる切り替わるので、スレッドとかそういうのはない。そして特権モードでの動作は通常モードとは異なるのでコンテキストが異なる。すなわち非同期だ。

 

私の場合は JavaScript の非同期をそんな世界観で眺めている。


非同期なところまではよいのだが、非同期で処理を何かしたいことがほとんどだろう。 setTimeout() みたいな遅延動作なんかはわかりやすい例で、何か動作を遅延させてから実行したいのだ。

しかし、実行し始めたコンテキストとは別のコンテキストで動作するので、素直に続きを行うわけにはいかない。コンソールにログを出す、みたいな影響を与えないものであれば気にしなくてよいのだが、変数に値を代入したりファイルに書き込んだりしたいことが多かろう。

グローバル変数みたいにスコープが広い変数であれば代入できると思うが、ローカル変数に戻したいという場合もあろう。そういうときは実行したい単位を関数にしてしまい、Promise<戻り値>を戻すようにして、asyncを付けておくと、呼び出し元はその関数を await つけて呼び出せば

 

例えば、このコードをそのまま実行すると、setTimeout()したログは一番最後に出力される。

01: console.log('hello');
02: setTimeout(() => { console.log('meme'); }, 1000);
03: console.log('world');

hello
world
meme

 

setTimeout()の後にworldを出したいだけなら、こんな感じか。
ちなみに TypeScript で書いていて、 tsconfig.json の "target" は "es2015" にしている。 tsc --init で作られたそのままだと怒られてダメだったのだよね。
vscode で実行していたのでデバッグ設定をしていたのだが、"Launch Program" で実行しても console.log の出力がどこにも出てこなくて悩んだ。実行するとタブが "TERMINAL" になるので、急いで "DEBUG CONSOLE"にすると出てきた。

01: async function setTimeoutPromise(msec: number): Promise<void> {
02:     return new Promise<void>((resolve: any, reject: any) => {
03:         setTimeout(() => { resolve();}, msec);
04:     });
05: }
06: 
07: const fn = async () => {
08:     console.log('hello');
09:     await setTimeoutPromise(1000);
10:     console.log('world');
11: };
12: fn();

なんかめんどくさいね。
await を使いたいがために async の関数が2つ必要になってしまった。

 

それはともかく、setTimeout() の第1引数は関数を受け取るタイプで、指定した時間後にこの関数を呼んでくれる。つまりコールバックだ。
これを呼び出し元で同期に見せかけたいので、Promise を使ってコールバックで resolve() を呼び出すようにしている。

 

自分で作った関数だけで非同期が発生することはなくて、そこからsetTimeout() なり npm でインストールしたライブラリなり、なんらかの外部に依存する場合しか非同期になることはないだろう。そして JavaScript で提供された関数を呼ばないで済むことはまずないので(たぶん変数の参照や代入だけか?)、前提条件はあまり考えなくて良いだろう。

 

非同期の基本的な処理完了通知はコールバックのはず。ただ、組み込み CPUでの割り込み処理で特権モードになったままになるのではなく、Node.js のコンテキストに戻されるはずだから、pthread の join で待って、戻ってきたらコールバック関数を呼び出す、みたいなことをやっているのか。

Promiseはその先の話で、いろいろ書き方はあるのだろうが、

  1. Promise を new するときにコールバックで結果を返す関数を最後に呼び出す関数を登録する
    1. 登録する関数の引数は (resolve, reject) にするのが一般的。
    2. コールバックが呼ばれるときに resolve() を呼び出す(失敗だったら reject() を呼び出す)。
    3. コールバックで結果を得てそれを次も使いたい場合は resolve() の引数にする。
  2. promise.then() を呼び出して、引数は resolve() の中身を実装するようなイメージの関数にする。
01: function setTimeoutPromise(message: string, msec: number): Promise<string> {
02:     return new Promise<string>((resolve: any, reject: any) => {
03:         setTimeout(() => { resolve(message);}, msec);
04:     });
05: }
06: 
07: console.log('hello');
08: setTimeoutPromise('world', 1000).then((msg: string) => {
09:     console.log(msg);
10: });

setTimeoutPromise()の引数をコールバック関数内の resolve()に渡せているのが面白いところだ。全然コンテキストが異なるならアクセスできないはずなので、うまいことやってるのだろう。深く探る必要もあるまい。

そして setTimeoutPromise() に async をつけて、呼び出すときに await をつけると、resolve() の引数がそのまま戻り値として使えるようになる。つまり同期になる。

 

なので、もうあまり難しいことは覚えずに async - await だけ覚えておけばいいかな、と思っていたのだ。


しかし、パフォーマンスを出さないといけないようになると、全部同期で済ませるのはもったいないこともある。非同期になる理由が「忙しいから」であれば待っていても良いのだが、「待ち時間が発生するため」の場合には、その間に他のことをやってしまいたいからだ。

 

まず、Promise.all()というものがある。

Promise.all() - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Promiseのインスタンスを配列で受け取って、then でも配列で結果を返している。面白い。
これを async - await で書くとこんな感じか。

01: const promise1 = Promise.resolve(3);
02: const promise2 = 42;
03: const promise3 = new Promise((resolve, reject) => {
04:     setTimeout(resolve, 100, 'foo');
05: });
06: 
07: const fn = async () => {
08:     const result = await Promise.all([promise1, promise2, promise3]);
09:     console.log(result);
10: }
11: 
12: fn();
13: 

all()以外にもいろいろある

順番にやっていかないといけないものは async - await でつなげれば良いし、特に順番はどうでもよいなら Promise.all() でまとめてしまえばよい。本当にどうでもよいならコールバック関数が呼ばれておしまい、でよいのだが、そうもいかんことが多かろう。

 

これでなんとかなるかと思っているのだが、どうなんだろうね。苦労してみないと分からんのだろう。

2021/06/13

[android] HTTPでJSONを取りたい

Android用のライブラリAARが提供されていて、それを起動するとHTTPで内部的に通信をして取得したり指示したりするようになっているものがあるとする。まあ、今の私の状況なんだけどね。

permissionの設定を乗り越えて起動できるようになったのは良かったのだが、HTTPで内部でやりとりをする方法が分からん。

HTTP クライアントを選択する
https://developer.android.com/training/basics/network-ops/connecting#choose-http-client

ネットで検索すると HttpURLConnection がよく出てくるのだが、Retrofit というものを使う例になっているようだ。 HttpURLConnection の方が基本的な雰囲気をかもし出しているが、Retrofit だと JSON なんかもうまく扱ってくれそうなことが書いてあるので、黙って使うことにする。
ただ、概要の方には「HTTPライブラリVolley」と書いてあるので、実はそっちの方が良いのかも。。。
まあよい。必要ならやるだけだ。

Retrofit #Download
https://square.github.io/retrofit/#download

Gradleの場合は implementation を追加すれば良いそうだ。 Android Studio の Project Structure から 検索してもよさそうだ。"retrofit" で検索するとたくさん出てきたので、com.squareup.retrofit2 で始まっているものを選択した。

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

Retrofit のサンプルでは GsonConverterFactory というものを使っていたので、そちらも書くとよいだろう。

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

他にもコンバータがあるので、必要に応じて使い分けるのだろう。


Introduction に従えば何か動いてくれるんじゃなかろうか。

Introduction
https://square.github.io/retrofit/#introduction

baseUrl と GitHubService の定義からそれっぽい URL にアクセスしてみよう。

https://api.github.com/users/hirokuma

いろいろ返ってきた。

image

あちこちサイトを見たが、結局こちらのと同じような実装になった。

Retrofitの使い方・HTTPリクエスト・サーバー通信【Android・Kotlin】
https://101010.fun/programming/android-try-retrofit.html

私が作ったのはこちら。

https://github.com/hirokuma/AndroidHttpSample1

味も素っ気もないね。Android Studioでプロジェクトを新規作成したところからcommitすればよかったな。
最終的には同じようになってしまったが、一応 Android Developer での書き方と、 Retrofit のサンプルを参考にしていたのだ。サンプルは Android ではないので

 

Android Developerのように suspend fun にするとうまく動かせなかった。コルーチンにしなさい、みたいなエラーになるのだが、理解できていないのだ。

Android での Kotlin コルーチン  |  Android デベロッパー  |  Android Developers
https://developer.android.com/kotlin/coroutines?hl=ja

go言語にもあったが、軽量スレッドみたいなものらしい。昔はスレッドを軽量プロセスと呼んでたこともあったと思うが、スレッドですら生ぬるい時代になったのか。

 

suspend fun というのも何だかわかってないし。Java にはそういうのなかったような気がするが、最近できたんだろうか。
いろいろわかってなくてつらいな。

2021/06/06

[kotlin] はてな はnullable

少し前に、JavaScript の ?? や ?. の意味を調べた。

hiro99ma blog: [js]はてな、はてなはてな、はてなドット
https://blog.hirokuma.work/2021/05/js.html

C言語だと ! は否定、 ? は三項演算子の1番目、という意味しかなかったのだが、JavaScript はいろいろ意味があるんだなぁ、くらいに思っていた。

 

が、Kotlin にも ? がけっこう出てくることに気付いた。
最近の言語の流行りなんだろうか。

 

以前も載せた気がするのだが、もう私の記憶に残っていないからよしとしよう。

Keywords and operators | Kotlin
https://kotlinlang.org/docs/keyword-reference.html#operators-and-special-symbols

演算子なので operator だ。

? だけだと「これは nullableですよ」、 ?. は、nullではない場合だけ呼び出すという意味だそうな。

 


Kotlin にも === と !== があった。
referential equality という名前で、 a === b は a と b が同じオブジェクトを指している場合に trueになる。

 

JavaScript はどうだったかというと、 strict equality という名前で、日本語だと「厳密等価」と呼ぶそうだ。アルゴリズムに従って判定するとのこと。 x === y の場合、

  1. Type(x) と Type(y) が異なるならば false
  2. Type(x) が undefined なら true
  3. Type(x) が null なら true
  4. Type(x) が 数値の場合、
    1. x が NaN なら false
    2. y が NaN なら false
    3. x と y の数値が同じ値なら true
    4. x が +0、 y が -0 なら true
    5. x が -0、 y が +0 なら true
  5. Type(x) が 文字列の場合、 x と y が同じ文字列(同じ文字長かつ各位置の文字が一致)なら true、それ以外は false
  6. Type(x) が 真偽値(boolean)の場合、どちらも true か どちらも falseの場合は true、それ以外は false
  7. x と y が同じオブジェクトを指しているなら true、それ以外は false

 

7番目が kotlin の定義と同じなのだけど、じゃあ Kotlin でオブジェクト以外だと === が使えないということなのか? いやそもそも Kotlin は JavaScript を意識した言語なのだろうか?

そんなことを考えながらページを見ていったのだが 「JS」 というのがあることに気付いた。

image

意識しているとかなんとかではなく、

Kotlin for JavaScript | Kotlin
https://kotlinlang.org/docs/js-overview.html

Kotlin for JS という JavaScript からトランスパイルするものだそうな。
トランスパイルって何だろうと検索すると、TypeScript を JavaScript にトランスパイルする、とか、トランスパイラーとか、そんなのが出てきた。中間言語に置き換える、というのともちょっと違うのかもしれんが、昔の cfront みたいなものと思ってもよいのかもしれん。

ともかく、Kotlin for JS は transpile to JavaScript なので、Kotlin で書いたものを JavaScript に変換して、あとは JavaScript でやってもらうのだろう。立ち位置としては TypeScript と同じようなところにいるのか。まあ、Android などの場合も JVM に transpile するものだし、 Kotlin/Native は iOSも対象に入っているようなので、Kotlin が各ネイティブ言語へのラッパー言語のような戦略をとっているということか。 Java や .NET がやっていることよりも階層が上なので、各言語に対応する大変さはあるものの、その言語が使える環境であれば仮想的なものを用意せずに動かせるという利点がある。今のように「Android は Java」「iOS は Swift」のようにプラットフォームと言語がかなり固定化している時代に向いていると思う。 Kotlin で書いておけば「Android は Java やめます」ってなったとしても、 Kotlin から新しい環境へのトランスパイラさえ作ってあれば済むしね。大変だろうけど。

今回の === も、対応する概念がない言語の場合はトランスパイラがそういうコードを吐いてくれればよいだけなので、言語的な共通さを探すのはあまり意味が無いのかもしれんな。

[android] JavaからKotlinに変換したい

Androidアプリをやるからには、興味がなくても画面が多少いじれないとねー、と思って、手軽そう(な気がした)React Nativeを使ってみようとした。

Setting up the development environment · React Native
https://reactnative.dev/docs/environment-setup

Android Studioはある。Node もインストールしている。Windows 環境変数も設定している。
よし。

image

ちょっと見栄を張って(?)、TypeScript で作ってみる。

npx react-native init RNSample1 --template react-native-template-typescript

書いてあるとおりにやると、エミュレータが立ち上がってサンプルアプリが起動した。
ちなみに React Native は v0.64.2 のようだ。あまり npm のしくみをわかってないのだが、登録されている最新版を使ってくれているのかな?
(注:書いたときはv0.64.2だと思い込んでいたが、package.json を見ると v0.64.1 だった。何か見間違えていたらしい。 npm i react-native@0.64.2 まで指定するとちゃんと更新された。)


このテンプレートは Android の部分が Javaでできているので、どうせなら Kotlinにしておきたい。

テストアプリを作っていて気付いたのだが、Java のコードをクリップボードでペースとすると自動的に Kotlin変換して貼り付けるか聞いてくれるのだ。
すばらしい!

というわけで、プロジェクトを丸々 Kotlin に変換してくれないだろうか。

既存のプロジェクトに Kotlin を追加するにはどうすればよいですか?
https://developer.android.com/kotlin/faq#convert

Java ファイルを開いて、メニューから Convert を選んでやるとよいそうだ。
確認ダイアログが出たあと、やり方を聞いてくる。

image

これで OKを押すと変換され・・・あれ? 初回だからかもしれないけど build.gradle(ルートとappの2ファイル)しか更新されないぞ??

もう一度同じことをやると、今度は変換された。ファイルの拡張子も .kt にしてくれている。
まあよしとしよう(偉そうに...)。


しかし、ファイルを開いてからというのがちょっと面倒だ。
ディレクトリ以下まるまるやってくれないだろうか?

まず、1回目はツリーのトップにある app を選択して実行する。
同じダイアログが出てくるので、OK する。

image

 

2回目はここで実行。
よしよし、複数ファイル行ってくれた。

image

 

と、あっさりうまくいったように書いたが、この手順に落ち着くまでいろいろ失敗した。
ツリー表示の種類のせいか、選択しているディレクトリのせいかわからないけど、 build.gradle の plugins がどうのこうのというエラーが出ていたのだ。

まあ、うまくいったからよいのだ。

変換されたのは、 Java→Kotlin 以外だと build.gradle が 2ファイルだけだ。
何がどう変わったのかは、経験不足で分からん。 ext.kotlin_version とか kotrin-android とか、そんな感じ。

2021/06/05

[android] Googleドライブアプリにintentは投げられないのか

Android で ACTION_SEND で intent を投げるときに chooserでくるんでアプリ選択をユーザにさせるようにしたけれども、Googleドライブにしかファイルを保存させたくないという場合だったら直接Googleドライブアプリに intentを投げればいいんじゃなかろうか?

 

そう思って明示的インテントを作ろうとしたのだが、はて Googleドライブアプリのパッケージが分からない。

端末にインストールされているアプリ情報を取得する - すいはんぶろぐ.io
https://suihan74.github.io/posts/2021/01_09_00_get_installed_applications_info/

こちらを見て、getInstalledApplications() と getApplicationLabel() を組み合わせて一覧を作ってみたのだが、Android アプリ一覧で「ドライブ」に相当するラベル名がない。
パッケージ名も "drive" とか "drv" のようなものがない。まあこっちは略称が違うだけかもしれんが。

Android Studio が getInstalledApplications() で リンク付きの warningを出していたので見てみた。

Android 11 でのパッケージへのアクセス  |  Android デベロッパー  |  Android Developers
https://developer.android.com/about/versions/11/privacy/package-visibility

なんか難解な日本語だが、Android 11 からは Android 11 以降をターゲットとしているアプリは manifest に <queries> を書いておかないと一覧から外れてしまうことがあると言っている?

たぶん Googleドライブアプリのような AOSP にはない Googleアプリは Android 11 以降をターゲットにしていると思うが、メニューには出てくるしなぁ。でもメニューだから出てくると言うこともありそうだし。。。

 

うん、よくわからんが、そこまでして Googleドライブアプリに投げたいわけでもないので、よしとしよう。

[android] intent

前回は、テキストファイルを Googleドライブに対して共有することができた。
正確にいえば、ファイルを受け付けるパッケージに対して共有できた、になるのかな?

気にしているのは intentを投げたときに logcatに出てくる例外。出てくるだけで動作としては問題ないので困っている。 grantUriPermission()で READ_URI_PERMISSION を与えると出なくなるので、さらに困っている。動作しないなら対策しようとするし、ログが消せないならあきらめるのだが、どちらもできてしまうからだ。

何から調べるとよいかわからないので、まずは intentからいってみよう。


Android にいくつ基本要素があるか知らないが、 intent はその中でもかなり上位に位置するだろう。

インテントとインテント フィルタ  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/guide/components/intents-filters?hl=ja

前回は startActivity() を呼んだので、アクティビティの開始になる。特定のパッケージを指定しなくても、暗黙的インテントであればマッチするパッケージの候補を出してくれるのは助かるね。
それより前には startService() とか bindService() とかしたので、サービスの開始もやったことがあることになる。ブロードキャストはまだだな。

 

intent への設定項目は多数ある。

気になっていたのは setData() と putExtra()。どっちも intent先に渡すデータを設定しているように見えるのだが、違いが分からん。
読むと、データはまあデータで、エクストラは key-value のペアとのこと。前回の intentでは URIしか送るものはなかったのだが、それはデータではなくエクストラだった。

どこでその使い分けをするかというと、アクションくらいしかなさそうだ。。。と思ったのだが、下の例を見ていくと「明示的インテントの例」では送り先のパッケージを指定してデータを設定して startService()しているだけで、アクション指定はない。そういうやり方もあるのか。

ACTION_SEND を見ると、エクストラの記述がいろいろある。 ACTION_PICK は getData()と書いてあるので、送り元としては setData()してあげればよいということかな。公式というか共通というか、そういうアクションについてはそのページの Input のところを見てやればよいのだろう。

 

改めて ACTION_SEND を見よう。とにかく書いてあることが多い。

  • Activity Action
    • データを誰かに送信する
    • データの配信先は特定されていない
    • データの送信先をユーザに尋ねるのは、アクションの受信者次第
    • SEND intentを立ち上げるとき chooserでくるんであげると、ユーザがデータの送信方法を選択するプロンプトが出てくる(通常はそうする)
  • Input
    • MIMEタイプ
    • EXTRA_TEXT か EXTRA_STREAM + 送信データ
      • EXTRA_TEXTの場合、MIMEは "text/plain"
        • その場合、EXTRA_HTML_TEXT でクライアントがHTMLフォーマットでテキストを取得するようにもできる
      • それ以外なら EXTRA_STREAM
    • JELLY_BEAN(Android 4.1)以降であれば、データを setClipData()経由で送ることもできる
      • コンテンツを共有するときに FLAG_GRANT_READ_URI_PERMISSIONを使用できる
      • このアプローチを使用する場合、互換性のために EXTRA_TEXT か EXTRA_STREAM で同じデータを提供する必要がある。ClipDataを設定しない場合、 setActivity()を呼んだときにコピーされる。
    • Oreo(Android 8)から、CATEGORY_TYPED_OPENABLEが渡された場合、EXTRA_STREAM または setClipData()を介して渡された URIsは openTypedAssetFileDescriptor()を使用するあっセットタイプファイルとしてのみ開くことができる
    • 受信側が解釈できるオプションの標準エクストラは以下
      • EXTRA_EMAIL, EXTRA_CC, EXTRA_BCC, EXTRA_SUBJECT
  • Output
    • 無し
  • Constant Value
    • "android.intent.action.SEND"

機械的に訳した。

 

EXTRA_TEXT や EXTRA_STREAM で渡す方法だけでなく、ClipData経由でも渡すことができるのが grantの例外と関わっているのだろう。

まず ClipDataだが、これはクリップボードのデータ、というわけではないようだ。というのも間違いなのかな。クリップボードに保存したデータは ClipData扱いになるから、結果としては同じで良いのかな?

intentに ClipDataを紐付ける関数として setClipData()があるが、ClipDataに含まれる URIに対して FLAG_GRANT_READ_URI_PERMISSION や FLAG_GRANT_WRITE_URI_PERMISSION が機能するそうだ。

 

つまり、今回のように ClipData を使うつもりがない場合には気にしなくてよいはずだ!という結論に至った。私の解釈が間違ってなければね。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(filesDir, filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         val chooseIntent: Intent = Intent.createChooser(shareIntent, null)
14:         // ClipDataを使う場合
15:         // grantUriPermission("android", pathUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
16:         startActivity(chooseIntent)
17:     }

AndroidStudio というか IntelliJ というかはショートカットキーでコメントアウトすると行頭になるのがちょっときになるね。思わず手動でやってしまった。

2021/06/04

[android] テキストファイルをGoogleドライブに置きたい (3)

はい、というわけでファイルをGoogleドライブに置きたいだけの記事が、これでもう3回目です。
がんばっていきましょう。

前回は、エミュレータで実行して Googleドライブに置けそうな画面になったものの、Googleアカウントの設定をしていなかったので確認できなかったところまでだ。
エミュレータにメインアカウントを登録するのって、気が引けるのは私だけだろうか。


手元にあった、ASUS の P008 というタブレットで動かしてみる。
Nougat の Android 7.0 の方だ。 Android 7って、 7.0系と 7.1系があるけど、けっこう違うのかな?

これがボタンを押して intent が飛んだとき。

image

マイドライブを選択すると、マイドライブの直下に myfile.txt が保存されていた。
中身は「Hello world!」。

期待通りである。
めでたしめでたし。


・・・とはいかない。

昨日の記事には書いたが、エミュレータで実行したときは共有先の候補が出るところまでなのに logcat にはずらずらと exceptionが出力されていたのだ。

それが P008 では出ていない。何も出ていない。
ということは Android OSが新しくなってセキュリティ的に厳しくなったことで出るようになったと考えるのが妥当か。

 

では、 Android 11 がインストールされている Pixel3aXL で動かしてみる。
動作はほぼそのままなのだが、logcatに出てきた。
最初に intentを飛ばすところで出ているようだ。それ以降は出ていない。

 

2021-06-03 22:03:24.606 28296-28316/com.example.sample3 E/DatabaseUtils: Writing exception to parcel
     java.lang.SecurityException: Permission Denial: reading androidx.core.content.FileProvider uri content://com.example.sample3.fileprovider/myfiles/myfile.txt from pid=28371, uid=1000 requires the provider be exported, or grantUriPermission()
        at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:820)
        at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:684)
         at android.content.ContentProvider$Transport.query(ContentProvider.java:239)
        at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:106)
        at android.os.Binder.execTransactInternal(Binder.java:1154)
        at android.os.Binder.execTransact(Binder.java:1123)

改行が入って見づらいのだが、探しやすそうなのはこの辺か。

E/DatabaseUtils: Writing exception to parcel

Permission Denial: reading androidx.core.content.FileProvider uri ..(略).. requires the provider be exported, or grantUriPermission()

FireProvider で grantUriPermission() ということで検索すると、こういうのが出てきた。

Granting Temporary Permissions to a URI
https://developer.android.com/reference/androidx/core/content/FileProvider#Permissions

一時的に、作成したコンテントURI に対してアクセス許可を与える話らしい。
Permission Denial なので何か権限がいるのは分かるが、URI に与えるのか。ファイルに Read 権限を与えないと読むことができないのと同じだろうか。

いや、Writing exception なので書込みに関しての例外が起きたのだろう。でも、URI って読めればよいのではないのか?

Stackoverflow で解決案を出している人は、

android - Permission Denial while sharing file with FileProvider - Stack Overflow
https://stackoverflow.com/questions/57689792/permission-denial-while-sharing-file-with-fileprovider

まずは試そう。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(getFilesDir(), filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         val chooseIntent: Intent = Intent.createChooser(shareIntent, null)
14:         val resInfoList =
15:             this.packageManager.queryIntentActivities(chooseIntent, PackageManager.MATCH_DEFAULT_ONLY)
16:         for (resolveInfo in resInfoList) {
17:             val packageName = resolveInfo.activityInfo.packageName
18:             grantUriPermission(
19:                 packageName,
20:                 pathUri,
21:                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
22:             )
23:         }
24:         startActivity(chooseIntent)
25:     }

雰囲気としては、intent を送ることができそうなところを MATCH_DEFAULT_ONLY という条件で検索して、それらのパッケージに対して個別に grantUriPermission() で READ | WRITE の一時的な権限を割り当てる、というところか。

URI に与えるのではなく、 URIにアクセスする方に与えているのかな?
例外のメッセージも 1つだけだったので、実はどれか特定のアプリだけが WRITE も必要なのかもしれん。

 

いや、そうだとしたら、この方法は WRITE がいらないアプリに対しても許可を与えているということになるんじゃないか?
というわけで、for文の中で packageName をログ出力したのだが・・・「android」 1つしか出てこなかった。

結果として、android に READ だけ付けてあげれば exceptionは出なくなった。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(filesDir, filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         val chooseIntent: Intent = Intent.createChooser(shareIntent, null)
14:         grantUriPermission("android", pathUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
15:         startActivity(chooseIntent)
16:     }

ただ・・・あまりすっきりはしない。
だって、grantUriPermission() しなくても Googleドライブにファイルは置けるのだから。
logcat に例外を見たくないというだけで権限を追加するのもなぁ。

とはいえ、READ くらい付けてやらないといかんのじゃないか、という気がしなくもない。
うーむ。。。

2021/06/03

[android] テキストファイルをGoogleドライブに置きたい (2)

前回はファイル共有の設定までだった。
今度は置く方だ。

他のアプリへの単純なデータの送信  |  Android デベロッパー  |  Android Developers
https://developer.android.com/training/sharing/send#send-binary-content

ファイルを置くということであれこれ悩んだのだが、ファイル共有することのできるURIデータを送信する、という考え方でよいのではなかろうか。
ここに載っていた例はテキストコンテンツやバイナリコンテンツで別々に書かれているものの、中を見れば putExtra()で送信したいデータを決め、setType()でデータの種類を教えているだけだと思うのだ。

テキストコンテンツの場合は文字列をそのまま渡しているが、バイナリコンテンツの場合はURIを渡している。だからバイナリコンテンツの送信をまねすれば良かろう。良いはずだ。

 

あれこれ調べながらやっているうちに寝る時間になってしまったじゃないか!
とりあえず、Activityの onCreate()でファイルを作って、ボタンを押したら共有のダイアログ?が出てくるところまで確認した。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(getFilesDir(), filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         startActivity(Intent.createChooser(shareIntent, null))
14:     }

なんで Googleドライブに保存するところまで確認しなかったかというと、エミュレータを使っていて Googleアカウントの設定がされていなかったためだ。

次回は実機でやろう。
それに、これを実行しても logcatには例外が出ていたので、そこも気になる。動いてるけど出ているから、よろしくはないのだろう。

2021/06/02

[android] テキストファイルをGoogleドライブに置きたい (1)

そう、ただそれだけのことだ。Android といえば Googleだから、Googleドライブに置くだけだったら簡単に違いない。

それがどういう処理なのか考えていなかったのだが、ファイル共有に相当するようだった。

ファイル共有の設定  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/secure-file-sharing/setup-sharing?hl=ja

ローカルファイルのパスを渡しておしまい、とはいかないようだ。


いや、その前にこれを見つけた。

他のアプリへの単純なデータの送信  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/sharing/send?hl=ja

単純こそ求めるものなのだよ。

テキストコンテンツとバイナリコンテンツがある。
テキストファイルをファイルのままで渡したい場合にはどうなるのだろうか?
ここの例に出てくるのは「コンテンツ」としてのデータなのだと思うが、私はあくまでファイルとして渡したいのだ。

 

そうなんだよ。タイトルも「データの送信」って書いてあるじゃないか。
となると、やはりファイルの共有を読むことになるだろう。


いろいろ設定がいる。

  • AndroidManifest.xml に FileProvider を追加
  • res/xml にファイルパス設定をする XMLファイルを追加
    • このファイルは AndroidManifest.xml で指定することになる
  • 実装する

 

AndroidManifest.xml は、まあなんとなくいいとしよう。
設定は必要だ。

<paths> の設定をする XMLファイル。
これは、共有ファイルを格納するディレクトリを指定するもののようだ。最初からディレクトリを指定しておくというのはわかる。
よくわからないのが、パスセグメント、というやつだ。

<files-path path="images/" name="myimages" />

attributeの pathに指定するのがアプリのデータディレクトリ/files の下に配置するディレクトリ名で、この中に入っているファイルが共有されるのだろう。そして name がパスセグメントというものなのだろう。 コンテンツURIに追加すると言われても、このページの中ではそれ以上の説明がないのだ。

content://com.example.myapp.fileprovider/myimages/default_image.jpg

この例からすると、 path の方が物理デバイスのディレクトリ名で、 name の方が URIのコンテンツ名のディレクトリ名ということなのかね。


ともかく、これで準備だけはできたようだ。
あとはファイルを置いて共有するだけなのだが・・・

ファイルの共有  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/secure-file-sharing/share-file?hl=ja

ファイルの共有って、共有アイコンをタップしたときに下からするっと選択画面が出てくるあれではないのだろうか?
さっきの「データの送信」の方はそういう画面例が出ていたのだが、ああいうのがほしいのだ。

共有ファイルのリクエスト  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/secure-file-sharing/request-file?hl=ja

よし、続きは後日だ。

2021/05/30

[android] WorkManager

今回、Androidの Activity以外で動作するコンポーネントについて集中して調査をしている。Activity は iOSなどと同時開発できるプラットフォームを使う前提だからだ。

主に Serviceを見ていたのだが、バックグラウンドで動作するものという調べ方をしていると WorkManagerなるものが出てきた。

WorkManager でタスクのスケジュールを設定する  |  Android デベロッパー  |  Android Developers
https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja

Android Jetpackの一部、と書かれているので、似たような素の機能があるならこれを使った方がバージョンアップなどに対応しやすいということだろう。

WorkManager は、アプリが終了した場合やデバイスが再起動した場合でも実行される、延期可能な非同期タスクのスケジュールを簡単に設定するための API です。

あとから1回や繰り返しとかの実行をすると決めたら、アプリの終了や再起動が行われても実行するというもののようだ。義理堅いというか執念深いというか。

実装するのは、doWork() が処理本体、 WorkRequest がスケジュールと思えば良いか。

 

Service とはちょっと違うが、後始末とか、必ず実行しないといけないとか、そういうのがあれば思い出すことにしよう。

[android] バックグラウンドサービスの停止がよくわからん

長生きしてほしい、けれども寿命があることも分かっている。スマホのプロセス管理はそんな感じじゃなかろうか。
ちなみに小さい組み込み環境の場合は、全部動作したとしても動くだけのメモリを確保することを考えることが多いので、アプローチが違う。火災警報器とかで「いま煙検知しててRAMが足りないのでアラームが鳴動できません」とかあったらダメだしね。

それはともかく、前回のアプリを強制終了しないといけない場合の候補優先度表があるので、その候補から下げてもらうためには何かしないといけない。

Google Developers Japan: 複数の JobService を活用する
https://developers-jp.googleblog.com/2017/10/working-with-multiple-jobservices.html

基本的に、アプリがフォアグラウンドで実行されている場合を除き、アプリのバックグラウンド サービスはシステムによって数分以内に停止されます。

API26 からそうなったらしい。
試してみよう。前回のサンプルは Activityの onStop()で Sericeを止めていたので、タイミングを onDestroy() にずらそう。

2021-05-30 12:42:42.990 D/MainActivity: onCreate
2021-05-30 12:42:43.417 D/MyService: onCreate
2021-05-30 12:42:43.417 D/MyService: onBind
2021-05-30 12:42:43.770 D/MainActivity: onServiceConnected

(Home画面)

2021-05-30 12:44:30.272 D/MainActivity: onStop

として 1時間程度放置したのだが、特に変化がない。Home画面にするだけでは甘くて、別のアプリがフォアグラウンドで動いていないといかんのだろうかと思ったのだが、別アプリを起動して4時間くらい経ったが変わらず。何か別のルールもあるのだろうか。

2021/05/29

[android] Activityの一生

前回、Activityが非表示になるとすぐ onStop()が呼ばれていた。まあ、何も実装していない Activityだったからかもしれないが、Activity がどう動くのか確認しよう。

アクティビティのライフサイクルに関するコンセプト
https://developer.android.com/guide/components/activities/activity-lifecycle#alc

手元に昔購入した Androidの本があるが、ライフサイクルはそのときから変わっていない。この本は APIレベル3 くらいの時代だから、最初から変わっていない、といってしまってもよいのかもしれない。

 

まずは、ライフサイクルに出てくる各メソッド+onRestartにログを埋め込んで見てみる。

2021-05-29 16:28:17.438 D/MainActivity: onCreate
2021-05-29 16:28:17.980 D/MainActivity: onStart
2021-05-29 16:28:17.988 D/MainActivity: onResume

(アプリ一覧)

2021-05-29 16:29:52.977 D/MainActivity: onPause
2021-05-29 16:29:53.811 D/MainActivity: onStop

(自アプリ選択)

2021-05-29 16:30:50.871 D/MainActivity: onRestart
2021-05-29 16:30:50.874 D/MainActivity: onStart
2021-05-29 16:30:50.879 D/MainActivity: onResume

(Home画面)

2021-05-29 16:31:45.120 D/MainActivity: onPause
2021-05-29 16:31:46.072 D/MainActivity: onStop

Home画面に戻してからいろいろアプリを立ち上げたり、1時間ほど放置したりしたのだが onDestroyは呼ばれなかった。アプリ一覧にして上にスワイプして終了させると onDestropyが呼ばれた。

 

Serviceのサンプルアプリでは onStopのタイミングで unbindService()したからそうなったが、別に onDestroyでもよいはずだ。どうせ同じプロセスだし。メモリの残りが少なくなったら、きっと onStopされたアプリは終了させられる候補に入るのだろう。

などということがライフサイクルのところに書いてあるんじゃないのかな?
読むとしよう。


Activityのライフサイクルについては、もう至る所で語り尽くされているに違いない。
だからここではアプリか終了させられるパターンだけに注目しよう。

onDestroy()
https://developer.android.com/guide/components/activities/activity-lifecycle#ondestroy

あれ、onDestroyされるのは 「アクティビティの終了」か「構成の変更によるアクティビティの破棄」しかない。メモリが少なくなったら、という条件がここに書かれていないということは、もっと面倒な話ということか。

 

アクティビティの状態とメモリからの退避
https://developer.android.com/guide/components/activities/activity-lifecycle#asem

ライフサイクルとは別に「プロセスの強制終了」があるのか。うん、あるよな。

表1によると、フォアグラウンドになっていても強制終了される可能性はあることになっている。が、たぶんそれは立ち上げられないほどメモリが無い状態だろうから考慮から外そう。
表の一番下の「破棄」も、まあ忘れていいだろう。プロセスが空だし。

残るは、優先度高の「バックグラウンド(非表示)」と優先度中の「バックグラウンド(フォーカスがない)」だ。 onStop と onPause の違いだろう。
ちなみに英語版だとこういう表になる。 Activity state がメソッド名と同じようになるので、多少わかりやすいか。あんまり変わらんか。

image

いくら翻訳が良くても、やっぱり原文でがしがし読めるようにならないと情報に追いつけないよなぁ、とは思うんだけどね.....

 

ライフサイクルがあるのはわかったとして、動作確認が大変すぎないか? 通常の遷移くらいであればやれば済むのだが、onPauseのときに強制終了、とかやりたかったとしても簡単にできるとは思えん。 onStopならアプリ一覧から終了させればよさそうだけど。

 

アプリのアクティビティをテストする  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/testing

うーん、ちょっと違うか。
しかしAndroidのテスト方法は知らないので、それはそれで知っておきたいものだ。

[android] Service (7) - bindしてみよう (4)

今回で同一プロセスでの bindシリーズを終わらせたい。

 

まず、動かす。
これはサンプルコードそのままやればよい。
ファイルを追加しながらやるよりも、AndroidStudioの Service追加を使う方がよいだろう。AndroidManifest.xml に <service> を追加してくれるからだ。まあ、これは初心者ならではか。exported設定は、今回は同一プロセスのみにするのでfalseでよいだろう。

 

AndroidManifest.xml (追加行)

01: ...
02:         <service
03:             android:name=".MyService"
04:             android:enabled="true"
05:             android:exported="false"></service>
06: ...

 

MyActivity.kt (importなどは省略)

01: class MainActivity : AppCompatActivity() {
02:     private val logTag: String = "MainActivity"
03:     private lateinit var mService: MyService
04:     private var mBound: Boolean = false
05: 
06:     private val mServiceConnection = object : ServiceConnection {
07:         override fun onServiceConnected(className: ComponentName, service: IBinder) {
08:             Log.d(logTag, "onServiceConnected")
09:             val binder = service as MyService.MyBinder
10:             mService = binder.getService()
11:             mBound = true
12:         }
13: 
14:         override fun onServiceDisconnected(p0: ComponentName?) {
15:             Log.d(logTag, "onServiceDisconnected")
16:             mBound = false
17:         }
18:     }
19: 
20:     override fun onCreate(savedInstanceState: Bundle?) {
21:         Log.d(logTag, "onCreate")
22:         super.onCreate(savedInstanceState)
23:         setContentView(R.layout.activity_main)
24: 
25:         intent = Intent(this, MyService::class.java)
26: //        startService(intent)
27:         bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
28:     }
29: 
30:     override fun onStop() {
31:         Log.d(logTag, "onStop")
32:         super.onStop()
33:         if (mBound) {
34:             unbindService(mServiceConnection)
35:             mBound = false
36:         }
37:     }
38: }

 

MyService.kt (importなどは省略)

01: class MyService : Service() {
02:     private val logTag: String = "MyService"
03: 
04:     private val mBinder = MyBinder()
05: 
06:     inner class MyBinder: Binder() {
07:         fun getService(): MyService = this@MyService
08:     }
09: 
10:     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
11:         Log.d(logTag, "onStartCommand")
12:         return super.onStartCommand(intent, flags, startId)
13:     }
14: 
15:     override fun onBind(intent: Intent): IBinder {
16:         Log.d(logTag, "onBind")
17:         return mBinder
18:     }
19: 
20:     override fun onCreate() {
21:         Log.d(logTag, "onCreate")
22:         super.onCreate()
23:     }
24: 
25:     override fun onDestroy() {
26:         Log.d(logTag, "onDestroy")
27:         super.onDestroy()
28:     }
29: }

これでビルドできたのでエミュレータにインストールして起動させると logcatが出た。

D/MainActivity: onCreate
D/MyService: onCreate
D/MyService: onBind
D/MainActivity: onServiceConnected

bindService()を呼んで Serviceが作られて、 onBindでインスタンスを返すという順番だ。

 

アプリを閉じる(Home画面を表示)とすぐに onDestroyのログも出た。時間も載せよう。

2021-05-29 15:59:35.365 D/MainActivity: onStop
2021-05-29 15:59:35.400 D/MyService: onDestroy

onSuspend()など実装していないので細かいのは分からんが、onStop()ってこんなにすぐ呼ばれるものなの? bindせずに startService()だけした場合には1分で onDestroyのログが出ていたので、bindしている=アプリとの結びつきが強くなる=アプリの onDestroy()に引っ張られる、ということかしらね。

startService()に変更して試してみよう。

D/MainActivity: onCreate
D/MyService: onCreate
D/MyService: onStartCommand

Home画面にする。

2021-05-29 16:00:41.858 D/MainActivity: onStop
2021-05-29 16:01:41.854 D/MyService: onDestroy

ほら、約1分。
不思議だねぇ。

2021/05/28

[android] Service (6) - bindしてみよう (3)

前回の続きで、Serviceをbindする方の入口となる IBinderの派生クラスを見ているところである。

inner classとして IBinderの派生クラスである LocalService.LocalBinder クラスを作っているのだが、そこで1つだけ実装されている関数が分からん。

01: // Return this instance of LocalService so clients can call public methods
02: fun getService(): LocalService = this@LocalService

関数名は getService。
戻り値の型は LocalService、つまり今回のサンプル実装である Service。
Kotlinでは戻り値しかない関数は括弧を書かずにイコールだけで表現できる。

this@LocalService が気になるのだが、おそらくここで this とだけ書くとどの classの thisなのかわからないので @で指定しているのだろう。どの言語でもそうだけど、記号関係は検索しづらくて大変だ。
ああ、ここだ。 superについても同様だ。

Service は intentを経由?して startService()で起動するので、インスタンスは OSが持っているようなものだろう。だからこの thisも OSが持っていると思われるインスタンスだろう。今回はプロセスが1つしかないのでどっかのメモリを指しているだけと思って良いか。

で、この getService()だが、overrideもなにもついていないので、勝手に作った関数である。これをどうするかというと、Binderのインスタンスを Activityなどに渡してあげて、そのインスタンスを LocalService.LocalBinder にキャストし、そうしてようやく getService()を呼び出してインスタンスを得るのだ。

同じメモリなんだから Binderとか経由せずに Serviceのインスタンスを渡せばいいんじゃないの?という気もするのだが、まあ何か理由があるんだろう。リモートオブジェクトのためのベースクラスらしいので、扱いを共通化するためにそうしているのかもしれん。


図にしたら何か分かるやすくなるかと思ったが、全然だ。

image

今回は現在の Serviceのインスタンスをそのまま渡したが、Binder自体に呼び出したい関数があっても良いし、Serviceが持っている他のクラスのインスタンスを返しても良いらしい。自由だ。同じメモリ空間だから、結局どうでもよいってことなんだろうか。他のクラスについても「サービスでホストされた」というのがよくわからんが、たぶん Serviceが直接呼び出すことができるインスタンスなんだろう。

 

Activityサンプルは、ボタンを押したら LocalServiceの randomNumber()を呼び出している。bind しているかどうかをフラグで持つのは分かるが、排他処理はいらないんだろうか? 乱数取得なんて排他とか気にしなくてよいとは思うのだけど気になるな。

というわけで、今日はここまで。

2021/05/27

[android] Service (5) - bindしてみよう (2)

なかなか進まないAndroidのServiceだが、技術日記だからのんびりいきます。

前回は、bindService()を呼べば良いけど、その前に ServiceConnectionなんかがいるということがわかるところまでだった。

 

bindしてもらう Serviceは、ただ onBind()を実装して IBinderを返せばよいというわけではなく、クライアントからの要求を受け付けられるようにしないといけない。インターフェースを提供する、と表現されているので、それに倣おう。

インターフェースの定義には3つの方法がある。

  • Binderクラスの拡張
  • Messengerの使用
  • AIDLの使用

説明を読む限りでは、こうか。

  • アプリ専用に Serviceを用意してアプリと同じプロセスで実行する場合は、Binderクラスを拡張する
  • 異なるプロセスで実行されるなら、Messengerを使用する
  • AIDLはおすすめしない

おすすめしない理由もしっかり調べるべきだろうが、今回はスルーだ。 Messenger を使うやり方は AIDLを内部で使っていそうなことを書いてある。 AIDL は昔からあったと思うのだが、使い方が危ういから Messengerでラップしたとかだろうか?

まあいい。ただでさえ進まないので、AIDL については私は調べないということにして、出てきたら考えよう。


まず、Binderクラスを拡張するやり方から見ていく。

同一プロセスで動作するのだから、メモリ空間も同じだ。
となると、マルチスレッドで動作して、オブジェクトは Mutexで排他しながら使うのだろうか。共有メモリのように大きな排他システムを使わなくてよいので、パフォーマンスが良くなるとかじゃなかろうか。

なにより、アプリから Serviceのメソッドを直接呼び出せるというのがよい。直感的だ。 Activityで全部実装した後で、やっぱりここは Serviceにしよう、という場合にも移動させやすいのだと思う。

 

この節で説明に使っている LocalServiceは、 randomNumberの getterだけが publicになったクラスだ。 getterが呼ばれるたびに乱数を返す。

そして onBind()では IBinderを返すのだが、これは constな LocalBinderクラスのインスタンスを返すだけ。この LocalBinderクラスが Binderクラスを拡張したものである。
何をやっているかというと、getService()関数だけを定義していて(Binderを継承しているのでオーバーライドしているのかな?)、それは自分自身を返している・・・?

ちょっと Kotlinの読み方が分からないので、調べる。

01:     inner class LocalBinder : Binder() {
02:         // Return this instance of LocalService so clients can call public methods
03:         fun getService(): LocalService = this@LocalService
04:     }

まず、 Binderクラスを継承している。 Kotlinは class内に classが定義できる(nested class)し、内部classも定義できる(inner class)。 あと、無名内部class(anonymous inner class)もある。 Javaもそんな感じだったか。

このページだけ読むと、inner classにしておくと外側の classが持つ変数もアクセスできるように見える。それだけなのか??

 

と、中途半端なところで今日は終わり。

2021/05/26

[android] Service (4) - bindしてみよう

前回はサービスのバインドというものについて眺めただけなので、今回は動かしてみよう。

バインドされたサービスの概要  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/bound-services?hl=ja

サービスを開始させるだけならActivityなどからintentを作ってstartService()すれば動いたのだが、bindするにはいろいろやってやらんといかんようだ。

 

上から順番に。

onBind()を実装してIBinderオブジェクトを返すと、それを通じてServiceに指示ができるようだ。サーバとクライアントの関係だったと思うので、socketみたいなイメージでよいのではなかろうか。

あれ?
今までもデフォルトで作ったサービスにonBind()があったけど、特に何もしていない。

01:     override fun onBind(intent: Intent): IBinder {
02:         Log.d(logTag, "onBind")
03:         TODO("Return the communication channel to the service.")
04:     }

確かに戻り値はIBinderなのだが、これ、戻り値をしていしていないのにコンパイルエラーになってないのだが、どういうことだろうか?

戻り値を指定しない場合はその型のデフォルト値になる、というタイプかと思ったのだが、Stringを返す関数を作ってみたのだが「returnがない」とエラーになった。

コンパイルエラーになっていないのは、コメントと勘違いしていた TODO だった。

TODO - Kotlin Programming Language
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-t-o-d-o.html

常に例外を投げるので、コンパイラもそれより後ろは評価しないと言うことなのだろう。まあ、手っ取り早くサンプルを動かしたいときは役に立つな。
できればwarningくらい出してほしかったが、コンパイラには何も出ない。出す理由がないのか。
AndroidStudioだと色が違って表示されるので、何かKotlinで指定があるのかと思ったが、単に「TODO」という文字列に対して反応しているだけのようだ。


さて、Kotlinの話はここまでにしてbindできるようにしていこう。

 

ActivityなどからbindService()を呼び出せば良いのだが、その引数にIntentとServiceConnectionがいる。IntentはstartService()したものと同じでよさそうな気がするが、ServiceConnectionはなんだろうか?

bindService()を実行した側からすると、

  • bindしようとしているServiceが存在する
  • bindしようとしているServiceに対するアクセスが許可されている

という条件が満たされているかチェックされて、ようやくbindできることになる。

 

bindされるServiceはどうするかというと、bindService()を呼ぶと Serviceの onBind()が呼び出される。 onBind()は IBinderオブジェクトを返して、これを Serviceと通信するのに使ってもらう。
そう書いてあるのだが、bindService()の戻り値はbooleanだ。さっき書いた2条件を満たしたかどうかという戻り値だ。

なので、IBinderオブジェクトは ServiceConnection の方に出てくる。onServiceConnected()の引数で IBinderが渡されてくるのだ。それを getSerivce()するとサービスのインスタンスが取れるようだ。

とはいっても、bindService を呼び出した方と Serviceが同じプロセスであるという保証はないはずだ。だから間に Androidが入って bindService() を経由してうまいこと扱えるようにしてくれているのだろう。

 

はあ、眠たいので今日もここまで。
全然進まない・・・。

2021/05/25

[android] Service (3) - bindって何よ2

びっくりしたことに、昨日はbindのことを記事にするつもりだったのに、一言も触れずに終わっていた。

hiro99ma blog: [android] Service (2) - bindって何よ
https://blog.hirokuma.work/2021/05/android-service-2-bind.html

かろうじてonBind()があるからセーフか?
・・・いや、そんなことはない。


「バインドされたサービスは、クライアント サーバー インターフェースにあるサーバーです」

バインドされたサービスの概要  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/bound-services?hl=ja

いきなり何を言っているかわからんな。ちなみに英語だと"bound services"と、bindの過去分詞になっている。bindedじゃないんだね。

bindされたServiceがサーバ、bindしたコンポーネントがクライアントのような立ち位置になるということだろう。

 

 

Serviceが実装されていて、ActivityなどからstartSerivce()でそのServiceを起動したとする。この状態が「開始された」サービスなのだろう。開始しているサービスがあって、それに対してbindService()すると「バインドされたサービス」になるようだ。
英語の方を見ると、別にわざわざ「開始された」を強調したりしてはいない。

 

前回のlogcatではonCreate, onStartCommandだけしか出ていないが、これにバインドまでするとonBindのログが出るんだろう。

 

眠いので、今回はここまで。

2021/05/23

[android] Service (2) - bindって何よ

前回に引き続き、Android OSのServiceについてだ。

うちのサイトは「技術日記」なので、学習としては向いていないと思う。
その代わり、正しくなかったことも書いているはず。
まあ、ネットで調べながらだとそういうことも少なくなってしまうが。


で、Serviceだ。

AndroidStudioでMyServiceというサービスを組み込んでみたのだが、その状態でアプリを起動させてもサービスが立ち上がった気配がない。気配というのは、overrideできる関数を全部オーバーライドしてLogを埋め込んだもののlogcatに何も出力されなかったからだ。

というわけで、ServiceはActivityからの起動が必要となる。
「別のアプリ コンポーネントがサービスを開始でき」と書いてあるので、別にActivityから起動させる必要はないのだが、知らないサービスを起動させることはできないだろうから、まあだいたいはServiceとセットになったActivityから起動させるのが妥当だろう。

01: package com.example.sample0
02: 
03: import android.content.Intent
04: import android.os.Bundle
05: import androidx.appcompat.app.AppCompatActivity
06: 
07: class MainActivity : AppCompatActivity() {
08:     override fun onCreate(savedInstanceState: Bundle?) {
09:         super.onCreate(savedInstanceState)
10:         setContentView(R.layout.activity_main)
11: 
12:         intent = Intent(this, MyService::class.java)
13:         startService(intent)
14:     }
15: }

Kotlinだとnewは不要なのは知っていたのだが、intentの書き方が分からんかった。
インテントとインテントフィルタ」に書いてあったしエラーにならなかったので、これでよいのだろう。

 

MyService.ktはオーバーライドできそうな関数にログを埋め込んだだけ。

01: class MyService : Service() {
02:     private val logTag: String = "MyService"
03: 
04:     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
05:         Log.d(logTag, "onStartCommand")
06:         return super.onStartCommand(intent, flags, startId)
07:     }
08: 
09:     override fun onBind(intent: Intent): IBinder {
10:         Log.d(logTag, "onBind")
11:         TODO("Return the communication channel to the service.")
12:     }
13: 
14:     override fun onCreate() {
15:         Log.d(logTag, "onCreate")
16:         super.onCreate()
17:     }
18: 
19:     override fun onDestroy() {
20:         Log.d(logTag, "onDestroy")
21:         super.onDestroy()
22:     }
23: }

これでActivityを立ち上げると、onCreate, onStartCommandの順でlogcatが出てきた(エミュレータで起動)。

そのまま放置していたが何も変わらない。
そしてアプリを閉じる(Androidのホーム表示)にすると、1分後にonDestroyがlogcatに出てきた。

たった1分かー。

[android] Service (1)

では、AndroidStudioを使ってServiceを追加してみよう。
特に何かしたいわけでもないので、毎分logcatで何か出し続けるようにしてみる。そうすればいつアプリが終了させられたのかもわかりやすいんじゃなかろうか。

 

デフォルトをちょっと変更したプロジェクトの設定はこんな感じ。

image

image

image

image

家にあるAndroid端末で一番古いのがNougatなので、Min SDK Versionはこの辺にしている。

 

プロジェクトを新規作成して取りあえずビルドしてみたのだが、トップディレクトリにあるbuild.gradleのjcenter()でwarningが出ていた。

JCenter サービスの更新  |  Android デベロッパー  |  Android Developers
https://developer.android.com/studio/build/jcenter-migration

既存のものは無期限にダウンロードできます、といっても「閉鎖されるまでは」という条件が付くだろう。メンテナンスしないとしても電気代はかかるし、自社サーバだったらストレージが故障したりするし、クラウドだったらストレージの費用も馬鹿にならんし。


Serviceを追加する!と気合いを入れずとも、AndroidStudioならメニュー操作で基本的なところはやってくれそうだ。

image

2つある・・・?

"Service"の場合

image

"Service(IntentService)"の場合

image

ちなみにActivityのあるディレクトリ?以外で実行するとどこに組み込むかのコンボボックスが増えていた。
main, debug, releaseの3択だったのだが、特定のVariantにだけ追加する(debug, release)か、全部に追加する(main)かを選んでいるのだろうか。

image

Build Variantsを追加すると増えていたので、たぶんそうなんだろう。

image


まずは、ただのServiceを追加した場合。

 

ファイルの追加

app\src\main\java\com\example\sample0\MyService.kt

01: package com.example.sample0
02: 
03: import android.app.Service
04: import android.content.Intent
05: import android.os.IBinder
06: 
07: class MyService : Service() {
08: 
09:     override fun onBind(intent: Intent): IBinder {
10:         TODO("Return the communication channel to the service.")
11:     }
12: }

 

AndroidManifest.xmlに追記。

01:         <service
02:             android:name=".MyService"
03:             android:enabled="true"
04:             android:exported="true"></service>


こちらはIntentServiceを追加した場合。

 

ファイルの追加

app/src/main/java/com/example/sample0/MyIntentService.kt

01: package com.example.sample0
02: 
03: import android.app.IntentService
04: import android.content.Intent
05: import android.content.Context
06: 
07: // TODO: Rename actions, choose action names that describe tasks that this
08: // IntentService can perform, e.g. ACTION_FETCH_NEW_ITEMS
09: private const val ACTION_FOO = "com.example.sample0.action.FOO"
10: private const val ACTION_BAZ = "com.example.sample0.action.BAZ"
11: 
12: // TODO: Rename parameters
13: private const val EXTRA_PARAM1 = "com.example.sample0.extra.PARAM1"
14: private const val EXTRA_PARAM2 = "com.example.sample0.extra.PARAM2"
15: 
16: /**
17:  * An [IntentService] subclass for handling asynchronous task requests in
18:  * a service on a separate handler thread.
19: 
20:  * TODO: Customize class - update intent actions, extra parameters and static
21:  * helper methods.
22: 
23:  */
24: class MyIntentService : IntentService("MyIntentService") {
25: 
26:     override fun onHandleIntent(intent: Intent?) {
27:         when (intent?.action) {
28:             ACTION_FOO -> {
29:                 val param1 = intent.getStringExtra(EXTRA_PARAM1)
30:                 val param2 = intent.getStringExtra(EXTRA_PARAM2)
31:                 handleActionFoo(param1, param2)
32:             }
33:             ACTION_BAZ -> {
34:                 val param1 = intent.getStringExtra(EXTRA_PARAM1)
35:                 val param2 = intent.getStringExtra(EXTRA_PARAM2)
36:                 handleActionBaz(param1, param2)
37:             }
38:         }
39:     }
40: 
41:     /**
42:      * Handle action Foo in the provided background thread with the provided
43:      * parameters.
44:      */
45:     private fun handleActionFoo(param1: String?, param2: String?) {
46:         TODO("Handle action Foo")
47:     }
48: 
49:     /**
50:      * Handle action Baz in the provided background thread with the provided
51:      * parameters.
52:      */
53:     private fun handleActionBaz(param1: String?, param2: String?) {
54:         TODO("Handle action Baz")
55:     }
56: 
57:     companion object {
58:         /**
59:          * Starts this service to perform action Foo with the given parameters. If
60:          * the service is already performing a task this action will be queued.
61:          *
62:          * @see IntentService
63:          */
64:         // TODO: Customize helper method
65:         @JvmStatic
66:         fun startActionFoo(context: Context, param1: String, param2: String) {
67:             val intent = Intent(context, MyIntentService::class.java).apply {
68:                 action = ACTION_FOO
69:                 putExtra(EXTRA_PARAM1, param1)
70:                 putExtra(EXTRA_PARAM2, param2)
71:             }
72:             context.startService(intent)
73:         }
74: 
75:         /**
76:          * Starts this service to perform action Baz with the given parameters. If
77:          * the service is already performing a task this action will be queued.
78:          *
79:          * @see IntentService
80:          */
81:         // TODO: Customize helper method
82:         @JvmStatic
83:         fun startActionBaz(context: Context, param1: String, param2: String) {
84:             val intent = Intent(context, MyIntentService::class.java).apply {
85:                 action = ACTION_BAZ
86:                 putExtra(EXTRA_PARAM1, param1)
87:                 putExtra(EXTRA_PARAM2, param2)
88:             }
89:             context.startService(intent)
90:         }
91:     }
92: }

AndroidManifest.xmlに追記。

01:         <service
02:             android:name=".MyIntentService"
03:             android:exported="false"></service>

なぜか知らんが、AndroidManifest.xmlはさっき追記した <service ...></service> が <service ... /> に置き換えられた。


名前に同じ「Service」が付いているものの、全然似たところがない。

だがIntentServiceServiceの派生クラスだ。
と思ったら! IntentServiceはAPI 30からdeprecatedとのことだ。代わりに androidx.work.WorkManager とか androidx.core.app.JobIntentService を使うことをおすすめされている。

WorkManagerバックグラウンドタスクのところに出ていた。 JobIntentService は「作業ステータスを報告する」に出てきていた。

 

一つ分かったのは、初学者にとってはclassから調べていくのは効率が悪そうだ、ということだ。「人間とは何か」を調べる命題があったとして、細胞とか動植物から調べ始めるようなもので、間違っているわけではないけど後で調べた方が効率がよさそう、というやつだ。

 

そして・・・私はIntentServiceは忘れることにした。
私のバッファはそんなに大きくないのだ。