在应用程序内部使用外部应用程序片段

时间:2011-10-11 13:21:27

标签: android android-fragments

是否可以使用外部应用程序中的片段/活动并在嵌入时使用? 例如,从PDF阅读器应用程序中嵌入PDF阅读器片段。

3 个答案:

答案 0 :(得分:31)

可能有点晚了,但仍然觉得它可以添加并可能有助于其他人。

对于活动,实际上没有必要嵌入它,有方便的方式来使用其他应用程序活动 - 有目的地启动它。对于在应用程序内部实现某种“插件”功能的情况下可能有意义的片段。

Android Blog 'Custom Class Loading in Dalvik'中有一种使用其他应用程序代码(或从网络加载代码)的官方方法。请注意,android与其他平台/环境没有太大区别,因此这两个部分(您的应用程序和片段,您希望加载到您的应用程序中)应该支持某种合同。这意味着你不能从任何应用程序加载任何组件,这是很常见的,并且有很多原因可以这样。

所以,这是一个小实现的例子。它由3部分组成:

  1. 接口项目 - 此项目包含主应用程序应加载的接口定义,以便使用外部类:

    package com.example.test_interfaces;
    
    import android.app.Fragment;
    
    /**
     * Interface of Fragment holder to be obtained from external application
     */
    public interface FragmentHolder {
        Fragment getFragment();
    }
    

    对于这个例子,我们只需要单个接口来演示如何加载片段。

  2. 插件应用程序,其中包含您需要加载的代码 - 在我们的示例中,它是一个片段。请注意,IDE中的这个项目应该依赖于使用“提供”类型且没有导出的接口,因为它将由主应用程序导入。

    片段,我们将加载 PlugInFragment

    package com.sandrstar.plugin;
    
    import com.example.test_interfaces.FragmentHolder;
    
    public class PlugInFragment extends Fragment implements FragmentHolder {
    
        @Override
        public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
    
            // Note that loading of resources is not the same as usual, because it loaded actually from another apk
            final XmlResourceParser parser = container.getContext().getPackageManager().getXml("com.sandrstar.plugin", R.layout.fragment_layout, null);
    
            return inflater.inflate(parser, container, false);
        }
    
        @Override
        public Fragment getFragment() {
            return this;
        }
    }
    

    它的布局是 fragment_layout.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/black">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="This is from fragment"
            android:textColor="@android:color/white"/>
    </LinearLayout>
    
  3. 想要从其他应用程序加载片段的主应用程序。它应该导入接口项目:

    活动本身 MyActivity

    public class MyActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            setContentView(R.layout.main);
    
            try {
                Class<?> requiredClass = null;
                final String apkPath = getPackageManager().getApplicationInfo("com.sandrstar.plugin",0).sourceDir;
                final File dexTemp = getDir("temp_folder", 0);
                final String fullName = "com.sandrstar.plugin.PlugInFragment";
                boolean isLoaded = true;
    
                // Check if class loaded
                try {
                    requiredClass = Class.forName(fullName);
                } catch(ClassNotFoundException e) {
                    isLoaded = false;
                }
    
                if (!isLoaded) {
                    final DexClassLoader classLoader = new DexClassLoader(apkPath,
                            dexTemp.getAbsolutePath(),
                            null,
                            getApplicationContext().getClassLoader());
    
                    requiredClass = classLoader.loadClass(fullName);
                }
    
                if (null != requiredClass) {
                    // Try to cast to required interface to ensure that it's can be cast
                    final FragmentHolder holder = FragmentHolder.class.cast(requiredClass.newInstance());
    
                    if (null != holder) {
                        final Fragment fragment = holder.getFragment();
    
                        if (null != fragment) {
                            final FragmentTransaction trans = getFragmentManager().beginTransaction();
    
                            trans.add(R.id.fragmentPlace, fragment, "MyFragment").commit();
                        }
                    }
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    

    它的布局是 main.xml

    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:clipChildren="false"
        android:id="@+id/root">
    
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@drawable/down_image" />
    
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/fragmentPlace"
            android:layout_centerInParent="true" />
    </RelativeLayout>
    
  4. 最后我们能够在真实设备上观察以下内容:

    enter image description here

    处理可能的问题(感谢 @MikeMiller 更新):

    1. 如果您在调用classLoader.loadClass时收到以下错误:
    2. java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation

      确保片段模块包含在主应用程序中(作为“已编译”)

      1. 如果在NameNotFoundException的调用中得到context.getPackageManager().getApplicationInfo(packageName,0).sourceDir,请确保该片段位于已安装的APPLICATION中(而不仅仅是库依赖项)。请按照以下步骤确保是这样的:

        1)在主应用程序的build.gradle中,将apply plugin: 'android-library'更改为apply plugin: 'android'并确保存在虚拟活动java文件。在主应用程序中,删除对片段模块的依赖(它未在步骤3中指定,但我必须在片段模块上添加对主应用程序的依赖。但片段模块现在是一个活动应用程序,你可以' t依赖于那些)或者你会得到这个:Error:Dependency unspecified on project resolves to an APK archive which is not supported as a compilation dependency.

        2)运行片段模块(您现在可以这样做,因为它是一个活动应用程序)。这将以getApplicationInfo调用可以找到它的方式安装它Revert build.gradle并在主应用程序中添加依赖关系(作为'compile'依赖项)现在一切都应该工作。当您对片段代码进行更新时,您不需要再次执行此过程。但是,如果要在新设备上运行或添加新的片段模块,您将会这样做。我希望能够节省一些时间来解决上述错误。

      2. Android L

        似乎,基于Android的L 普通 multidex support,不需要上面的步骤,因为类加载是不同的。可以使用multidex支持中描述的方法代替Android Blog 'Custom Class Loading in Dalvik',因为它明确指出:

          

        注意:本文档中提供的指南取代了指南   在Android开发者博客文章中给出自定义类加载   的Dalvik。

        可能需要对android.support.multidex进行更改才能重复使用该方法。

答案 1 :(得分:1)

不,您无法“重用”其他应用程序中的代码。唯一的官方方法是使用Intent来调用整个Activity。

答案 2 :(得分:0)

我对sandrstart使用了非常相似的方法。也许它不太安全。 我使用从使用插件包名称创建的packagecontext派生的普通Classloader。所有插件的软件包名称都将与其他配置一起从config网站加载和保存。

Context ctx = createPackageContext(packetName, Context.CONTEXT_INCLUDE_CODE | 
               Context.CONTEXT_IGNORE_SECURITY);
ClassLoader cl = ctx.getClassLoader();
Class<?> c = cl.loadClass(className);
Fragment fragObj = (Fragment)c.newInstance();

但是我想强调一下我的方法,我认为sandrstar的方法仅适用于android内部android.app.Fragment类。

我试图(再次)作为采用android 9的一部分,从android.app.Fragment(已描述)切换到android.support.v4.Fragment,但无法正常工作。

原因是:    apk1:framework.class.A == apk2.framework.class.A但 apk1:someJarOrAar.class.B!= aps2.someJarOrAar.class.B 即使在两个项目中使用了完全相同的jar / aar。 因此,我将始终在(Fragment)c.newInstance();上收到ClassCastException。

我试图通过插件的classloader进行强制转换,并通过主包的classloader进行强制转换,但无济于事。我认为没有办法解决,因为即使罐子/ aar中具有相同的名称和相同的类名,也无法保证罐子/ aar确实是相同的,因此即使它们相同也将它们视为不同是一个安全问题

我当然希望有一些解决方法(除了像我现在要做的那样,即使在android 9下也继续使用android.app.Fragment),但是我也很感谢提供注释,说明为什么共享类绝对不能使用它(例如support.v4.Fragment)。