Scene is an open source Android page navigation and composition framework from bytedance technology team for Single Activity Applications, with flexible stack management, page splitting, and complete support for various animations.

Scene was initially used to solve the problems encountered in the evolution of watermelon Video’s live streaming business, and was later introduced into Douyin’s shooting tool. After practice and verification, the team decided to open source it to the community, hoping to help people solve problems in more scenarios.

Making address and use the document of the project: https://github.com/bytedance/scene.

Development background

Watermelon video faces problems

Watermelon Video has been optimized for playback experience in version 1.0.8. It is hoped that there will be a smooth animation transition when the short video being played on the home page jumps to the details page.

The video below shows the old version of the overshoot:

The video below shows the new version of the transition effect:

This kind of complex transition animation is impossible to implement with an Activity. At that time, however, the Fragment would also have all sorts of weird state-saving crashes (although we knew how the crashes worked, we couldn’t accept them), so the Watermelon Video technology team designed a UI solution called Page to fulfill the need for transition animations.

But the Page itself is so heavily coupled to the business that it cannot be isolated for other scenarios. Later, with the growth of watermelon’s live streaming business, there was a demand for a similar framework. In order to solve problems such as weak Activity stack management, various black screens and weak animation capabilities, as well as too many Fragment crashes, we developed the universal framework Scene.

Below is a screenshot of the Scene on the watermelon long video details page and douyin shooting page:

The lack of Activity/fragments

Here is a brief list of the shortcomings of Activity and Support 28 fragments. Some of the problems have been fixed on Android X fragments.

Page navigation compares activities

  1. Stack management is weak, and the Intent+LaunchMode design makes it very error-prone for developers to use hacks properly but animation is too black.
  2. Activity performance is poor, common blank page switching also takes 60 to 70ms (based on Samsung S9 device test);
  3. Because of the destruction recovery mandate:
    • As a result, the Activity animation ability is very weak, and it cannot directly get the View of the front and back pages, so it cannot simply achieve complex interactive animation;
    • SharedElement animation ability is weak, animation moment has to pass back and forth between the two activities of various controls Bitmap;
    • Before Android 9, every time an Activity starts a new Activity, it needs to finish onSaveInstance on the previous page, which affects the speed of page opening.
  4. Activity depends on Manifest to increase the difficulty of Android dynamic, need to the system Instrumentation ActivityThread Hack;
  5. Dependency injection is difficult because the process of creating an Activity object had no API exposed to external processing prior to Android 8;
  6. Windowing is also a problem because of the windowing mechanism, so windowing must rely on a dangerous windowing permission.
  7. Shared element animation has NPE in some versions of the Framework and cannot be resolved.
java.lang.NullPointerException(android.app.EnterTransitionCoordinator);
Copy the code

Page group comparison Fragment

  1. All sorts of weird crashes that can cause an AppCompatActivity to crash on onBackPressed, even without fragments;
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
Copy the code

In this case, the watermelon does a try catch on super.onbackpressed () directly in its Activity base class.

  1. Add /remove/hide/show operations are not performed immediately. Even if commitNow executes the Fragment state, there is no guarantee that its Child Fragment state is updated to the latest. In performing thegetChildFragmentManager().executePendingTransactions()Developers mistakenly assume that Child Fragments are already in the latest Parent Fragment state when they are not;
  2. Fragment has two sets of Lifecycle, View Lifecycle and Fragment instance Lifecycle;
  3. Fragment show/hide methods do not trigger lifecycle callbacks. Calling hide does not trigger onPause/onStop, but changes the visibility of the View.
  4. Fragment animation is limited, can only use resource files, and page navigation can’t ensure the correct Z axis;
  5. Even if the Fragment has been destroyed, view.onClickListeneronClickCallbacks continue to trigger, forcing a lot of short logic inside the callback.
if (getActivity() == null) {
            return;
        }
Copy the code
  1. The navigation function is very weak, in addition to open and close, there is no more advanced stack management, navigation callback even order is not guaranteed, it is possible to trigger multiple callbacks;
  2. The life cycles of native and Support fragments are not exactly the same;
  3. Support for add/remove/hide/show+addToBackStack makes Fragment code extremely confusing.

Scene framework

Functional features

Scene provides two functions of page navigation and page combination, featuring as follows:

  1. View based implementation, very lightweight;
  2. Lifecycle will be destroyed if there is only one View and the Scene will be destroyed if there is only one View that Lifecycle will be destroyed.
  3. Navigation stack management is very flexible, no page switching black screen;
  4. Both navigation and combined operations are usually performed directly, with no distinction between COMMIT and commitNow.
  5. State saving is not mandatory, and can even be controlled at the page level to enhance the ability of component communication.
  6. Full shared element animation support;
  7. Page navigation and page composition functions can be used independently.

The basic concept

The Scene framework has three basic components: Scene, NavigationScene, and GroupScene.

use
Scene Base class for all scenes, with lifecycle and View support components
NavigationScene Support page navigation
GroupScene Support to combine any Scene

Scene

NavigationScene

GroupScene

Scene using

Simple to use

Here’s how to get started. See the Github repository example for more usage.

Access to the

Add dependencies:

dependencies {
  implementation 'com.bytedance.scene:scene:$latest_version'
  implementation 'com.bytedance.scene:scene-ui:$latest_version'
  implementation 'com.bytedance.scene:scene-shared-element-animation:$latest_version'

  // Kotlin
  implementation 'com.bytedance.scene:scene-ktx:$latest_version'
}
Copy the code

Create a home page:

class MainScene : AppCompatScene() { override fun onCreateContentView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?) : View? { return View(requireSceneContext()) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setTitle("Main") toolbar? .navigationIcon = null } }Copy the code

Create the Activity:

