Skip to content

Conversation

tuancoltech
Copy link
Member

@tuancoltech tuancoltech commented May 21, 2025

Decouple storybooks retrieval logic from UI, which eases:

  • Unit testing
  • Further development and maintenance of the codebase.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced a ViewModel-driven architecture for loading and displaying storybooks asynchronously.
    • Added loading indicators to enhance user feedback during data fetch.
    • Improved UI responsiveness by observing storybook data changes in real-time.
    • Implemented dependency injection for streamlined data source and repository management.
  • Refactor

    • Migrated storybook data handling to use state flows and coroutines, enhancing code maintainability and scalability.

@tuancoltech tuancoltech requested a review from a team as a code owner May 21, 2025 07:30
@tuancoltech tuancoltech self-assigned this May 21, 2025
Copy link

coderabbitai bot commented May 21, 2025

Walkthrough

This change introduces a ViewModel-driven architecture for loading and displaying storybooks in the app. New repository and data source abstractions are implemented, with Dagger Hilt modules set up for dependency injection. The UI now observes state from the ViewModel, reflecting asynchronous data loading and success states.

Changes

File(s) Change Summary
.../data/repository/LocalStoryBooksDataSource.kt, .../data/repository/StoryBookRepository.kt Added interfaces LocalStoryBooksDataSource and StoryBookRepository, each declaring a suspend function to asynchronously fetch a list of StoryBookGson objects.
.../data/repository/LocalStoryBooksDataSourceImpl.kt, .../data/repository/StoryBooksRepositoryImpl.kt Implemented the above interfaces: LocalStoryBooksDataSourceImpl fetches storybooks from a content provider using the application context; StoryBooksRepositoryImpl delegates fetching to the local data source. Both use coroutine-based suspend functions.
.../di/DataModule.kt Introduced a Dagger Hilt module (DataModule) with bindings for repository and data source interfaces to their implementations, scoped to the ViewModel lifecycle.
.../ui/StoryBooksActivity.kt Refactored to use a ViewModel (StoryBookViewModel) for state management. Added lifecycle-aware data observation, explicit loading UI, and reactive display of storybooks. Annotated with @AndroidEntryPoint for dependency injection.
.../viewmodel/StoryBookViewModel.kt, .../viewmodel/StoryBookViewModelImpl.kt Introduced a sealed UI state interface (LoadStoryBooksUiState) and a ViewModel interface (StoryBookViewModel). Implemented StoryBookViewModelImpl using Hilt for injection, exposing a StateFlow for UI state and a method to trigger loading of storybooks asynchronously.

Sequence Diagram(s)

sequenceDiagram
    participant UI as StoryBooksActivity
    participant VM as StoryBookViewModelImpl
    participant Repo as StoryBookRepository
    participant DS as LocalStoryBooksDataSource

    UI->>VM: getAllStoryBooks()
    VM->>VM: Emit Loading state
    VM->>Repo: getStoryBooks()
    Repo->>DS: getStoryBooks()
    DS-->>Repo: List<StoryBookGson>
    Repo-->>VM: List<StoryBookGson>
    VM->>VM: Emit Success state with storybooks
    VM-->>UI: uiState emits Success
    UI->>UI: showStoryBooks(storyBooks)
Loading

Suggested labels

enhancement

Suggested reviewers

  • jo-elimu

elimu.ai's mission is to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79a0671 and 77523c7.

📒 Files selected for processing (8)
  • app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSource.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSourceImpl.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/data/repository/StoryBookRepository.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/data/repository/StoryBooksRepositoryImpl.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/di/DataModule.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/ui/StoryBooksActivity.kt (4 hunks)
  • app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModel.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModelImpl.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • app/src/main/java/ai/elimu/vitabu/data/repository/StoryBookRepository.kt
  • app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSource.kt
  • app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModelImpl.kt
  • app/src/main/java/ai/elimu/vitabu/data/repository/StoryBooksRepositoryImpl.kt
  • app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSourceImpl.kt
  • app/src/main/java/ai/elimu/vitabu/di/DataModule.kt
  • app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModel.kt
  • app/src/main/java/ai/elimu/vitabu/ui/StoryBooksActivity.kt
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: build (windows-latest, 17)
  • GitHub Check: build (windows-latest, 21)
  • GitHub Check: build (macos-latest, 21)
  • GitHub Check: build (ubuntu-latest, 17)
  • GitHub Check: build (ubuntu-latest, 21)
  • GitHub Check: build (macos-latest, 17)
  • GitHub Check: lint
  • GitHub Check: test (28)
  • GitHub Check: test (29)
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
app/src/main/java/ai/elimu/vitabu/ui/StoryBooksActivity.kt (1)

