Nestydy questions about the life cycle



    Each developer faced questions about the life cycle of an Activity: what a bind service is, how to maintain the interface state when the screen is rotated, and how Fragment differs from an Activity.
    We in FunCorp have accumulated a list of questions on similar topics, but with certain nuances. Some of them I want to share with you.


    1. Everyone knows that if you open the second activation over the first one and rotate the screen, then the chain of life cycle calls will look like this:


    Discovery Activity

    FirstActivity: onPause
    SecondActivity: onCreate
    SecondActivity: onStart
    SecondActivity: onResume
    FirstActivity: onSaveInstanceState
    FirstActivity: onStop


    Turn

    SecondActivity: onPause
    SecondActivity: onSaveInstanceState
    SecondActivity: onStop
    SecondActivity: onCreate
    SecondActivity: onStart
    SecondActivity: onRestoreInstanceState
    SecondActivity: onResume


    Return back

    SecondActivity: onPause
    FirstActivity: onCreate
    FirstActivity: onStart
    FirstActivity: onRestoreInstanceState
    SecondActivity: onStop


    And what will happen if the second activation is transparent?


    Decision


    In the case of a transparent top activation, in terms of logic, everything is a little different. Precisely because it is transparent, after turning it is necessary to restore the contents and the activation that is directly below it. Therefore, the order of calls will be slightly different:


    Opening activity

    FirstActivity: onPause
    SecondActivity: onCreate
    SecondActivity: onStart
    SecondActivity: onResume


    Turn

    SecondActivity: onPause
    SecondActivity: onSaveInstanceState
    SecondActivity: onStop
    SecondActivity: onCreate
    SecondActivity: onStart
    SecondActivity: onRestoreInstanceState
    SecondActivity: onResume
    FirstActivity: onSaveInstanceState
    FirstActivity: onStop
    FirstActivity: onCreate
    FirstActivity: onStart
    FirstActivity: onRestoreInstanceState
    FirstActivity: onResume
    FirstActivity: onPause


    2. No application can do without dynamically adding a view, but sometimes you have to move the same view between different screens. Can one and the same object be added simultaneously to two different activations? What happens if I create it with the Application context and want to add to different activites at the same time?


    Why do you need it?
    There are “not very nice” libraries that hold important business logic inside custom view, and the re-creation of these views within each new activation is a bad decision, because I want to have one set of data.



    Decision


    Nothing prevents to create a view with the Application context. It will simply apply default styles that are not related to any activations. Also, without any problems, you can move this view between different activites, but you need to make sure that it is added to only one parent.


    privatevoidaddViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout){  
            ...
            if (child.getParent() != null) {  
                thrownew IllegalStateException("The specified child already has a parent. " +  
                        "You must call removeView() on the child's parent first.");  
          }
          ...
        }

    You can, for example, subscribe to ActivityLifecycleCallbacks, delete onStop (removeView) from the current activation, add onStart to the next one opened (addView).


    3. Fragment can be added via add and replace. And what is the difference between these two options in terms of the order of calling life-cycle methods? What are the advantages of each of them?


    Decision


    Even if you add a fragment through replace, this does not mean that it is completely replaced. This means that at this place in the container, I twist it with a twist, therefore, onDestroyView will be called on the current fragment, and onCreateView will be called again on returning back.



    This is quite a change in the rules of the game. It is necessary to detail all controllers and classes related to the UI in onDestroyView. It is necessary to clearly separate the receipt of the data needed by the fragment and the filling in of the twist (lists, etc.), since the filling and destruction of the twist will occur much more often than the retrieval of data (reading some data from the database).


    Also, there are nuances with state recovery: for example, onSaveInstanceState sometimes comes after onDestroyView. In addition, it should be borne in mind that if null came to onViewStateRestored, this means that there is no need to restore anything, and not be reset to the default state.


    If we talk about convenience between add and replace, then replace is more economical from memory if you have deep navigation (we have user depth of navigation - one of the product KPIs). It is also much more convenient with replace to manage the toolbar, since in onCreateView you can reinfluit it. Of the advantages of add: fewer problems with the life cycle, when you go back, I do not recreate the view and you do not need to re-fill anything.


    4. Sometimes you still have to work directly with services and even with bind services. An activity interacts with one of these services (only one activity). It connects to the service and sends data to it. When you rotate the screen, our activism collapses, and we must ottanditsya from this service. But if there is no connection, then the service collapses, and after turning the bind will be to a completely different service. How to make the service live when turning?


    Decision


    If you know a beautiful solution, then write in the comments. Only something like this comes to mind:


    @OverrideprotectedvoidonDestroy(){
            super.onDestroy();
            ThreadsUtils.postOnUiThread(new Runnable() {
                @Overridepublicvoidrun(){
                    unbindService(mConnection);
                }
            });
        }

    5. Recently we redesigned navigation within our application to Single Activity (using one of the available libraries). Previously, each application screen was a separate activation, now navigation works on fragments. The problem of returning to activation in the middle of the stack was solved by intent flags. How can I go back to the fragment in the middle of the stack?


    Decision


    Yes, the solution out of the box FragmentManager does not provide. Cicerone does something similar inside itself:


    protectedvoidbackTo(BackTo command){
            String key = command.getScreenKey();
            if (key == null) {
                backToRoot();
            } else {
                int index = localStackCopy.indexOf(key);
                int size = localStackCopy.size();
                if (index != -1) {
                    for (int i = 1; i < size - index; i++) {
                        localStackCopy.pop();
                    }
                    fragmentManager.popBackStack(key, 0);
                } else {
                    backToUnexisting(command.getScreenKey());
                }
            }
        }

    6. Also recently we got rid of such an inefficient and complex component as ViewPager, because the logic of interaction with it is very complex, and the behavior of the fragments is unpredictable in certain cases. In some fragments we used Inner fragments. What happens when fragments are used inside RecycleView items?


    Decision


    In general, there will be nothing bad. The fragment will be added without problems and will be displayed. The only thing we have encountered is inconsistencies with its life cycle. The implementation on the ViewPager controls the life cycle of the fragments via setUserVisibleHint, and RecycleView does everything head-on without thinking about the actual visibility and accessibility of the fragments.


    7. All the same reason for the transition from the ViewPager, we are faced with the problem of restoring the state. In the case of fragments, this was implemented by the framework: in the right places, we simply redefined onSaveInstanceState and saved all the necessary data in the Bundle. When the ViewPager was re-created, all fragments were restored by the FragmentManager and returned to their state. What to do with RecycleView and its ViewHolder?


    Decision


    “We need to write everything into the database and read from it every time,” you will say. Or the state preservation logic should be outside, and the list is just a display. In an ideal world, it is. But in our case, each element of the list is a complex screen with its own logic. Therefore, I had to invent my own bike in the style of “let's make the same logic as in the ViewPager and fragment”:


    Adapter
    publicclassRecycleViewGalleryAdapterextendsRecyclerView.Adapter<GalleryItemViewHolder> implementsGalleryAdapter{
        privatestaticfinal String RV_STATE_KEY = "RV_STATE";
        @Nullableprivate Bundle mSavedState;
        @OverridepublicvoidonBindViewHolder(GalleryItemViewHolder holder, int position){
            if (holder.isAttached()) {
                holder.detach();
            }
            holder.attach(createArgs(position, getItemViewType(position)));
            restoreItemState(holder);
        }
        @OverridepublicvoidsaveState(Bundle bundle){
            Bundle adapterState = new Bundle();
            saveItemsState(adapterState);
            bundle.putBundle(RV_STATE_KEY, adapterState);
        }
        @OverridepublicvoidrestoreState(@Nullable Bundle bundle){
            if (bundle == null) {
                return;
            }
            mSavedState = bundle.getBundle(RV_STATE_KEY);
        }
        privatevoidrestoreItemState(GalleryItemViewHolder holder){
            if (mSavedState == null) {
                holder.restoreState(null);
                return;  }
            String stateKey = String.valueOf(holder.getGalleryItemId());
            Bundle state = mSavedState.getBundle(stateKey);
            if (state == null) {
                holder.restoreState(null);
                mSavedState = null;
                return;  }
            holder.restoreState(state);
            mSavedState.remove(stateKey);
        }
        privatevoidsaveItemsState(Bundle outState){
            GalleryItemHolder holder = getCurrentGalleryViewItem();
            saveItemState(outState, (GalleryItemViewHolder) holder);
        }
        privatevoidsaveItemState(Bundle bundle, GalleryItemViewHolder holder){
            Bundle itemState = new Bundle();
            holder.saveState(itemState);
            bundle.putBundle(String.valueOf(holder.getGalleryItemId()), itemState);
        }
    }

    On Fragment.onSaveInstanceState, we read the state of the holders we need and put them in the Bundle. When recreating the holders, we retrieve the saved Bundle and on the onBindViewHolder we pass the found states to the inside of the holders:


    8. How does this threaten us?


    @OverrideprotectedvoidonCreate(Bundle savedInstanceState){  
              super.onCreate(savedInstanceState);  
              setContentView(R.layout.activity); 
              ViewGroup root = findViewById(R.id.default_id);  
              ViewGroup view1 = new LinearLayout(this);  
              view1.setId(R.id.default_id);  
              root.addView(view1);  
              ViewGroup view2 = new FrameLayout(this);  
              view2.setId(R.id.default_id);  
              view1.addView(view2);  
              ViewGroup view3 = new RelativeLayout(this);  
              view3.setId(R.id.default_id);  
              view2.addView(view3);  
          }

    Decision


    In fact, there is nothing wrong with that. In the same RecycleView lists of elements with the same id are stored. However, there is still a small nuance:


    @Overrideprotected <T extends View> T findViewTraversal(@IdRes int id){
            if (id == mID) {
                return (T) this;
            }
            final View[] where = mChildren;
            finalint len = mChildrenCount;
            for (int i = 0; i < len; i++) {
                View v = where[i];
                if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
                    v = v.findViewById(id);
                    if (v != null) {
                        return (T) v;
                    }
                }
            }
            returnnull;
        }

    It is worth being careful if we have elements with the same id in the hierarchy, since the first found element is always returned, and at different levels of the call to findViewById it can be different objects.


    9. You fall from TooLargeTransaction when you rotate the screen (yes, our ViewPager is still indirectly to blame). How to find the culprit?


    Decision


    It's pretty simple: hang the ActivityLifecycleCallbacks on the Application, catch everything onActivitySaveInstanceState and parse everything inside the Bundle. You can also get the status of all the views and all the fragments inside this activit.


    Below is an example of how we get the state of the fragments from the Bundle:


    /**
     * Tries to find saved [FragmentState] in bundle using 'android:support:fragments' key. 
    */
    fun Bundle.getFragmentsStateList(): List<FragmentBundle>? {
        try {
            val fragmentManagerState: FragmentManagerState? = getParcelable("android:support:fragments")
            val active = fragmentManagerState?.mActive
                    ?: return emptyList()
            return active.filter {
                it.mSavedFragmentState != null
            }.map { fragmentState ->
                FragmentBundle(fragmentState.mClassName, fragmentState.mSavedFragmentState)
            }
        } catch (throwable: Throwable) {
            Assert.fail(throwable)
            returnnull
        }
    }
    fun init(){
        application.registerActivityLifecycleCallbacks(object : SimpleActivityLifecycleCallback() {
            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?){
                super.onActivitySaveInstanceState(activity, outState)
                outState?.let {
                    ThreadsUtils.runOnMainThread {
                        trackActivitySaveState(activity, outState)
                    }
                }  }
        })
    }
    @MainThreadprivate fun trackActivitySaveState(activity: Activity, outState: Bundle){
        val sizeInBytes = outState.getSizeInBytes()
        val fragmentsInfos = outState.getFragmentsStateList()
                ?.map {
                    mapFragmentsSaveInstanceSaveInfo(it)
                }
        ...
    }

    Next, we simply calculate the size of the bundle and log it:


        fun Bundle.getSizeInBytes(): Int {  
           val parcel = Parcel.obtain()  
           returntry {  
              parcel.writeValue(this)  
              parcel.dataSize()  
           } finally {  
              parcel.recycle()  
           }  
        }
    

    10. Suppose we have an activity and a set of dependencies on it. Under certain conditions, we need to recreate a set of these dependencies (for example, on a click, run some experiment with another UI). How do we realize this?


    Decision


    Of course, you can tinker with the flags and make it some kind of “crutch” restart of the activation through the launch of the intent. But in fact, everything is very simple - activity has a method to recreate.


    Most likely, a large part of this knowledge will not be useful to you, as you do not come to each of them from a good life. However, some of them demonstrate well how a person can reason and propose his own solutions. We use similar questions in interviews. If you have interesting tasks that you were offered to solve at interviews, or you put them yourself, write them in the comments - it will be interesting to discuss!


    Also popular now: