- 投稿日:2019-07-16T22:21:46+09:00
Android入門
アプリ開発のメリット
-Java、Kotlinの勉強になる
-自分好みのアプリを作れる
-プロへの道が開けるリリースについて
GooglePlayConsoleからリリースする
諸注意、作業方法
パッケージ名がcom.exampleだと通らない
manifestからリファクタリング、リネームするbuild.gradle(app)のバージョンも変えていく
1ずつ増やすなどアイコンのサイズ(解像度)はペイントで変える
インストール後のアプリ名は、manifestの以下の
AndroidManifest.xmlandroid:label="@string/app_name"Sourcetreeについて
・クローンのやり方
保存先のパスは空フォルダにして、名前は空フォルダと同じにするソースコードについて
inflaterはリソース(xml)を読み込んでビューにする
AndroidStudioショートカット
ctrl + shift + enter
コロン自動補完AndroidStudio
Andoid Studioをバージョンアップしたら端末が認識しなくなった
USBデバッグの許可の取り消しを行うと復活したライブラリについて
ライブラリの使用時はバージョンに注意
バージョンがあっていないとコンパイルエラー、Gradleのエラーになる場合がある
SDKのバージョンが合っていないといけない例)
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.android.support:design:28.0.0'
これはセットで使うJavaについて
public指定したクラス名とファイル名は同一にする
ArrayListはサイズ関係ない?
キャストでdoubleをintに代入すると、小数点以下が切り捨てられる
this.s
インスタンス変数s
引数がない場合は、sでOK継承
サブクラスが生成される際、子クラスのコンストラクタが実行される前に、親クラスのコンストラクタが実行されることがわかりますtoStringメソッド、getClass()メソッド、暗黙の親クラスであるObjectクラスのメソッド
抽象クラスは変数(配列OK)で保持して、サブクラスのインスタンスを生成して代入すると便利
抽象メソッドは呼び出しの抽象化のためか?
インスタンスをインターフェースでキャストして代入することで、インターフェースに記述してある機能しか使えなくする
インターフェースでもstaticならばそのまま使える
throwsはメソッドとセットで使い、例外発生時はメソッドの呼び出しもとに戻り、catchする
独自例外処理クラスを作って、throw(例外発生の目印)を使ってcatchする
equalsは同一の参照か比較してbooleanを返す
protected
サブクラスのインスタンスからアクセスできる(スーパークラスのインスタンスからはアクセスできない)無名クラスがメインルーチンの中に定義されているから、上から順番の処理にならない
メソッドの戻り値をまず見ることで、何をする処理かわかる
アダプタは最後にビューを返す(getView)インターフェースはimplementsして、そのクラスの中に実装する書き方と、
インターフェースをnewして(いるように見えるが実際は無名クラス)、無名クラスで実装する書き方がある。
- 投稿日:2019-07-16T07:43:49+09:00
[iOS / Android] ハイブリッド&クロスプラットフォーム開発の簡潔なまとめ
はじめに
私はiOS Nativeの実務経験しかなく、ハイブリッドやクロスプラットフォーム開発の実務経験はありません。
しかし、それらのネット記事を読んで常に気になっており、後学のために簡潔に整理してみました。ご注意
- 私個人のネット記事観測に基づき独断でchoiceしました。
- この記事で取り上げている以外にも、たくさんのツール・フレームワーク・サービスがあります。
- 私の所属企業における立場、戦略、意見を代表するものではありません。
- この記事の情報は2019年7月時点です。
お気付きの点がありましたら、コメントをお願いします。
なお、PWAを含む、WebサイトをWebView(内部ブラウザ)で表示する形式のアプリは当記事の対象外です。
ハイブリッド系
名称 提供元 PhoneGap/PhoneGap Build アドビシステムズ Monaca アシアル Ionic Ionic 共通の特徴
- UIはWebViewで表示します。
- HTMLとCSSでUIを構築し、実装はJavaScriptで行います。
- すなわち、Webフロントエンドの知識を活用できます。
- Native APIとの橋渡しをApache Cordovaが行います。
- PhoneGapはアドビシステムズによるApache Cordovaの製品版です。
- 上のようなフレームワークやサービスを使わないで、HTML+CSS+JS+Cordovaで開発する選択肢もあります。
- ニュースアプリなど情報表示主体のアプリに向いています。
- 一方、入力に対する反応速度や微細なアニメーションを求められるアプリには向きません。
PhoneGap BuildとMonacaの特徴
- クラウド上で開発ができる統合サービスで、コンセプトはよく似ています。
- いずれも、無料枠がありますが、制約があるので注意が必要です。
Ionicの特徴
- 上記2つはクラウドサービスの部類ですが、Ionicはフレームワークです。
- Angularをベースにしているため、TypeScriptの知識も必要となります。
クロスプラットフォーム系
名称 提供元 Xamarin マイクロソフト React Native Flutter Kotlin/Native JetBrains 共通の特徴
- UIがNativeであるため、反応速度や画面描画に違和感がなく、ハイブリッド系が苦手なアプリ分野に強みがあります。
- 実現したい機能によっては、Native APIのラッパーを自作しなければならない場合もあり、その分の工数を見込まなければならない場合も。
- UIの作成方法はツール・フレームワークよってコンセプトが異なります。
Xamarinの特徴
- 開発言語はC#。
- Visual Studioに統合されています。
- Xamarin.iOS / Xamarin.Android
- UIはOSごとに書き、ロジックのみを共通化します。
- Xamarin.Forms
- C#もしくはXAMLでUIを記述でき、同じUIのコードをOSごとのUIコンポーネントにマッピングします。
React Nativeの特徴
- ReactはWeb UIフレームワークであり、ブラウザ上でのDOM制御を行う役割を担います。
- React Nativeは、ReactをiOS/Android Nativeで利用できるようにしたもの。
- 開発言語は、Reactと同様、JavaScriptと、JSXというマークアップ言語です。
Flutterの特徴
- 開発言語はDart。
- 2018年12月にVersion 1.0となり正式リリースとなりました。
- "Skia"という、GoogleがOSSとして開発している2Dグラフィックライブラリを用いて、UIを独自に描画します。
Kotlin/Nativeの特徴
- 開発言語はKotlin。
- iOSについては、まだUI部分を作ることはできません。(2019年7月時点)
- AndroidStudioで共通ロジックを書き、framework形式でexportして、Xcodeに組み込むことができます。
参考リンク
Apache Cordovaで本格スマホアプリに挑戦しよう
MonacaとPhoneGap Buildを試してみる
Ionicでのアプリ開発の始め方
Xamarin(ザマリン) とはなんぞや
10分間で分かった気になれるXamarin概要
React Nativeとは何なのか
Flutterとは? エヌ次元が企業としてFlutter開発を採用する理由
Kotlin/Native を Android/iOS アプリ開発に導入しよう
クロスプラットフォームモバイルアプリ開発ツール総ざらい2019
- 投稿日:2019-07-16T06:39:12+09:00
Flutterウィークリー #67
Flutterウィークリーとは?
FlutterファンによるFlutterファンのためのニュースレター
https://flutterweekly.net/この記事は#67の日本語訳です
https://mailchi.mp/flutterweekly/flutter-weekly-67※Google翻訳を使って自動翻訳を行っています。翻訳に問題がある箇所を発見しましたら編集リクエストを送っていただければ幸いです。
アナウンス
Flutter 1.7を発表
https://medium.com/flutter/announcing-flutter-1-7-9cab4f34eacf
Flutter 1.7はこちら! Tim Sneathが新機能の概要を説明します。読み物&チュートリアル
Flutter波形を描く
https://matt.aimonetti.net/posts/2019-07-drawing-waveforms-in-flutter/
Flutterで波形を描画する方法についてのMatt Aimonettiによる詳細な説明。Flutter時間依存性注入のコンパイル
Sagar Suriがあなたのアプリケーションにコンパイル時DIエンジンを含める方法を詳しく述べています。Flutter材料範囲スライダ
https://medium.com/flutter/material-range-slider-in-flutter-a285c6e3447d
FlutterチームのAnthony Robledoによるこのチュートリアルで、 Flutter 1.7に含まれている空想の新しい範囲のスライダーの使い方を学びましょう。Dartを使った定型コード生成
https://medium.com/@saifulislamadar_12003/boilerplate-code-generation-using-dart-e2c08aa21bb7
Saiful Islam Adarは、BLoCクラスを生成するために彼が作成したコードジェネレータへの説明をします。FlutterとDialogflowを使用して20分でチャットボットを構築する
Promise Nzubechi Amadiが私のお気に入りの2つのツールFlutterとDialogflowを組み合わせて、20分でチャットボットを作成しました。Flutter Eコマースアプリケーションバックエンドのコーディング
https://medium.com/flutter-community/coding-an-e-commerce-app-backend-in-flutter-9bd11ed5dcce
Rishi Banerjeeが、eコマースAPIを使用するアプリを作成するための基本を説明しています。カスタムアプリケーションバー-作成Flutter
https://medium.com/@ketanchoyal/create-a-custom-app-bar-flutter-e32164e0be6f
Ketan Choyalによるこのチュートリアルに従って、独自のカスタムappbarsを作成してください。Flutterドラッグ可能なウィジェットを作成する
https://medium.com/flutter-community/create-a-draggable-widget-in-flutter-50b61f12635d
Dane Mackierは、画面上でドラッグできるウィジェットを簡単に作成できることを示しています。FlutterとZeplin:設計から開発プロセスをスピードアップ
RobertoJuárezは、デザインの各コンポーネントに対してFlutterウィジェットのコードを生成する素晴らしいZeplin拡張を作成しました。ボトムシートを使用するためのFlutter初心者ガイド
https://medium.com/flutter-community/flutter-beginners-guide-to-using-the-bottom-sheet-b8025573c433
Dane Mackierによるこの記事で、ボトムシートの基本を学んでください。Flutter状態管理:setState、BLoC、ValueNotifier、Provider
Andrea Bizzottoは最も一般的な状態管理ソリューションを比較し、それぞれの長所と短所を示します。Flutter :カウンターアプリを理解する
https://medium.com/flutter-community/flutter-understanding-counter-app-ca89de564170
Flutterを始めとするあなたのために、Souvik BiswasはサンプルCounterアプリの詳細を分析します。Flutterパッケージを作成、公開、管理する方法
https://medium.com/flutter-community/how-to-create-publish-and-manage-flutter-packages-b4f2cd2c6b90
natÇipliによるこのチュートリアルのおかげであなた自身のパッケージを作成し公開する方法を学びましょう。Flutterプロジェクトを構築する方法
https://medium.com/@kelvengalvao/how-to-structure-your-flutter-project-51f34254a5ae
KelvenGalvãoさんが、 Flutter用のnpm風のパッケージマネージャ兼コードジェネレータSlidyを紹介します。FlutterボーリングタブからFlutter
https://mightytechno.com/flutter-boring-tab-to-cool-tab/
Ishan Fernandoによる、さまざまなタブの作成方法に関するいくつかの例。ビデオ&メディア
Async / Await - Flutterインフォーカス
https://www.youtube.com/watch?v=SmTCmDMi4BY
これは、 Dart非同期コーディングに関するFlutter in Focusシリーズの4番目のビデオです。このエピソードでは、 Dartの先物でasyncキーワードとawaitキーワードを使用する方法を学びます。Flutterチュートリアル - Flutterチャート+ Firestore
https://www.youtube.com/watch?v=HGkbPrTSndM&feature=youtu.be
このビデオでは、chart_flutterプラグインを使用してチャートを作成し、チャートデータをFirestoreから取得する方法を説明します。Flutter UI卑劣な私のキャラクター - パート1
https://www.youtube.com/watch?v=-5DTrcXxGs8&feature=youtu.be
Flutter卑劣な私のキャラクターのデモ。グラデーション、カスタムクリッパー、ヒーロートランジション、レスポンシブUIの作成を学びます。Flutter - モバイル、Web、およびデスクトップアプリケーションに関するGoogleの最新の技術革新
https://www.youtube.com/watch?v=80pRyn7fZRk
ベルリンで開催されたWeAreDevelopersカンファレンスでのMartin AguinisとMatt Sullivanによる基調講演。Dart & Flutterでpubパッケージを作成する方法
https://www.youtube.com/watch?v=rsbk0kb_tdE&feature=youtu.be
FlutterとHugoでブログを作る - FunWith Devlog 01
https://www.youtube.com/watch?v=3VTTrGZrYS0&feature=youtu.be
Hugoファイルに変更が加えられるたびに自動的にFlutter Webアプリケーションを更新するためにHugoとFlutter for webを混在させる方法の例。RichText(今週のFlutterウィジェット)
https://www.youtube.com/watch?v=rykDVh-QFfw&feature=share
複数のスタイルを組み合わせた線または段落を表示しますか? RichTextウィジェットを使用すると、テキストのスタイルを設定できます。Flutter 1.7の新機能
https://www.youtube.com/watch?v=8U9eYVse2Hw
AndoidXおよび64ビットビルドのサポートを含む、 Flutter 1.7の新機能に関するビデオ。ライブラリ&コード
aloisdeniel / flutter_shared_ui_poc
https://github.com/aloisdeniel/flutter_shared_ui_poc
フライングモバイルとウェブの間でuiを共有できることの証明。
ashishrawat2911 / flutter_web_portfolio
https://github.com/ashishrawat2911/flutter_web_portfolio
レスポンシブWebポートフォリオがフラッターで構築されています。ButterCMS / buttercms-dart
https://github.com/ButterCMS/buttercms-dart
ButterCMS APIのDart SDK
csells / flutter_mplat_ttt
https://github.com/csells/flutter_mplat_ttt
Flutterマルチプラットフォームのサンプルゲーム
devrnt / book-library:
https://github.com/devrnt/book-library
AndroidとIOSの両方のための本図書館アプリ
- 投稿日:2019-07-16T02:08:25+09:00
AndroidでViewを押したときにぶわーってなるエフェクトをつける
タイトルひどすぎだろ。こんばんは。
今回は前から気になってたこと書きます。本題
Buttonとかを押したときにぶわーって広がるアニメーション。
あれどうやって作るのかGoogle先生に聞いてみました。実装する
今回はこの記事を書くために適当なアプリを作りました。
xmlはこんな感じ。
MainActivity.kt
には手をつけてないです。xmlだけでできます。
レイアウトになんで2つもImageView
あるんだよって話ですが、2つ種類があるんですよね。追加した部分は以下の3行です。
もう一つの方はbackground
の値を?attr/selectableItemBackground
にしています。android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:focusable="true"全体
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:gravity="center" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> <ImageView android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" android:focusable="true" android:padding="10dp" android:layout_width="100dp" android:layout_height="100dp" app:srcCompat="@drawable/ic_adb_black_24dp" android:id="@+id/imageView2"/> <ImageView android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" android:padding="10dp" android:layout_width="100dp" android:layout_height="100dp" app:srcCompat="@drawable/ic_adb_black_24dp" android:id="@+id/imageView3"/> </LinearLayout>試してみよう
実行して押してみましょう。
スクリーンショット見にくくてごめんね。AVDならCtrl + S で取れます。上
下
結果
種類 効果 ?attr/selectableItemBackgroundBorderless Viewの範囲を超えてぶわーって広がった。 ?attr/selectableItemBackground Viewの範囲内でぶわーって広がった。 こんな感じでしょうか。
お疲れ様です
色んな所で使えそう。
参考にしました
- 投稿日:2019-07-16T00:48:41+09:00
アプリ開発にGoを利用する(Android/iOS/Flutter)
この記事の読み方
- Android/iOS + Go
- Flutter + Android/iOS ネイティブ
- その組み合わせ
この3つから成る記事です。
複数のアプリを作りながら手順等を確認していきます。Flutter は使わない、Go は知らない、といった方にも参考にしていただけると思います。
Android 開発者の方
Flutter を使わない方は Android + Go までをご覧ください。iOS 開発者の方
iOS 開発環境がなく未検証のため、iOS の情報は少なめです。
特に Flutter で使うために Objective-C/Swift で橋渡しする部分はほぼありません。
それでも、Android での MethodChannel に近いと思われますので、雰囲気は掴めるはずです。
ライブラリ作成自体や Dart/Flutter で使う部分は OS に関わらず共通です。Flutter 開発者の方
Go を使わない方は Flutter + Android と Flutter + iOS をご覧ください。細かなことは 付録 にまとめましたので、そちらも参考になさってください。
gomobileとは
Go をモバイルアプリ開発に活用できるという素晴らしい代物です。
Mobile · golang/go Wiki · GitHubこれを使って作れるアプリは二種類あります。
ネイティブアプリ
Go だけで作るネイティブアプリ。SDKアプリ
Go で作ったライブラリを使って作るアプリ。この記事で扱うのは SDK アプリのほうです。
SDKアプリ
gomobile によって Go のパッケージを基にバインディングが行われてライブラリ化されます。
Kotlin、Swift 等からライブラリを使えるだけでなく、逆方向に呼び出すこともできます。ライブラリとして生成されるのは次のファイルです。
Android
aar ファイル(Android Archive)iOS
framework ファイル(Framework Bundle)Android では ARM / ARM64 / 386 / AMD64 のアーキテクチャに対応しています。
MIPS は非対応です。Flutter を使い始めるまでは Android/iOS のロジックをこれで共通化して楽をしようと考えていました。
Goを使う理由
Dart でやりにくいことを Go に任せられる
Go には有用なパッケージがあるのに、相当するものが Dart にない場合など。Go が得意なことを Dart/Flutter に持ち込める
Go は簡単に使える便利な標準ライブラリが豊富です。
Goroutine による並行処理も得意です。
サーバサイドで人気の Go をアプリで使えればコードを流用できます。実行速度の優位性
Flutter では Dart のコードがネイティブのライブラリにコンパイルされる 1 ので速度に大きな差はなさそうに思えますが、試してみると違いがありました。2C/C++ より扱いやすい
C/C++ など Go 以外の言語でもライブラリは作れます。
でも Go ならシンプルな文法、GC 等によって楽をして安全に書けます。Dart より Go に慣れている人が書きやすい
Dart を使ってみると Web でも使ってみたくなるような素敵な言語でしたが、好みの問題や人的リソースの都合があるので・・・。楽しい!
楽しい Go と楽しい Dart/Flutter を組み合わせて使えるなんて至福(著者調べ)。Goを使うデメリット
- Android NDK が必要(Android のみ)3
- アプリのサイズが大きくなる
- 言語間のバインディングにオーバーヘッドがある 4
- ターゲット言語側の制限により、エクスポートされた API の見た目に少し制限がある 4 5
- 使える型が限られている
- ライブラリ内に作った環境のパスが含まれる 6
- gomobile 製ライブラリと Flutter を繋ぐ Java/Kotlin、Objective-C/Swift のコードも必要
- Flutter がせっかくマルチプラットフォーム対応なのに Dart 以外も使うなんて面倒
- ウェブアプリも作れる Flutter で Go を使うとモバイルアプリ限定になってしまう
- ライブラリには
compute()
を使えず、重い処理だとメインスレッドがブロックされるこう見ると結構ありますね。
メリットとデメリットのどちらが大きいか、ご自身で判断ください。準備
Windows での手順になりますが、他の環境でもほぼ同じだと思います。7
Android/iOS の開発環境は既に用意されている前提です。
Android NDK のインストール(Android のみ)
Android Studio にて
Tools
>SDK Manager
> 右ペインのSDK Tools
タブ
⇒NDK
にチェックが付いていなければ付けてOK
またはApply
gomobile のインストール
コマンドプロンプトか PowerShell にて
> go get -d golang.org/x/mobile/example/bind/...
> gomobile init
これだけです。
-ndk /path/to/ndk
という NDK のパス指定を説明しているサイトがありますが、> gomobile init -ndk /path/to/ndk flag provided but not defined: -ndk
のように怒られました。
NDK のパスを指定する必要はないようです。8
Windows 以外では未確認ですので、もし NDK のパスのエラーが出たら指定してみてください。Goによるライブラリ作成
1. コード
非常にシンプルなライブラリを作ってみます。
わざわざ Go でライブラリにしたい類ではありませんが、あくまでわかりやすい例として。
- 整数を受け取り、倍にした値を返す
- 受け取る整数の範囲は 0 ~ 10 とする
- 範囲外の値ならエラーを返す
- 値を LogCat で確認できるように出力
simple.gopackage simple import "fmt" func Multiply(value int32) (int32, error) { fmt.Println(value) if value < 0 || value > 10 { return 0, fmt.Errorf("value out of range: must be within the range of 0 to 10") } return value * 2, nil }
- これを
GOPATH
以下のどこかに作ったフォルダの中に置く- パッケージ名がライブラリの名前になる(フォルダ名は関係ない)
- Android/iOS のコードや Dart/Flutter から利用したい関数は、先頭を大文字にして export する
→ Android で使うときは先頭は小文字、先頭以外は Go で書いたまま
[例] Go で GetHoge なら Android で使うときは getHoge(iOS では異なるようです)関数にコメントを付けておいても、ライブラリの使用時にその情報を参照することはできませんでした。
整数型
Go の int はアーキテクチャに依存し、64 ビット実装の Go では 64 ビット の整数になります。
それに対応する Java と Objective-C の型は それぞれLong
、numberWithLong
です。
Integer
、numberWithInt
にするには、より小さなサイズの int32 等を使いましょう。型の対応 については付録にまとめています。
情報出力とエラーの扱い
複数の方法で動作を見てみると、かなり癖がありました。
基本的に次のように考えておけば大丈夫かと思います。
詳細は 付録 をご覧ください。
情報を LogCat や Run のウィンドウに表示したい
fmt.Println()
を使う。
fmt.Print()
やfmt.Printf()
で第一引数の末尾に改行するのも OK。Android/iOS や Dart/Flutter で例外として捕捉したい
ライブラリで値を返すとき、二つ目の戻り値にerror
型のデータを付ける。他のポイント
長くなるので 付録 に収めました。
2. ライブラリ生成
Android では aar ファイル、iOS では framework ファイルを生成します。
次のようなパスになっているとします。
GOPATH
C:\Gosimple.go
C:\Go\src\hoge\gomobile_example\simple.go生成には
gomobile bind
を使います。
ファイルのあるディレクトリを指定する方法と指定しない方法があります。
Android 向けに生成する場合は下のようになります。コマンド
(a) ディレクトリへの相対パスを指定して生成する場合
※最後の引数は GOPATH/src/ からの相対パス です。
※Windows でもスラッシュ区切りにしないとエラーになりました。> gomobile bind -target android hoge/gomobile_example(b) ディレクトリに移動してから生成する場合
> cd C:\Go\src\hoge\gomobile_example\simple.go > gomobile bind -target androidオプション
-o
出力先を指定するには-o
を使います(例: -o path/to/library.aar)。
ここで指定するパスは カレントディレクトリからの相対パス ですのでご注意ください。
(a) のほうでは相対パスの起点がややこしいので (b) がオススメです。
なお、パスにはファイル名まで含める必要があります。
また、存在しないディレクトリを指定した場合、自動的に作ってくれるわけではありません。-target android/arm64
Android ではターゲットのアーキテクチャも指定できます。
スラッシュの後ろはarm
、arm64
、386
、amd64
のいずれかです。
指定しない場合、サポートする全4アーキテクチャの so ファイルを含んだ aar ファイルになります。9-target ios
iOS では-target
にios
を指定します。
Android のようなアーキテクチャの指定には対応していないようです。
そもそも幅広いバリエーションがあるわけでもないので不要ですね。他
オプションは他にもあり、gomobile bind -h
で確認できます。ビルド時間、aarファイルのサイズ
これくらい小規模のコードを普段 Go でビルドするときと比べて長くかかります。
環境によりますが、私の PC で Android 向けにビルドしたところ 40 秒ほどでした。また、aar 内の共有ライブラリ(.so)が一つあたり 2MB 以上、圧縮状態で 1MB 程度になりました。
aar ファイルには 4 アーキテクチャ分が入っていて計 4MB 台です。大きめですね。
ユーザが Google Play ストアからダウンロードするときにはもっと小さくなります。9Android + Go
ライブラリ導入
Android Studio を使っていきます。
使わずに、次の 1 ~ 2 に載せた diff を参考にしてファイル追加や記述変更を手動で行っても OK です。1. ライブラリのモジュールを追加
モジュールとは、プロジェクトを分割した機能ごとのアプリのようなものです。
ここでは、ライブラリを一つのモジュールとしてプロジェクトに追加します。
起動後のウィンドウで「Start a new Android Studio project」を選ぶ
開いたウィザードで「Empty Activity」を選び、プロジェクトが開くところまで進める
フォルダアイコンを押し、先ほど生成された aar ファイルを指定してから「Finish」
Subproject name のところには自動的にサブプロジェクト(モジュール)の名前が入ります。
自分で変えても良いでしょう。
これで simple モジュールが追加された状態になりました。
ここまでの操作による変化は次のとおりです(Android Studio 関連ファイルは省いています)。
settings.gradle-include ':app' +include ':app', ':simple'simple/build.gradlenew file mode 100644 +configurations.maybeCreate("default") +artifacts.add("default", file('simple.aar')) \ No newline at end of file
simple/simple.aarnew file mode 1006442. 追加したモジュールを使う設定
追加しただけでは使えません。
メインのモジュールである app から simple を利用できるように依存関係の設定を行います。
使うための設定はこれで完了です。
この操作による変化は次のとおりです。app/build.gradledependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation project(path: ':simple') }
ライブラリを使う
Simple ライブラリを実際に使ったアプリを作ります。
app/src/main/res/values/strings.xml<resources> <string name="app_name">GomobileAndroid</string> <string name="button">Tap here!</string> </resources>app/src/main/res/layout/activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="32sp"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button"/> </LinearLayout>app/src/main/java/com/example/gomobile/gomobileandroid/MainActivity.ktimport simple.Simple // これ以外のインポートは割愛 class MainActivity : AppCompatActivity() { private lateinit var textView: TextView private var value = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById(R.id.textView) updateText() findViewById<Button>(R.id.button).setOnClickListener { value++ updateText() } } private fun updateText() { try { textView.text = Simple.multiply(value).toString() } catch (e: Exception) { Log.e("MainActivity", e.message) } } }とても簡単ですね。
ポイントは下記箇所のみです。import simple.Simple .... try { textView.text = Simple.multiply(value).toString() } catch (e: Exception) { Log.e("MainActivity", e.message) }ライブラリのメソッドを使っているだけです。
そのメソッドではエラー時に例外を発生させるようにしているためtry
~catch
を使っています。ボタンを 12 回押したときの LogCat の出力は下のようになります(途中省略)。
端末画面上の表示は 10 回目の「20」で止まります。07-14 13:58:37.951 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 1 07-14 13:58:38.246 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 2 ... 07-14 13:58:41.244 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 10 07-14 13:58:41.630 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 11 07-14 13:58:41.635 1622-1622/com.example.gomobile.gomobileandroid E/MainActivity: value out of range: must be within the range of 0 to 10 07-14 13:59:00.962 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 12 07-14 13:59:00.966 1622-1622/com.example.gomobile.gomobileandroid E/MainActivity: value out of range: must be within the range of 0 to 10ライブラリを使う記述をする際のコード補完等については 付録 をご覧ください。
iOS + Go
iOS 開発環境がないため動作は未確認です。
ライブラリ導入
生成した framework ファイルを Xcode でプロジェクトに導入する方法は Wiki に書かれています。
参考にしながら導入してみてください。ライブラリを使う
Wiki には挨拶のテキストを出力するサンプルコードのスクリーンショットがあります。
ライブラリを利用するためのメソッド使用箇所は次のようになっています。bind/ViewController.m(スクショより)textLabel.text = GoHelloGreetings(@"iOS and Gopher");しかし こちらのサンプル ではメソッド名が異なります。
bind/ViewController.m(サンプルより)textLabel.text = HelloGreetings(@"iOS and Gopher");いずれかの情報がアップデートされていなくて古いのかもしれません。
なお、Go で書いた関数 は下記のとおりです。
上記の二つのメソッド名はどちらも、この元の関数名と異なります。
iOS で使うときにはその点の注意が必要です。hello/hello.goの一部func Greetings(name string) string { return fmt.Sprintf("Hello, %s!", name) }他にも異なる部分があるかもしれませんので、サンプル全体を一度ご確認ください。
Flutter + Android
Go 製ライブラリを Flutter で使う前に、Android 側に書いた機能を Flutter で使う方法を見てみます。
Flutter と Android/iOS の間で連携できるようにする Platform Channel というものを使います。この図は上記リンク先より拝借したものです。
Platform Channel
というのはこの全体の仕組みのことだと思われます。
使うのはMethodChannel
(iOS 側だけはFlutterMethodChannel
)というものです。1. Android側(使う機能の作成)
Go で作ったライブラリと同様の機能にしてみます。
まず Flutter の新しいプロジェクトを作りますが、今回も simple という名前にしておきます。Kotlin をサポートするプロジェクトにするには、Android Studio のウィザードで「Include Kotlin support for Android code」にチェックを付けるか、
flutter create
コマンドで-a kotlin
を付けます。private fun multiply(value: Int): Int? { return if (value in 0..10) value * 2 else null }受け取る値が 0 ~ 10 の範囲なら倍数、範囲外なら null を返します。
2. Android側(連携処理)
作ったメソッドを Flutter から使えるように Android 側に連携処理を書きます。
そのために用意されているMethodChannel
を使います。android/app/src/main/kotlin/com/example/simple/MainActivity.ktclass MainActivity: FlutterActivity() { ... val methodChannel = MethodChannel(flutterView, "example.com/simple") methodChannel.setMethodCallHandler { call, result -> when { call.method == "simple_multiply" -> { val v = call.argument<Int>("value") ?: 0 val r = multiply(v) result.success(r) } else -> result.notImplemented() } } ... }
MethodChannel(flutterView, "example.com/simple")
第二引数のexample.com/simple
はチャンネルの名前です。
Flutter のほうでも同じ名前を使うことでやり取りできるようになります。call.method == "simple_multiply"
simple_multiply
は Flutter から機能を呼び出すときの名前です。
使いたいメソッドがmultiply()
なので、それを使うことがわかる名前にしました。
そのようなわかりやすい名前であれば何でも大丈夫です。call.argument("value")
Flutter 側から渡された引数を取り出す部分です。
value
という引数名を Android と Flutter で共通使用する必要があります。
受け取った値はnull
の場合もあるため、そのことを考慮しておく必要があります。result.success(~)
成功したときに結果を返す処理です。
( ) 内に指定した値を Flutter 側で受け取ることができます。result.error("エラーコード", "エラーメッセージ", "エラー詳細")
上のコードにはありませんが、これを使うと Flutter 側でPlatformException
になります。
各引数に指定する情報を Flutter で取得できます。
第3引数は Object 型なので String に限りません(使わないなら null で OK)。result.notImplemented()
存在しない名前で機能を呼び出された場合にこれを使っています。
このとき Flutter 側でMissingPluginException
として捕捉することができます。MainActivity 全体のコードは次のようになります。
android/app/src/main/kotlin/com/example/simple/MainActivity.ktpackage com.example.simple import android.os.Bundle import io.flutter.app.FlutterActivity import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant class MainActivity: FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith(this) val methodChannel = MethodChannel(flutterView, "example.com/simple") methodChannel.setMethodCallHandler { call, result -> when { call.method == "simple_multiply" -> { val v = call.argument<Int>("value") ?: 0 val r = multiply(v) if (r == null) { result.error("Out of range", "value must be within the range of 0 to 10", v) } else { result.success(r) } } else -> result.notImplemented() } } } private fun multiply(value: Int): Int? { return if (value in 0..10) value * 2 else null } }
multiply()
の結果がnull
のときはresult.error()
でエラーにしています。3. Flutter側(ヘルパークラス)
Flutter 側でも
MethodChannel
を使います。
使うためにはpackage:flutter/services.dart
のインポートが必要です。UI のコードにロジックが混じらないように、Simple というヘルパークラスを作ることにしました。
lib/simple.dartimport 'package:flutter/services.dart'; class Simple { static const _platform = MethodChannel('example.com/simple'); static Future<int> multiply(int count) async { final arguments = {'value': count}; try { return await _platform.invokeMethod<int>('simple_multiply', arguments); } on PlatformException catch (e) { print(e); } catch (e) { print(e); } return null; } }
MethodChannel('example.com/simple')
Android 側で設定したのと同じチャンネル名を指定します。Future<int> multiply(int count) async
Android 側から返ってくるのはFuture
です。final arguments = {'value': count};
Android 側に値を渡すには、このように Map にする必要があります。
キーは Android 側で設定した名前に合わせます。return await platform.invokeMethod('simplemultiply', arguments);
第一引数は Android 側で設定した呼び出し名です。
第二引数には渡したい引数の Map を指定します。
await
はここでしないと例外を補足できません。
「invoke」で始まるメソッドは他にinvokeListMethod()
とinvokeMapMethod()
があります。on PlatformException catch (e)
Android 側でresult.error()
に指定した情報をここで得ることができます。
- e.code エラーコード
- e.message エラーメッセージ
- e.details エラー詳細
4. Flutter側(完成)
ヘルパークラスを使うメインのファイルは次のようにしました(一部省略)。
lib/main.dartimport 'simple.dart'; ... class _MyAppState extends State<MyApp> { int _count = 0; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ FutureBuilder<int>( future: Simple.multiply(_count), initialData: 0, builder: (_, snapshot) { return Text(snapshot.hasData ? snapshot.data.toString() : '--'); }, ), RaisedButton( onPressed: () { setState(() => ++_count); }, child: const Text('Tap Here!'), ), ], ), ), ), ); } }カウンターの値を Dart で持ち、ボタンが押されたときにインクリメントします。
その際にsetState()
しているため全体がリビルドされます。
都度Simple.multiply(_count)
が実行され、返ってきたFuture
をFutureBuilder
で処理しています。
multiply()
に渡す値が 11 以上だとエラーになり、Flutter 側では例外が発生します。
アプリを起動してボタンを 11 回押すと、Run ウィンドウに次のように出力されました。I/flutter ( 3664): PlatformException(Out of range, value must be within the range of 0 to 10, 11)例外については、付録 にもう少し細かく書いています。
Hot Reload/Restart
Hot Reload/Restart は Flutter の機能です。
Android 側の処理を変えたときには当然 Hot Restart しても反映されません。しかし Android 側はしっかりと書いてしまえばその後はあまり変えることはないはずです。
さほど不便ではないと思います。Flutter + iOS
flutter.dev のドキュメント を参考にしてみてください。
iOS ホスト側でバッテリーの情報を取得して Flutter で利用する方法が解説されています。Android の
MethodChannel
に相当するものは iOS ではFlutterMethodChannel
です。
環境の都合で未検証ですが、コードを見るとMethodChannel
の使い方に近いです。
チャンネルや呼び出しの名前を設定する点や、エラー時にコード等3種類の情報を返せる点が同じです。
OS によって Flutter 側の書き方を変えなくていいように共通化されているようです。Flutter + Android + Go
Go で書いた処理を Flutter で使うのはもうここまでのことを組み合わせるだけです。
操作に関して少しだけ違いがあります。Flutter のプロジェクトで Android の MainActivity.kt を開くと、ライブラリが認識されません。
上のスクリーンショットでは Nudity が赤くなっています。
右上に出るリンクで Android のプロジェクトを開き、ライブラリの導入等の操作はそちらで行いましょう。Simple カウンター
Go で作った Simple ライブラリを Flutter で使ってみます。
Flutter + Android のコードと違うのは下記の
try
~catch
の部分だけです。
ライブラリのエラーによって発生した例外を Android 側 で catch し、result.error()
を使って Flutter 側でも catch できるようにしました。MainActivity.ktの一部val methodChannel = MethodChannel(flutterView, "example.com/simple") methodChannel.setMethodCallHandler { call, result -> when { call.method == "simple_multiply" -> { val v = call.argument<Int>("value") ?: 0 try { result.success(Simple.multiply(v)) } catch (e: Exception) { result.error("Go Simple", e.message, null) } } else -> result.notImplemented() } }このように、本当にここまでの技術の組み合わせるだけでできてしまいます。
ヌード写真判定
Awesome Go で go-nude という Go のパッケージを見つけました。
nude.js というライブラリを Go に移植したものだそうです。JavaScript でできるなら Dart でもできそうですが、まだ存在しないようです。
使いたくてもまだ無いケースとして Go の利用が適していると考えました。※このスクリーンキャスト内では go-nude の example/images/ にある画像を使いました。
※実用性は低そうです。
判定が厳しすぎるかと思ったら、逆に景色の写真がヌードと判定されることもあったりします…。10nudity.gopackage nudity import ( "github.com/koyachi/go-nude" ) const ( Unknown int = iota IsNotNude IsNude ) func Check(path string) (int, error) { isNude, err := nude.IsNude(path) if err != nil { return Unknown, err } if isNude { return IsNude, nil } return IsNotNude, nil }MainActivity.ktの一部val methodChannel = MethodChannel(flutterView, "example.com/nudity") methodChannel.setMethodCallHandler { call, result -> when { call.method == "nudity_check" -> { val imagePath = call.argument<String>("imagePath") result.success(Nudity.check(imagePath)) } else -> result.notImplemented() } }nudity.dartimport 'package:flutter/services.dart'; class Nudity { static const _platform = MethodChannel('example.com/nudity'); static const unknown = 0; static const isNotNude = 1; static const isNude = 2; static Future<int> check(String path) async { final arguments = {'imagePath': path}; return await _platform.invokeMethod<int>('nudity_check', arguments); } }画像選択は Flutter で行い、画像パスをライブラリに渡して判定結果の数値を受け取っています。
ここまでに見てきたことと大差なく、特筆することはありません。
例外処理は省きました(以下同様)。画像変換
画像変換は時間がかかることがあります。
でもグレースケール変換くらいは一瞬でできてほしいところです。ところが、Dart で変換してみると待たされてしまいました(画像サイズ等にもよります)。
こういったものは Go でやれば速くなるのではないかと考えました。grayscale.dartpackage grayscale import ( "bytes" "fmt" "github.com/anthonynsimon/bild/effect" "github.com/anthonynsimon/bild/imgio" "image/jpeg" ) func Convert(path string) ([]byte, error) { img, err := imgio.Open(path) if err != nil { return nil, fmt.Errorf("failed to open image: %v", err) } img = effect.Grayscale(img) buf := new(bytes.Buffer) err = jpeg.Encode(buf, img, nil) if err != nil { return nil, fmt.Errorf("failed to save image: %v", err) } return buf.Bytes(), nil }MainActivity.ktの一部val methodChannel = MethodChannel(flutterView, "example.com/grayscale") methodChannel.setMethodCallHandler { call, result -> when { call.method == "grayscale_convert" -> { val path = call.argument<String>("imagePath") result.success(Grayscale.convert(path)) } else -> result.notImplemented() } }grayscale.dartimport 'dart:typed_data'; import 'package:flutter/services.dart'; class GrayScale { static const _platform = MethodChannel('example.com/grayscale'); static Future<Uint8List> convert(String path) async { final arguments = {'imagePath': path}; return await _platform.invokeMethod<Uint8List>('grayscale_convert', arguments); } }
MainActivity
は先ほどとほとんど同じです。
Go では変換後の画像を[]byte
型にして返します。
それを Kotlin ではByteArray
、Dart ではUint8List
として受け取っています。
Uint8List
のデータは Flutter でImage.memory()
にそのまま渡して画像表示できます。リリースビルドして大きめの画像を変換したところ、所要時間に差が出ました。
Dart Go 1 回目 11.96 秒 1.55 秒 2 回目 11.92 秒 1.41 秒 3 回目 11.89 秒 1.44 秒 4 回目 11.96 秒 1.46 秒 5 回目 11.94 秒 1.49 秒 メインスレッドのブロッキング
画像変換の間に CircularProgressIndicator を表示すると、クルクル回るアニメーションが止まりました。
そこでcompute()
を使うようにしてみたのですが、ライブラリのほうに使うと例外が発生しました。
解決法は不明です。
これが解決できないと辛い場合があるかもしれません。ojichat
これで最後です。
Go のパッケージに ojichat というものがあります。
おじさんがLINEやメールで送ってきそうな文を生成してくれる楽しいパッケージです。これを使ったチャット風アプリが簡単にできました。
コードは省略します。付録
ここまでに書いた以外に知っておくと良いことをまとめました。
gomobileのポイント
型の対応
Go からエクスポートするものは全てサポートされている型である必要があります。
Type restrictions(gobind - GoDoc)
符号付きの整数型・浮動小数点型
文字列型、論理型
byte スライス型
参照渡しとなり、渡した先での変更は元のスライスに反映されます。関数型
仮引数や戻り値はサポート対象の型にすること。
戻り値を二つにする場合、二つ目は error 型に限られます。インタフェース型
export されるメソッドはサポート対象の関数型にすること。構造体型
export されるメソッドはサポート対象の関数型にすること。
export されるフィールドはサポート対象の型にすること。サポートされている型は以上です。
スライスとマップが含まれていないのが気になるので試すと、やはりどちらもダメでした。
Go ではよく使うものなので、これらが使えないのはちょっと不便かもしれません。また、byte スライス型の「参照渡し」も試しました。
Kotlin から受け取って中身を Go で変えると Kotlin 側でも変わっていました。
しかし、逆だと変化がありませんでした。
(Kotlin に不慣れで、扱い方を間違えた可能性もあります。)情報出力、エラー
★印が付いている二つのどちらかを用途に合わせて使いましょう。
fmt.Print("message")
fmt.Printf("%s", "message")
意外なことに、何も起こりませんでした。
fmt.Println("message")
fmt.Printf("message\n")
★
LogCat や Flutter の Run ウィンドウに Info レベルの情報として出てきます。
タグは「GoLog」です。
Printf()
でもPrintln()
と同じ意味になるように末尾に改行を置けば OK のようです。
fmt.Print("message\nhoge")
fmt.Printf("%s\nhoge", "message")
なんと!
メッセージの途中に改行があると、順序が逆転して「hoge message」になりました…。関数の二つ目の戻り値としてエラーを返す ★
Android 側や Flutter 側で例外として補足できます。
例外処理をしない場合、Flutter ではないネイティブの Android アプリは異常終了します。
一方 Flutter のアプリは、Android 側で例外処理をし忘れていても生き続けます。
アプリが丸ごと落ちないように対策されているようで、例外の情報が出力されるだけです。
その場合、Flutter 側ではMissingPluginException
となります。
Android 側で例外を無視して Flutter でハンドルすることもできますが、微妙です。
ライブラリの異常は全て上記例外となり、種類をメッセージで判別するしかなくなります。
それよりもきちんと Android 側で対応したほうが良さそうです。
log.Fatal("message")
log.Fatalf("message")
log.Fatalln("message")
os.Exit(1) を呼ぶものなのでアプリごと終了します。
その際、指定したメッセージが Info レベルの情報として出力されます。
改行の有無は関係なく情報が出力されました。
また、途中に改行があっても出力順序は逆転せず、改行は改行として出ました。
panic("message")
Android 側を巻き込んで異常終了してしまいます。
その際、指定したメッセージが Error レベルの情報として出力されます。
タグは「GoLog」ではなく「Go」です。
当然ですが、次のようにrecover()
で回復させれば異常終了は防げます。defer func() { if r := recover(); r != nil { fmt.Println(r) } }()構造体、レシーバー
Passing Go objects to target languages(gobind - GoDoc)
この記事で見てきた例では、値を保持するのは Flutter 側や Android 側でした。
実験的に Go のライブラリ内で状態保持させてみたいと思います。
ライブラリのパッケージ内の変数に持たせれば簡単ですが、あえて構造体を使ってみます。counter.gopackage counter type GoCounter struct { value int32 } func NewGoCounter() *GoCounter { c := new(GoCounter) c.value = 0 return c } func (c *GoCounter) Increment() int32 { c.value++ return c.value }
NewGoCounter()
でGoCounter
という構造体を初期化してそのポインタを返します。11
それをレシーバーとするIncrement()
では、構造体が持つ value の値が 1 増やして結果を返します。Java や Dart にレシーバーはありませんが、どうなるのでしょうか。
上記コードで作ったライブラリを Android のプロジェクトに導入し、デコンパイルした情報を見てみます。
見方については バインディングの中身 を参照してください。Counter.class(デコンパイル)package counter; public abstract class Counter { private Counter() { /* compiled code */ } public static void touch() { /* compiled code */ } private static native void _init(); public static native counter.GoCounter newGoCounter(); }
Counter
というクラスが作られ、newGoCounter()
をメソッドとして持っているのがわかります。
newGoCounter()
が返すのはGoCounter
型であり、ポインタではありません。
GoCounter
のクラスも作られているので見てみましょう。GoCounter.class(デコンパイル)package counter; public final class GoCounter implements go.Seq.Proxy { private final int refnum; public final int incRefnum() { /* compiled code */ } public GoCounter() { /* compiled code */ } private static native int __NewGoCounter(); GoCounter(int i) { /* compiled code */ } public native int increment(); public boolean equals(java.lang.Object o) { /* compiled code */ } public int hashCode() { /* compiled code */ } public java.lang.String toString() { /* compiled code */ } }こちらには自分で書かなかったプロパティやメソッドも含まれています。
それよりも注目すべきはコンストラクタです。
NewGoCounter
という名前から判断して勝手にコンストラクタを用意してくれています。また、もし構造体のフィールドを export していた場合にはゲッターとセッターが自動的に用意されます。
今回は value を export していないので含まれていません。次は Android 側です。
MainActivity.ktimport counter.GoCounter ... val methodChannel = MethodChannel(flutterView, "example.com/counter") methodChannel.setMethodCallHandler { call, result -> when { call.method == "counter_init" -> { goCounter = GoCounter() } call.method == "counter_increment" -> result.success(goCounter.increment()) else -> result.notImplemented() } }
GoCounter
の初期化はCounter.newGoCounter()
でもできますが、先ほどのコンストラクタを使ってGoCounter()
としました。
これにより、Counter
は使わずに済みました。
GoCounter
のインスタンスを Flutter に渡せると良いのですが、無理でした。
Java/Kotlin のオブジェクトを渡す何らかの方法ができるかもしれません。
代わりに Android 側で保持しておくことにしました。Flutter 側のヘルパークラスは次のようにしました。
gocounter.dartimport 'package:flutter/services.dart'; class Counter { static const _platform = MethodChannel('example.com/counter'); static Future init() async { await _platform.invokeMethod('counter_init'); } static Future<int> increment() async { return _platform.invokeMethod<int>('counter_increment'); } }ライブラリの
increment()
を呼び出し、結果を UI に反映するだけで済みます。
Flutter側で状態管理しないでカウンターのアプリが実現しました。
残りのコードは割愛します。インタフェース
gobind のドキュメント のコード例がわかりやすいので抜粋します。
Goのインタフェースpackage myfmt type Printer interface { Print(s string) } func PrintHello(p Printer) { p.Print("Hello, World!") }bindによって自動生成されるJavaのインタフェースpublic abstract class Myfmt { public static void printHello(Printer p0); } public interface Printer { public void print(String s); }Javaでインタフェースを実装して利用public class SysPrint implements Printer { public void print(String s) { System.out.println(s); } } Printer printer = new SysPrint(); Myfmt.printHello(printer);
- Go で書いたライブラリに Printer というインタフェースがある
- そのインタフェースを Java で実装して SysPrint クラスとする
- インタフェースが持つメソッドである print() を SysPrint 内で具象化する
- SysPrint のインスタンスを生成し、それをライブラリの PrintHello() に渡す
- PrintHello() の結果が SysPrint.print() を使って出力される
Go と Java/Kotlin ではインタフェースの書き方が大きく異なります。
それにもかかわらず、違和感なく使えるようにうまくできていますね。先ほどの 構造体、レシーバー のところでもそうでしたが、言語間の差異がうまく緩衝されているのがわかると思います。
Goからアプリ側ネイティブAPIへのアクセス
Reverse bindings(gobind - GoDoc)
ここまでとは逆に Go から Java や Objective-C で用意された API にアクセスできる旨が書かれています。
上記ページには次のような例が掲載されています。java.lang.SystemをGoで読み込んでcurrentTimeMillisメソッドを利用import "Java/java/lang/System" t := System.CurrentTimeMillis()実際にやってみると確かにできました。
ただし、GoLand 等の IDE では存在しないメソッドのように扱われ、利用しにくかったです。NSDateをGoで読み込んでdateメソッドを利用import "ObjC/Foundation/NSDate" d := NSDate.Date()これだけに留まらず、例えば Android なら次のように Go で Activity を継承することもできるようです。
面白いですね。GoでAndroidのActivityを継承してMainActivityを作るimport "Java/android/app/Activity" type MainActivity struct { app.Activity }メモリリークの危険性
Avoid reference cycles(gobind - GoDoc)
今見たように、Go とターゲットの間で双方向にデータをやり取りできます。
片方が他方のオブジェクトへの参照を持っている場合、そのオブジェクトへのアクセスがなくなると、オブジェクトの実体を持っているほうの言語で GC によって適切に参照が破棄されるようです。12しかし、もし参照を相互に持っているとオブジェクトを回収できなくなり、メモリリークが発生します。
そんなことはあまりしないと思いますが、ちょっと注意が必要なところだと思います。Flutter側の例外処理
Flutter + Android で扱ったコードを使って見ていきます。
simple.dart の
on PlatformException catch (e)
のブロックを変えてみましょう。
次のように変えると、Android 側のresult.error()
で指定した情報がちゃんと出力されます。lib/simple.dartの一部を改変on PlatformException catch (e) { print(e.code); print(e.message); print(e.details); }I/flutter ( 3664): Out of range I/flutter ( 3664): value must be within the range of 0 to 10 I/flutter ( 3664): 11今後はチャンネル名を変えてみます。
「simple」を「hoge」に変えるとMissingPluginException
が出ました。
括弧内を訳すと「example.com/hoge チャンネルには simple_multiply メソッドの実装が見つからない」です。lib/simple.dartの一部を改変static const _platform = MethodChannel('example.com/hoge');I/flutter ( 3664): MissingPluginException(No implementation found for method simple_multiply on channel example.com/hoge)最後に
try
~catch
を使わないようにしてみます。lib/simple.dartの一部を改変final arguments = {'value': count}; return await _platform.invokeMethod<int>('simple_multiply', arguments);ボタンを 11 回以上押してもチャンネル名を変えても、何も出力されませんでした。
意図的に無視することもできるようになっているようです。
しかし、異常に気づいて対応できるようにtry
~catch
しておくのが良いと思います。Docker
go4droid/Dockerfile at master · mpl/go4droid · GitHub
https://github.com/mpl/go4droid/blob/master/Dockerfilegomobile の Wiki からリンクされている Dockerfile です。
既存環境を汚したくない方にはおすすめです。
また、Go で作ったライブラリに環境の情報(パス)が含まれるのを気にする方は対策に使えます。ただし、ファイルの中身を見ると対象の環境が古いです。
Android や Go のバージョンを書き換えて使う必要があると思います。Android Studioについて
バインディングの中身
自作ライブラリであっても、Android Studio は使い方がわかるように補助してくれます。13
MainActivity のコードの中でライブラリのクラス名にカーソルの上で
右クリック >Go To
>Declaration
と操作すると、ライブラリの class ファイルをデコンパイルしたものが表示されます。Simple.class(デコンパイル)package simple; public abstract class Simple { private Simple() { /* compiled code */ } public static void touch() { /* compiled code */ } private static native void _init(); public static native int multiply(int i) throws java.lang.Exception; }最初に見たサンプル(Simple ライブラリ)だと次のようになります。
作ったライブラリを Java/Kotlin でどう使えばいいのかわかりやすくて助かりますね。
int multiply(int i)
仮引数も戻り値も int になっています。
これは Go でMultiply(value int32) int32
のようにint32
を使ったためです。
64 ビットの Go でMultiply(value int) int
とするとlong multiply(long l)
になります。throws java.lang.Exception
multiply() で二つ目の戻り値によってエラーを返さない場合、例外はスローされません。デコンパイルで得られたクラス/メソッド等の定義の情報は
View
>Quick Definition
の操作でも表示されます。
コード補完やクラス・メソッド等の情報表示もしてくれて助かります。
ライブラリの更新方法
ライブラリの中身を変えた場合、aar ファイルを上書きするだけで変更が適用されます。
ただし、Android Studio はその変更をすぐに認識しません。
変更をコード補完などにも反映するには、プロジェクトを開き直す必要があります。その方法で反映されないときは、app の build.gradle から
implementation project(path: ':simple')を消してから再追加し、プロジェクトの sync をしたところ、ようやく反映されました。
少し手間ですが、そこまですると確実です。Android App Bundle
Go で作ったライブラリはサイズが大きめになりがちです。
特に複数のアーキテクチャ向けのファイルが含まれていると大きくなります。少しでもユーザにやさしいサイズになるよう、ストアには APK ではなく App Bundle にしましょう。
そうすれば、必要なアーキテクチャの APKs にしてくれたり、モジュール単位のダウンロードが可能になったりします。
Flutter の FAQ の中で説明されています。 ↩
必要に応じて CGO を使って C も組み合わせれば速度の違いは更に大きくなるかもしれません。 ↩
C/C++ で作る場合も NDK は必要で、Go だからではありません。 ↩
https://github.com/golang/go/wiki/Mobile#sdk-applications-and-generating-bindings ↩
"The equivalent of calling newCounter in Go is GoMypkgNewCounter in Objective-C. The returned GoMypkgCounter* holds a reference to an underlying Go *Counter." 見た目の制限とはこのあたりのことかなと思います。https://godoc.org/golang.org/x/mobile/cmd/gobind#hdr-Passing_Go_objects_to_target_languages ↩
gomobile に限らず Go 自体がそういうものです ↩
記事執筆時の調査等には Go 1.12.7 (windows/amd64)、Flutter 1.7.8+hotfix.3 (channel stable)、Dart 2.4.0、Android Studio 3.4.2 を使用しました。 ↩
NDK のパスを環境変数の
Path
に設定する必要もありませんでした。数年前に使っていたときには設定した記憶があるのですが、不要になったのかもしれません。 ↩サポートしたいアーキテクチャ分をすべて含んだ App Bundle をストアにアップロードすると、ユーザの利用端末に合わせて自動的に最適化した APK を配信してくれるため、複数を含んでいることを気にする必要はないと思います。32/64 ビット両方を対象に含めた App Bundle の生成は、先日リリースされたばかりの Flutter 1.7 で可能になりました。 ↩
研究論文に基づいて実装されたものだそうです。また、nude.js の作者の ブログ には "I wouldn’t recommend using the library in production mode right now because the detection rate is about 60%" と書かれています。 ↩
型を初期化する関数(コンストラクタのようなもの)の名前の先頭に「New」を付けるのは Go の慣習です。 ↩
ちょっと理解があやふやです。間違っていればご指摘ください。 ↩
Visual Studio Code はこの点は不十分なようです。 ↩
- 投稿日:2019-07-16T00:07:06+09:00
PWAをlocalhostで実機デバッグしながら、ホームに追加、キャッシュ、push通知を試してみた
参考
- Service Workerの基本とそれを使ってできること - Qiita
- Service Worker の紹介 | Web Fundamentals
- Cache - Web API | MDN
- FetchEvent.respondWith() - Web API | MDN
- Acheiving CORS via a Node HTTP Server - GitHubGist
PWAとは
- いまさら聞けないPWAとAMP - Qiita
- PWAとは(Progressive Web Appsとは) | SEO用語集:意味/解説/SEO効果など [SEO HACKS]
- PWA(Progressive Web Apps)とは?メリットと実装事例について | デジ研
- PWA(Progressive Web Apps)とは?PWAの機能や導入メリットを徹底解説!
- 実際に触って理解する! ウェブをアプリ化できるPWAとは? | BACKYARD
私がこの投稿内で作成したコードは以下です
https://github.com/okumurakengo/pwa-testPWAを試す準備
1. テスト用アプリ作成
↓こちらのチュートリアルがとても参考になったので、こちらを元に簡単なニュースアプリを作成します。
1-1. News API に登録
Service Workerを使って、取得したAPIの情報をキャッシュしてオフラインで表示といったことを行います。
News APIを使うと、無料で手軽にニュース情報のJSONを取得できるのでアカウント登録してAPI keyを取得します。
News APIのAPI keyを取得する
- Get API key をクリック
- 必要事項を記入し、Submitをクリック
API keyが取得できました、これでアプリからNews APIを使用できます。
1-2. ニュース情報を表示する簡単なアプリ作成
※まだこの状態ではPWAに関することは何もありません
この時点までのサンプルは↓です
https://github.com/okumurakengo/pwa-test/tree/1-newsapp
. ├── app.js ├── index.html └── styles.cssindex.html<!DOCTYPE html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>sample</title> <link rel="stylesheet" href="styles.css"> <script src="app.js" defer></script> <header> <h1>News</h1> <select id="sourceSelector"></select> </header> <main></main> <footer><a href="https://newsapi.org/">https://newsapi.org/</a></footer>※
NEWS_API_KEY
は自分でアカウント登録した際に取得したapiキーを設定するapp.jsconst NEWS_API_KEY = "67ece92e536e4852a075c3a35621b9ea"; // 自分のapiキー const main = document.querySelector("main"); const sourceSelector = document.querySelector("#sourceSelector"); const defaultSource = "abc-news-au"; (async () => { updateNews(); await updateSources(); // 画面表示時のプルダウンの初期値を設定 sourceSelector.value = defaultSource; sourceSelector.addEventListener("change", e => { updateNews(e.target.value); }); })(); /** * プルダウンの値を設定 */ async function updateSources() { const res = await fetch(`https://newsapi.org/v2/sources?apiKey=${NEWS_API_KEY}`); const json = await res.json(); json.sources.forEach(src => { sourceSelector.insertAdjacentHTML("beforeend", `<option value="${src.id}">${src.name}</option>`); }); } /** * ニュースの内容を表示 * @param {string} source */ async function updateNews(source = defaultSource) { const res = await fetch(`https://newsapi.org/v2/everything?q=${source}&apiKey=${NEWS_API_KEY}`); const json = await res.json(); main.innerHTML = ""; json.articles.forEach(article => { main.insertAdjacentHTML("beforeend", ` <div class="article"> <a href="${article.url}"> <h2>${article.title}</h2> <img src="${article.urlToImage}"> <p>${article.description}</p> </a> </div> `); }); }
styles.css
styles.css* { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: Verdana, sans-serif; } .article a { color: #2a3443; text-decoration: none; } header, main { padding: 16px; } header h1 { font-weight: 400; } .article { margin: 0 0 20px 0; } img { max-width: 300px; }なんでもいいので簡易サーバーなどでlocalhostで表示する
bash#nodeで簡易サーバ yarn add -D node-static yarn static -p 8080 #phpで簡易サーバ php -S 0.0.0.0:8080 #pythonで簡易サーバ python3 -m http.server 8080このような画面が表示されたらOK
2. PCのlocalhostをAndroidから見れるようにする
Service Wrokerを使う場合には、localhostかhttpsの必要があるため、
今回はPCのlocalhostにAndroidからアクセスしてみます。2-1. AndroidのUSBデバッグをONにする
2-2. Chromeを設定してAndroidから
localhost:8080
を開く
Chromeを設定してAndroidから
localhost:8080
を開くまで1 . PCとAndroidをUSBでつなぐ
2 . AndroidでChromeを開く
3 .chrome://inspect/#device
を開くRemote Targetの部分にAndroidで現在開いているページが表示されていたらOKです。
※表示されない場合はchromeを再起動すると表示してくれました。4 . 「Port forwarding...」を押す
5 .
localhost:8080
を設定以下のように設定し、
「Enable port forwargind」にチェックを入れ
「Done」をクリック6 . AndroidのChromeで
http:localhost:8080
にアクセスAndroidからPCのlocalhostを表示できました
7 . AndroidのChromeをコンソールで見れるようにする
inspectを押すとdev toolを表示してくれました
↑の設定を行うことで、PCのlocalhostを表示し、デバッグできることも確認できました。
PWAを試す
AndroidのChromeでlocalhostをみれたので、PWAの機能を追加していきます。
1. ホーム画面に追加する
ホーム画面に追加するまでを私が実際に試したコードは以下です。
https://github.com/okumurakengo/pwa-test/tree/2-home
ホーム画面に追加できる条件は以下を参考
1-1.
manifest.json
を用意する
manifest.json
が必要なので用意します。
manifest.json
の書き方については、dev toolからリンクがあるのでそれが参考になります。The Web App Manifest | Web Fundamentals | Google Developers
またはManifest Generatorという便利なサイトもあるのでこちらでも大丈夫です。
今回はこちらのサイトを使用させていただきました。今回はこのように設定しました。
画像は512x512でアップロードしましょう。
生成してダウンロードすると、このように
manifest.json
とアイコンが確認できます。
manifest.json
は、このようになりましたmanifest.json{ "name": "News", "short_name": "News", "theme_color": "#000000", "background_color": "#000000", "display": "standalone", "Scope": "/", "start_url": "/", "icons": [ { "src": "images/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "images/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png" }, { "src": "images/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png" }, { "src": "images/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png" }, { "src": "images/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png" }, { "src": "images/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "images/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png" }, { "src": "images/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" } ], "splash_pages": null }
manifest.json
とiconの画像ファイルをアプリのルートに移動し、htmlで読み込みます。index.html<!DOCTYPE html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>sample</title> <link rel="stylesheet" href="styles.css"> <script src="app.js" defer></script> + <link rel="manifest" href="manifest.json"> <header> <h1>News</h1> <select id="sourceSelector"></select> </header> <main></main> <footer><a href="https://newsapi.org/">https://newsapi.org/</a></footer>
1-2. Service Workerを読み込む
参考
ホーム画面に追加する場合は、Service Workerで
fetch
イベントがないとうまく動作してくれないようです。
sw.js
を作成し、それを読み込みます。
※fetch
イベントがあれば特に処理は必要なしsw.jsself.addEventListener("fetch", event => {});
app.js
でsw.js
を読み込みます。app.jsif ("serviceWorker" in navigator) { try { navigator.serviceWorker.register("sw.js") console.log("SW registered") } catch (e) { console.log("SW faild") } }
app.js
全体app.jsconst NEWS_API_KEY = "67ece92e536e4852a075c3a35621b9ea"; // 自分のapiキー const main = document.querySelector("main"); const sourceSelector = document.querySelector("#sourceSelector"); const defaultSource = "abc-news-au"; (async () => { updateNews(); await updateSources(); // 画面表示時のプルダウンの初期値を設定 sourceSelector.value = defaultSource; sourceSelector.addEventListener("change", e => { updateNews(e.target.value); }); if ("serviceWorker" in navigator) { try { navigator.serviceWorker.register("sw.js") console.log("SW registered") } catch (e) { console.log("SW faild") } } })(); /** * プルダウンの値を設定 */ async function updateSources() { const res = await fetch(`https://newsapi.org/v2/sources?apiKey=${NEWS_API_KEY}`); const json = await res.json(); json.sources.forEach(src => { sourceSelector.insertAdjacentHTML("beforeend", `<option value="${src.id}">${src.name}</option>`); }); } /** * ニュースの内容を表示 * @param {string} source */ async function updateNews(source = defaultSource) { const res = await fetch(`https://newsapi.org/v2/everything?q=${source}&apiKey=${NEWS_API_KEY}`); const json = await res.json(); main.innerHTML = ""; json.articles.forEach(article => { main.insertAdjacentHTML("beforeend", ` <div class="article"> <a href="${article.url}"> <h2>${article.title}</h2> <img src="${article.urlToImage}"> <p>${article.description}</p> </a> </div> `); }); }1-3. ホーム画面に追加する
この状態でAndroidから
http:localhost:8080
にアクセスすると画面の下に「ホーム画面に News を追加」と表示させることができました。
画面の指示にしたがって操作してホームに追加できました。
アプリを起動すると、URLバーの表示がない状態の画面になっていることを確認できます。
2. キャッシュから表示する
オフライン状態でも表示できるように、
オンライン状態でキャッシュ -> オフライン時にキャッシュ表示 となるように変更します。キャッシュ確認までを私が実際に試したコードは以下です。
https://github.com/okumurakengo/pwa-test/tree/3-cache
※AndroidではなくPCのchromeで試してます
Service Workerが更新されたら再度読み込む方法について
キャッシュするように
sw.js
を変更します。
開発中だとService Workerのsw.js
を頻繁に更新するのですが、更新してもwaiting状態となり、
ブラウザを再起動するなどしないと古いService Workerの内容しか実行されません。
なので開発者ツールから、
「Application」>「Service Workers」
「Update on reload」にチェックをつけておきます。
チェックをつけると、ブラウザを再起動しないでも、Service Workerしリロードのたびにその都度更新してくれるようになりました。※チェックをつけるとService Workerの 新規インストール/更新時 に起きる
install
イベントもリロードのたびに起きます2-1. ローカルのファイルをキャッシュする
sw.jsconst staticAssets = [ "./", "./styles.css", "./app.js", ]; // Service Workerの 新規インストール/更新時 のイベント self.addEventListener("install", async e => { const cache = await caches.open("news-static"); cache.addAll(staticAssets); }); // 何かしらのリクエストが発生した時のイベント self.addEventListener("fetch", async e => { const req = e.request; // respondWith()を使うことで、 // 既定の fetch ハンドリングを抑止して、 // 自分で Response用のPromiseを引数で指定できる e.respondWith(cacheFirst(req)); }); /** * 指定のリクエストの結果が * キャッシュに存在する場合はキャッシュを返し、 * キャッシュに存在しない場合はfetchでリクエストした結果を返す * * 今回の場合だと、"./", "./styles.css", "./app.js" へのリクエストが発生するとキャッシュから表示 * それ以外のAjaxやimgなどのリクエストの場合はfetchしてそのままのレスポンスを表示する * * @param {RequestInfo} req * @returns {Promise<Response>} */ async function cacheFirst(req) { const cachedResponse = await caches.match(req) return cachedResponse || fetch(req) }下は、Service Workerを読み込みキャッシュした後に、wifiをoffにして表示した時の画像です。
キャッシュされているHTMLは表示されているが、Offlineのためajaxが表示されていないという状況になっています。
キャッシュされているファイルは「Application」>「Chache Storage」で確認することができます。
2-2. 別オリジンへのajaxやimgファイルをキャッシュする
先ほどはローカルファイルだけだったため、それ以外もキャッシュするようにします。
sw.jsconst staticAssets = [ "./", "./styles.css", "./app.js", "./fallback.json", "./images/no-fetch.jpg", ]; // Service Workerの 新規インストール/更新時 のイベント self.addEventListener("install", async e => { const cache = await caches.open("news-static"); cache.addAll(staticAssets); }); // 何かしらのリクエストが発生した時のイベント self.addEventListener("fetch", async e => { const req = e.request; const url = new URL(req.url) // respondWith()を使うことで、 // 既定の fetch ハンドリングを抑止して、 // 自分で Response用のPromiseを引数で指定できる if (url.origin === location.origin) { // 同一オリジン(今回はlocalhost)へのリクエストの場合 e.respondWith(cacheFirst(req)) } else { // 別オリジンへのリクエストの場合 e.respondWith(networkFirst(req)) } }); /** * 同一オリジン(今回はlocalhost)へのリクエストの場合 * * 指定のリクエストの結果が * キャッシュに存在する場合はキャッシュを返し、 * キャッシュに存在しない場合はfetchでリクエストした結果を返す * * 今回の場合だと、"./", "./styles.css", "./app.js" などへのリクエストが発生するとキャッシュから表示 * それ以外のリクエストの場合はfetchしてそのままのレスポンスを表示する * * @param {RequestInfo} req * @returns {Promise<Response>} */ async function cacheFirst(req) { const cachedResponse = await caches.match(req) return cachedResponse || fetch(req) } /** * 別オリジンへのリクエストの場合 * * APIの情報は常に最新を表示するようにする * オフラインの場合に限りキャッシュを利用する * * 指定のリクエストをそのままfetchする * ↓ * 1. 正常にレスポンスが取得できた場合 * - レスポンスをキャッシュに保存 * - レスポンスを返す * 2. オフラインなどでリクエストが失敗 * - キャッシュにあればそれを返す * - キャッシュになければ fallback.json を返す * * @param {RequestInfo} req * @returns {Promise<Response>} */ async function networkFirst(req) { const cache = await caches.open("news-dynamic") try { const res = await fetch(req) cache.put(req, res.clone()) return res; } catch (e) { const cachedResponse = await cache.match(req) return cachedResponse || await caches.match("./fallback.json") } }※こちらの
fallback.json
はnewsapiの形式に合わせた内容です。fallback.json{ "articles": [ { "title": "表示できませんでした", "description": "Try loading the page again when you're online.", "url": "", "urlToImage": "images/no-fetch.jpg" } ] }ページを開くと、別オリジンの画像もcacheに保存できていることを確認できました。
オフラインでキャッシュされている場合はこちらの情報が表示されます。
オフラインでキャッシュされていない場合は
fallback.json
を表示してくれます。プルダウンを変更するたびにajaxリクエストしてデータを取得し、その度にキャッシュします。
開発者ツールでOfflineに設定し、プルダウンでキャッシュされていない項目を選ぶと、
fallback.json
の内容を表示してくれました。3. push通知する
参考
push通知確認までを私が実際に試したコードは以下です。
https://github.com/okumurakengo/pwa-test3-1.
web-push
をインストール
web-push
を使ってpush通知してみますので、installしますyarn init -y yarn add web-push
3-2. push通知に必要なvapidキーを取得する
↓のようにコマンドを実行すると、
Public Key
とPrivate Key
が表示されるのでこれを使います。$ yarn web-push generate-vapid-keys yarn run v1.12.3 ======================================= Public Key: BLGgW2eVUdKoSx2R4k80hCsTSLKPd0YmvHHm2CaW5JfXIlHm92sMHMUGOgBHpaweTRERkCyrT_42cDTmtWCF6zo Private Key: 1amdB2vaa5tm6YfV33LvguNJeutDLY0FoC7IzhZR-T8 ======================================= ✨ Done in 0.22s.3-3. push通知用のサーバーを作成
先ほど取得した、
Public Key
とPrivate Key
を設定します。
このサーバーに対してfetchでリクエストしてpush通知します。※今回はサーバーにリクエストがきたらpushの処理を行うようにしましたが、サーバーにせずnodeで実行してそのまま
webpush.sendNotification
を実行でもpush通知できます。push-server.jsconst http = require("http"); const webpush = require("web-push"); const publicVapidKey = "BLGgW2eVUdKoSx2R4k80hCsTSLKPd0YmvHHm2CaW5JfXIlHm92sMHMUGOgBHpaweTRERkCyrT_42cDTmtWCF6zo"; const privateVapidKey = "1amdB2vaa5tm6YfV33LvguNJeutDLY0FoC7IzhZR-T8"; webpush.setVapidDetails( "mailto:test@test.com", // アプリケーションのmailtoまたはURL publicVapidKey, privateVapidKey ); const server = http.createServer((req, res) => { // 今回は localhost:8080 -> localhost:8081 と // クロスドメインfetchでリクエストするので、corsの設定をしておく res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Request-Method", "*"); res.setHeader("Access-Control-Allow-Methods", "OPTIONS, POST"); res.setHeader("Access-Control-Allow-Headers", "*"); if (req.method === "OPTIONS") { res.end(); return; } }); server.on("request", async (req, res) => { if (req.method === "POST") { // リクエストボディのjson文字列を取得 const subscription = await new Promise(resolve => { req.on("data", resolve); }); try { // sendNotificationを実行すると、Service Workerでpushイベントを起こせました const payload = JSON.stringify({ title: "Push Test" }); await webpush.sendNotification(JSON.parse(subscription), payload); } catch(e) { console.log(e) } } res.end(); }); server.listen(8081); console.log("push server listening 8081");サーバーを起動しておく
node push-server.js #localhost:8081でサーバーが起動する
3-4. push通知用サーバーにリクエストする
Public Key
を設定し、先ほどのlocalhost:8081
へfetchします。app.jsconst publicVapidKey = "BL31MrWtf-Q74RvQHgKB3WbKz-qBGvz-RCXayyPzCkH1FqyiYCGftirS1UjeK5UBRyb0saFwFYMhVMLn8Ete6ts"; // .. if ("serviceWorker" in navigator) { try { register = await navigator.serviceWorker.register("sw.js") console.log("SW registered") const subscription = await register.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicVapidKey), }); console.log("Push registered"); fetch("http://localhost:8081", { method: "POST", body: JSON.stringify(subscription), headers: { "Content-Type": "application/json", }, }); console.log("Push Sent"); } catch (e) { console.log("SW faild") } // ... /** * @see https://github.com/web-push-libs/web-push#using-vapid-key-for-applicationserverkey * @param {string} base64String */ function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; }
app.js
全体app.jsconst NEWS_API_KEY = "67ece92e536e4852a075c3a35621b9ea"; // 自分のapiキー const main = document.querySelector("main"); const sourceSelector = document.querySelector("#sourceSelector"); const defaultSource = "abc-news-au"; const publicVapidKey = "BLGgW2eVUdKoSx2R4k80hCsTSLKPd0YmvHHm2CaW5JfXIlHm92sMHMUGOgBHpaweTRERkCyrT_42cDTmtWCF6zo"; (async () => { updateNews(); await updateSources(); // 画面表示時のプルダウンの初期値を設定 sourceSelector.value = defaultSource; sourceSelector.addEventListener("change", e => { updateNews(e.target.value); }); if ("serviceWorker" in navigator) { try { register = await navigator.serviceWorker.register("sw.js") console.log("SW registered") const subscription = await register.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicVapidKey), }); console.log("Push registered"); fetch("http://localhost:8081", { method: "POST", body: JSON.stringify(subscription), headers: { "Content-Type": "application/json", }, }); console.log("Push Sent"); } catch (e) { console.log("SW faild") } } })(); /** * プルダウンの値を設定 */ async function updateSources() { const res = await fetch(`https://newsapi.org/v2/sources?apiKey=${NEWS_API_KEY}`); const json = await res.json(); json.sources.forEach(src => { sourceSelector.insertAdjacentHTML("beforeend", `<option value="${src.id}">${src.name}</option>`); }); } /** * ニュースの内容を表示 * @param {string} source */ async function updateNews(source = defaultSource) { const res = await fetch(`https://newsapi.org/v2/everything?q=${source}&apiKey=${NEWS_API_KEY}`); const json = await res.json(); main.innerHTML = ""; json.articles.forEach(article => { main.insertAdjacentHTML("beforeend", ` <div class="article"> <a href="${article.url}"> <h2>${article.title}</h2> <img src="${article.urlToImage}"> <p>${article.description}</p> </a> </div> `); }); } /** * @see https://github.com/web-push-libs/web-push#using-vapid-key-for-applicationserverkey * @param {string} base64String */ function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; }3-5. Service Workerでpush通知する
sw.js
の最後にpush
イベントを追加します。sw.jsself.addEventListener("push", e => { const { title } = e.data.json(); self.registration.showNotification(title, { body: "Notification Test form SW !", icon: "./images/icons/icon-192x192.png", }); });3-6. push通知してみる
PCの場合
Androidの場合
chrome://inspect/#deviceを開き、
localhost:8081
を追加しますその状態で再読み込みをすると
push通知がきたことを確認できました。感動の瞬間です。
最後まで読んでいただいてありがとうございました。m(_ _)m