How to add a View to an app from another app

2019-04-11 09:34发布

问题:

My app is called MyNiceApp. MyNiceApp is mostly just a core that loads a view called coreView in the MainActivity onCreate. coreView gets populated by views from other plugins which the user downloads as wishes. I define the various areas on the core view that can be populated by the plugins via Interfaces in MyNiceApp. How can I load and pass Views from plugins into the coreView ?

I've been told that RemoteViews are a good option, but I don't know how to implement it. What other options are there?

Are RemoteViews the best way to go? I'm willing to try out anything that will work, even if not the best approach. A hack will do. Anything that could service this functionality will suffice, for the moment. Improvements could be made later.

Thank you all in advance.

UPDATE

I'm thinking of having them hosted on my private server. They will be downloaded to a dedicated folder called /data/app/com.myniceapp.plugins

I'm thinking it would be better organized if I had a folder created under /data/app/com.myniceapp./plugins, then have DexClassLoader crawl /data/app/com.myniceapp/plugins for downloaded plugins, then I could call my Class implementations, and dynamically load the plugin views to the core view at runtime.

TEMPORARY UPDATE

Hi @lelloman, and everyone else. I've been trying to make your solution work, but I've been unsuccessful so far.

I created a new project called Test View. It has an XML layout which I try to inflate and send to the Core View as follows:

package rev.ca.testview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;

import ca.rev.libs.core.MyViewCreator;

public class TestView implements MyViewCreator {
    @Override
    public View createMyView(Context context) {
        LayoutInflater revInfl = LayoutInflater.from(context);
        View toolBarItemsLL = revInfl.inflate(R.layout.layout, null, false);

        Button button = (Button) toolBarItemsLL.findViewById(R.id.testButton);
        return button;
    }
}  

This, however doesn't work. Here's the rest of it:

In the MainActivity view, which is supposed to get the views from the plugins:

NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);

String dexPath = "/data/app/rev.ca.testview";
String optimizedDirectory = this.getCacheDir().getAbsolutePath();
String libraryPath = null;

DexClassLoader dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, null, ClassLoader.getSystemClassLoader());
DexFile dexFile = null;
try {
    dexFile = DexFile.loadDex(dexPath, File.createTempFile("opt", "dex", this.getCacheDir()).getPath(), 0);

    for (Enumeration<String> classNames = dexFile.entries(); classNames.hasMoreElements(); ) {
        String className = classNames.nextElement();
        Class myClass = dexClassLoader.loadClass(className);
        if (myClass.isAssignableFrom(MyViewCreator.class)) {
            MyViewCreator creator = (MyViewCreator) myClass.getConstructor().newInstance();
            View myView = creator.createMyView(this);
                    // add myView wherever you want
            navigationView.addView(myView);
        }
    }
} catch (IOException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}  

DRAWER LAYOUT IMPLEMENTATION ISSUES UPDATE

Hi @lelloman again. I've been trying to implement your solution into my main project which has a Drawer Layout. It breaks at final View toolBarItemsLL = revInfl.inflate(R.layout.activity_main, null, false);.

Why won't it work with Drawer Layout. If you add a Navigation Drawer Activity (android-plugins/MyNiceApps/app/src/main/ : New -> Activity -> Navigation Drawer Activity) into the, that is when it all falls apart. Hope you can help.

Here is the StackTrace:

08-14 21:44:27.564 13390-13390/rev.ca.revcore W/ResourceType: For resource 0x7f0b005e, entry index(94) is beyond type entryCount(9)
08-14 21:44:27.564 13390-13390/rev.ca.revcore W/ResourceType: Failure getting entry for 0x7f0b005e (t=10 e=94) (error -75)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: For resource 0x7f0a002c, entry index(44) is beyond type entryCount(5)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: Failure getting entry for 0x7f0a002c (t=9 e=44) (error -75)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: For resource 0x7f060022, entry index(34) is beyond type entryCount(1)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: Failure getting entry for 0x7f060022 (t=5 e=34) (error -75)
08-14 21:44:27.565 13390-13390/rev.ca.revcore D/AndroidRuntime: Shutting down VM
08-14 21:44:27.566 13390-13390/rev.ca.revcore E/AndroidRuntime: FATAL EXCEPTION: main
                                                                Process: rev.ca.revcore, PID: 13390
                                                                android.view.InflateException: Binary XML file line #7: Binary XML file line #7: Error inflating class TextView
                                                                Caused by: android.view.InflateException: Binary XML file line #7: Error inflating class TextView
                                                                Caused by: java.lang.UnsupportedOperationException: Can't convert to ComplexColor: type=0x1
                                                                    at android.content.res.ResourcesImpl.loadComplexColorForCookie(ResourcesImpl.java:879)
                                                                    at android.content.res.ResourcesImpl.loadComplexColorFromName(ResourcesImpl.java:756)
                                                                    at android.content.res.ResourcesImpl.loadColorStateList(ResourcesImpl.java:835)
                                                                    at android.content.res.Resources.loadColorStateList(Resources.java:1002)
                                                                    at android.content.res.TypedArray.getColorStateList(TypedArray.java:531)
                                                                    at android.widget.TextView.<init>(TextView.java:1076)
                                                                    at android.widget.TextView.<init>(TextView.java:704)
                                                                    at android.support.v7.widget.AppCompatTextView.<init>(AppCompatTextView.java:62)
                                                                    at android.support.v7.widget.AppCompatTextView.<init>(AppCompatTextView.java:58)
                                                                    at android.support.v7.app.AppCompatViewInflater.createView(AppCompatViewInflater.java:103)
                                                                    at android.support.v7.app.AppCompatDelegateImplV9.createView(AppCompatDelegateImplV9.java:1029)
                                                                    at android.support.v7.app.AppCompatDelegateImplV9.onCreateView(AppCompatDelegateImplV9.java:1087)
                                                                    at android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC.onCreateView(LayoutInflaterCompatHC.java:47)
                                                                    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:769)
                                                                    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:727)
                                                                    at android.view.LayoutInflater.rInflate(LayoutInflater.java:858)
                                                                    at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:821)
                                                                    at android.view.LayoutInflater.inflate(LayoutInflater.java:518)
                                                                    at android.view.LayoutInflater.inflate(LayoutInflater.java:426)
                                                                    at rev.ca.revbags.MyViewCreator.createView(MyViewCreator.java:24)
                                                                    at rev.ca.revcore.rev_plugin_loader.RevPluginLoader.revLoadView(RevPluginLoader.java:36)
                                                                    at rev.ca.revcore.RevCoreMainActivity$1.handleMessage(RevCoreMainActivity.java:21)
                                                                    at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                    at android.os.Looper.loop(Looper.java:154)
                                                                    at android.app.ActivityThread.main(ActivityThread.java:6119)
                                                                    at java.lang.reflect.Method.invoke(Native Method)
                                                                    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
                                                                    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

回答1:

First thing, it seems you don't need RemoteViews at all, you can instantiate the view in your process and attach them to the Activity.

Then you need to clarify with yourself one thing: do you need to load classes at runtime? This would be the case if you want to use new classes in your app without updating it. This is not trivial, you will be forced to use some already defined interfaces in your app or crawl your way through it with reflection. It would be a pain.

Another, much simpler option would be to download an xml layout for each of your views and associated with that a configuration file which could describe some behaviors. If you decide to load classes at runtime from an external plugin, you could go this way:

  • define a ViewCreator class inside a library
  • when you want to create a plugin you need to make an apk which contains one (or more?) of these ViewCreator class
  • in your app you then load the apk with DexClassLoader, find the ViewCreator class and instantiate it
  • the ViewCreator instance can then generate your View

This approach gives you a tremendous power, far beyond mere View instantiation, but it brings also a lot of complexity. If you're doing this for fun, well, I think you're on the right track, however I wouldn't recommend to do this for a commercial project. I created a sample repository with a minimal working example here.

The bulk of this approach is that you create a "plugins" library which will contain the common interfaces for your app and each plugin. In my sample, the library contains only one class:

public abstract class AbstractViewCreator {

    private final Context context;

    public AbstractViewCreator(Context context) {
        this.context = context;
    }

    public abstract View createView();

    protected Context getContext() {
        return context;
    }
}

Then you need to create a "plugin" app, in the sample is PluginA. This app must contain one implementation of AbstractViewCreator. Then the NiceApp needs to download the plugin apk, in the sample the apk is copied into the assets folder. After that you need to load the apk:

DexClassLoader dexClassLoader = new DexClassLoader(apkPath, codeCachePath, librariesPath, parentClassLoader);

then you need to load a class, you could derive the name of the class you want to load, for instance:

String className = "com.lelloman." + assetsFileName.replace(".apk", "").toLowerCase() + ".MyViewCreator");
Class fooClass = dexClassLoader.loadClass(className);

Then get the constructor and instantiate the ViewCreator via reflection

Constructor constructor = myClass.getConstructor(Context.class);
AbstractViewCreator creator = (AbstractViewCreator) constructor.newInstance(context);

then you can create your View

View viewFromPlugin = creator.createView();