All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.uber.rib.core.Router.kt Maven / Gradle / Ivy

Go to download

RIBs is the cross-platform architecture behind many mobile apps at Uber. This framework is designed for mobile apps with a large number of engineers and nested states.

There is a newer version: 0.16.3
Show newest version
/*
 * Copyright (C) 2017. Uber Technologies
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.uber.rib.core

import android.os.Looper
import androidx.annotation.CallSuper
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import java.util.Locale
import java.util.concurrent.CopyOnWriteArrayList

/**
 * Responsible for handling the addition and removal of children routers.
 *
 * @param  type of interactor this router routes.
 */
abstract class Router protected constructor(
  component: InteractorBaseComponent<*>?,
  open val interactor: I,
  private val ribRefWatcher: RibRefWatcher,
  private val mainThread: Thread
) {
  private val children: MutableList> = CopyOnWriteArrayList()
  private val interactorGeneric: Interactor<*, *>
    get() = interactor as Interactor<*, *>

  /** @return the Tag. */
  internal var tag: String? = null
    private set
  private var savedInstanceState: Bundle? = null
  private var isLoaded = false

  protected constructor(interactor: I, component: InteractorBaseComponent<*>?) : this(component, interactor, RibRefWatcher.getInstance(), getMainThread())
  protected constructor(interactor: I) : this(null, interactor, RibRefWatcher.getInstance(), getMainThread())

  @Suppress("UNCHECKED_CAST")
  protected fun inject(component: InteractorBaseComponent<*>?) {
    (component as? InteractorBaseComponent>)?.inject(interactorGeneric)
  }

  protected open fun attachToInteractor() {
    interactorGeneric.setRouterInternal(this)
  }

  /**
   * Dispatch back press to the associated interactor. Do not override this.
   *
   * @return TRUE if the interactor handles the back press.
   */
  open fun handleBackPress(): Boolean {
    ribRefWatcher.logBreadcrumb("BACKPRESS", null, null)
    return interactorGeneric.handleBackPress()
  }

  /** Called after the router has been loaded and initialized.  */
  @Initializer
  protected open fun didLoad() {
  }

  /**
   * Called when a router is being attached. Router subclasses can perform setup here for anything
   * that is needed again but is cleaned up in willDetach(). Use didLoad() if the setup is only
   * needed once.
   */
  protected open fun willAttach() {}

  /**
   * Called when a router is being a detached, router subclasses should perform any required clean
   * up here.
   */
  protected open fun willDetach() {}

  @MainThread
  public open fun attachChild(childRouter: Router<*>) {
    attachChild(childRouter, childRouter.javaClass.name)
  }

  /**
   * Attaches a child router to this router.
   *
   * @param childRouter the [Router] to be attached.
   * @param tag an identifier to namespace saved instance state [Bundle] objects.
   */
  @MainThread
  public open fun attachChild(childRouter: Router<*>, tag: String) {
    for (child in children) {
      if (tag == child.tag) {
        Rib.getConfiguration().handleNonFatalWarning(
          String.format(
            Locale.getDefault(), "There is already a child router with tag: %s", tag
          ),
          null
        )
      }
    }
    children.add(childRouter)
    ribRefWatcher.logBreadcrumb(
      "ATTACHED", childRouter.javaClass.simpleName, this.javaClass.simpleName
    )
    RibEvents.getInstance().emitEvent(RibEventType.ATTACHED, childRouter, this)
    var childBundle: Bundle? = null
    if (savedInstanceState != null) {
      val previousChildren = savedInstanceState?.getBundleExtra(KEY_CHILD_ROUTERS)
      childBundle = previousChildren?.getBundleExtra(tag)
    }
    childRouter.dispatchAttach(childBundle, tag)
  }

  /**
   * Detaches the {@param childFactory} from the current [Interactor]. NOTE: No consumers of
   * this API should ever keep a reference to the detached child router, leak canary will enforce
   * that it gets garbage collected.
   *
   *
   * If you need to keep references to previous routers, use RouterNavigator.
   *
   * @param childRouter the [Router] to be detached.
   */
  @MainThread
  public open fun detachChild(childRouter: Router<*>) {
    val isChildRemoved = children.remove(childRouter)
    val interactor = childRouter.interactor
    ribRefWatcher.watchDeletedObject(interactor)
    ribRefWatcher.logBreadcrumb(
      "DETACHED", childRouter.javaClass.simpleName, this.javaClass.simpleName
    )
    if (savedInstanceState != null) {
      val childrenBundles = savedInstanceState?.getBundleExtra(KEY_CHILD_ROUTERS)
      val childRouterTag = childRouter.tag
      if (childRouterTag != null) {
        childrenBundles?.putBundleExtra(childRouterTag, null)
      } else {
        Rib.getConfiguration()
          .handleNonFatalWarning("A RIB tried to detach a child that was never attached", null)
      }
    }
    childRouter.dispatchDetach()
    if (isChildRemoved) {
      RibEvents.getInstance().emitEvent(RibEventType.DETACHED, childRouter, this)
    }
  }

  @CallSuper
  public open fun dispatchAttach(savedInstanceState: Bundle?) {
    dispatchAttach(savedInstanceState, javaClass.name)
  }

  @CallSuper
  @Initializer
  public open fun dispatchAttach(savedInstanceState: Bundle?, tag: String) {
    checkForMainThread()
    if (!isLoaded) {
      isLoaded = true
      didLoad()
    }
    this.savedInstanceState = savedInstanceState
    this.tag = tag
    willAttach()
    var interactorBundle: Bundle? = null
    if (this.savedInstanceState != null) {
      interactorBundle = this.savedInstanceState!!.getBundleExtra(KEY_INTERACTOR)
    }
    interactorGeneric.dispatchAttach(interactorBundle)
  }

  public open fun dispatchDetach() {
    checkForMainThread()

    interactorGeneric.dispatchDetach()
    willDetach()
    for (child in children) {
      detachChild(child)
    }
  }

  /**
   * Gets the children of this [Router].
   *
   * @return Children.
   */
  internal open fun getChildren(): List> {
    return children
  }

  internal fun saveInstanceStateInternal(outState: Bundle) {
    saveInstanceState(outState)
  }

  protected open fun saveInstanceState(outState: Bundle) {
    val interactorSavedInstanceState = Bundle()
    interactorGeneric.onSaveInstanceStateInternal(interactorSavedInstanceState)
    outState.putBundleExtra(KEY_INTERACTOR, interactorSavedInstanceState)
    val childBundles = Bundle()
    for (child in children) {
      val childBundle = Bundle()
      child.saveInstanceState(childBundle)
      childBundles.putBundleExtra(child.tag!!, childBundle)
    }
    outState.putBundleExtra(KEY_CHILD_ROUTERS, childBundles)
  }

  private fun checkForMainThread() {
    if (mainThread !== Thread.currentThread()) {
      val errorMessage = "Call must happen on the main thread"
      val exception = IllegalStateException(errorMessage)
      Rib.getConfiguration().handleNonFatalError(errorMessage, exception)
    }
  }

  companion object {
    @VisibleForTesting
    val KEY_CHILD_ROUTERS = "Router.childRouters"

    @JvmField
    @VisibleForTesting
    val KEY_INTERACTOR = "Router.interactor"
    @JvmStatic
    fun getMainThread(): Thread {
      return try {
        Looper.getMainLooper().thread
      } catch (e: Exception) {
        Thread.currentThread()
      }
    }
  }

  init {
    inject(component)
    attachToInteractor()
  }
}