Problem with Butterknife when using Fragments

Many developers uses ButterKnife to get rid of old fashion findViewById() and make the life easy! well it's true and as long as it's a matter of human taste and laziness, so why not!? But sometimes we have to pay for our laziness! This article is about this cost in just one case!

If you are planing to bind your views using ButterKnife , the ButterKnife documentation is very easy and clear about it , in an Activity we will bind your Activity by :

class MyActivity extends AppCompatActivity{

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(int layoutResId)⇐1

    ButterKnife.bind(this);⇐2 // injection is done after setContentView()

    }

}

As you can see the injection is done after setContentView() ,as long as your are in the context of a single Activity , there is no any problem , Butterknife binding works perfectly all your views , subviews , widgets are binded with Butterknife annotations.But there are many situation in which along your application you inherite the chain of Activities from a base Activity in order to expose some common logic or behaviour.
A smart solution consists to inject it in BaseActivity rather than in every single Activity,so once again you will inject the same way ,but it doesn't work and crash!':


class BaseActivity extends AppCompatActivity{

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(int layoutResId)⇐1

        ButterKnife.bind(this);⇐2 // Error-java.lang.IllegalStateException

    }

}


There are many reported feedback about this issues , in fact if the BaseActivity uses ButterKnife to inject views in setContentView(int) and inflate the layoutResId passed from sub activity and add the inflated view to the layout, the sub activity can't inject its view, this is normal because setContentView is called in super class and by the time ChildActivity's view is assigned to binded variables , those variable are null (as binding not completed yet!)

The solution:

In this case ButterKnife recommend to invert the order so this work around hopefuly works and our codes changed to this :

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

ButterKnife.bind(this);⇐1 // hopefuly It works!

setContentView(int layoutResId)⇐2

}

Yes this solution can save us ...........until our ChildActivity doesn't contain' any Fragment! if our child Activity's layout has a child view <Fragment> , we are not lucky with this solution anymore. if you run your application it crash with exception :Butter Knife @bind in Fragment error getContext() reference null


Why is that?:
To see why in our pattern Fragment Binding is not working , have to look at Fragment lifecycle , Fragments need an activity in order to get in world! take a look at Injector again:we are still in onCreate ! Fragment is not attached to activity yet, because onAttach will be called later after onCreate is done! so at the time Fragment is binded by ButterKnife ,getActivity() return null , and there is why , app generate exception .

So what solution can save me if I can't Inject neither before nor after setContentView??

At the first look , a bad solution is to check if layout is a Fragment or not!! like that:


Bad Solution:

@Override

protected void onCreate(Bundle savedInstanceState) {

    if(isFragment()) {

    setContentView(getContentLayout());

    ButterKnife.bind(this);

    }else{

    ButterKnife.bind(this);

    setContentView(getContentLayout());

    }

But this kind of solution is a patch rather than a solution because it's not a consistant solution , a good pattern make BaseActivity abstract from other child Activities and not relay on thier layout contents , BaseActivity should not know anything about its sub Activites.

So What we can do?
The solution consists to use Override setContentView in BaseActivity , it's not a good solution to inject inside onCreate, we inject ButterKnife at the end of setContentView method

in Base Class and use the normal order of injection as we did for a single Activity like this:

Good Solution:

class BaseActivity extends AppCompatActivity{

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

    }

    @Override

    public void setContentView(@LayoutRes int layoutResID) {

        super.setContentView(layoutResID);

        // Bind to ButterKinfe.

        ButterKnife.bind(this);⇐ It works and bind all views including Fragments!

    }

}


Khosrov

March,7,2017

 © Xosrov 2016