20190716のAndroidに関する記事は6件です。

Android入門

アプリ開発のメリット

-Java、Kotlinの勉強になる
-自分好みのアプリを作れる
-プロへの道が開ける

リリースについて

GooglePlayConsoleからリリースする

諸注意、作業方法

パッケージ名がcom.exampleだと通らない
manifestからリファクタリング、リネームする

build.gradle(app)のバージョンも変えていく
1ずつ増やすなど

アイコンのサイズ(解像度)はペイントで変える

インストール後のアプリ名は、manifestの以下の

AndroidManifest.xml
android: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して(いるように見えるが実際は無名クラス)、無名クラスで実装する書き方がある。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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 Facebook
Flutter Google
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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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時間依存性注入のコンパイル

https://sagarsuri56.hashnode.dev/compile-time-dependency-injection-in-flutter-cjxty3efh005fkvs14va5w339


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分でチャットボットを構築する

https://medium.com/flutter-community/build-a-chatbot-in-20-minutes-using-flutter-and-dialogflow-8e9af1014463


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:設計から開発プロセスをスピードアップ

https://medium.com/flutter-community/flutter-and-zeplin-speed-up-the-development-process-from-your-design-45ff5d21166a


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

https://medium.com/coding-with-flutter/flutter-state-management-setstate-bloc-valuenotifier-provider-2c11022d871b


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パッケージを作成して公開する方法の紹介。

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の両方のための本図書館アプリ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidでViewを押したときにぶわーってなるエフェクトをつける

タイトルひどすぎだろ。こんばんは。
今回は前から気になってたこと書きます。

本題

Buttonとかを押したときにぶわーって広がるアニメーション。
あれどうやって作るのかGoogle先生に聞いてみました。

実装する

今回はこの記事を書くために適当なアプリを作りました。

SnapCrab_NoName_2019-7-16_1-49-11_No-00.png

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 で取れます。

Screenshot_1563209361.png

Screenshot_1563209367.png

結果

種類 効果
?attr/selectableItemBackgroundBorderless Viewの範囲を超えてぶわーって広がった。
?attr/selectableItemBackground Viewの範囲内でぶわーって広がった。

こんな感じでしょうか。

お疲れ様です

色んな所で使えそう。

参考にしました

https://stackoverflow.com/questions/33477025/how-to-set-a-ripple-effect-on-textview-or-imageview-on-android

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリ開発に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 + AndroidFlutter + 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 ので速度に大きな差はなさそうに思えますが、試してみると違いがありました。2

  • C/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 の開発環境は既に用意されている前提です。

  1. Android NDK のインストール(Android のみ)
    Android Studio にて
    Tools > SDK Manager > 右ペインの SDK Tools タブ
    NDK にチェックが付いていなければ付けて OK または Apply

  2. 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.go
package 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 の型は それぞれ LongnumberWithLong です。
IntegernumberWithInt にするには、より小さなサイズの int32 等を使いましょう。

型の対応 については付録にまとめています。

情報出力とエラーの扱い

複数の方法で動作を見てみると、かなり癖がありました。
基本的に次のように考えておけば大丈夫かと思います。
詳細は 付録 をご覧ください。

  • 情報を LogCat や Run のウィンドウに表示したい
    fmt.Println() を使う。
    fmt.Print()fmt.Printf() で第一引数の末尾に改行するのも OK。

  • Android/iOS や Dart/Flutter で例外として捕捉したい
    ライブラリで値を返すとき、二つ目の戻り値に error 型のデータを付ける。

他のポイント

長くなるので 付録 に収めました。

2. ライブラリ生成

Android では aar ファイル、iOS では framework ファイルを生成します。
次のようなパスになっているとします。

  • GOPATH
    C:\Go

  • simple.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 ではターゲットのアーキテクチャも指定できます。
    スラッシュの後ろは armarm64386amd64 のいずれかです。
    指定しない場合、サポートする全4アーキテクチャの so ファイルを含んだ aar ファイルになります。9

  • -target ios
    iOS では -targetios を指定します。
    Android のようなアーキテクチャの指定には対応していないようです。
    そもそも幅広いバリエーションがあるわけでもないので不要ですね。


  • オプションは他にもあり、gomobile bind -h で確認できます。

ビルド時間、aarファイルのサイズ

これくらい小規模のコードを普段 Go でビルドするときと比べて長くかかります。
環境によりますが、私の PC で Android 向けにビルドしたところ 40 秒ほどでした。