99-217: ⚠️ Potential issue

Unscoped CoroutineScope(Dispatchers.IO) can leak work past Activity destruction

Launching a new root scope here means:

• Work is not cancelled when the Activity stops/finishes
• Potential memory leaks and wasted battery if the user navigates away quickly

Prefer lifecycleScope.launch(Dispatchers.IO) { … }, which is automatically cancelled with the Activity, or move the heavy work to the ViewModel.

- CoroutineScope(Dispatchers.IO).launch {
+ lifecycleScope.launch(Dispatchers.IO) {

Inside the loop you can replace the nested CoroutineScope(Dispatchers.IO).launch with withContext(Dispatchers.IO) to avoid spawning an unbounded number of coroutines.

These tweaks keep the app efficient while it works toward elimu.ai’s mission of empowering learners on resource-constrained devices.

🧹 Nitpick comments (5)
app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModel.kt (1)

26-29: Clarify contract naming & intent

Because StoryBookViewModel is an interface, newcomers may assume it’s the concrete AndroidX ViewModel subclass. Consider renaming to StoryBookViewModelContract (or prefixing with I) or keep the name but add KDoc that stresses it is a contract. This avoids confusion and keeps room for a real @HiltViewModel implementation class.

app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSourceImpl.kt (1)

9-16: Tie heavy I/O to the IO dispatcher & scope

ContentProviderUtil.getAllStoryBookGsons() performs disk / IPC I/O. Although the surrounding API is suspend, nothing here forces the call onto Dispatchers.IO. Make the intention explicit (or inject a dispatcher) so accidental withContext(Dispatchers.Main) calls from upper layers don’t block the UI thread.

override suspend fun getStoryBooks(): List<StoryBookGson> =
    withContext(Dispatchers.IO) {
        ContentProviderUtil.getAllStoryBookGsons(
            context,
            BuildConfig.CONTENT_PROVIDER_APPLICATION_ID
        )
    }

Also consider annotating the class with @Singleton and moving the binding to SingletonComponent if its state is stateless and safe to share.

app/src/main/java/ai/elimu/vitabu/di/DataModule.kt (1)

13-24: Scope mismatch may cause duplicate instances

The module is installed in ViewModelComponent, so each ViewModel instance gets its own repository and data-source. Unless these classes hold per-ViewModel state, that is unnecessary overhead.
Moving the bindings to SingletonComponent (or ActivityRetainedComponent) will:

• Re-use the same repository across ViewModels
• Avoid needless allocations
• Make it easier to cache data

@InstallIn(SingletonComponent::class)
abstract class DataModule {
    ...
}

If per-ViewModel scope is intentional, please add a comment explaining the rationale.

app/src/main/java/ai/elimu/vitabu/ui/StoryBooksActivity.kt (2)

17-20: Depend on the contract, not the implementation

Importing StoryBookViewModelImpl couples the Activity to one concrete class, defeating the benefit of the interface you just created.
Use Hilt’s by viewModels<StoryBookViewModelImpl>() or request the ViewModel through the interface if you keep manual factories.

private val storyBookViewModel: StoryBookViewModel by viewModels()

This keeps the Activity agnostic of the implementation and eases testing/replacement.


74-77: Simplify ViewModel retrieval with Hilt helpers

Manual ViewModelProvider boilerplate can be replaced by:

private val storyBookViewModel: StoryBookViewModelImpl by viewModels()

when the implementation is annotated with @HiltViewModel. This reduces verbosity and prevents accidental retention leaks.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5e22a0 and 79a0671.

📒 Files selected for processing (8)
  • app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSource.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSourceImpl.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/data/repository/StoryBookRepository.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/data/repository/StoryBooksRepositoryImpl.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/di/DataModule.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/ui/StoryBooksActivity.kt (4 hunks)
  • app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModel.kt (1 hunks)
  • app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModelImpl.kt (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: test (28)
  • GitHub Check: test (29)
  • GitHub Check: build (macos-latest, 17)
  • GitHub Check: build (windows-latest, 17)
  • GitHub Check: build (macos-latest, 21)
  • GitHub Check: build (windows-latest, 21)
  • GitHub Check: build (ubuntu-latest, 17)
  • GitHub Check: build (ubuntu-latest, 21)
  • GitHub Check: lint
🔇 Additional comments (5)
app/src/main/java/ai/elimu/vitabu/data/repository/StoryBookRepository.kt (1)

5-7: Well-designed repository interface.

This clean interface provides a clear contract for asynchronous retrieval of storybooks, supporting elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.

The suspend function indicates proper coroutine support for background operations, which is essential for smooth UI experiences when loading educational content.

app/src/main/java/ai/elimu/vitabu/data/repository/LocalStoryBooksDataSource.kt (1)

5-7: Good separation of data source concerns.

This interface effectively isolates the local data retrieval logic, supporting elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.

The clean separation between repository and data source layers will make the codebase more maintainable and testable, allowing easier extensions to support different book types or sources in the future.

app/src/main/java/ai/elimu/vitabu/data/repository/StoryBooksRepositoryImpl.kt (1)

6-12: Clean repository implementation with proper DI.

This implementation follows good architectural patterns with dependency injection, supporting elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.

The code properly delegates to the injected data source, following the Dependency Inversion Principle and making the component easily testable through mocking.

app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModelImpl.kt (1)

14-22: Well-structured ViewModel with reactive state handling.

The ViewModel implementation properly manages UI state through StateFlow, supporting elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.

The injected IO scope ensures background processing for data operations, keeping the UI responsive for young learners.

app/src/main/java/ai/elimu/vitabu/ui/StoryBooksActivity.kt (1)

82-96: Handle the new Error state to keep the UI responsive

Once an Error state is added to LoadStoryBooksUiState, extend this when block to react (e.g., hide the progress bar and show a retry Snackbar). Otherwise the user sees a spinner forever on network/content-provider failures.

Comment on lines +23 to +29
override fun getAllStoryBooks() {
ioScope.launch {
_uiState.emit(LoadStoryBooksUiState.Loading)
val storyBooks = storyBookRepository.getStoryBooks()
_uiState.emit(LoadStoryBooksUiState.Success(storyBooks))
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to improve user experience.

While the implementation is solid, it doesn't handle potential errors during storybook retrieval, which could lead to poor experiences for children using the app to learn reading📖.

Consider adding error handling to ensure the UI properly reflects failure states:

override fun getAllStoryBooks() {
    ioScope.launch {
        _uiState.emit(LoadStoryBooksUiState.Loading)
-       val storyBooks = storyBookRepository.getStoryBooks()
-       _uiState.emit(LoadStoryBooksUiState.Success(storyBooks))
+       try {
+           val storyBooks = storyBookRepository.getStoryBooks()
+           _uiState.emit(LoadStoryBooksUiState.Success(storyBooks))
+       } catch (e: Exception) {
+           _uiState.emit(LoadStoryBooksUiState.Error(e.message ?: "Unknown error"))
+       }
    }
}

Make sure to also add an Error state to your LoadStoryBooksUiState sealed class if it doesn't already exist.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun getAllStoryBooks() {
ioScope.launch {
_uiState.emit(LoadStoryBooksUiState.Loading)
val storyBooks = storyBookRepository.getStoryBooks()
_uiState.emit(LoadStoryBooksUiState.Success(storyBooks))
}
}
override fun getAllStoryBooks() {
ioScope.launch {
_uiState.emit(LoadStoryBooksUiState.Loading)
try {
val storyBooks = storyBookRepository.getStoryBooks()
_uiState.emit(LoadStoryBooksUiState.Success(storyBooks))
} catch (e: Exception) {
_uiState.emit(LoadStoryBooksUiState.Error(e.message ?: "Unknown error"))
}
}
}
🤖 Prompt for AI Agents
In app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModelImpl.kt around
lines 23 to 29, the getAllStoryBooks function lacks error handling for potential
failures during storybook retrieval. To fix this, wrap the repository call and
UI state emissions inside a try-catch block within the coroutine. Emit a new
Error state from LoadStoryBooksUiState in the catch block to reflect failure in
the UI. Also, ensure the LoadStoryBooksUiState sealed class includes an Error
state to represent error conditions.

Comment on lines +9 to +24
sealed interface LoadStoryBooksUiState {
/**
* The storybooks are still loading.
*/
data object Loading : LoadStoryBooksUiState

/**
* The storybooks are loaded with the given list of storybooks resources.
*/
data class Success(
/**
* The list of storybooks resources
*/
val storyBooks: List<StoryBookGson>,
) : LoadStoryBooksUiState
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add a failure branch to the UI-state sealed interface

At the moment the UI can represent only “Loading” and “Success”. Any I/O or parsing exception thrown by the repository will currently crash the flow or leave the UI stuck in the loading state.
Adding an Error (or Failure) state gives the ViewModel a chance to surface problems gracefully and lets the Activity decide whether to show a retry button or a friendly message – an important usability detail when your users are the out-of-school children that elimu.ai’s mission is championing. 📖✍🏽🔢

sealed interface LoadStoryBooksUiState {
    data object Loading : LoadStoryBooksUiState

    data class Success(val storyBooks: List<StoryBookGson>) : LoadStoryBooksUiState

+   /**
+    * Loading failed with the given throwable.
+    */
+   data class Error(val throwable: Throwable) : LoadStoryBooksUiState
}
🤖 Prompt for AI Agents
In app/src/main/java/ai/elimu/vitabu/viewmodel/StoryBookViewModel.kt between
lines 9 and 24, the sealed interface LoadStoryBooksUiState currently only has
Loading and Success states, which does not handle errors. Add a new data class
Error that takes a Throwable parameter to represent failure states. This will
allow the ViewModel to emit an Error state when loading fails, enabling the UI
to show appropriate error messages or retry options.

@tuancoltech tuancoltech requested a review from jo-elimu May 21, 2025 09:33
Copy link
Member

@jo-elimu jo-elimu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tuancoltech I see that the changes in this pull request adds 160 lines of code, and another level of abstraction. How will that make it easier for new engineers to understand the code? From my perspective, this change would have the opposite effect.

My recommendation would be to not use Dagger due to its abstraction and complexity. Instead, if we keep things simple, it will be easier for other engineers to understand and modify the code in the future.

@tuancoltech
Copy link
Member Author

tuancoltech commented May 24, 2025

@tuancoltech I see that the changes in this pull request adds 160 lines of code, and another level of abstraction. How will that make it easier for new engineers to understand the code? From my perspective, this change would have the opposite effect.

My recommendation would be to not use Dagger due to its abstraction and complexity. Instead, if we keep things simple, it will be easier for other engineers to understand and modify the code in the future.

@jo-elimu There're a few problems with current codebase:

1. Non-UI logic execution in main thread.

Some works (e.g, interaction with database) are being executed in main thread which will cause sluggish app UI, or even freeze UI on some low end devices. Those should be executed in worker thread instead.

Below is an example of such logic.

storyBooks = ContentProviderUtil.getAllStoryBookGsons(
            applicationContext,
            BuildConfig.CONTENT_PROVIDER_APPLICATION_ID
        )

2. Tightly coupled business logic and UI logic

UI (StoryBooksActivity) knows about underlying data interaction logic (getAllStoryBookGsons via ContentProvider), and depends directly on it, which as you know, goes against code abstraction idea (Dependency inversion principle).
When we change the data source implementation of the storybooks, we have to change the UI code as well, which is bad.

Speaking about understandability.

I don't think the newly introduced changes are hard to be understood even for new comers, for 2 reasons:

  1. It's using MVVM, an industrial practice in mobile development. Everyone will (and should) understand it.
  2. The new changes only replace the direct call to ContentProvider with an interface call, and adds some data observers logic. Nothing more complicated than that.

@tuancoltech tuancoltech force-pushed the decouple_ui_from_data_layer branch from 79a0671 to 77523c7 Compare May 24, 2025 03:44
@jo-elimu
Copy link
Member

Some works (e.g, interaction with database) are being executed in main thread which will cause sluggish app UI, or even freeze UI on some low end devices.

@tuancoltech I have a low-end device here, so did a test to see if the loading speed is different:

main branch:

API_34_Screen_recording_20250526_170346.mp4

decouple_ui_from_data_layer branch:

API_34_Screen_recording_20250526.mp4

The loading speeds looks similar (5-6 seconds), maybe slightly faster with the changes in this PR (0.5 seconds or so).

And note that the loading indicator disappeared with the changes in this PR.

@jo-elimu
Copy link
Member

Speaking about understandability.

I don't think the newly introduced changes are hard to be understood even for new comers

@tuancoltech Different engineers have different perspectives, which is normal. In this case you think the changes are an improvement, while my opinion is different.

Either way; As an organizational principle it should be the person doing the actual work that is making the final decision. So since you have been working the most on this app recently, you should also be the person deciding whether or not to merge these changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants