One, a brief introduction

IoC and AOP are essential for getting started with backend development (related to Spring), but they are just concepts, not implementations. Similarly, Android can use IoC and AOP. We’ve already written about how to use AOP in Android development. If you are interested, please check out my previous blog (and follow it by the way), so the topic of this article is IoC.

Inversion of Control (IoC) is an important feature of the framework, not a special term for object-oriented programming. It includes Dependency Injection (DI) and Dependency Lookup.

The above source from Baidu Baike may be obscure to those who are in touch with IoC for the first time. In fact, in popular terms, it means that I don’t want to do what I could have done and leave it to the framework to do. A practical example is ButterKnife, which is typical of IoC on Android, implementing dynamic injection of controls and binding of click events. So let’s build an IoC framework similar to ButterKnife’s.

Second, framework implementation

Here is an example of code for ButterKnife on GitHub:

class ExampleActivity extends Activity { @BindView(R.id.user) EditText username; @BindView(R.id.pass) EditText password; @OnClick(R.id.submit) void submit() { // TODO call server... } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this); // TODO Use fields... }}Copy the code

It consists of three parts:

  • Control injection uses the @bindView annotation
  • Click events are bound using the @onclick annotation
  • Call butterknip.bind (this) in the onCreate() method

So, to emulate ButterKnife, start with @bindView and @onclick annotations.

1, annotations,

Note that both control injection and click event binding must be associated with the control’S ID, so both annotations will have a property representing the control’s ID. The code is as follows:

@target (elementtype.field) @Retention(retentionPolicy.runtime) public @interface BindView {int value(); @target (ElementType.METHOD) @Retention(retentionPolicy.runtime) public @interface ClickView {int value();  }Copy the code

Since I don’t want the name of the event-bound annotation to be OnClick, I’ll name the annotation ClickView, which will have the same effect.

The BindView annotation is used for the injection of the control, i.e. the class FIELD, so its Target value is elementType. FIELD, and the ClickView annotation is used for the click event binding of the control, i.e. the METHOD, so its Target value is elementType. METHOD. In addition, both annotations are used by the framework during App RUNTIME (i.e., visible at RUNTIME), so Retention is retentionPolicy.runtime. The use of these two annotations in coding is shown in the following code:

public class MainActivity extends AppCompatActivity { @BindView(R.id.btn_hello) Button mBtnHello; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @ClickView(R.id.btn_hello) public void sayHello() { Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show(); }}Copy the code

This is not enough, however, because annotations can be thought of as static tags that do not bind control injection to events. The control is actually retrieved by findViewById(), and the event is bound by setOnClickListener(). That’s what the framework is trying to do for us.

2, control injection and event binding implementation

That’s not how ButterKnife works, it’s just my idea.

  1. Control injection: The framework calls the Activity’s findViewById() method to retrieve the corresponding control id, and then assigns a value to the control (class field) by reflection.
  2. Event binding: The framework calls the Activity’s findViewById() method to retrieve the corresponding control id, and then calls the control’s setOnClickListener() to set the click event for the control. The ClickView annotated sayHello() method in the Activity is called by reflection in the click event.

Here’s how to do it:

Public class ViewUtil {public static void inject(final Activity Activity) {// Get the Activity class object class clazz = activity.getClass(); Field[] fields = clazz.getDeclaredFields(); For (Field Field: fields) {// Find the property annotated with BindView BindView = field.getannotation (bindView.class); if (bindView ! = null) {try {// Make the property accessible (it must be accessible if the property is final and jprivate, otherwise the following operation will report an error) field.setaccessible (true); Field.set (activity, activity.findViewById(bindView.value()))); } catch (IllegalAccessException e) { e.printStackTrace(); [] methods = clazz.getDeclaredMethods(); ClickView ClickView = method.getannotation (clickView.class); if (clickView ! = null) {// Get View from id, FindViewById (ClickView.value ()).setonClickListener (new view.onClickListener () {@override public void onClick(View v) { try { method.setAccessible(true); // Invoke the clickView-annotated method.invoke(activity); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }}}); }}}}Copy the code

3, try

Add viewutil.inject (this) to the onCreate() method:

public class MainActivity extends AppCompatActivity { @BindView(R.id.btn_hello) Button mBtnHello; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ViewUtil.inject(this); } @ClickView(R.id.btn_hello) public void sayHello() { Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show(); }}Copy the code

If the control was successfully injected, “Hello” is toast when the control is clicked.

Third, expand

The above test was successful, but the framework is currently only for activities, while ButterKnife can be used for both activities and fragments, so we need to use the framework for fragments as well.

1. The difference between Activity and Fragment getting controls

Both control injection and event binding are dependent on the first point, which is the acquisition of the control, findViewById(). An Activity can retrieve a control by calling its own findViewById() method. This is not the case with a Fragment.

public class MyFragment extends Fragment { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { if (mRootView == null) { mRootView = inflater.inflate(R.layout.fragment_my, null, false); } return mRootView; }}Copy the code

The reason an Activity can call its own findViewById() method to retrieve controls is that an Activity is a layout, not a Fragment. The layout of a Fragment is its own View (mRootView). To get a control in your Fragment, you must call mRootView’s findViewById() method.

2. Code extraction

Reviewing ViewUtil’s Inject (Activity Activity) method, the Activity parameter plays two roles in this method: one is a class (container) and the other is a layout. When in the role of a container, the purpose is to use reflection to get fields and methods and assign or execute them. In the layout role, you get the layout control by ID (findViewById). Look at the Fragment, isn’t there some clues? Fragment is the container role, and its mRootView is the layout role, so inject() method body can be extracted like this:

private static Context context; private static void injectReal(final Object container, Object rootView) { if (container instanceof Activity) { context = (Activity) container; } else if (container instanceof Fragment) { context = ((Fragment) container).getActivity(); } else if (container instanceof android.app.Fragment) { context = ((android.app.Fragment) container).getActivity(); } Class clazz = container.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { BindView bindView = field.getAnnotation(BindView.class); if (bindView ! = null) { try { field.setAccessible(true); field.set(container, findViewById(rootView, bindView.value())); } catch (IllegalAccessException e) { e.printStackTrace(); [] methods = clazz.getDeclaredMethods(); for (final Method method : methods) { ClickView clickView = method.getAnnotation(ClickView.class); if (clickView ! = null) { findViewById(rootView, clickView.value()).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { method.setAccessible(true); method.invoke(container); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }}}); } } } private static View findViewById(Object layout, int resId) { if (layout instanceof Activity) { return ((Activity) layout).findViewById(resId); } else if (layout instanceof View) { return ((View) layout).findViewById(resId); } return null; }Copy the code

So after extraction, the Activity and the Fragment corresponding inject () method can be used jointly the injectReal () method:

// Activity
public static void inject(Activity activity) {
    injectReal(activity, activity);
}

// v4 Fragment
public static void inject(Fragment container, View rootView) {
    injectReal(container, rootView);
}

// app Fragment
public static void inject(android.app.Fragment container, View rootView) {
    injectReal(container, rootView);
}
Copy the code

It’s pretty clear, and it can work, so I’m not going to test it here.

Finally, paste the Demo address

Github.com/GitLqr/IocD…