大学院生のアプリ開発奮闘記

将来への不安を抱く大学院生二人がアプリ開発に奮闘する困難と過程を綴っていくブログです.現在androidアプリ開発中

6行書くだけで誰でもできるウィジェット開発

皆さん,はじめまして.
CheerAppsで主にAndroidアプリ開発をしているshiitaです.
Androidアプリ開発で役立つTIPSなどについて、
伝えていけたらいいなと思います!

しばらくの間は、ウィジェットカウンターの開発で学んだことについて,
記事をいくつか書いていきます!


今回は簡単なウィジェットの開発について話したいと思います.
具体的には付箋のようにメモができるウィジェットを作ります!
使用するプログラミング言語は,私の一番好きなKotlinで書いていきたいと思います.

AndroidStudioで自動生成されるサンプルのプログラムをうまく活用して,
数行のコード編集だけで作れるものを紹介します.




プロジェクトの作成

今回作るウィジェットは次の2つの機能を持ちます.

  1. ホーム画面に貼り付けられるウィジェットの本体
  2. ウィジェットの内容を書き換える設定画面

アプリを起動すると最初に表示される画面は不要なので,
下の画像のようにAdd No Activityを選択します.


f:id:cheerapps:20180417003635p:plain




ウィジェットの追加

右クリックをして出てくるメニューから,Widgetを選択し,追加します.


f:id:cheerapps:20180417003643p:plain


下の画像のように,クラス名やウィジェットのサイズなどを設定していくのですが,
ここで1つポイントがあります.


f:id:cheerapps:20180417003651p:plain


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と異なることに注意が必要です.




実行結果

ウィジェット追加 ウィジェット選択
f:id:cheerapps:20180417003719p:plain f:id:cheerapps:20180417003733p:plain
ウィジェット設定 ウィジェット
f:id:cheerapps:20180417003744p:plain f:id:cheerapps:20180417003505p:plain

実行結果はこの表のようになっています.
ウィジェットの設定画面で書いたテキストが,
ウィジェットにちゃんと追加されていることが確認できます.




default activity not found エラー

default activity not foundのエラーが出て,以下の画像の様に実行できない場合があります.


f:id:cheerapps:20180417003658p:plain


これはConfigurationのLaunchをNothingにすることで解消することができます.

f:id:cheerapps:20180417003706p:plain




おわりに

ここまで読んでいただきありがとうございました!
いかがでしたか?記事の感想,コードへの指摘など頂けると嬉しいです.
CheerAppが開発した最初のアプリ,「ウィジェットカウンター」は
こちらからダウンロードできます!

play.google.com


ではでは.