Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RepositoryのSSoT化

mikan
March 13, 2025

 RepositoryのSSoT化

mikan

March 13, 2025
Tweet

More Decks by mikan

Other Decks in Technology

Transcript

  1. 自己紹介 object Mikan { val name = " 一瀬喜弘" val

    company = "karabiner.tech" val work = Engineer.Android val hobby = listOf( " 漫画", " アニメ", " ゲーム", " 折り紙", "OSS 開発・コントリビュート", ) }
  2. ViewModel.init() class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel()

    { private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList())) val uiState: StateFlow<LatestNewsUiState> = _uiState init { viewModelScope.launch { newsRepository.favoriteLatestNews .collect { favoriteNews -> _uiState.value = LatestNewsUiState.Success(favoriteNews) } } } }
  3. onResume class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel(),

    DefaultLifecycleObserver { private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList())) val uiState: StateFlow<LatestNewsUiState> = _uiState override fun onResume(owner: LifecycleOwner) { viewModelScope.launch { newsRepository.favoriteLatestNews .collect { favoriteNews -> _uiState.value = LatestNewsUiState.Success(favoriteNews) } } } }
  4. 副作用実行後に更新 class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel(),

    DefaultLifecycleObserver { private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList())) val uiState: StateFlow<LatestNewsUiState> = _uiState fun onClickAddFavorite(newsId: String) { // お気に入り追加処理... fetchLatestnews() } private fun fetchLatestnews() { viewModelScope.launch { newsRepository.favoriteLatestNews .collect { favoriteNews -> _uiState.value = LatestNewsUiState.Success(favoriteNews) } } } }
  5. onResume class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel(),

    DefaultLifecycleObserver { private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList())) val uiState: StateFlow<LatestNewsUiState> = _uiState override fun onResume(owner: LifecycleOwner) { // 別の画面から復帰してきたときとかでも最新の状態を保てる fetchLatestnews() } private fun fetchLatestnews() { viewModelScope.launch { newsRepository.favoriteLatestNews .collect { favoriteNews -> _uiState.value = LatestNewsUiState.Success(favoriteNews) } } } }
  6. 自動更新型のデータソースを使えばこんな実装でよくなる class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel(),

    DefaultLifecycleObserver { val uiState = newsRepository.favoriteLatestNews // 状態が更新されたら新しい値をemit する .map { LatestNewsUiState.Success(it) } // 勝手に更新処理が実行される .stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), LatestNewsUiState.Success(emptyList()), ) } @Composable fun NewsScreen( // ... ) { val news by viewModel.uiState.collectAsStateWithLifecycle() // --------------------------- // collect し始めたタイミングで通信が始まる // ... }
  7. @Singleton class TrendRepository @Inject internal constructor( private val apolloClient: ApolloClient,

    private val refreshTrigger: RefreshTrigger, ) { private var cache: TrendQuery.Data? = null fun fetchTrendData(): Flow<TrendQuery.Data> = flow { cache?.let { emit(it) } ?: run { val data = apolloClient.query(TrendQuery()).execute().dataAssertNoErrors cache = data emit(data) } refreshTrigger.refreshEvent.collect { val data = apolloClient.query(TrendQuery()).execute().dataAssertNoErrors cache = data emit(data) } } suspend fun addStar(repoId: String) { val mutation = AddStarMutation(AddStarInput(starrableId = repoId)) apolloClient.mutation(mutation).execute() refreshTrigger.refresh() } d f St ( Id St i ) {
  8. REST っぽい書き方にして単純化させます ) { fun fetchTrendData(): Flow<TrendQuery.Data> = flow {

    val data = apolloClient.fetchTrendData() emit(data) refreshTrigger.refreshEvent.collect { val data = apolloClient.fetchTrendData() emit(data) } } suspend fun addStar(repoId: String) { apolloClient.addStar(repoId) refreshTrigger.refresh() } suspend fun removeStar(repoId: String) { apolloClient.removeStar(repoId) refreshTrigger.refresh() } suspend fun refresh() { refreshTrigger.refresh()
  9. class TrendRepository @Inject internal constructor( private val apiClient: ApiClient, private

    val refreshTrigger: RefreshTrigger, ) { fun fetchTrendData(): Flow<TrendQuery.Data> = flow { // 初回のデータ取得 val data = apolloClient.fetchTrendData() emit(data) refreshTrigger.refreshEvent.collect { // 次回以降のデータ更新 val data = apolloClient.fetchTrendData() emit(data) } }
  10. リフレッシュを誘発させるための機構としてRefreshTrigger というやつを作っています interface RefreshTrigger { val refreshEvent: Flow<Unit> suspend fun

    refresh() } internal class DefaultRefreshTrigger @Inject constructor() : RefreshTrigger { private val _refreshEvent = MutableSharedFlow<Unit>() override val refreshEvent: Flow<Unit> = _refreshEvent.asSharedFlow() override suspend fun refresh() { _refreshEvent.emit(Unit) } }
  11. 副作用による内部からのリフレッシュ suspend fun addStar(repoId: String) { apolloClient.addStar(repoId) // 副作用発生 refreshTrigger.refresh()

    // リフレッシュを要求 } fun fetchTrendData(): Flow<TrendQuery.Data> = flow { val data = apolloClient.fetchTrendData() emit(data) refreshTrigger.refreshEvent.collect { // リフレッシュ開始 val data = apolloClient.fetchTrendData() emit(data) } }
  12. 副作用による内部からのリフレッシュ fun fetchTrendData(): Flow<TrendQuery.Data> = flow { val data =

    apolloClient.fetchTrendData() emit(data) refreshTrigger.refreshEvent.collect { // リフレッシュ開始 val data = apolloClient.fetchTrendData() emit(data) } } suspend fun addStar(repoId: String) { apolloClient.addStar(repoId) // 副作用発生 refreshTrigger.refresh() // リフレッシュを要求 }
  13. 副作用による外部からのリフレッシュ onClickAdd = { scope.launch { trendViewModel.addStar(it) // 副作用 myAccountViewModel.refresh()

    // リフレッシュ } }, onClickRemove = { scope.launch { trendViewModel.removeStar(it) // 副作用 myAccountViewModel.refresh() // リフレッシュ } }, HomeScreen( myAccountUiState = myAccountUiState, trendUiState = trendUiState, navigateToRepositoryDetail = navigateToRepositoryDetail, modifier = modifier, )
  14. 課題はどう改善したのか 1. いつ開始するか collect し始めたときに通信が始まる @HiltViewModel class TrendViewModel @Inject constructor(

    private val trendRepository: TrendRepository ) : ViewModel() { val uiState = trendRepository.fetchTrendData() .map { TrendUiState(it, null) } .catch { emit(TrendUiState(null, Error(it))) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = TrendUiState.Initial ) // UI val trendUiState by trendViewModel.uiState.collectAsStateWithLifecycle()