また、aar 内の共有ライブラリ(.so)が一つあたり 2MB 以上、圧縮状態で 1MB 程度になりました。
aar ファイルには 4 アーキテクチャ分が入っていて計 4MB 台です。大きめですね。
ユーザが Google Play ストアからダウンロードするときにはもっと小さくなります。9

Android + Go

ライブラリ導入

Android Studio を使っていきます。
使わずに、次の 1 ~ 2 に載せた diff を参考にしてファイル追加や記述変更を手動で行っても OK です。

1. ライブラリのモジュールを追加

モジュールとは、プロジェクトを分割した機能ごとのアプリのようなものです。
ここでは、ライブラリを一つのモジュールとしてプロジェクトに追加します。

  1. 起動後のウィンドウで「Start a new Android Studio project」を選ぶ

  2. 開いたウィザードで「Empty Activity」を選び、プロジェクトが開くところまで進める

  3. New Module のダイアログを開く

  4. 「Import .JAR/.AAR Package」を選んで「Next」

  5. フォルダアイコンを押し、先ほど生成された aar ファイルを指定してから「Finish」

    Subproject name のところには自動的にサブプロジェクト(モジュール)の名前が入ります。
    自分で変えても良いでしょう。

    これで simple モジュールが追加された状態になりました。

ここまでの操作による変化は次のとおりです(Android Studio 関連ファイルは省いています)。

settings.gradle
-include ':app'
+include ':app', ':simple'
simple/build.gradle
new file mode 100644
+configurations.maybeCreate("default")
+artifacts.add("default", file('simple.aar'))
\ No newline at end of file
simple/simple.aar
new file mode 100644

2. 追加したモジュールを使う設定

追加しただけでは使えません。
メインのモジュールである app から simple を利用できるように依存関係の設定を行います。

  1. Project Structure のダイアログを開く

  2. Dependencies > app を選び、右ペインで「+」を押して「Module Dependency」を選ぶ

  3. 「simple」にチェックをつけて「OK」を押す

  4. 右ペインに「simple」が追加されているのを確認して「OK」を押す

使うための設定はこれで完了です。
この操作による変化は次のとおりです。

app/build.gradle
 dependencies {
     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 ライブラリを実際に使ったアプリを作ります。

gomobile_android.gif

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.kt
import 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)
}

ライブラリのメソッドを使っているだけです。
そのメソッドではエラー時に例外を発生させるようにしているため trycatch を使っています。

ボタンを 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.kt
class 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.kt
package 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.dart
import '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.dart
import '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) が実行され、返ってきた FutureFutureBuilder で処理しています。

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 のコードと違うのは下記の trycatch の部分だけです。
ライブラリのエラーによって発生した例外を 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 Gogo-nude という Go のパッケージを見つけました。
nude.js というライブラリを Go に移植したものだそうです。

JavaScript でできるなら Dart でもできそうですが、まだ存在しないようです。
使いたくてもまだ無いケースとして Go の利用が適していると考えました。

flutter_nudiry.gif

※このスクリーンキャスト内では go-nude の example/images/ にある画像を使いました。

※実用性は低そうです。
 判定が厳しすぎるかと思ったら、逆に景色の写真がヌードと判定されることもあったりします…。10

nudity.go
package 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.dart
import '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 でやれば速くなるのではないかと考えました。

flutter_grayscale.gif

grayscale.dart
package 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.dart
import '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やメールで送ってきそうな文を生成してくれる楽しいパッケージです。

これを使ったチャット風アプリが簡単にできました。
コードは省略します。

flutter_ojichat.gif

付録

ここまでに書いた以外に知っておくと良いことをまとめました。

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.go
package 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.kt
import 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.dart
import '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);
  1. Go で書いたライブラリに Printer というインタフェースがある
  2. そのインタフェースを Java で実装して SysPrint クラスとする
  3. インタフェースが持つメソッドである print() を SysPrint 内で具象化する
  4. SysPrint のインスタンスを生成し、それをライブラリの PrintHello() に渡す
  5. 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)

最後に trycatch を使わないようにしてみます。

lib/simple.dartの一部を改変
final arguments = {'value': count};
return await _platform.invokeMethod<int>('simple_multiply', arguments);

