Skip to content

feat: edit screen experiments (preview) - #195

Draft
michaldrabik wants to merge 2 commits into
mainfrom
feature/edit-screen
Draft

feat: edit screen experiments (preview)#195
michaldrabik wants to merge 2 commits into
mainfrom
feature/edit-screen

Conversation

@michaldrabik

Copy link
Copy Markdown
Collaborator

No description provided.

@michaldrabik michaldrabik self-assigned this Jun 13, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new 'Edit Screen' feature, enabling users to customize the layout of various screens by toggling the visibility of specific sections. This replaces the previous collapsing mechanism with a more comprehensive EditScreenManager architecture, allowing for better control over screen content across the application.

Highlights

  • Edit Screen Feature: Introduced a new 'Edit Screen' feature that allows users to toggle the visibility of different sections on various screens (Discover, Home, Lists, Profile, Movie Details, Episode Details, Show Details).
  • Architecture Change: Replaced the legacy CollapsingManager with a new EditScreenManager to handle section visibility settings, providing a more robust and flexible way to manage UI components.
  • UI/UX Improvements: Added a new bottom sheet for managing section visibility and updated existing screens to dynamically show or hide sections based on user preferences.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request replaces the legacy section collapsing functionality with a new Edit Screen feature, allowing users to toggle the visibility of various screen sections. The state is managed by a new EditScreenManager backed by a DataStore. Key feedback includes resolving a memory leak in EditScreenSheet caused by generating random ViewModel keys, simplifying the EditScreenViewModel and DefaultEditScreenManager by removing the synchronous isVisible cache in favor of a fully reactive flow, clearing visibility preferences on logout to prevent state leakage, using != false checks to prevent startup UI flickering, and wrapping the header more icon in an IconButton to meet accessibility touch target guidelines.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +29 to +44
val viewModelKey = remember(active, enabledValues) {
nextInt().toString()
}

