Search code examples
javaandroidandroid-fragmentslayoutdynamic-loading

Load fragment with layout from another APK


I have two APKs. First APK is a main APK which loads second APK.

MainActivity.java (first APK): Object mainFragment was loaded by DexClassLoader early

setContentView(R.layout.activity_main);

LinearLayout fragContainer = findViewById(R.id.main_fragment);

LinearLayout ll = new LinearLayout(context);
ll.setOrientation(LinearLayout.HORIZONTAL);
ll.setId(View.generateViewId());

getSupportFragmentManager().beginTransaction().add(ll.getId(), mainFragment, "mainFragment").commit();

fragContainer.addView(ll);

activity_main.xml (first APK):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >
    <LinearLayout
            android:layout_height="match_parent"
            android:layout_width="match_parent"
            android:id="@+id/main_fragment"
            android:orientation="vertical"
            />

</LinearLayout>

MainFragment.java (second APK):

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_main, viewGroup, false);

    view.findViewById(R.id.emailSignInButton).setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {

        }
    });

    return view;
}

fragment_main.xml (second APK):

<?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">
    <Button
            android:text="@string/sign_in"
            android:layout_width="88dp"
            android:layout_height="wrap_content"
            android:id="@+id/emailSignInButton"

            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="32dp"
            android:layout_marginEnd="32dp" android:layout_marginTop="8dp"
            tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>

I don't understand, why in the line

View view = inflater.inflate(R.layout.fragment_main, viewGroup, false);

view is always NULL. Do I need to load R.layout.fragment_main into memory like mainFragment?


Solution

  • @CommonsWare already answered you: you are loading classes, not resources.

    In Android´s packages, you have both classes (Like R), and resources (Preprocessed XML files, like layouts, for example). A ClassLoader is capable of reading and loading the first ones, so, loading another´s APK R class is possible. But R´s indexes just point towards resource files, they don´t contain the actual values of said resources. So, the R class you sideloaded does not provide the route to a valid resource. If R could do that, then the APK/AAR/APKLib formats would not exists; regular JAR files would suffice. But they don`t,because android resource files are different from class files, and thus, a "link" of sorts is necessary to make them visible to your classes: the R class.

    if you want to make your current code work, you need to replace fragment_main.xml with a coded viewgroup instance. Create a class/method providing a constraintLayout instance housing your button (MyViewFactory.createConstraintLayoutWithButton, for example), set the properties via code, and replace

        @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, viewGroup, false);
    
        view.findViewById(R.id.emailSignInButton).setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
    
            }
        });
    
        return view;
    }
    

    with something like

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) {
        View view = MyViewFactory.createConstraintLayoutWithButton(getContext)
    
        view.findViewById(R.id.emailSignInButton).setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
    
            }
        });
    
        return view;
    }
    

    As long as you don´t use resources, you´ll be fine. That said, this approach is quite fragile, so maybe it would be better to create intent filters between your APKs in order to make APK 2 to work for APK 1.

    Edit: You can use the R constants as ids for your code generated views, as long as you set them manually first. For example:

    public static ConstraintLayout createConstraintLayoutWithButton(Context context){
    
      Constraintlayout mylayout = new ConstraintLayout(context);
      //set up of the layout
      Button myButton = new Button(context);
      //set up of the button
      button.setId(R.id.emailSignInButton);
      myLayout.addView(button);
      return mylayout;
    }
    

    And then you can locate that button with the id.