ボタンを 11 回以上押してもチャンネル名を変えても、何も出力されませんでした。
意図的に無視することもできるようになっているようです。
しかし、異常に気づいて対応できるように trycatch しておくのが良いと思います。

Docker

go4droid/Dockerfile at master · mpl/go4droid · GitHub
https://github.com/mpl/go4droid/blob/master/Dockerfile

gomobile の 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 にしてくれたり、モジュール単位のダウンロードが可能になったりします。


  1. Flutter の FAQ の中で説明されています。 

  2. 必要に応じて CGO を使って C も組み合わせれば速度の違いは更に大きくなるかもしれません。 

  3. C/C++ で作る場合も NDK は必要で、Go だからではありません。 

  4. https://github.com/golang/go/wiki/Mobile#sdk-applications-and-generating-bindings 

  5. "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 

  6. gomobile に限らず Go 自体がそういうものです 

  7. 記事執筆時の調査等には Go 1.12.7 (windows/amd64)、Flutter 1.7.8+hotfix.3 (channel stable)、Dart 2.4.0、Android Studio 3.4.2 を使用しました。 

  8. NDK のパスを環境変数の Path に設定する必要もありませんでした。数年前に使っていたときには設定した記憶があるのですが、不要になったのかもしれません。 

  9. サポートしたいアーキテクチャ分をすべて含んだ App Bundle をストアにアップロードすると、ユーザの利用端末に合わせて自動的に最適化した APK を配信してくれるため、複数を含んでいることを気にする必要はないと思います。32/64 ビット両方を対象に含めた App Bundle の生成は、先日リリースされたばかりの Flutter 1.7 で可能になりました。 

  10. 研究論文に基づいて実装されたものだそうです。また、nude.js の作者の ブログ には "I wouldn’t recommend using the library in production mode right now because the detection rate is about 60%" と書かれています。 

  11. 型を初期化する関数(コンストラクタのようなもの)の名前の先頭に「New」を付けるのは Go の慣習です。 

  12. ちょっと理解があやふやです。間違っていればご指摘ください。 

  13. Visual Studio Code はこの点は不十分なようです。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PWAをlocalhostで実機デバッグしながら、ホームに追加、キャッシュ、push通知を試してみた

参考

PWAとは


私がこの投稿内で作成したコードは以下です
https://github.com/okumurakengo/pwa-test

PWAを試す準備

1. テスト用アプリ作成

↓こちらのチュートリアルがとても参考になったので、こちらを元に簡単なニュースアプリを作成します。

1-1. News API に登録

Service Workerを使って、取得したAPIの情報をキャッシュしてオフラインで表示といったことを行います。
News APIを使うと、無料で手軽にニュース情報のJSONを取得できるのでアカウント登録してAPI keyを取得します。

News APIのAPI keyを取得する

https://newsapi.org/

  • Get API key をクリック

Screen Shot 2019-07-15 at 13 (1).png

  • 必要事項を記入し、Submitをクリック

Screen Shot 2019-07-15 at 13.50.00.png

API keyが取得できました、これでアプリからNews APIを使用できます。

Screen Shot 2019-07-15 at 13.51.02.png

1-2. ニュース情報を表示する簡単なアプリ作成

※まだこの状態ではPWAに関することは何もありません

この時点までのサンプルは↓です

https://github.com/okumurakengo/pwa-test/tree/1-newsapp

.
├── app.js
├── index.html
└── styles.css
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>
<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.js
const 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

Screen Shot 2019-07-15 at 14.19.15.png

2. PCのlocalhostをAndroidから見れるようにする

Service Wrokerを使う場合には、localhostかhttpsの必要があるため、
今回はPCのlocalhostにAndroidからアクセスしてみます。

2-1. AndroidのUSBデバッグをONにする

screenshotshare_20190715_144025 (1).png

2-2. Chromeを設定してAndroidからlocalhost:8080を開く

Chromeを設定してAndroidからlocalhost:8080を開くまで

1 . PCとAndroidをUSBでつなぐ
2 . AndroidでChromeを開く
3 . chrome://inspect/#deviceを開く

IMG_1454 (1).png

Remote Targetの部分にAndroidで現在開いているページが表示されていたらOKです。
※表示されない場合はchromeを再起動すると表示してくれました。

4 . 「Port forwarding...」を押す

Screen Shot 2019-07-15 at 15.png

5 . localhost:8080 を設定

以下のように設定し、
「Enable port forwargind」にチェックを入れ
「Done」をクリック

