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

RepositoryのSSoT化

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.
Avatar for mikan mikan
March 13, 2025

 RepositoryのSSoT化

Avatar for mikan

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