News reader UI that fetches articles from BackendApp's ContentProvider via IPC. Built with Jetpack Compose (MVVM), Hilt for DI, and Coil for image loading.
BackendApp must be installed before running UIApp. The ContentProvider authority com.unity.backendapp.provider is resolved at the system level — if BackendApp isn't installed, ContentResolver.query() returns null and the user sees an error.
Both APKs must be signed with the same key so that UIApp can hold BackendApp's signature-protected read permission. Debug builds from the same machine satisfy this automatically.
Requires JDK 17+ and the Android SDK. Android Studio (Ladybug or newer) resolves both
automatically; from the command line, point JAVA_HOME at a JDK 17+ if your shell default is older.
cd UIApp
./gradlew assembleDebugAPK at app/build/outputs/apk/debug/app-debug.apk.
adb install app/build/outputs/apk/debug/app-debug.apk- Launch the app.
- Articles load automatically from BackendApp via ContentProvider IPC.
- Search field — filter by title (case-insensitive substring match).
- Rating slider — set minimum rating (1–5, discrete steps).
- Apply Filters button — sends the filter to BackendApp.
- If no articles match, a "No articles found" message is shown.
- If the backend is unreachable, an error message is shown (distinct from "no matches").
The bundled dataset has 129 articles. If the list shows 129 items on first launch with no filters, IPC is working.
- Force-stop BackendApp from Settings → Apps → BackendApp → Force Stop.
- In UIApp, tap "Apply Filters" with no filter changes.
- Articles should still load — Android re-creates the ContentProvider on demand (Room DB persists on disk).
adb shell dumpsys package com.unity.backendapp | grep providerYou should see com.unity.backendapp.provider listed as an exported provider.
If the screen stays on "Loading..." indefinitely, BackendApp is not installed or the provider is blocked. On Android 11+ devices, verify that UIApp's manifest includes:
<queries>
<package android:name="com.unity.backendapp" />
</queries>BackendApp ContentProvider (query → Cursor)
→ ContentResolverDataSource (ContentResolver.query, cursor → List<Article>)
→ ArticleViewModel (StateFlow<ArticleUiState>)
→ ArticleListScreen (Compose) → ArticleCard
ArticleFilterdata class: Both apps share the same filter shape. UIApp'sArticleUiState.toFilter()converts form fields to the immutableArticleFilterthat crosses the data-source boundary. Adding a new filter = add a field + one UI control — no interface or data-source signature changes.ArticlesDataSourceinterface: Abstracts the IPC mechanism — allows testing with an in-memory fake instead of mocking the (final)ContentResolver.query()method.ContentResolverDataSource: Maps aCursortoList<Article>. ThrowsIOExceptionon failure (null cursor, timeout, IPC error) so the VM can distinguish "no matches" from "backend unreachable". Column indices are cached once per query.CancellationSignalis wired to coroutine cancellation for proper timeout behavior.- MVVM:
ArticleViewModelholds aStateFlow<ArticleUiState>. Filter params are exposed as mutable state;applyFilters()launches a coroutine on an injected@IoDispatcherand updates state on completion. - Concurrency protection:
fetchJob?.cancel()prevents overlapping filter calls from overwriting each other out of order. - Hilt:
@HiltViewModel,@AndroidEntryPoint,@BindsforArticlesDataSource,@IoDispatcherqualifier for testable dispatchers. - Coil 2.7.x:
AsyncImagefor article images from network URLs, with a per-article placeholder color (fromplaceholderColorfield) shown behind the image until it loads. INTERNETpermission: Required by Coil for network image loading.READ_ARTICLESpermission: Required to query BackendApp's provider (signature-level, auto-granted for same-key APKs).
# Unit tests — ArticleViewModel (10)
./gradlew testDebugUnitTest
# Instrumentation tests — ArticleCard Compose (3), ArticleFilterUri (5), cursor mapping (3)
./gradlew connectedDebugAndroidTest| Suite | Location | Tests | What they cover |
|---|---|---|---|
ArticleViewModelTest |
src/test |
10 | Loading state, filter application, title/rating filter, empty result, error state, rating clamping, error vs empty distinction |
ArticleFilterUriTest |
src/androidTest |
5 | URI encoding: empty filter, title only, rating only, both, blank-title skipping |
ArticleCardTest |
src/androidTest |
3 | Title/description rendered, rating number, low-rating variant |
ContentResolverMappingTest |
src/androidTest |
3 | Cursor → Article mapping: all columns, empty cursor, missing-column failure |
<queries>manifest entry: Android 11+ hides BackendApp's ContentProvider from UIApp unless<queries><package android:name="com.unity.backendapp"/>is declared.- Signature permission: Both APKs must be signed with the same key (see Prerequisites).
INTERNETpermission: Coil silently crashes without it.- Filter key parity: URI param names (
titleQuery,ratingMin) inArticleFiltercompanion constants must match BackendApp'sArticleFilterconstants exactly — no compile-time cross-app checking. Centralized constants in both apps minimize drift.