Screen Shot 2019-07-15 at 15.32.52.png

6 . AndroidのChromeでhttp:localhost:8080にアクセス

screenshotshare_20190715_153743 (1).png

AndroidからPCのlocalhostを表示できました

7 . AndroidのChromeをコンソールで見れるようにする

Screen Shot 2019-07-15 at 15 (1).png

inspectを押すとdev toolを表示してくれました

IMG_1459.png

↑の設定を行うことで、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からリンクがあるのでそれが参考になります。

DoCx7OeYqY.gif

The Web App Manifest  |  Web Fundamentals  |  Google Developers


またはManifest Generatorという便利なサイトもあるのでこちらでも大丈夫です。
今回はこちらのサイトを使用させていただきました。

Screen Shot 2019-07-15 at 16.58.15.png

今回はこのように設定しました。
画像は512x512でアップロードしましょう。


生成してダウンロードすると、このようにmanifest.jsonとアイコンが確認できます。

Screen Shot 2019-07-15 at 17.02.09.png

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.js
self.addEventListener("fetch", event => {});

app.jssw.jsを読み込みます。

app.js
    if ("serviceWorker" in navigator) {
        try {
            navigator.serviceWorker.register("sw.js")
            console.log("SW registered")
        } catch (e) {
            console.log("SW faild")
        }
    }

app.js全体
app.js
const 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にアクセスすると

screenshotshare_20190715_172201 (1).png

画面の下に「ホーム画面に News を追加」と表示させることができました。
画面の指示にしたがって操作してホームに追加できました。

screenshotshare_20190715_172706 (1).png


アプリを起動すると、URLバーの表示がない状態の画面になっていることを確認できます。

screenshotshare_20190715_172727 (1).png

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イベントもリロードのたびに起きます

Screen Shot 2019-07-15 at 17.png

2-1. ローカルのファイルをキャッシュする

sw.js
const 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が表示されていないという状況になっています。

Screen Shot 2019-07-15 at 18.45.12.png


wifiを切らなくても、Offlineにチェックしても良い

こちらでもキャッシュが使われているかのテストにはなるのですが、
私の環境ではキャッシュが使われたり、使われなかったりとうまく動かないことがありました。
原因がわからないのですが、何かわかったら追記しようと思いますm(_ _)m

Screen Shot 2019-07-15 at 18.png


キャッシュされているファイルは「Application」>「Chache Storage」で確認することができます。

Screen Shot 2019-07-15 at 18 (1).png

2-2. 別オリジンへのajaxやimgファイルをキャッシュする

先ほどはローカルファイルだけだったため、それ以外もキャッシュするようにします。

sw.js
const 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に保存できていることを確認できました。

Screen Shot 2019-07-15 at 19.27.13.png

オフラインでキャッシュされている場合はこちらの情報が表示されます。


オフラインでキャッシュされていない場合はfallback.jsonを表示してくれます。

プルダウンを変更するたびにajaxリクエストしてデータを取得し、その度にキャッシュします。
開発者ツールでOfflineに設定し、プルダウンでキャッシュされていない項目を選ぶと、
fallback.jsonの内容を表示してくれました。

Screen Shot 2019-07-15 at 19.37.38.png

3. push通知する

参考

push通知確認までを私が実際に試したコードは以下です。
https://github.com/okumurakengo/pwa-test

3-1. web-pushをインストール

web-pushを使ってpush通知してみますので、installします

yarn init -y
yarn add web-push

3-2. push通知に必要なvapidキーを取得する

↓のようにコマンドを実行すると、Public KeyPrivate 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 KeyPrivate Keyを設定します。
このサーバーに対してfetchでリクエストしてpush通知します。

※今回はサーバーにリクエストがきたらpushの処理を行うようにしましたが、サーバーにせずnodeで実行してそのままwebpush.sendNotificationを実行でもpush通知できます。

push-server.js
const 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.js
const 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.js
const 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.js
self.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の場合

hvg3RT6T6g.gif


Androidの場合

chrome://inspect/#deviceを開き、localhost:8081 を追加します

Screen Shot 2019-07-15 at 20.45.52.png

その状態で再読み込みをすると

screenshotshare_20190715_204744 (1).png

Screen Shot 2019-07-15 at 20.56.45.png

push通知がきたことを確認できました。感動の瞬間です。


最後まで読んでいただいてありがとうございました。m(_ _)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む