Androidバージョンの動物園用のWebアプリケーションを準備しています

ごく最近、そして私自身にとって非常に意外なことに、私はAndroid用プログラムの開発を担当しました。 しかし、AndroidやJavaで何かを書く必要はまったくありませんでした。 ブラウザーコンポーネントでほぼ完全に機能する、phonegapなどのWebアプリケーションを作成する必要がありました。 そして、これはすべてバージョン2.2-4.3(SDK 8-18)の下で。



Androidのトリックとその下にある松葉杖の観点から、最初にすべてを見た人の観点から、私は伝えたいと思います。 HelloWorldなしで「OMG! Java」など



画面の回転/向きの変更

ネットワークに到達できません

ローカルリソースをロードします

JavaとJavaScriptの橋渡し





プログラム全体は、基本的に1つのWebViewであり、そこにローカルリソース(アセット)がロードされ、Javaでの追加のバインディングです。 JSがそれ自体でできないこと-javascript over Java <=> JSを同期/非同期でブリッジします。 後者は、ページ上で「静かに」誘canされる可能性もあります。



スクリーン回転



Androidでは、デバイスの向きを変更すると、アクティビティ(UI)が再作成されます。 WebViewの場合、これは現在のページをリロードします。 したがって、入力フィールドの入力、現在のスクロール位置、JSの状態などは失われます。

インターネット上では、パフォーマンスの程度が異なり、SDKの異なるバージョンを考慮に入れるこの状況に対処する方法がたくさんあります。 その結果、必要なすべてのバージョンのSDKで次のオプションが機能するようになりました。



必要なアクティビティのマニフェストで、android:configChanges "screenSize | orientation"を指定します。 両方とも、そうでなければ何も動作しないバージョンがあります。

レイアウトでは、WebViewをまったく記述しません。 ブラウザが配置される場所で、スタブについて説明します。

<FrameLayout android:id="@+id/webViewPlaceholder" ... />
      
      





ブラウザなし-問題ありません:)



アクティビティのクラスで:

 protected FrameLayout mWebViewPlaceholder; //    protected WebView mWebView; //  -    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initUI(); } protected void initUI() { mWebViewPlaceholder = ((FrameLayout)findViewById(R.id.webViewPlaceholder)); if (mWebView == null) { mWebView = new WebView(this); mWebView.loadUrl("file:///android_asset/index.html"); } mWebViewPlaceholder.addView(mWebView); } @Override public void onConfigurationChanged(Configuration newConfig) { if (mWebView != null) { mWebViewPlaceholder.removeView(mWebView); //     WebView  UI } super.onConfigurationChanged(newConfig); setContentView(R.layout.activity_main); initUI(); //  WebView   UI } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mWebView.saveState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mWebView.restoreState(savedInstanceState); }
      
      





結果は何ですか?

回転および解像度の変更のイベントを処理するとき、UIからWebViewを一時的に「引き出し」ます。 同時に、WebViewの状態を個別に保存および復元します。 システム/別のプログラムのメイン画面に移動するか、プログラムで他のアクティビティを開くと、アクティビティは破棄されます。

ところで、ページ要素の値(同じ入力)を保存する必要がある場合は、それらにidを割り当てる必要があります。 それ以外の場合は保存されません。



ネットワークに到達できません



外部サーバーからWebViewでページを開くと、デバイスでネットワークが切断された状態(エアモード、wifiオフ...)は非常に面白いです。 WebViewClient.onReceivedErrorをキャッチして宣誓しているようです。 そして、すべてが落ち着いていて、WebViewClient.onPageFinishedへの呼び出しが発生した場合、ページがロードされています。 これは以前は正常に機能していましたが、4.3(sdk 18)では、読み込み開始の2 後にonReceivedErrorが呼び出されます。 検索と変更を試みていないタイムアウト設定... onPageStartedが機能し、すべてが2分間無音になる...

解決策? ローカルリソースを開き、そこからすでにサーバーから必要なものを汲み上げます。

誰かが問題を知っている場合は、コメントがあれば、それが役に立つときに書きます。



ローカルリソースをロードします



WebViewでは、ネットワークにアクセスせずに、文字列loadData( "...")またはローカルリソースloadUrlのアドレス( "file:/// ...")を渡すことでデータをロードできます。 loadDataの最初のオプションは、UTF-8キリル文字で予期せず表示されました。 loadDataはパラメーターの1つとして文字列エンコードを受け入れますが、4.1 +からのAndroidでは(4.0では、覚えている限りでは表示されませんでした)、ページはシングルバイトエンコードで明示的にレンダリングされます。 また、メタ文字セット= utf-8もwebView.getSettings()。SetDefaultTextEncodingName( "utf-8")も状況を修正しませんでした。

解決策? loadUrlを介してファイルから読み込みます。



JavaとJavaScriptの橋渡し



標準のブラウザコンポーネントでページを開くことは、 もちろん不可能なタスクのみの準備です。 重要なのは、JSコードとJavaの相互作用であり、いずれかの側の主導です。

そして、もっと便利なものがあるようです-addJavascriptInterface(オフドキュメントの例):

 class JsObject { @JavascriptInterface public String toString() { return "injectedObject"; } } webView.addJavascriptInterface(new JsObject(), "injectedObject"); webView.loadData("", "text/html", null); webView.loadUrl("javascript:alert(injectedObject.toString())");
      
      





「ブリッジ」とその名前をJSに設定し、それだけです。 ページのコンテンツなしでloadDataを呼び出すことは奇妙に思えるかもしれませんが、少なくとも何かがロードされるまで(インタラクションインターフェイスはJSで作成されません)(初期化されません)(空の行さえ持つことができます)。

JSからJavaにパラメーターを渡すことも可能です。これは、パラメーターの数とタイプの主な一致です。

エミュレーターおよび実際のデバイスで実行します:4.3-動作中、4.2-動作中、... 2.3-アプリケーションのクラッシュ(OS起動画面へのクラッシュ)...待って、何?... 2.2-動作します。 再び2.3-地殻へ。

jectedObject.toStringにアクセスすると、クラッシュが発生します(呼び出しがなくても)。 jectedObjectへのアクセスは穏やかです。

バックトレースの「callJNIMethod <>」の出現は、近い将来の興味深いレジャー活動を約束します...

グーグルとコーミングSOは、問題が大規模であり、2.3行全体で100%に近い確率で発生することを示しています。*(Sdk 9-10)。 2.2(sdk 8)でも時々表示されるようですが、このようなメッセージは2、3しか出会っておらず、このバージョンには実際のデバイスがないため、確認できません。 エミュレータと実際のデバイスの両方。 主なことは、WebViewがJSエンジンとして非V8エンジンを使用する必要があることです。

大衆の叫び声とバグ修正のGoogleへのリクエストは2010年に始まりましたが、公式の修正はリリースしませんでした。



長年にわたり、問題の少なくともいくつかの回避策、具体的には2.3の下での回避策のために、いくつかのオプションが登場しました。 オプションを考慮せずに、影響を受けるバージョンでプログラムが呪われ、動作を拒否した場合、ソリューションは次のアプローチに分割できます。



1. Javaに直接リクエストする代わりに、次のことを比perform的に実行します。

  window.location = 'http://OurMegaPrefix:MethodName:Param0:Param1';
      
      





次に、Javaでこのようなリクエストを処理します

 webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!url.startsWith("OurMegaPrefix")) { return false; } //  :  ,     ,   } //    : public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (javascriptInterfaceBroken) { String handleGingerbreadStupidity = "javascript:function openQuestion(id) { window.location='http://jshandler:openQuestion:'+id; }; " + "javascript: function handler() { this.openQuestion=openQuestion; }; " + "javascript: var jshandler = new handler();"; webView.loadUrl(handleGingerbreadStupidity); } } }); if (!javascriptInterfaceBroken) { webView.addJavascriptInterface(new JsObject(), "jshandler"); }
      
      





onPageFinishedでページをロードした後、必要なプロキシオブジェクトを実装するためのコードをページに挿入します。

率直に言って松葉杖のソリューションに加えて、このメソッドでは結果を使用して同期呼び出しを行うこともできません。 関数にコールバックの名前を渡すことができます...しかし、それはしません。先に進みましょう。



2.別のオプションは、標準javascriptインターフェースを使用して、ユーザーからの同期(js用)応答を受信することです:prompt()。 主なことは彼を捕まえることです:)

 var res = prompt('OurMegaPrefix:meth:param0:param1');
      
      





resでは、どういうわけか、Javaからの呼び出しの結果を既に保存する必要があります。 そして、ユーザーに何も見せません。

 webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { final String prefix = JSAPI.class.getSimpleName(); if (!message.startsWith(prefix)) { return super.onJsPrompt(view, url, message, defaultValue, result); } //  return true; //   prompt   } });
      
      





ここで、JSAPIはプロキシクラスの名前です(上記は「JsObject」と呼ばれていました)。

何らかの方法でメソッド名とパラメーター(存在する場合)を受け取った後、JSAPIから対応するメソッドを呼び出す必要があります。

 var jsapi = new JSAPI();
      
      





この記事はJavaでのリフレクションに関するものではないので、簡単に次のようにします。

 Method method = JSAPI.class.getMethod(methodName, new Class[] {String.class});
      
      





1つの文字列パラメーターでメソッドを呼び出します。

または、名前で簡単に検索するには:

 Method method = null; for (Method meth : JSAPI.class.getMethods()) { if (meth.getName().equals(methodName)) { method = meth; break; } }
      
      





次に、見つかったメソッドを呼び出します(もう一度、1つのパラメーターの例)。

 method.invoke(jsapi, parameter);
      
      







一般的な場合、これはすでにaddJavascriptInterfaceをバイパスする相互作用に十分です。 しかし、私はこの全体のラッパーを提供します。

私自身はバックエンド(php、python、bash、Cなど)であり、JSはあまりクールではなく、恥ずかしいことではありません。 コードを改善するための提案を受け入れます:)



