background:
In the android application development process, we will write the layout in the xml file. When instantiating the controls in XML, we will write a lot of find ViewById (int viewid). When there are many controls to be found, it will be very cumbersome and write a lot of template code.
Usually, we can use the ButterKnife framework to optimize, and only need one annotation to save writing findViewById().
In this article, we will create our own "ButterKnife".
project structure
Create a baselibrary module where the dependency injection framework is placed.
1. Create ViewById annotation
package com.example.baselibrary.ioc; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD)//Can only be used on variables to take effect @Retention(RetentionPolicy.RUNTIME) //At runtime, it takes effect even when compiled into bytecode public @interface ViewById { int value(); }
We created a new annotation ViewById and also used meta-annotations @Target, @Rentention.
@Target : The scope of the object modified by the annotation (class, variable, method, etc.)
@Rentention: the retention policy used to declare annotations
There are three types of @Rentention annotations:
RetentionPolicy.SOURCE: Source-level annotations. It only exists in the .java file, the compiled annotations are discarded, and the .class does not exist.
RetentionPolicy.CLASS: Compile-time annotation. Exists in .java\.class files. When running a java program, the JVM will discard the annotation information and will not keep it in the JVM
RetentionPolicy.RUNTIME: runtime annotation. When running a java program, the JVM will also retain the annotation information, which can be obtained through reflection.
Only one annotation is not enough, it needs to be injected (use reflection to get id, findViewById).
2. Create ViewUtils tool class
package com.example.baselibrary.ioc; import android.app.Activity; import android.view.View; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * @Name: EassyJoke * @Description: Description * @Author: jingyil * @Date: 2/6/23 2:09 PM * Modified by: jingyil * Modified: 2/6/23 2:09 PM * Modification notes: */ public class ViewUtils { //use in activity public static void inject(Activity activity) { inject(new ViewFinder(activity), activity); } //Use in View public static void inject(View view) { inject(new ViewFinder(view), view); } //Late stage public static void inject(View view, Object object) { inject(new ViewFinder(view), object); } //Compatible with the above three methods //Object --> The class that reflection needs to execute private static void inject(ViewFinder viewFinder, Object object) { //Inject properties and events injectField(viewFinder, object); injectEvent(viewFinder, object); } private static void injectEvent(ViewFinder viewFinder, Object object) { } private static void injectField(ViewFinder viewFinder, Object object) { //1. Get all the properties in the class Class<?> clazz = object.getClass(); Field[] fields = clazz.getDeclaredFields();//get all properties //2. Get the value in the ViewById annotation for (Field field : fields) { ViewById viewById = field.getAnnotation(ViewById.class); if (viewById != null) { //Get the value in ViewById int viewId = viewById.value(); //3.findViewById find View View view = viewFinder.findViewById(viewId); if (view != null) { //Ability to inject all modifiers field.setAccessible(true); //4. Dynamic injection into View //obj – the object whose field should be modified //value – the new value for the field of obj being modified try { field.set(object, view); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } }
Focus on the injectField() method
private static void injectField(ViewFinder viewFinder, Object object) { //1. Get all the properties in the class Class<?> clazz = object.getClass(); Field[] fields = clazz.getDeclaredFields();//get all properties //2. Get the value in the ViewById annotation for (Field field : fields) { ViewById viewById = field.getAnnotation(ViewById.class); if (viewById != null) { //Get the value in ViewById int viewId = viewById.value(); //3.findViewById find View View view = viewFinder.findViewById(viewId); if (view != null) { //Ability to inject all modifiers field.setAccessible(true); //4. Dynamic injection into View //obj – the object whose field should be modified //value – the new value for the field of obj being modified try { field.set(object, view); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
What does this method do?
1. Get all variables of the object passed in by calling the inject() method
2. Traverse these variables and find the variables that use @ViewById
3. Get the value passed in by @ViewById
4. Call findViewById of viewFinder to find View
5. Set the found View to the variable using @ViewById
Among them, ViewFinder is also used
package com.example.baselibrary.ioc; import android.app.Activity; import android.view.View; public class ViewFinder { private Activity mActivity; private View mView; public ViewFinder(Activity activity) { this.mActivity = activity; } public ViewFinder(View view) { this.mView = view; } public View findViewById(int viewId) { return mActivity != null ? mActivity.findViewById(viewId) : mView.findViewById(viewId); } }
With the above code, the instantiation of variables can be realized.
3. Test the @ViewById annotation
package com.example.eassyjoke; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.TextView; import android.widget.Toast; import com.example.baselibrary.ioc.OnClick; import com.example.baselibrary.ioc.ViewById; import com.example.baselibrary.ioc.ViewUtils; public class MainActivity extends AppCompatActivity { @ViewById(R.id.text_tv) private TextView textView; @ViewById(R.id.text_new_tv) private TextView textNewView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ViewUtils.inject(this); textView.setText("Inject Test"); textNewView.setText("New Text"); } }
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/text_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/text_new_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/text_tv"/> </androidx.constraintlayout.widget.ConstraintLayout>
No more manually calling findViewById
You can setText() directly, and after calling ViewUtils.inject(this), all controls with @ViewById annotations added are instantiated.
Annotate the variables above
4. Add annotations to the method
package com.example.baselibrary.ioc; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD)//Can only be used on variables to take effect @Retention(RetentionPolicy.RUNTIME) //At runtime, it takes effect even when compiled into bytecode public @interface OnClick { int[] value(); }
Attention @Target.
private static void injectEvent(ViewFinder viewFinder, Object object) { //1. Get all events in the class Class<?> clazz = object.getClass(); Method[] methods = clazz.getDeclaredMethods();//get all methods //Get the values of OnClick for (Method method : methods) { OnClick onClick = method.getAnnotation(OnClick.class); if (onClick != null) { int[] values = onClick.value(); for (int value : values) { View view = viewFinder.findViewById(value); if (view != null) { view.setOnClickListener(new DeclaredOnclickLister(method, object)); } } } } }
What does this method do?
1. Get all the methods in the class
2. Traverse to find the method using @onClick annotation
3. Get the viewid value passed into the @onClick annotation
4. Call findViewById of viewFinder to find View
5. Click on the listener for the view facility, and the listener must pass in the method annotated with @onClick obtained through reflection.
When clicked, the onclick of DeclaredOnclickLister will be executed
private static class DeclaredOnclickLister implements View.OnClickListener { private Object mObject; private Method mMethod; public DeclaredOnclickLister(Method method, Object object) { this.mMethod = method; this.mObject = object; } @Override public void onClick(View v) { //Click will call this method try { mMethod.setAccessible(true); mMethod.invoke(mObject, v); } catch (Exception e) { try { mMethod.invoke(mObject, null); } catch (Exception illegalAccessException) { illegalAccessException.printStackTrace(); } e.printStackTrace(); } } }
mMethod.invoke(mObject, v); will be executed when clicked, in fact, the method annotated with @Onclick is executed.
5. Annotate the case with @OnClick
in MainActivity
@OnClick({R.id.text_tv, R.id.text_new_tv}) private void onClick(View view) { Toast.makeText(this, "click", Toast.LENGTH_SHORT).show(); }
Summarize:
Annotating variables and methods is not difficult
The key is to get the variable or method that uses this type of annotation through reflection.
Assigning values to variables is: field.set(object, view);
The calling method is: mMethod.invoke(mObject, v);