Mobile Development 12 min read

Android Plugin‑Based Skinning: Implementation Principles and Framework Analysis

This article explains how to implement Android plugin-based skinning by defining a skinning interface, customizing LayoutInflater.Factory2, loading external skin resources, and analyzing the Android‑skin‑support library’s code, providing a clear step‑by‑step guide for mobile developers.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Android Plugin‑Based Skinning: Implementation Principles and Framework Analysis

This blog builds on a previous article and shares the implementation ideas for an Android skinning feature, along with a source‑code analysis of the popular Android‑skin‑support library.

Typical skinning changes view attributes such as setColor , setBackgroundColor , and setDrawable . To avoid calling these methods individually, a skinning interface is defined and views that need skinning implement their own logic.

Overall Approach

Define a skinning interface for views that require skin changes.

Set a custom LayoutInflater.Factory2 to replace XML‑defined views with those implementing the skinning interface and record them.

Create a skin package that mirrors the app’s resource names but with different values; during skin change, look up resources by name in the skin package.

Iterate over the recorded skinning views and invoke their skin‑change method.

The following code shows the skinning interface:

interface SkinSupportable {
    fun applySkin() // skin method
}

A custom SkinButton implements this interface:

class SkinButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : Button(context, attrs), SkinSupportable {

    // skin logic goes here
    override fun applySkin() {
        // path to skin APK
        val skinApkPath = "xxx/skin/skin.apk"
        // obtain PackageInfo of the skin APK
        val pInfo = context.packageManager.getPackageArchiveInfo(skinApkPath, PackageManager.GET_ACTIVITIES)
        pInfo.applicationInfo.sourceDir = skinApkPath
        pInfo.applicationInfo.publicSourceDir = skinApkPath
        // get Resources of the skin package
        val skinRes = context.packageManager.getResourcesForApplication(pInfo.applicationInfo)
        // app Resources for display metrics and configuration
        val res = context.resources
        // construct new Resources for the skin package
        val newRes = Resources(skinRes.assets, res.displayMetrics, res.configuration)
        // default resource id (e.g., background color)
        val defaultResId = R.color.colorPrimary
        val resName = res.getResourceEntryName(defaultResId) // name used to find the resource in the skin package
        val resType = res.getResourceTypeName(defaultResId)
        // find the same‑named resource in the skin package
        val skinResId = newRes.getIdentifier(resName, resType, pInfo.packageName)
        val skinColor = newRes.getColor(skinResId)
        setBackgroundColor(skinColor)
    }
}

In an activity, LayoutInflater.Factory2 is replaced in onCreate to swap a standard Button with SkinButton and record the view:

val skinViews = mutableListOf
()

override fun onCreate(savedInstanceState: Bundle?) {
    LayoutInflater.from(this).factory2 = object : LayoutInflater.Factory2 {
        override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
            return onCreateView(null, name, context, attrs)
        }
        override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
            if (name == "Button") {
                val view = SkinButton(context, attrs)
                skinViews.add(view) // record
                return view
            }
            return null
        }
    }
    super.onCreate(savedInstanceState)
}

bnSkin.setOnClickListener {
    for (view in skinViews) {
        view.applySkin() // apply skin
    }
}

The Android framework’s LayoutInflater.setFactory2 throws an exception if a factory has already been set:

private boolean mFactorySet; // false

public void setFactory2(Factory2 factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    // ...
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    }
    // ...
}

To avoid this limitation, the Android‑skin‑support library registers a global ActivityLifecycleCallbacks in the application and sets the factory via reflection when necessary. The core class is SkinActivityLifecycle :

public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
    public static SkinActivityLifecycle init(Application application) {
        if (sInstance == null) {
            synchronized (SkinActivityLifecycle.class) {
                if (sInstance == null) {
                    sInstance = new SkinActivityLifecycle(application);
                }
            }
        }
        return sInstance;
    }

    private SkinActivityLifecycle(Application application) {
        application.registerActivityLifecycleCallbacks(this);
        installLayoutFactory(application);
        SkinCompatManager.getInstance().addObserver(getObserver(application));
    }

    private void installLayoutFactory(Context context) {
        try {
            LayoutInflater layoutInflater = LayoutInflater.from(context);
            LayoutInflaterCompat.setFactory2(layoutInflater, getSkinDelegate(context));
        } catch (Throwable e) {
            Slog.i("SkinActivity", "A factory has already been set on this LayoutInflater");
        }
    }
}

The compatibility helper LayoutInflaterCompat.setFactory2 sets the factory directly on newer APIs and uses reflection on API < 21:

public static void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
    inflater.setFactory2(factory);
    if (Build.VERSION.SDK_INT < 21) {
        final LayoutInflater.Factory f = inflater.getFactory();
        if (f instanceof LayoutInflater.Factory2) {
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            forceSetFactory2(inflater, factory);
        }
    }
}

private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
    // reflection to set mFactory2 field
    sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
    sLayoutInflaterFactory2Field.setAccessible(true);
    sLayoutInflaterFactory2Field.set(inflater, factory);
}

The library’s SkinCompatDelegate implements LayoutInflater.Factory2 , creates views, and stores those that implement SkinCompatSupportable using weak references to avoid memory leaks:

private List
> mSkinHelpers = new CopyOnWriteArrayList<>();

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    View view = createView(parent, name, context, attrs);
    if (view instanceof SkinCompatSupportable) {
        mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
    }
    return view;
}

public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    View view = createViewFromHackInflater(context, name, attrs);
    if (view == null) {
        view = createViewFromInflater(context, name, attrs);
    }
    if (view == null) {
        view = createViewFromTag(context, name, attrs);
    }
    return view;
}

Loading resources from an external skin package is handled by SkinCompatManager.getSkinResources :

public Resources getSkinResources(String skinPkgPath) {
    PackageInfo packageInfo = mAppContext.getPackageManager().getPackageArchiveInfo(skinPkgPath, 0);
    packageInfo.applicationInfo.sourceDir = skinPkgPath;
    packageInfo.applicationInfo.publicSourceDir = skinPkgPath;
    Resources res = mAppContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
    Resources superRes = mAppContext.getResources();
    return new Resources(res.getAssets(), superRes.getDisplayMetrics(), superRes.getConfiguration());
}

When a view requests a resource, SkinCompatResources.getTargetResId finds the corresponding resource in the skin package by name and type:

public int getTargetResId(Context context, int resId) {
    String resName = null;
    if (mStrategy != null) {
        resName = mStrategy.getTargetResourceEntryName(context, mSkinName, resId);
    }
    if (TextUtils.isEmpty(resName)) {
        resName = context.getResources().getResourceEntryName(resId);
    }
    String type = context.getResources().getResourceTypeName(resId);
    // mResources is the skin package Resources
    return mResources.getIdentifier(resName, type, mSkinPkgName);
}

In summary, the plugin‑based skinning concept is straightforward: intercept view creation via a custom Factory2 , replace target views with skin‑aware implementations, and dynamically fetch matching resources from an external skin APK. The Android‑skin‑support library demonstrates a clean, memory‑safe design with weak references, caching, and extensible skin strategies, making it an excellent reference for developers implementing runtime skinning on Android.

JavaAndroidPluginKotlinresourcesLayoutInflaterSkinning
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.