if (active) {
TraktBottomSheet(
sheetState = state,
onDismiss = {
onDismiss()
},
) {
EditScreenView(
viewModel = koinViewModel(
key = viewModelKey,
parameters = { parametersOf(enabledValues) },
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Generating a random key via nextInt().toString() on every activation of the sheet causes a new EditScreenViewModel instance to be created and stored in the ViewModelStore every time the sheet is opened. These instances are never cleared and will accumulate in memory, leading to a memory leak. Since enabledValues is static for any given screen, you should retrieve the ViewModel without a key, allowing Koin to manage its lifecycle correctly scoped to the destination.

    if (active) {
        TraktBottomSheet(
            sheetState = state,
            onDismiss = {
                onDismiss()
            },
        ) {
            EditScreenView(
                viewModel = koinViewModel { parametersOf(enabledValues) },

Comment on lines +21 to +65
val initialState = EditScreenState(
values = enabledValues
.associateWith { editScreenManager.isVisible(setOf(it)) }
.toImmutableMap(),
)

private val valuesState = MutableStateFlow(initialState.values)

init {
editScreenManager.observe(enabledValues)
.onEach { values ->
valuesState.update {
values.toImmutableMap()
}
}.launchIn(viewModelScope)
}

fun toggle(key: EditScreenKey) {
val currentValues = valuesState.value ?: return
val isVisible = currentValues[key] ?: return

viewModelScope.launch {
if (isVisible) {
// Only hide if there's more than 1 visible, to prevent hiding all sections.
val visibleCount = currentValues.values.count { it }
if (visibleCount > 1) {
editScreenManager.hide(key)
}
} else {
editScreenManager.show(key)
}
}
}

val state = combine(
valuesState,
) { state ->
EditScreenState(
values = state[0],
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = EditScreenState(),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ViewModel can be significantly simplified by observing the EditScreenManager flow directly and mapping it to EditScreenState. This eliminates the need for initialState, valuesState, the init block, and the non-idiomatic combine call on a single flow, while also preventing potential UI flickering caused by the synchronous isVisible check during initialization. Note that you will need to add import kotlinx.coroutines.flow.map and can remove unused imports like MutableStateFlow, combine, launchIn, onEach, update.

    val state = editScreenManager.observe(enabledValues)
        .map { values -> EditScreenState(values.toImmutableMap()) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = EditScreenState(),
        )

    fun toggle(key: EditScreenKey) {
        val currentValues = state.value.values ?: return
        val isVisible = currentValues[key] ?: return

        viewModelScope.launch {
            if (isVisible) {
                val visibleCount = currentValues.values.count { it }
                if (visibleCount > 1) {
                    editScreenManager.hide(key)
                }
            } else {
                editScreenManager.show(key)
            }
        }
    }

import tv.trakt.trakt.helpers.editscreen.data.model.EditScreenKey

internal interface EditScreenManager {
fun isVisible(keys: Set<EditScreenKey>): Boolean

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since isVisible is no longer needed by the ViewModel, we can remove it from the interface to simplify the API and avoid synchronous race conditions.

Comment on lines +20 to +72
internal class DefaultEditScreenManager(
private val dataStore: DataStore<Preferences>,
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
) : EditScreenManager {
private val cache = ConcurrentHashMap<String, Boolean>()
private val mutex = Mutex()

init {
scope.launch {
val prefs = dataStore.data.first()
EditScreenKey.entries.forEach { key ->
cache[key.preferenceKey] = prefs[booleanPreferencesKey(key.preferenceKey)] ?: true
}
}
}

override fun isVisible(keys: Set<EditScreenKey>): Boolean {
return keys.all { key ->
cache[key.preferenceKey] ?: true
}
}

override fun observe(keys: Set<EditScreenKey>): Flow<Map<EditScreenKey, Boolean>> =
dataStore.data
.map { prefs ->
keys.associateWith { key -> prefs[booleanPreferencesKey(key.preferenceKey)] ?: true }
}
.distinctUntilChanged()

override suspend fun hide(key: EditScreenKey) {
mutex.withLock {
cache[key.preferenceKey] = false
dataStore.edit { prefs ->
prefs[booleanPreferencesKey(key.preferenceKey)] = false
}
}
}

override suspend fun show(key: EditScreenKey) {
mutex.withLock {
cache[key.preferenceKey] = true
dataStore.edit { prefs ->
prefs[booleanPreferencesKey(key.preferenceKey)] = true
}
}
}

override suspend fun clear() {
mutex.withLock {
cache.clear()
dataStore.edit { it.clear() }
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since isVisible is no longer needed, we can completely remove the synchronous cache, mutex, and background initialization coroutine. This simplifies DefaultEditScreenManager to a stateless, fully reactive wrapper around DataStore, leveraging its built-in transactional safety and thread-safe operations.

internal class DefaultEditScreenManager(
    private val dataStore: DataStore<Preferences>,
) : EditScreenManager {

    override fun observe(keys: Set<EditScreenKey>): Flow<Map<EditScreenKey, Boolean>> =
        dataStore.data
            .map { prefs ->
                keys.associateWith { key -> prefs[booleanPreferencesKey(key.preferenceKey)] ?: true }
            }
            .distinctUntilChanged()

    override suspend fun hide(key: EditScreenKey) {
        dataStore.edit { prefs ->
            prefs[booleanPreferencesKey(key.preferenceKey)] = false
        }
    }

    override suspend fun show(key: EditScreenKey) {
        dataStore.edit { prefs ->
            prefs[booleanPreferencesKey(key.preferenceKey)] = true
        }
    }

    override suspend fun clear() {
        dataStore.edit { it.clear() }
    }

Comment on lines 42 to 47
internal class LogoutUserUseCase(
private val appContext: Context,
private val sessionManager: SessionManager,
private val collapsingManager: CollapsingManager,
private val checkInManager: CheckInManager,
private val apiClients: Array<ApiClient>,
private val younifyApiClient: YounifyRemoteDataSource,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When a user logs out, their screen visibility preferences should be cleared to prevent state leakage if another user logs in on the same device. Inject EditScreenManager here to clear it on logout.

Suggested change
internal class LogoutUserUseCase(
private val appContext: Context,
private val sessionManager: SessionManager,
private val collapsingManager: CollapsingManager,
private val checkInManager: CheckInManager,
private val apiClients: Array<ApiClient>,
private val younifyApiClient: YounifyRemoteDataSource,
internal class LogoutUserUseCase(
private val appContext: Context,
private val sessionManager: SessionManager,
private val editScreenManager: EditScreenManager,
private val checkInManager: CheckInManager,
private val apiClients: Array<ApiClient>,
private val younifyApiClient: YounifyRemoteDataSource,

Comment on lines 77 to 80
suspend fun logoutUser() {
sessionManager.clear()
collapsingManager.clear()
checkInManager.stop(
source = CheckInUpdates.Source.Default,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Clear the screen visibility preferences on logout.

Suggested change
suspend fun logoutUser() {
sessionManager.clear()
collapsingManager.clear()
checkInManager.stop(
source = CheckInUpdates.Source.Default,
suspend fun logoutUser() {
sessionManager.clear()
editScreenManager.clear()
checkInManager.stop(

Comment on lines 336 to 341
LogoutUserUseCase(
appContext = androidApplication(),
sessionManager = get(),
collapsingManager = get(),
checkInManager = get(),
apiClients = get(named("apiClients")),
younifyApiClient = get(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Pass editScreenManager = get() to the LogoutUserUseCase constructor.

Suggested change
LogoutUserUseCase(
appContext = androidApplication(),
sessionManager = get(),
collapsingManager = get(),
checkInManager = get(),
apiClients = get(named("apiClients")),
younifyApiClient = get(),
LogoutUserUseCase(
appContext = androidApplication(),
sessionManager = get(),
editScreenManager = get(),
checkInManager = get(),
apiClients = get(named("apiClients")),
younifyApiClient = get(),

onMovieClick = onMovieClick,
onMoreClick = onMoreTrendingClick,
)
if (state.visibility?.get(DiscoverTrending) == true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using != false instead of == true ensures that during the initial load (when state.visibility is null before the DataStore Flow emits), the sections default to being visible. This prevents a jarring blank screen flicker on startup. Please apply this pattern to all visibility checks across all screens.

Suggested change
if (state.visibility?.get(DiscoverTrending) == true) {
if (state.visibility?.get(DiscoverTrending) != false) {

Comment on lines +221 to +230
Icon(
painter = painterResource(R.drawable.ic_more_vertical),
contentDescription = null,
tint = TraktTheme.colors.textPrimary,
modifier = Modifier
.size(18.dp)
.onClick {
showMenu = true
},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The touch target for the more icon is only 18.dp, which is extremely small and violates Material Design accessibility guidelines (minimum 48.dp). Wrapping the Icon in an IconButton will ensure a comfortable touch target for users.

        IconButton(
            onClick = { showMenu = true },
            modifier = Modifier.size(48.dp)
        ) {
            Icon(
                painter = painterResource(R.drawable.ic_more_vertical),
                contentDescription = null,
                tint = TraktTheme.colors.textPrimary,
                modifier = Modifier.size(18.dp),
            )
        }

@michaldrabik
michaldrabik marked this pull request as draft June 16, 2026 10:44
@michaldrabik michaldrabik removed their assignment Jul 27, 2026
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