仮に作成していた一覧画面(:feature:list01)を実装します。
※:feature:list02の実装は省略します。
一覧画面は、設定画面で設定したURLのサイトからデータを取得して表示します。
サイトからのデータ取得は、JavaScriptを使用した動的サイトに対応可能なWebViewと静的サイト用にOkHttpを使用します。
開発や保守のし易さを考慮して、下記の機能クラス/関数を作成することにします。
一覧画面でSiteUrlを安全に操作するため、必要な情報のみにアクセスするDAOを追記します。
:
interface SiteUrlDao {
:
@Query("UPDATE site_urls SET getting_at = :gettingAt WHERE id = :id")
suspend fun updateGettingAt(id: String, gettingAt: String): Int
:
}
設定画面で:core:dataモジュールを参照するように:feature:list01のbuild.gradle.ktsを設定します。
:
dependencies {
implementation(project(":core:data"))
}
追記後、『同期アイコン』で内容をプロジェクトに反映させます。
SiteUrlの一覧画面のリポジトリ層を作成します。
package jp.co.example.android01.core.data.repository
import jp.co.example.android01.core.data.SiteUrl
interface SiteUrlListRepository {
suspend fun getSiteUrlById(id: String): SiteUrl?
suspend fun updateGettingAt(id: String, gettingAt: String): Int
}
実装クラス SiteUrlListRepositoryImpl.kt を作成します。
package jp.co.example.android01.core.data.repository
import javax.inject.Inject
import javax.inject.Singleton
import jp.co.example.android01.core.data.SiteUrl
import jp.co.example.android01.core.data.SiteUrlDao
@Singleton
class SiteUrlListRepositoryImpl @Inject constructor(
private val siteUrlDao: SiteUrlDao
) : SiteUrlListRepository {
override suspend fun getSiteUrlById(id: String): SiteUrl? {
return siteUrlDao.getSiteUrlById(id)
}
override suspend fun updateGettingAt(id: String, gettingAt: String): Int {
return siteUrlDao.updateGettingAt(id, gettingAt)
}
}
HiltにSiteUrlListRepositoryの実装がSiteUrlListRepositoryImplであることを伝えるため、DI(Hilt)モジュールに追記します。
:
import jp.co.example.android01.core.data.repository.SiteUrlListRepository
import jp.co.example.android01.core.data.repository.SiteUrlListRepositoryImpl
:
object HiltModule {
:
@Provides
@Singleton
fun provideSiteUrlListRepository(
siteUrlDao: SiteUrlDao
): SiteUrlListRepository {
return SiteUrlListRepositoryImpl(siteUrlDao)
}
}
一覧画面に表示するデータを取得するため、SiteUrlsに登録されているサイトからHTMLを取得するクラスHtmlGetterを作成します。
複数の一覧画面から使うことが想定されるため、:core:dataモジュールに作成します。
package jp.co.example.android01.core.data
import javax.inject.Inject
import javax.inject.Singleton
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import jp.co.example.android01.core.data.repository.SiteUrlListRepository
import jp.co.example.android01.core.data.repository.OkHttpHtmlRepository
import jp.co.example.android01.core.data.repository.WebViewHtmlRepository
/**
* 3時間経過していない場合の例外
*/
class GettingHtmlException : Exception()
@Singleton
class HtmlGetter @Inject constructor(
private val siteUrlListRepository: SiteUrlListRepository,
private val okHttpHtmlRepository: OkHttpHtmlRepository,
private val webViewHtmlRepository: WebViewHtmlRepository,
) {
/**
* HTML取得機能
* @param siteId サイトID(例: "site01", "site02")
* @return Result<String> HTMLコンテンツまたはエラー
*/
suspend fun getHtml(siteId: String): Result<String> {
return try {
// 指定されたsiteIdのSiteUrlを取得
val siteUrl = siteUrlListRepository.getSiteUrlById(siteId)!!
// 3時間経過チェック
if (!isGettingHtml(siteUrl.gettingAt)) {
return Result.failure(GettingHtmlException())
}
// HTML取得
val htmlResult = if (siteUrl.isDynamic) {
webViewHtmlRepository.getHtmlContent(siteUrl.url)
} else {
okHttpHtmlRepository.getHtmlContent(siteUrl.url)
}
htmlResult.fold(
onSuccess = { htmlContent ->
// gettingAtを現在日時で更新
siteUrlListRepository.updateGettingAt(siteId, getCurrentDateTimeJST())
Result.success(htmlContent)
},
onFailure = { exception ->
Result.failure(Exception("HTML取得に失敗しました: ${exception.message}"))
}
)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* gettingAtから3時間経過しているかチェック
*/
private fun isGettingHtml(gettingAt: String): Boolean {
if (gettingAt.isEmpty()) return true
return try {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val lastGettingTime = LocalDateTime.parse(gettingAt, formatter)
val hoursPassed = ChronoUnit.HOURS.between(lastGettingTime, LocalDateTime.now())
hoursPassed >= 3
} catch (e: Exception) {
// パースエラーの場合は経過している
true
}
}
/**
* 現在の日時をJST形式で取得
*/
private fun getCurrentDateTimeJST(): String {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return LocalDateTime.now().format(formatter)
}
}
Hiltによる注入を行うため、DI(Hilt)モジュールにHtmlGetterを追記します。
:
import jp.co.example.android01.core.data.HtmlGetter
:
object HiltModule {
:
@Provides
@Singleton
fun provideHtmlGetter(
siteUrlListRepository: SiteUrlListRepository,
okHttpHtmlRepository: OkHttpHtmlRepository,
webViewHtmlRepository: WebViewHtmlRepository
): HtmlGetter {
return HtmlGetter(
siteUrlListRepository,
okHttpHtmlRepository,
webViewHtmlRepository
)
}
:
サイトから取得したHTMLから一覧に表示するデータをパースするクラスHtmlParserを作成します。
package jp.co.example.android01.feature.list01
import javax.inject.Inject
import javax.inject.Singleton
import jp.co.example.android01.core.data.List01Data
@Singleton
class HtmlParser @Inject constructor() {
/**
* HTMLパース機能
* @param htmlContent パースするHTMLコンテンツ
* @return Result<List<List01Data>> パースされたList01エンティティのリストまたはエラー
*/
suspend fun parseHtml(htmlContent: String): Result<List<List01Data>> {
return try {
// TODO: 実際のHTMLパースロジックを実装
// 仮の実装
val list01Data = listOf(
List01Data(
key = "key001",
occDate = "2025-10-01",
data1 = "2025-10-01 Data1",
data2 = "2025-10-01 Data2",
data3 = "2025-10-01 Data3"
),
List01Data(
key = "key002",
occDate = "2025-10-02",
data1 = "2025-10-02 Data1",
data2 = "2025-10-02 Data2",
data3 = "2025-10-02 Data3"
)
)
Result.success(list01Data)
} catch (e: Exception) {
Result.failure(Exception("HTMLパースに失敗しました: ${e.message}"))
}
}
}
Hiltによる注入を行うため、DI(Hilt)モジュールにHtmlParserを登録します。
HtmlParserは:feature:list01モジュール専用の機能ですので、:feature:list01モジュール内にDI(Hilt)モジュールを作成します。
package jp.co.example.android01.feature.list01.di
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.Provides
import javax.inject.Singleton
import jp.co.example.android01.feature.list01.HtmlParser
@Module
@InstallIn(SingletonComponent::class)
object HiltModule {
@Provides
@Singleton
fun provideHtmlParser(): HtmlParser {
return HtmlParser()
}
}
一覧画面に表示するデータの保存と取得を管理するDataManagerを作成します。
package jp.co.example.android01.feature.list01
import javax.inject.Inject
import javax.inject.Singleton
import jp.co.example.android01.core.data.List01Data
@Singleton
class List01DataManager @Inject constructor() {
/**
* データベース保存機能
* @param list01Data 保存するList01エンティティのリスト
* @return Result<Unit> 保存成功またはエラー
*/
suspend fun saveListData(list01Data: List<List01Data>): Result<Unit> {
return try {
// TODO: 実際のデータベース保存ロジックを実装
// 例: Roomデータベースに保存
// 仮の実装 - 実際のデータベース保存ロジックに置き換える
println("データベースに保存: $list01Data")
Result.success(Unit)
} catch (e: Exception) {
Result.failure(Exception("データの保存に失敗しました: ${e.message}"))
}
}
/**
* データベースからリストデータを取得します
* @return Result<List<List01>> リストデータまたはエラー
*/
suspend fun getListData(): Result<List<List01Data>> {
return try {
// 仮の実装 - 実際のデータベース取得ロジックに置き換える
val dataList = listOf(
List01Data(
key = "sample001",
col1 = "サンプルデータ1",
col2 = "カラム2のサンプル1",
col3 = "カラム3のサンプル1"
),
List01Data(
key = "sample002",
col1 = "サンプルデータ2",
col2 = "カラム2のサンプル2",
col3 = "カラム3のサンプル2"
)
)
Result.success(dataList)
} catch (e: Exception) {
Result.failure(Exception("データの取得に失敗しました: ${e.message}"))
}
}
}
一覧画面に表示するデータや状態を保持・管理するためViewModelを作成します。