目次

42.一覧画面のリポジトリ層の作成

仮に作成していた一覧画面(:feature:list01)を実装します。
:feature:list02の実装は省略します。

一覧画面は、設定画面で設定したURLのサイトからデータを取得して表示します。
サイトからのデータ取得は、JavaScriptを使用した動的サイトに対応可能なWebViewと静的サイト用にOkHttpを使用します。

開発や保守のし易さを考慮して、下記の機能クラス/関数を作成することにします。

SiteUrlDAOへの追記

一覧画面でSiteUrlを安全に操作するため、必要な情報のみにアクセスするDAOを追記します。

   :
interface SiteUrlDao {
   :
  @Query("UPDATE site_urls SET getting_at = :gettingAt WHERE id = :id")
  suspend fun updateGettingAt(id: String, gettingAt: String): Int
   :
}

4行目~5行目
idのレコードのgetting_atを更新します。

データモジュールの参照

設定画面で:core:dataモジュールを参照するように:feature:list01build.gradle.ktsを設定します。

   :
dependencies {
  implementation(project(":core:data"))
}

3行目
: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
}

2行目
SiteUrlエンティティをimportします。
6行目~7行目
feature:listxxで使用するSiteUrlのDAOを指定しています。

実装クラス 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)
  }
}

13行目~14行目、17行目~18行目
SiteUrlListRepositoryの実装です。

DI(Hilt)モジュールへの追記

HiltSiteUrlListRepositoryの実装が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)
  }
}

2行目~3行目
必要なimportを追記しています。
11行目~12行目
SiteUrlListRepositoryの実装はSiteUrlListRepositoryImplであることをHiltに伝えています。

HTML取得クラスの作成

一覧画面に表示するデータを取得するため、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)
  }
}

17行目~18行目
Hiltによる注入を可能にしています。
28行目~55行目
SiteUrlsurlでサイトからHTMLを取得します。
最終取得日時から3時間を経過しないと、再取得しないようにしています。

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
    )
  }
   :

2行目
HtmlGetterをimportしています。
6行目~18行目
HtmlGetterを注入対象としてHiltに伝えています。
使用しているHilt対象リポジトリも併せて伝えています。

HTMLパースクラスの作成

サイトから取得した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}"))
    }
  }
}

7行目~8行目
Hiltによる注入を可能にしています。
16行目~34行目
仮実装で固定値のリストを結果として返却しています。

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()
  }
}

13行目~17行目
HtmlParserを注入対象としてHiltに伝えています。

DataManagerの作成

一覧画面に表示するデータの保存と取得を管理する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の作成

一覧画面に表示するデータや状態を保持・管理するためViewModelを作成します。