AtCoderのABC問題のAC一覧をAndroidアプリで表示させた話(即席アプリ)

f:id:ehu_J:20190226121450j:plain 2019年2月26日現在

動機

AtCoderをやっていて、ABCのC問題の壁にぶち当たり、他の問題を解こうとしたのですが、自分のやった問題が分からない!!ということになりました。@kenkoooo さんのGitHub - kenkoooo/AtCoderProblems: Problem manager for AtCoder usersを使えばできそうだったので、やってみることにしました。

開発環境

アーキテクチャjetpackガイドラインを参考にしました。 developer.android.com

アーキテクチャはしっかり考えたいところなのですが、即席なのであまり考えていません。 テストやったことないので、いつかやりないなぁ

package分け f:id:ehu_J:20190227130444p:plain

API & DB

  • HTTP client library:retorofit
  • Json parser:moshi
  • BDMS:Room

Submission APIを叩いています。帰ってきたレスポンスをmoshiでパースして、Repositotyに返しています。レスポンスのListはLiveDataで包んでいます。Fragment内でLivedata型を受け取り、変更があった時に、Viewの更新をしています。

APIのModel

class StaticsModel(
    @Json(name = "executionTime")
    val execution_time: String,
    val point: String,
    val result: String,
    @Json(name = "problemId")
    val problem_id: String,
    @Json(name = "userId")
    val user_id: String,
    @Json(name = "epochSecond")
    val epoch_second: String,
    @Json(name = "contestId")
    val contest_id: String,
    val id: String,
    val language: String,
    val length: String
)

Service

interface StaticsServise {
    @GET("atcoder-api/results")
    fun getProblems(@Query("user") user: String): Call<List<StaticsModel>>
}

DBのModel

@Entity
data class RoomStatics(
    val execution_time: String?,
    val point: String,
    val result: String,
    val problem_id: String,
    val user_id: String,
    val epoch_second: String,
    val contest_id: String,
    @PrimaryKey val id: String,
    val language: String,
    val length: String
)

DBのDao

@Dao
interface RoomStaticsDao{
    // get
    @Query("SELECT * FROM roomStatics ORDER BY epoch_second ASC")
    fun getAll(): List<RoomStatics>

    // insert
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(roomTask: RoomStatics)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(vararg roomTask: RoomStatics)

    // update
    @Update
    fun updateTasks(vararg roomTask:RoomStatics)

    // delete
    @Delete
    fun delete(roomTask: RoomStatics)
}

Database

