main

square/leakcanary

Last updated at: 29/12/2023 09:39

BackStackViewModel.kt

TLDR

The BackStackViewModel.kt file in the org.leakcanary.screens package contains the implementation of a view model for managing a back stack of screen destinations. It also provides functionality for navigation and updating the app bar title.

Methods

goBack

This method is called to navigate back to the previous screen destination in the back stack.

goTo

This method is called to navigate to a new screen destination and add it to the back stack.

resetTo

This method is called to reset the back stack to a specific screen destination.

filterDestination

This inline function is used to filter the screen destinations based on the specified type.

Classes

BackStackHolder

This class holds an instance of the BackStackViewModel and provides injection of it into other activity-scoped view models.

BackStackModule

This module provides dependencies for the back stack navigation, such as the Navigator and AppBarTitle.

BackStackViewModel

This class is a view model that manages a back stack of screen destinations. It implements the Navigator and AppBarTitle interfaces to provide navigation and app bar title functionality.

CurrentScreenState

This data class represents the current state of the screen, including the current destination, whether it's possible to navigate back, and the direction of navigation.

END

package org.leakcanary.screens

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.scopes.ActivityRetainedScoped
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import org.leakcanary.screens.Destination.ClientAppsDestination

/**
 * Makes the BackStack state stream injectable in activity scoped view models. This
 * is a dumb hack as I couldn't figure out how to inject a view model into a view model.
 * This is doubly dumb as we need to ensure the backstack is created before
 * BackStackHolder.backStack is accessed.
 */
@ActivityRetainedScoped
class BackStackHolder @Inject constructor() {
  lateinit var backStackViewModel: BackStackViewModel
}

@InstallIn(ActivityRetainedComponent::class)
@Module
class BackStackModule {
  @Provides fun provideNavigator(holder: BackStackHolder): Navigator = holder.backStackViewModel
  @Provides fun provideAppBarTitle(holder: BackStackHolder): AppBarTitle = holder.backStackViewModel
}

interface Navigator {
  val currentScreenState: StateFlow<CurrentScreenState>

  fun goBack()

  fun goTo(destination: Destination)

  fun resetTo(destination: Destination)
}

inline fun <reified T : Destination> Navigator.filterDestination(): Flow<T> {
  return currentScreenState
    .map { it.destination }
    .filterIsInstance()
}

interface AppBarTitle {
  fun updateAppBarTitle(title: String)
}

// TODO This currently does not save UI state in the backstack.
@HiltViewModel
class BackStackViewModel @Inject constructor(
  private val savedStateHandle: SavedStateHandle,
  stateStream: BackStackHolder
) : ViewModel(), Navigator, AppBarTitle {

  private var destinationStack: List<Destination> =
    savedStateHandle[BACKSTACK_KEY] ?: arrayListOf(ClientAppsDestination)
    set(value) {
      field = value
      savedStateHandle[BACKSTACK_KEY] = ArrayList(value)
    }

  private val _currentScreenState = MutableStateFlow(destinationStack.asState(true))

  private val currentScreenTitle: String
    get() = _currentScreenState.value.destination.title

  private val _appBarTitle = MutableStateFlow(currentScreenTitle)

  override val currentScreenState = _currentScreenState.asStateFlow()

  val appBarTitle = _appBarTitle.asStateFlow()

  init {
    stateStream.backStackViewModel = this
  }

  override fun goBack() {
    check(_currentScreenState.value.canGoBack) {
      "Backstack cannot go further back."
    }
    navigate(destinationStack.dropLast(1), forward = false)
  }

  override fun goTo(destination: Destination) {
    navigate(destinationStack + destination, forward = true)
  }

  private fun navigate(newDestinationStack: List<Destination>, forward: Boolean) {
    destinationStack = newDestinationStack
    _currentScreenState.value = newDestinationStack.asState(forward)
    _appBarTitle.value = currentScreenTitle
  }

  companion object {
    private const val BACKSTACK_KEY = "backstack"
  }

  override fun resetTo(destination: Destination) {
    navigate(listOf(destination), forward = false)
  }

  override fun updateAppBarTitle(title: String) {
    _appBarTitle.value = title
  }
}

private fun List<Destination>.asState(forward: Boolean) =
  CurrentScreenState(destination = last(), canGoBack = size > 1, forward)

data class CurrentScreenState(
  val destination: Destination,
  val canGoBack: Boolean,
  val forward: Boolean
)