Android Advanced Project 01 - Create a set of IOC annotation framework

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);

Tags: Java Android jvm

Posted by mcmuney on Wed, 08 Feb 2023 15:08:52 +0530