Rubotoで AsyncTaskとProgressDialogを使ったところ、いろいろとハマったのでメモ。
■環境
Platform | JDK | ant | Ruby | ruboto | jruby-jars | Device | API level |
---|---|---|---|---|---|---|---|
Windows7 x64 | 1.7.0_25 | 1.9.1 | RubyInstaller 1.9.3-p448 | 0.13.0 | 1.7.4 | Nexus7 2012 | android-17 |
■生成
ruboto gen app --package com.example.async_task_progress_dialog --target android-17 --with-jruby
■調べたこと
AsyncTaskとProgressDialogを使う
ProgressDialogの表示やキャンセル、キャンセルボタンの実装など、ProgressDialog全般について以下のページが参考になりました。
この時点でのソースは以下のGistです。
ただ、ProgressDialog表示中に端末を回転させた場合、ProgressDialogが消えてしまう不具合があります。
ProgressDialog表示中に端末を回転させた際、ProgressDialogが消えないようにする
以下のページが参考になりました。
なお、dialog#dismissをするイベントはいくつかありましたが、今回はいずれの場合でも通過する「onPause」にて実装しました。
ちなみに、参考ページのうち、後者では「onKeyDown」や「onUserLeaveHint」で実装しています。
他に、回転時にTextViewが初期化されてしまう問題もありました。
回転時にActivityの再構築が行われるようでしたので、以下を参考に、Bundleを介してTextViewの値をやりとりするようにしました。
この時点でのソースは以下のGistです。
この時点でも、ProgressDialog表示中に端末を回転させたときにTextViewの値が更新されないという不具合が残っています。
ProgressDialog表示中に端末を回転させた際、TextViewの値が更新されるようにする
この問題は有名なようで、stackoverflowでも多くのコメントが寄せられていました。
今回は、以下を参考に、API14からサポートされた「ActivityLifecycleCallbacks」を利用してActivityを管理する方法を取りました。
なお、「ActivityLifecycleCallbacks」については、以下が参考になりました。
- 技術見聞録 - Application.ActivityLifecycleCallbacksでActivityを監視する
- mjbshaw: Determining if your Android Application is in Background or Foreground
- Android Activity in background - Stack Overflow
他には、AndroidManifest.xmlに以下のタグを追加し、再構築を防ぐ方法もあるようです。
android:configChanges="orientation|keyboardHidden"
- 画面回転時の再構築対応 - 戌印-INUJIRUSHI- (Androidあれこれ) -
- Android/View_not_attached_to_window_manager - tech.cm55.com
最終的なソースコードは以下のGistです。他に、AndroidManifest.xmlへの記載も追加する必要があります。
一応、末尾にもソースコード全体を載せておきます。
■JRuby関連
Javaのクラスにある引数なしのコンストラクタに加え、JRuby側で引数ありコンストラクタのオーバーロードを記述する
以下を参考に「super()」としました。なお、「super」とカッコなしで呼び出すとエラーとなります。
JRubyでのキャスト
一時期、JRuby側でApplicationを継承したAsyncHelperApplicationを実装しようと考えてたため、AsyncHelperApplicationへのキャストする場合はどうすればよいか悩みました。
その時に調べた結果のリンクを残しておきます。
- java - Casting objects in JRuby - Stack Overflow
- java - Casting objects in JRuby - Stack Overflow
- Casting Java Objects From JRuby - Stack Overflow
なお、最終的には、AndroidManifest.xmlへの記載が必要なことからJavaのコードである必要があると分かり、JRubyで実装することは諦めました。
さらに、JRuby側でうまいこと型変換をしてくれるので、キャストは基本不要だというところに落ち着きました。
■公式ドキュメント
■悩んだこと
doInBackgroundで例外をrescueしないと、onCancelledが発生しなかった
最初は手を抜いてrescueしていなかったため、onCancelledが発生しないのはなぜだろうと思いましたが、rescueしたら発生しました。手抜きはよくない。
■最終的なソースコード
# ver 0.1 # バックグラウンドへ回った時でもProgressDialogが動作するバージョン # ただし、端末を回転させると、うまく動作しない # ver 0.2 # 端末を回転させても動作する # ただし、ProgressDialogが表示されている時に回転させると、うまく動作しない # ver 0.3 # ProgressDialogが表示されている時に回転させても動作する # 以下を参考にActivityManager.javaとAsyncHelperApplication.javaを実装し、組み込む # (AndroidManifest.xml への追記も忘れずに) # http://blog.kotemaru.org/2013/02/android-asynctask-orientation.html require 'ruboto/widget' ruboto_import_widgets :Button, :LinearLayout, :TextView java_import 'android.os.AsyncTask' java_import 'java.lang.Thread' # Thread.sleepを使いたいため追加 # Progressバーまわり java_import 'android.app.ProgressDialog' java_import 'android.content.DialogInterface' java_import 'android.util.Log' class AsyncTaskProgressDialogActivity attr_accessor :status @@task = nil def onCreate(bundle) super self.content_view = linear_layout orientation: :vertical do @status = text_view text: 'before', text_size: 48 button text: 'start', on_click_listener: proc { run_async_task } end end def onPause super @@task.dismiss_dialog if showing_dialog? end def onResume super @@task.show_dialog if showing_dialog? end def onSaveInstanceState(bundle) super bundle.putString('TEXT_VIEW', @status.text) end def onRestoreInstanceState(bundle) super @status.text = bundle.getString('TEXT_VIEW') end def run_async_task @@task = MyAsyncTask.new(self) @@task.execute '' end def showing_dialog? !@@task.nil? && @@task.is_showing end end class MyAsyncTask < AsyncTask attr_reader :is_showing def initialize(activity) # カッコ重要。無しだと、エラーになる super() @activity_manager = activity.getApplication.getActivityManager @activity_id = @activity_manager.getActivityId(activity) end def onPreExecute show_dialog @is_showing = true end def doInBackground(params) # 例外を捕捉しないとonCancelledが呼ばれない等が発生することに注意 begin (0..9).each do |i| return false if isCancelled Thread.sleep(1000) publishProgress((i + 1) * 10) end rescue end true end def onProgressUpdate(values) @dialog.setProgress(values.first) unless @dialog.nil? end def onCancelled dismiss_dialog @activity_manager.getActivity(@activity_id).status.text = 'cancel' @is_showing = false end def onPostExecute(result) dismiss_dialog @activity_manager.getActivity(@activity_id).status.text = 'after' @is_showing = false end def show_dialog @dialog = ProgressDialog.new(@activity_manager.getActivity(@activity_id)) @dialog.setTitle('Please wait') @dialog.setMessage('Loading data...') @dialog.setProgressStyle(ProgressDialog::STYLE_HORIZONTAL) @dialog.setCancelable(true) @dialog.setIndeterminate(false) @dialog.setMax(100) @dialog.setProgress(0) # 戻るボタンを押したとき等の処理:これを記載しないと、キャンセルボタン以外でキャンセルした時に、正しくキャンセルが動作しない @dialog.setOnCancelListener(proc { cancel_progress_dialog }) # ProgressDialogのCancelボタンを追加して押したときの処理 @dialog.setButton(DialogInterface::BUTTON_NEGATIVE, "cancel", proc { cancel_progress_dialog }) @dialog.show end def dismiss_dialog @dialog.dismiss unless @dialog.nil? @dialog = nil end def cancel_progress_dialog @dialog.cancel cancel(true) end end