@Database(entities = arrayOf(RoomStatics::class), version = 1)
@TypeConverters(DateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {

    // DAOを取得する。
    abstract fun roomTaskDao(): RoomStaticsDao
}

Repository

APIでインターネットから情報を受け取るメソッドと、DBに保存するメソッドとDBから情報を取得するメソッドを実装しています。

class StaticsRepository {
    //Retrofitインターフェース
    private var staticsServise: StaticsServise

    init {
        //okhttpのclient作成
        val interceptor = HttpLoggingInterceptor()
        interceptor.level = HttpLoggingInterceptor.Level.BODY
        val client = OkHttpClient.Builder().addInterceptor(interceptor).build()

        //クライアント生成

        var retrofit = Retrofit.Builder()
            .baseUrl("https://kenkoooo.com/atcoder/")
            .addConverterFactory(MoshiConverterFactory.create())
            .client(client)
            .build()
        staticsServise = retrofit.create(StaticsServise::class.java)
    }

    //APIにリクエストし、レスポンスをLiveDataで返す(StaticsModel)
    fun getReqListStatic(user: String): LiveData<List<StaticsModel>> {
        val data = MutableLiveData<List<StaticsModel>>()
        runBlocking {
            thread {
                data.postValue(staticsServise.getProblems(user).execute().body())
            }
        }
        return data
    }
    fun saveToDB(context: Context, list: List<StaticsModel>){
        thread {
            //save to room
            val db = Room.databaseBuilder(context, AppDatabase::class.java, "database-name").build()
            list.map {
                db.roomTaskDao().insert(
                    RoomStatics(
                        execution_time = it.execution_time,
                        point = it.point,
                        result = it.result,
                        problem_id = it.problem_id,
                        user_id = it.user_id,
                        epoch_second = it.epoch_second,
                        contest_id = it.contest_id,
                        id = it.id,
                        language = it.language,
                        length = it.length
                    )
                )
            }

        }
    }
    fun getDBData(context: Context): LiveData<List<StaticsModel>>{
        val data= MutableLiveData<List<StaticsModel>>()
        runBlocking {
            thread {
                val db = Room.databaseBuilder(context, AppDatabase::class.java, "database-name").build()
                val result = db.roomTaskDao().getAll()
                val model = result.map {
                    StaticsModel(
                        execution_time = it.execution_time ?: "0",
                        point = it.point,
                        result = it.result,
                        problem_id = it.problem_id,
                        user_id = it.user_id,
                        epoch_second = it.epoch_second,
                        contest_id = it.contest_id,
                        id = it.id,
                        language = it.language,
                        length = it.length
                    )
                }
                data.postValue(model)
            }
        }
        return data
    }
}

ViewModel

Androidの画面で必要な情報を保持しておくためのクラスとして設計しました。LiveDataの集約の集まりといってもいいでしょうか。

class StaticsAllProblemsViewModel : ViewModel() {
    var apiData: LiveData<List<StaticsModel>>? = null
    var dbData: LiveData<List<StaticsModel>>? = null

    fun getListDataByAPI(user: String): LiveData<List<StaticsModel>> {
        return apiData ?: StaticsRepository().getReqListStatic(user)
    }

    fun save(context: Context, list: List<StaticsModel>) {
        StaticsRepository().saveToDB(context, list)
    }

    fun getDBData(context: Context): LiveData<List<StaticsModel>> {
        return dbData ?: StaticsRepository().getDBData(context)
    }
}

Fragment

class StaticsAllProblemsFragment : Fragment() {
    lateinit var bindong: FragmentStaticsAllProblemsBinding
    lateinit var viewModel: StaticsAllProblemsViewModel

   ...

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        bindong = DataBindingUtil.inflate<FragmentStaticsAllProblemsBinding>(
            inflater,
            R.layout.fragment_statics_all_problems,
            container,
            false
        )
        //ViewModel初期化
        viewModel = ViewModelProviders.of(activity!!)
            .get(StaticsAllProblemsViewModel::class.java)

        //wifiにつながっているなら、APIで取得
        val wifiManager: WifiManager = context?.getSystemService(Context.WIFI_SERVICE) as WifiManager
        if (wifiManager.connectionInfo.supplicantState == SupplicantState.COMPLETED) {
            //有効なら
            setListDataByApiAndDB()
        } else {
            //無効なら
            setListDataByDB()
        }
        return bindong.root
    }

    // APIでデータをリクエスト&ローカルデータベースに保存&表示:wifi接続時推奨
    fun setListDataByApiAndDB() {

        viewModel.getListDataByAPI("ehu_151").observe(this, Observer {
            //save
            viewModel.save(context!!, it)
            setListDataByDB()
        })
    }

    // ローカルデータベースからデータを参照:wifi未接続時推奨
    fun setListDataByDB() {
        viewModel.getDBData(context!!).observe(this, Observer {
            val group = it.filter { Regex("abc").containsMatchIn(it.contest_id) }.groupBy { it.contest_id }
            val res = group.map {
                var contestName = it.key
                var a_result = ""
                var b_result = ""
                var c_result = ""
                var d_result = ""
                for (model in it.value) {
                    when (model.problem_id) {
                        contestName + "_a" -> a_result = model.result
                        contestName + "_b" -> b_result = model.result
                        contestName + "_c" -> c_result = model.result
                        contestName + "_d" -> d_result = model.result
                    }
                }
                StaticsAllProblemEntity(contestName, a_result, b_result, c_result, d_result)
            }
            // ここでbindingやadapterに代入
            bindong.staticsListView.adapter =
                StaticsAllProblemsAdapter(context!!, res.sortedByDescending { it.problemName })
        })
    }
}

直したいところと反省点

DBとAPIのModelがほとんど一緒

個人的な意見ですが、ほとんど同じならば、インターフェースを作成したくなりますね。しかし、自分には上手い設計方法が分からなかったので、雑な作りになっています。

Fragmentがロジックを持ってしまっている

DBから受け取ったデータをViewに表示する用のEntityに変換するために、Fragmentで変換を行う形になってしまいました。MVPアーキテクチャをやったことがある自分としては、あまりよく感じていません。本当ならば、FragmentはViewにデータを渡すだけにしたいですね。その為に、UseCaseに変換ロジック負わせて、MVVM+CleanArchitectureの形にしてもいいかもしれませんね。

以上です。良い設計に改良出来たら、またブログを書こうと思います