buto > /dev/null

だいたい急に挑戦してゴールにたどり着かずに飽きる日々です

Kotlin入門 おみくじアプリを作ってみた

簡単なアプリを作りました

アプリを起動時 ボタンを押すとおみくじ結果画面に遷移します

f:id:butorisa:20201020175453p:plain

おみくじ結果画面 3秒ほど「Fortune Telling...」と表示され、結果が表示されます

f:id:butorisa:20201020175519p:plain f:id:butorisa:20201020175528p:plain

githubで公開しています Kotlin学習にどうぞ! https://github.com/butorisa/fortune-app

※画像には表示されていませんが、利用しているAPIのリンク・占い提供元のリンクを3枚目画面の下部に表示するようにしました

アプリの概要

  • 開発端末OS:Android 8.1.0(Oreo)
  • 利用APIWeb ad Fortune 無料版API
  • HTTP通信ライブラリ:OkHttp3

  • MainActivity

    • ActionBar(正確にはToolBar)のみ
    • 画面部分はFragmentで描画
  • EntryFragment
    • 起動時の画面(1枚目)
    • MainActivityの上から描画
    • FortuneResultFragmentに遷移するボタンを保持
  • FortuneResultFragment
    • おみくじ結果描画画面(2、3枚目)
    • 画面描画時に占いAPIを実行して、結果の12星座運勢からランダムな結果を表示
    • API実行はCoroutineで非同期実行
    • おみくじ結果が表示されるまで[GO BACK]ボタンは動作しない
    • ボタン押下で前画面へ戻る(EntryFragment指定はしていない)
  • FortuneResultBean
  • RestApiClient
    • 占いAPI呼び出しクラス(バックエンド)
    • 引数にURLを取り、APIを実行、レスポンスjson(String型)を返す
    • BeanクラスへのマッピングはRestApiClient呼び出し元で行う

MainActivity

MainActivityはActionBarしかない空っぽの画面なのでEntryFragmentを表示するだけ アプリテーマを「NoActionBar」にしているのでActionBarの設定コードを書いている

    /**
     * Activity描画
     */
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // actionbar
        val toolbar = findViewById<Toolbar>(R.id.toolbar_fortune)
        toolbar.title = "pretty chipper"
        setSupportActionBar(toolbar)

        // EntryFragment描画
        val transaction = supportFragmentManager.beginTransaction()
        transaction.addToBackStack(null)
        transaction.replace(R.id.container, EntryFragment())
        transaction.commit()
    }

EntryFragment

inflater.inflate() でFragmentのxmlファイルをバインド ボタンに押下時の処理を設定

    /**
     * EntryFragment描画
     */
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        val view = inflater.inflate(R.layout.fragment_entry, container, false)

        // ボタン押下でFortuneResultFragmentに移動
        view?.findViewById<Button>(R.id.button_telling_fortune)?.setOnClickListener {
            val transaction = fragmentManager?.beginTransaction()
            transaction?.addToBackStack(null)
            transaction?.replace(R.id.container, FortuneResultFragment())
            transaction?.commit()
        }
        return view
    }

FortuneResultFragment

onCreateView()で画面描画時にCoroutineを使って非同期で占いAPIを実行 APIのURLには「yyyy/MM/dd」形式で現在日付をつけると本日の占い結果が返却される APIレスポンスが返ってきたら、TextViewに占い結果を設定 レスポンスjsonを全てgetJSONObject()で取得するのは面倒なので、12星座の運勢のレスポンス部分だけBeanクラスを作りマッピング

    /**
     * FortuneResultFragment描画
     */
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        val view = inflater.inflate(R.layout.fragment_fortune_result, container, false)

        /* Coroutine start */
        this.lifecycleScope.launch {
            // call API
            withContext(Dispatchers.Default) {
                val url = "http://api.jugemkey.jp/api/horoscope/free/" + getCurrentDate()
                RestApiClient().requestGet(url)
            }.let {
                delay(2000)
                // mapping json to bean
                val fortuneResult = parseJson(getCurrentDate(), it)
                // set TextView
                view?.findViewById<TextView>(R.id.fortune_content)?.text =
                    fortuneResult.get((0..11).random())?.content // TODO ランダムな星座を表示

                // ボタン押下で前画面へ移動
                view.findViewById<Button>(R.id.button_back_to_entry).setOnClickListener {
                    fragmentManager?.popBackStack()
                }
            }
        }
        /* Coroutine end */
        return view
    }

    /**
     * 現在日付取得
     * @return 現在日付(yyyy/MM/dd)
     */
    @RequiresApi(Build.VERSION_CODES.O)
    fun getCurrentDate(): String {
        val today = LocalDate.now()
        val dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
        return dateFormatter.format(today)
    }

    /**
     * json→FortuneResultBean変換
     * @param today:現在日付(yyyy/MM/dd)
     * @param strJson:レスポンスjson
     * @return 運勢一覧(FortuneResultBeanリスト)
     */
    private fun parseJson(today: String, strJson: String): List<FortuneResultBean> {
        val json = JSONObject(strJson)
        // remove "\" from json-property
        val strResult = json.getJSONObject("horoscope").getString(today.replace("\\", ""))
        return ObjectMapper().readValue(strResult, object: TypeReference<List<FortuneResultBean>>(){})
    }

FortuneResultBean

占いAPIの運勢部分のjsonマッピングするBeanクラス このBeanクラスを12星座分のリストにして受け取る

/**
 * 占いAPIレスポンスBean
 */
class FortuneResultBean {
    @JsonProperty("content")
    var content: String? = null

    @JsonProperty("money")
    var money: String? = null

    @JsonProperty("job")
    var job: String? = null

    @JsonProperty("love")
    var love: String? = null

    @JsonProperty("total")
    var total: String? = null

    @JsonProperty("item")
    var item: String? = null

    @JsonProperty("color")
    var color: String? = null

    @JsonProperty("day")
    var day: String? = null

    @JsonProperty("rank")
    var rank: String? = null

    @JsonProperty("sign")
    var sign: String? = null
}

RestApiClient

OkHttp3を利用してGETリクエスト レスポンスボディをString型のまま呼び出し元へ返す

    /**
     * GETリクエスト
     * @param APIリクエストURL
     * @param リクエストパラメータ
     * @return APIレスポンス(String)
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    fun requestGet(url: String, vararg param: String?): String {
        val request = Request.Builder().url(url!!).build()
        val httpClient = OkHttpClient()
        try {
            val response = httpClient.newCall(request).execute()
            return response.body!!.string()

        } catch (e: IOException) {
            // TODO handle exception
            e.printStackTrace()
        }
        // TODO Handle Exception
        throw NullPointerException()
    }

    companion object {
        private val JSON: MediaType = "application/json; charset=utf-8".toMediaTypeOrNull() as MediaType
    }

きれいに書くよう意識しました

今までとりあえず動くことだけ意識していたのですが、後で見返すと分かりづらい。。。 FragmentにあるボタンのクリックリスナーをActivityに書いていたり、API呼び出し関数にUI操作を書いていたり、不要な変数を宣言していたり… このあたりをスッキリするようにコーディングしてみました Androidのコード設計、デザインパターンも意識しながらコーディングできるようになりたい!