文字列よりも複雑なオブジェクトを渡すには、jsonを使用します。



そしてDOMContentLoadedによって:

 if (typeof JSAPI == 'object') { return; } (function(){ var JSAPI = function() { function bridge(func) { var args = Array.prototype.slice.call(arguments.callee.caller.arguments, 0); var res = prompt('JSAPI.'+func, JSON.stringify(args)); try { res = JSON.parse(res); res = (res && res.result) ? res.result : null; } catch (e) { res = null; } return res; } this.getUserAccounts = function() { //    return bridge('getUserAccounts'); } }; window['JSAPI'] = new JSAPI(); })(window);
      
      





JSAPIが宣言されていない場合(addJavascriptInterfaceが呼び出されなかった場合)-ラッパーを作成します。

ラッパーの本質-プロンプトへの呼び出し( 'JSAPI.getUserAccounts'、 '[]')を作成します。2番目のパラメーターは呼び出しパラメーターのjson配列です。

プロンプトの結果は{result:ANSWER}を返す必要があります。



次に、Android SDKのバージョンに関係なく、メソッドを呼び出します。

 var res = JSAPI.getUserAccounts(); //       json   : console.log(JSON.parse(res)[0]);
      
      







Java側:

 webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { //     prompt(message='JSAPI.method', defaultValue='params') final String prefix = JSAPI.class.getSimpleName() + "."; if (!message.startsWith(prefix)) { return super.onJsPrompt(view, url, message, defaultValue, result); } final String meth_name = message.substring(prefix.length()); String json_result = null; try { //    json  final JSONArray params = new JSONArray(defaultValue); final int len = params.length(); final Object []paramValues = new Object[len]; for (int i = 0; i < len; ++i) { paramValues[i] = params.opt(i); } //  ,      Method method = null; for (Method meth : JSAPI.class.getMethods()) { if (meth.getName().equals(meth_name)) { method = meth; break; } } if (null == method) { throw new NoSuchMethodException(); } //      "{\"result\":}" final JSONObject res = new JSONObject(); res.put("result", method.invoke(new JSAPI(), paramValues)); json_result = res.toString(); } catch (JSONException e) { // ... } catch (NoSuchMethodException e) { // ... } catch (InvocationTargetException e) { // ... } catch (IllegalAccessException e) { // ... } result.confirm(json_result); return true; } });
      
      





そして、例の整合性のためにメソッド自体を直接:

 class JSAPI { @JavascriptInterface public String getUserAccounts() { final JSONArray json = new JSONArray(); final String emailPattern = Patterns.EMAIL_ADDRESS.pattern(); final AccountManager am = AccountManager.get(getApplicationContext()); if (am != null) { for (Account ac : am.getAccounts()) { if (!ac.name.matches(emailPattern)) { continue; } final JSONArray item = new JSONArray(); item.put(ac.type); item.put(ac.name); json.put(item); } } return json.toString(); } }
      
      





メソッドがパラメーターを受け入れる場合、それらを指定する必要があります。 私の結果は、返された結果に関係なく、常に統一のために文字列に設定されます。

SDKの新しいバージョンにはアノテーション@JavascriptInterfaceが必要です。 古いとそれなしで働いた。



結論として、「Androidが壊れた」という条件として、Build.VERSION.RELEASE ==“ 2.3”をチェックするコードが頻繁にあったことを言いたいと思います。 Build.VERSION.RELEASE.startsWith( "2.3")を使用した方が良いチェックです。 さらに良いことに、私見、直接SDKバージョンを確認してください:(Build.VERSION.SDK_INT == 9)|| (Build.VERSION.SDK_INT == 10)。



JSアセンブリもチェックしたい人(V8では、繰り返しますが、バグは再生されません)私は歩くことをお勧めします、すべてがそこで簡単です。





まず、Javaによって開始される呼び出し:必要な場合、またはコールバックとして。

 public void callJS(final String jsCode) { runOnUiThread(new Runnable() { @Override public void run() { webView.loadUrl("javascript:" + jsCode); } }); } jsapi.callJS("alert('Hello, Habr!');");
      
      





callJSが非UIスレッドで呼び出される場合、runOnUiThreadが必要です(ほぼ確実に呼び出されます)。

しかし、:) 2.2では、私なしで機能しました。 古いバージョンでは、 修正はより論理的になりました。



これが私の最初のJavaアプリケーションの書き方です:)誰かが興味を持ったり、突然、さらには有用になればと思います。



PS Android Studioは最近0.2.11に成長しました。JetBrains製品が好きなら、すでに使用することができます。 不快な欠陥がないわけではありませんが、非常に機能的です。



PPS受け取ったコメントに基づいて、コード例にいくつかの変更が加えられました。



All Articles