AppWidgetProviderで複数のクリックイベントを扱う
はじめに
皆さんこんにちは,shiitaです.
今回は,ウィジェット実装で私が詰まった部分についての話をしたいと思います.
ウィジェットのクリックイベントの処理は特殊で,ブロードキャストを行うことで実装します.この記事では複数のクリックイベントを扱う際の注意点と,Oreoからの暗黙的なブロードキャストの制限についての対策を書いていきます.
※ ウィジェットのクリックイベントの処理方法をある程度知っていることが前提の記事です
PendingIntentの作成
ここで注意すべきことは次の3つです.
Intent
を明示的にするrequestCode
を一意にする- ブロードキャスト受け取り時の処理分岐を可能にする
以下のコードでIntent
とrequestCode
の作成をします.
private enum class ClickType { TYPE1, TYPE2, TYPE3 } private fun createIntentAndRequestCode(context: Context, appWidgetId: Int, type: ClickType): Pair<Intent, Int>{ val intent = Intent(context, WidgetCounter::class.java).apply { putExtra(APP_WIDGET_ID, appWidgetId) putExtra(CLICK_TYPE, type) } val requestCode = appWidgetId * ClickType.values().size + type.ordinal return Pair(intent, requestCode) } private const val CLICK_TYPE = "clickType" private const val APP_WIDGET_ID = "appWidgetId"
その後,PendingIntent.getBroadcast()
でブロードキャストするPendingIntent
を生成し,RemoteViews.setOnClickPendingIntent()
でクリックが発生した時の処理をセットします.
val (intent, code) = createIntentAndRequestCode(context, appWidgetId, ClickType.TYPE1) // viewsはウィジェット本体(RemoteViews) views.setOnClickPendingIntent(R.id.layout_id, // クリックイベントを設定するviewのID PendingIntent.getBroadcast(context, code, intent, PendingIntent.FLAG_UPDATE_CURRENT))
Intentを明示的にする
Oreoから暗黙的なブロードキャストに制限がかかるため,自身(AppWidgetProvider
)に対して明示的なブロードキャストする必要があります.Intent(context, WidgetCounter::class.java)
の様にクラスを直接指定します.
requestCodeを一意にする
ブロードキャストするPendingIntent
には一意なrequestCode
を割り当てる必要があります.ウィジェットの一意なIDであるappWidgetId
を利用することで,一意なrequestCode
を生成することが出来ます.
val requestCode = appWidgetId * ClickType.values().size + type.ordinal
後述するClickType
が異なればappWidgetId
が同じでもrequestCode
が区別出来るようにするため,ClickType
ごとにtype.ordinal
で値をずらしています.
ブロードキャスト受け取り時の処理分岐を可能にする
処理分岐のためにenum class
であるClickType
を定義します.Int
の定数を複数定義して,その値によって処理を分岐させても良いのですが,enum class
の方が便利なのでこちらを利用しました.Intent
にputExtra(CLICK_TYPE, type)
でClickType
を渡して,ブロードキャスト受け取り時に分岐が出来るようにします.
onReceiveでイベント処理
onReceive
では,クリックイベント以外のブロードキャストも受け取ってしまう点に注意します.ウィジェットの更新を行うACTION_APPWIDGET_UPDATE
などのIntent
も受け取り,スーパークラスで処理されるため,フィルタリングをする必要があります.
override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) // ACTION_APPWIDGET_UPDATEなどのIntentが処理される if (!intent.hasExtra(APP_WIDGET_ID) || !intent.hasExtra(CLICK_TYPE)) return val appWidgetId = intent.getIntExtra(APP_WIDGET_ID, 0) when (intent.getSerializableExtra(CLICK_TYPE) as ClickType) { ClickType.TYPE1 -> { // 処理1 } ClickType.TYPE2 -> { // 処理2 } ClickType.TYPE3 -> { // 処理3 } } }
おわりに
明示的なブロードキャストという不思議なことをしていますが,これでOreoでも動くようになります.同じrequestCode
を利用したために,処理分岐が出来ないというミスで数時間無駄にしてしまったので,同様の原因で詰まっている人の助けになればいいなと思います.
質問などありましたら,コメントして頂けると嬉しいです.
この記事の内容を活用してCheerAppsが作成した「ウィジェットカウンター」は
こちらからダウンロードできます!
ではでは.
AndroidとiOSどっちで開発した方がおトクか?
皆さん、こんにちは!
ご無沙汰してます!
大学院生のきゅうりです!
更新お待たせしました!!
本日は個人でアプリを開発する際に、
お金の面で見ると
どちらのOSで開発したほうがお得なのか??
ってことについてまとめていきます!
なお今回は、
多くの個人の開発者がとるであろう、
広告収入型の無料アプリのケースを想定しています!
アカウント登録料
アプリを公開するためにはアカウントに登録する必要があります
Androidでは「Google Play デベロッパーアカウント」、
iOSでは「iOS Developer Proglam」に登録する必要があります。
それぞれの費用は、、、
Google Play デベロッパーアカウント
登録料$25(約2750円)初回のみ
iOS Developer Proglam
参加費$99(約10900円)/年
となっています。
Androidの場合初めの登録料のみ支払えば良いですが、
iOSだと毎年参加費が必要です。
圧倒的にAndroidの方が低コストで開発できますね!
勝者:Android
シェアについて
OSの使用者が多い方が、当たり前ですが、
アプリのユーザも増え、広告収入も増えます。
ではシェアはどちらの方が高いのでしょうか??
2017年12月時点で
となっています。
参考:
https://www.kantarworldpanel.com/global
http://gs.statcounter.com/os-market-share/mobile/
日本国内ではほぼ半々ですが、
世界的にみるとAndroid方が圧倒的に高いシェアです。
日本限定のアプリなら:引き分け
世界でリリースするなら:Android
広告単価
1広告あたりの収入が高い方が
アプリの広告による収入に関する指標として
eCPMというものがあります。
eCPMとは、、、広告1000回表示あたりの収益
です!
使用する広告によっても異なりますが、
今回は最もメジャーな広告であるAdMobのケースで
eCPMを比較してみます!
2018年3月地点で
Android $2.01(約220円)
iOS $4.45(約490円)
となっています
貼られている広告の種類によって価格が異なるので、
(例えば、動画広告の方が画像の広告より単価が高いなど)
一概には言えませんがこの指標で判断するとiOSの方が
1ダウンロードあたりの広告収入は期待できそうですね。
勝者:iOS
まとめ
以上のように、個人で無料アプリを作るなら、、、どっちのOS??
という話について
金銭的な観点からAndroidとiOSどちらがおトクか比較してみました!!
総合的に判断すると
Androidの方が敷居は低いのかな!って印象です。
僕らも、まずはAndroidで開発をしています!
6行書くだけで誰でもできるウィジェット開発
皆さん,はじめまして.
CheerAppsで主にAndroidアプリ開発をしているshiitaです.
Androidアプリ開発で役立つTIPSなどについて、
伝えていけたらいいなと思います!
しばらくの間は、ウィジェットカウンターの開発で学んだことについて,
記事をいくつか書いていきます!
今回は簡単なウィジェットの開発について話したいと思います.
具体的には付箋のようにメモができるウィジェットを作ります!
使用するプログラミング言語は,私の一番好きなKotlinで書いていきたいと思います.
AndroidStudioで自動生成されるサンプルのプログラムをうまく活用して,
数行のコード編集だけで作れるものを紹介します.
ウィジェットの追加
右クリックをして出てくるメニューから,Widgetを選択し,追加します.
下の画像のように,クラス名やウィジェットのサイズなどを設定していくのですが,
ここで1つポイントがあります.
Configuration Screenにチェック
これを行うことで,ウィジェットの設定画面もAndroidStudioが自動生成してくれます!
できるだけ簡単にウィジェットを作って行きたいため,
楽できるところは楽して行きましょうwww
コードの編集
付箋のようにメモができるウィジェットを作るために,
コードの編集をしていきます.
編集する箇所はたったの6行です!!!
エラーの除去
AndroidStudioで自動生成されるコードはJavaで書かれたもので,
その後にKotlinへのコンバートが行われます.
このときに入り込んでしまうエラーを解消していきます.
class NoteConfigureActivity : Activity() { internal var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID internal lateinit var mAppWidgetText: EditText // onCreateで初期化するのでlateinitをつける internal var mOnClickListener: View.OnClickListener = View.OnClickListener { val context = this@NoteConfigureActivity // When the button is clicked, store the string locally val widgetText = mAppWidgetText.text.toString() saveTitlePref(context, mAppWidgetId, widgetText) // It is the responsibility of the configuration activity to update the app widget val appWidgetManager = AppWidgetManager.getInstance(context) Note.updateAppWidget(context, appWidgetManager, mAppWidgetId) // Make sure we pass back the original appWidgetId val resultValue = Intent() resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId) setResult(Activity.RESULT_OK, resultValue) finish() } public override fun onCreate(icicle: Bundle?) { super.onCreate(icicle) // Set the result to CANCELED. This will cause the widget host to cancel // out of the widget placement if the user presses the back button. setResult(Activity.RESULT_CANCELED) setContentView(R.layout.note_configure) mAppWidgetText = findViewById<View>(R.id.appwidget_text) as EditText findViewById<View>(R.id.add_button).setOnClickListener(mOnClickListener) // Find the widget id from the intent. val intent = intent val extras = intent.extras if (extras != null) { mAppWidgetId = extras.getInt( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) } // If this activity was started with an intent without an app widget ID, finish with an error. if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { finish() return } mAppWidgetText.setText(loadTitlePref(this@NoteConfigureActivity, mAppWidgetId)) } companion object { private val PREFS_NAME = "jp.cheerapps.note.Note" private val PREF_PREFIX_KEY = "appwidget_" // Write the prefix to the SharedPreferences object for this widget internal fun saveTitlePref(context: Context, appWidgetId: Int, text: String) { val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit() prefs.putString(PREF_PREFIX_KEY + appWidgetId, text) prefs.apply() } // Read the prefix from the SharedPreferences object for this widget. // If there is no preference saved, get the default from a resource internal fun loadTitlePref(context: Context, appWidgetId: Int): String { val prefs = context.getSharedPreferences(PREFS_NAME, 0) val titleValue = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null) return titleValue ?: context.getString(R.string.appwidget_text) } internal fun deleteTitlePref(context: Context, appWidgetId: Int) { val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit() prefs.remove(PREF_PREFIX_KEY + appWidgetId) prefs.apply() } } }
コードを全て載せましたが,この部分で変更した箇所は
internal var mAppWidgetText: EditText
を
internal lateinit var mAppWidgetText: EditText
のようにlateinitを追加しただけです!
mAppWidgetTextの初期化はonCreate()で行うため,
後から初期化をすることを,明示的に示す必要があるのです.
設定画面の呼び出し
現在,設定画面はウィジェット追加時だけしか表示されません.
そこで,ウィジェットに記述したテキストを後から編集できるようにします.
具体的な処理は,ウィジェットをタップすると設定画面を呼び出すようにしたいと思います.
class Note : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } } override fun onDeleted(context: Context, appWidgetIds: IntArray) { // When the user deletes the widget, delete the preference associated with it. for (appWidgetId in appWidgetIds) { NoteConfigureActivity.deleteTitlePref(context, appWidgetId) } } override fun onEnabled(context: Context) { // Enter relevant functionality for when the first widget is created } override fun onDisabled(context: Context) { // Enter relevant functionality for when the last widget is disabled } companion object { internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) { val widgetText = NoteConfigureActivity.loadTitlePref(context, appWidgetId) // Construct the RemoteViews object val views = RemoteViews(context.packageName, R.layout.note) views.setTextViewText(R.id.appwidget_text, widgetText) // クリックで設定画面を開く val intent = Intent(context, NoteConfigureActivity::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } val pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT) views.setOnClickPendingIntent(R.id.appwidget_text, pendingIntent) // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views) } } }
追加部分は以下の5行です.
val intent = Intent(context, NoteConfigureActivity::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } val pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT) views.setOnClickPendingIntent(R.id.appwidget_text, pendingIntent)
設定画面であるNoteConfigureActivityに対するPendingIntentを作成し,
ウィジェットのViewであるRemoteViewsに対してsetOnClickPendingIntent()
でクリック時の処理を追加します.
RemoteViewsはクリックイベントの処理は,通常のViewと異なることに注意が必要です.
実行結果
ウィジェット追加 | ウィジェット選択 |
---|---|
ウィジェット設定 | ウィジェット例 |
実行結果はこの表のようになっています.
ウィジェットの設定画面で書いたテキストが,
ウィジェットにちゃんと追加されていることが確認できます.
default activity not found エラー
default activity not foundのエラーが出て,以下の画像の様に実行できない場合があります.
これはConfigurationのLaunchをNothingにすることで解消することができます.
おわりに
ここまで読んでいただきありがとうございました!
いかがでしたか?記事の感想,コードへの指摘など頂けると嬉しいです.
CheerAppが開発した最初のアプリ,「ウィジェットカウンター」は
こちらからダウンロードできます!
ではでは.
gitについてのとても基礎的な話②〜アプリ開発への道 その3〜
皆さん、こんにちは!
大学院生のきゅうりです!
本日も読んでいただいてありがとうございます!!
今日は前回に引き続いて、
git
に関するごくごく簡単な話です!!
今日の目標は
「Aさんはlayoutのブランチを切ってレイアウトに関する作業を、
Bさんはmasterブランチでシステムに関する作業しよう」
「マージするときにコンフリクトしてないか確認してください」
これら二つが何を言っているのか、理解することです!
この2回の記事を読めば、
gitの基礎の基礎については
マスターしたと言っても過言ではないです!!(多分)
では、それぞれの用語について解説していきます!
ブランチ:
元のリポジトリに対して全く同じ複製を作成し、分岐を作ることです。
簡単にいうと、並行作業をする場所の作成です。
複数の作業を並列的に行いたい時に重宝します。
Aというブランチでは、レイアウトに関する作業を進める、
Bというブランチでは、システムに関する作業を進める
といった感じです。
gitでは初めから存在しているメインのブランチをmasterブランチと呼びます。
ウィジェットカウンターの場合
図で説明します。
この場合はlayoutブランチを作成し、
レイアウトやフォントに関する変更をlayoutブランチで、
カウンター機能に関する作業をmasterブランチで、
並列的に行っています。
最後のマージについては次で説明します、
新たなブランチを作ることをブランチを切るとも言います。
マージ:
複数のブランチのプログラムを統合することです。
複数のブランチそれぞれでの変更した部分を共に反映したリポジトリが作成されます。
具体的には、上の図のマージの部分ですね!
コンフリクト:
マージをする際に、
二つのブランチでそれぞれ同じ部分を変更してしまっていて
どっちをとったらいいのかわからない!!
という状況のことです。
つまり、衝突です(直訳ですが汗)。
ウィジェットカウンターを例にとって
図で説明すると次のような感じです!!
この場合は、
masterブランチでもlayoutブランチでも
カウンター機能の同じ部分を変更してしまったために、
コンフリクトが起きてしまったといった状況です。
コンフリクトしてしまった場合は、
自動でマージすることができず、
手動でマージすることになります。
以上です!
2回に分けて、gitを最近学んだ初心者が初心者に向けて
記事を書いてみました。
僕なりに分かりやすく説明したつもりですが、
いかがだったでしょうか?
コメント等々お待ちしています!
また、今回gitやGitHubについて学ぶきっかけなり、
例として挙げたウィジェットカウンターについては
google play で先日リリースしていますので、
是非ダウンロードお願いします!!
本日も読んでいただいて、
ありがとうございました!
それでは!
gitについてのとても基礎的な話① 〜アプリ開発への道 その2〜
皆さんこんにちは!
大学院生のきゅうりです!!
自分でも完全に理解しきれていなかった部分を
復習しながら記事を書いているので、
大変じかんがかかってしまいます、、、
申し訳ありません(-_-;)
さて、前回、バージョン管理システムがどのようなものなのか
簡単に説明しました!
本日は、
“git”
について簡単に説明していきたいと思います!
gitとは、、、
現在最もメジャーなバージョン管理システムのひとつです。
多くのプログラマーや企業が
GitHubといったWedサービスを用いて
gitを使用しています。
そのためネット上で検索するだけでもたくさんの解説サイトが存在しています。
詳しい説明については
「git 初心者」
などで検索すればヒットする他サイトに譲りたいと思います!
僕もshiitaくんとcheerappsを結成し、
開発を行うにあったてネット上のサイトを参照したのですが、
リポジトリ??コミット??
ダイエットでもするの??
ナニコレ、ムズカシイ…
知らないカタカナ語ばっかで、
全然頭に入ってこないって感じでした( ;∀;)
というわけで今回から、
gitについてのごくごく簡単な部分について
二回に分けて説明していこうと思います!
今回の目標は、
「リモートリポジトリからローカルリポジトリにクローンしよう!」
「プルして×××を変更して、コミットしてプッシュしよう!」
これらが何を言っているのか理解する。ことです!
理解するために、それぞれの用語について説明していきます!
ざっくり言えば開発するプログラムが入っているフォルダのことです。
ウィジェットカウンターを例にすると、
各プログラムやフォントや画像などすべてのファイルをまとめたものです!
ローカルリポジトリ:
個人で作業するためのリポジトリです。
各個人のPC上に作られるものです。
つまり、個人の作業場所です。
リモートリポジトリ:
ネット上に存在しているリポジトリです。
GitHubなどのWebサービスのよってWeb上に作られます。
開発者みんなの共有の作業場所ってイメージです。
クローン:
リモートリポジトリ(共有の作業場所)を全てコピーして、
ローカルリポジトリ(個人の作業場所)に持ってくる、
つまり、ダウンロードです。
リモートリポジトリ上で作業をしてしまうと、
他の人の作業に影響が出てしまうので、
自分のPC上(ローカルリポジトリ)にクローンして作業します。
プル:
リモートリポジトリ(共有の作業場所)の中から、
ローカルリポジトリ(個人の作業場所)にないファイルや
更新されたファイルだけをローカルリポジトリ(個人の作業場所)に持ってくること、
つまり、差分だけダウンロードです。
コミット:
プログラムを変更した際に、変更部分とそれに付随する情報を保存することです。
つまり、バージョンアップです
プッシュ:
コミットしたものをリモートリポジトリ(共有の作業場所)に送信する、
つまり、アップロードです。
プッシュすることでリモートリポジトリ(共有の作業場所)が更新され、
その情報を開発者全員が共有することができます。
以上、ごくごく簡単なgitの説明でした。
今回目標にした二つの文章も理解できるようになっていると思います!
次回は引き続きgitについて、ブランチなどの説明をしていきます!
それでは!!