AtCoderのABC問題のAC一覧をAndroidアプリで表示させた話(即席アプリ)
2019年2月26日現在
動機
AtCoderをやっていて、ABCのC問題の壁にぶち当たり、他の問題を解こうとしたのですが、自分のやった問題が分からない!!ということになりました。@kenkoooo さんのGitHub - kenkoooo/AtCoderProblems: Problem manager for AtCoder usersを使えばできそうだったので、やってみることにしました。
開発環境
- 言語:kotlin
- アーキテクチャ:MVVM(多分合ってるはず)
アーキテクチャはjetpackのガイドラインを参考にしました。 developer.android.com
アーキテクチャはしっかり考えたいところなのですが、即席なのであまり考えていません。 テストやったことないので、いつかやりないなぁ
package分け
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の形にしてもいいかもしれませんね。
以上です。良い設計に改良出来たら、またブログを書こうと思います