class MainActivity : SceneActivity() {
    override fun getHomeSceneClass(): Class<out Scene> {
        return MainScene::class.java
    }

    override fun supportRestore(): Boolean {
        return false
    }
}
Copy the code

Add to manifest.xml, notice that the input method schema has also been changed:

<activity
    android:name=".MainActivity"
    android:windowSoftInputMode="adjustNothing">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
Copy the code

Just run it.

This is how the new application wants to write all of Scene. If you are reconstructing or migrating an old app, or just want to use page combinations instead of fragments, you can still use activities to navigate, as shown in the Github Demo.

navigation

Open a new page:

requireNavigationScene().push(TargetScene::class.java)
Copy the code

Returns:

requireNavigationScene().pop()
Copy the code

Open the page to get results:

requireNavigationScene().push(TargetScene::class.java.null.PushOptions.Builder().setPushResultCallback { result ->
            }
        }.build())
Copy the code

Setting result:

requireNavigationScene().setResult(this@TargetScene, YOUR_RESULT)
Copy the code

combination

The composition API is similar to a Fragment, inheriting GroupScene, and then adding any Scene to your View layout:

void add(@IdRes int viewId, @NonNull Scene childScene, @NonNull String tag);
void remove(@NonNull Scene childScene);
void show(@NonNull Scene childScene);
void hide(@NonNull Scene childScene);
@Nullable
<T extends Scene> T findSceneByTag(@NonNull String tag);
Copy the code

Example:

class SecondScene : AppCompatScene() { private val mId: Int by lazy { View.generateViewId() } override fun onCreateContentView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?) : View? { val frameLayout = FrameLayout(requireSceneContext()) frameLayout.id = mId return frameLayout } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setTitle("Second") add(mId, ChildScene(), "TAG") } } class ChildScene : Scene() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?) : View { val view = View(requireSceneContext()) view.setBackgroundColor(Color.GREEN) return view } }Copy the code

communication

Scene supports viewModels. You can get a ViewModel hosted by your Activity or your own by activityViewModels:

class ViewModelSceneSamples : GroupScene() {
    private val viewModel: SampleViewModel by activityViewModels()
Copy the code

Example:

class ViewModelSceneSamples : GroupScene() { private val viewModel: SampleViewModel by activityViewModels() private lateinit var textView: TextView override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.counter.observe(this, Observer<Int> { t -> textView.text = "" + t }) add(R.id.child, ViewModelSceneSamplesChild(), "Child") } } class ViewModelSceneSamplesChild : Scene() { private val viewModel: SampleViewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?) : View { return Button(requireSceneContext()).apply { text = "Click to +1" } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) requireView().setOnClickListener { val countValue = viewModel.counter.value ? : 0 viewModel.counter.value = countValue + 1 } } } class SampleViewModel : ViewModel() { val counter: MutableLiveData<Int> = MutableLiveData() }Copy the code

animation

When pushing, you can configure simple cutscenes with PushOptions:

val enter = R.anim.slide_in_from_right
val exit = R.anim.slide_out_to_left
requireNavigationScene(a).push(TargetScene::class.java, null,
        PushOptions.Builder().setAnimation(requireActivity(), enter, exit).build(a))
Copy the code

Complex shared element animation, gesture animation, refer to Demo.

Rowed back right

Scene has a built-in right swipe to return a gesture, you can just inherit appScene and turn on the gesture:

setSwipeEnabled(true)
Copy the code

Core design idea

  1. The Scene itself contains a lifecycle on the View, which is distributed internally through a native Fragment called LifeCycleFragment, which is then synchronized by the parent component to its children.
  2. Parent component synchronization lifecycle, in principle:
    • On entry, the parent component’s lifecycle callback is executed first, followed by the child component’s lifecycle callback;
    • On exit, the life cycle callback of the child component is executed first, and then the life cycle callback of the parent component.
  3. NavigationScene is responsible for the processing of the navigation stack, GroupScene responsible for Page combination treatment, similar to iOS UINavigationController/UIViewController, WinRT Page. The reason for the split is performance, because the task of navigation, due to the animation requirements, is inherently more complex than normal page composition, and the animation API is more powerful. These two things, by themselves, affect the life cycle differently, navigation affects the previous page, composition does not.
  4. The principle of life cycle and animation is to execute the life cycle first and then animate the views of the first and second pages. This avoids the tedious step of passing a Bitmap back and forth between pages to simulate controls, and avoids the Activity animation’s black screen.
  5. Finally, because the Transition library is too weak, we use GhostView and Scene, the core of the system, to achieve a shared element animation.

Future and Summary

Scene Router is under development to support popular Android componentization development.

Scene Dialog, in development, to solve the Android framework Dialog because it is based on the Window will be overlaid on the ordinary View problem.

The idea of single Activity was discussed in the industry as early as the launch of Fragment. A framework like Conductor was born in the community. Even in the past two years, Google has officially made Navigation Component. However, after all, the fragmentation hole is too large, navigation based on the Fragment is always limited by the compatibility of the Fragment, so that later, In order to solve these compatibility problems, Google directly decided to modify the Fragment, abolishing the interface used for many years.

The navigation and composition solution based on View is free of the previous technical debt, on the one hand, it is free of Google’s ideas, such as the ability to control the scope of state saving, to achieve more powerful animation capabilities and component communication capabilities, which the official component does not provide to developers.

The Demo in the warehouse has made up the examples of most of the scenes in the daily development of Android. For functions not listed in this article, you can refer to the writing method of Demo.

The resources

Single Activity: Why, When, and How (Android Dev Summit ’18)

Fragments: Past, Present, and Future (Android Dev Summit ’19)

Conductor

Uber RIBs

Welcome to Bytedance Technical Team