Skip to content

Refactor: simplify session bootstrap, repositories, app state and swipe rows - #79

Merged
dadachi merged 6 commits into
mainfrom
refactor/simplify
Oct 13, 2026
Merged

dadachi merged 6 commits into
mainfrom
refactor/simplify

Conversation

@dadachi

@dadachi dadachi commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Phase 5 (refactoring) for NativeAppTemplate-Free-Android. No intended behavior change apart from the sign-in bootstrap / email-change sign-out now surviving their screen closing (aligned with NativeAppTemplate-Android #112).

Changes (one commit each)

  1. Remove the no-op Data.updateRelationships – it returned a copy with the same relationships (or the same instance), so getDatumWithRelationships() was datum and Shop/ItemTag.getData() was datum. Callers read datum; nothing merges included, as before. DataRelationshipsTest: the cases that called the removed functions are gone (they tested the no-op itself); the behavior they guarded (included resources are not merged, getData() is the decoded data, getShopName() does not come from included) stays covered.
  2. Drop the unused reveal state of the swipe rows – Shop Detail and the item tag list wrote Data.isOptionsRevealed into a toMutableList() copy built during composition; nothing observed it and the next composition rebuilt it, so isRevealed was always false. SwipeableItemWithActions loses isRevealed/onExpanded/onCollapsed; Data loses isOptionsRevealed; the screens read the list from UI state. New SwipeableItemWithActionsTest (swipe left opens and stays open across a parent recomposition; swipe right closes) passed against the old API before the change and after it.
  3. Trim NativeAppTemplateAppState – unused isLoggedIn/shouldUpdateApp/shouldUpdatePrivacy/shouldUpdateTerms removed; shouldShowBottomBar = authDestination == SHOPS (same condition, same initial false); one stateIn helper. Existing NativeAppTemplateAppStateTest unchanged.
  4. Share the repositories' API request boilerplate – apiRequest/apiAction (network/ApiResponseExtensions.kt) and currentAccountId(), same helpers as paid #112. RepositoryRequestsTest (every endpoint) unchanged.
  5. Run the sign-in bootstrap and email-change sign-out in the app scope – SessionLifecycle (@singleton on @ApplicationScope: start, refreshPermissions, signOut; callers await). Storing the session routes away from sign-in and cancelled the screen's scope while the permissions/locale requests were in flight; the email-change sign-out could likewise be cut short. The "fetch permissions, sign out only on 401" logic of the sign-in VM and MainActivityViewModel lives there once. Adds runCatchingCancellable (as in paid).
  6. Share the item tag toggle and the account deletion error path – completeItemTag/idleItemTag go through toggle(); deleteShopkeeper uses catchShowingError (identical state update).

New tests and red checks

  • SignInEmailAndPasswordViewModelTest.login_whenTheScreenClosesMidBootstrap_stillStoresThePermissionsAndSyncsTheLocale – red on the pre-change code (shopLimitCount expected 99, was -1), green after.
  • ShopkeeperEditViewModelTest.updateShopkeeper_withANewEmail_whenTheScreenClosesDuringTheSignOutRequest_stillSignsOut – red on the pre-change code, green after (uses the new TestLoginRepository.onLogoutRequest hook).
  • SwipeableItemWithActionsTest – each case red when its drag-end branch was broken (open → animate to 0; close → animate to open).

Existing tests otherwise only changed how view models are constructed (new sessionLifecycle parameter).

Local checks

  • ./scripts/pre-push (Spotless, lint, R8 shrink, all unit tests): passed; 395 app unit tests, 0 failures.
  • ./gradlew :app:assembleRelease: passed.
  • No ProGuard rule or @Parcelize annotation changed.

Device check (Pixel, Free debug build, local API)

Shop list (counts), Shop1 detail: swipe reveals Complete, completing/idling works and rows close after reload; Manage Item Tags: swipe reveals Delete and swipe back closes it; item tag detail: Mark as completed / Mark as idled (toggle); Edit Item Tag: rename and rename back ("Item tag updated successfully."); Settings and Edit Profile load. Sign-out → sign-in as shopkeeper1 with this build: sign-out routed to onboarding (DELETE /shopkeeper_auth/sign_out 200), sign-in landed on the shop list with the tabs shown, and the server log shows POST /shopkeeper_auth/sign_in 200 followed by GET /{account}/api/v1/shopkeeper/permissions 200 and GET .../shops 200 (the bootstrap's permissions request now runs in the app scope). The app was left signed in as shopkeeper1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH

dadachi and others added 6 commits October 13, 2026 10:39
Data.updateRelationships returned a copy with the same relationships (or
the same instance), so getDatumWithRelationships() was the decoded datum
and Shop/ItemTag.getData() was datum. The callers now read datum directly
and the no-op is gone; nothing merges "included", as before.

DataRelationshipsTest pinned the no-op itself (equal copy, same instance,
getDatumWithRelationships == datum). Those cases tested the removed
functions, so they go; the behavior they guarded (included resources are
not merged, getData() is the decoded data, getShopName() does not come
from included) stays covered by the remaining cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
Shop Detail and the item tag list wrote Data.isOptionsRevealed into a
toMutableList() copy built during composition. Nothing observed that copy
and the next composition rebuilt it, so the writes were lost and
isRevealed was always the decoded default, false (the API never sends the
field). Whether a row is open has always lived in the row's own Animatable.

SwipeableItemWithActions loses isRevealed/onExpanded/onCollapsed (its
LaunchedEffect keeps animating to closed when the actions width changes,
as it did with isRevealed = false), the screens read the list from the UI
state directly, and Data loses isOptionsRevealed.

SwipeableItemWithActionsTest characterizes the row (swipe left opens it and
it stays open across a parent recomposition; swipe right closes it). It
passed against the old API (with isRevealed = false, as the screens passed)
before the change and passes after; each case went red when the matching
drag-end branch was broken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
- Drop isLoggedIn, shouldUpdateApp, shouldUpdatePrivacy and
  shouldUpdateTerms: nothing reads them (routing uses authDestination).
- shouldShowBottomBar repeated authDestination's combine of the same four
  flows; it is now authDestination == SHOPS, which is the same condition
  (signed in, no app/privacy/terms update pending) and the same initial
  false.
- The flag StateFlows share one stateIn helper.

NativeAppTemplateAppStateTest (unchanged) covers shouldShowBottomBar,
authDestination and the flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
Every pass-through repository method built the same flow by hand: read the
account id, call the API, emitApiResponse (or emitApiResponse<Status,
Boolean> { true }), flowOn(ioDispatcher). apiRequest/apiAction
(network/ApiResponseExtensions.kt) and
NativeAppTemplatePreferencesDataSource.currentAccountId() now hold that
once; each method only names its API call. The requests, the dispatcher and
the error mapping are unchanged (RepositoryRequestsTest covers every
endpoint; no test changed). Same helpers as NativeAppTemplate-Android #112.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
Sign-in stored the session, synced the locale and fetched the permissions
in the sign-in screen's viewModelScope. Storing the session routes to the
shop list, which clears that view model and cancels its scope while the
permissions request (and the locale request) are still in flight, so a
fresh session could run without permissions until the next resume.
Likewise the sign-out after an email change ran in Edit Profile's scope
and stopped if the screen closed before the session was cleared.

SessionLifecycle (@singleton, @ApplicationScope) now owns these steps:
start(), refreshPermissions() and signOut() run in the application scope
and the caller awaits them, so the screens behave as before but
cancelling them no longer cancels the work. The "fetch permissions, sign
out only on 401" logic that SignInEmailAndPasswordViewModel and
MainActivityViewModel each carried now lives there once. Ported from
NativeAppTemplate-Android #112 (with its runCatchingCancellable helper).

New tests (each seen red with the steps run in the caller's scope, i.e.
on the code before this commit):
- SignInEmailAndPasswordViewModelTest
  .login_whenTheScreenClosesMidBootstrap_stillStoresThePermissionsAndSyncsTheLocale
- ShopkeeperEditViewModelTest
  .updateShopkeeper_withANewEmail_whenTheScreenClosesDuringTheSignOutRequest_stillSignsOut
TestLoginRepository gains an onLogoutRequest hook for the second one.
Existing tests only changed how the view models are constructed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
- ItemTagDetailViewModel.completeItemTag and idleItemTag were the same
  function apart from the repository call; both now go through toggle().
- ShopkeeperEditViewModel.deleteShopkeeper wrote out by hand what
  catchShowingError does (message, loading stopped); use it.

No test changed (ItemTagDetailViewModelTest and ShopkeeperEditViewModelTest
cover both paths).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5hZEFqXhBsUQwSWsgR6eH
@dadachi
dadachi merged commit 9996a1b into main Oct 13, 2026
1 check passed
@dadachi
dadachi deleted the refactor/simplify branch October 13, 2026 01:59
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.

1 participant