Android之组件化路由器的实现

一、背景

随着App开发的规模体积越来越大,开发编译的时间越来越久,同时,代码的耦合性可能变高,现在更需要组件化框架重新规划App的开发。在组件化开发的过程中,需要路由器来进行页面的跳转和传参数。其中,以阿里的ARouter最为火热,今天,给大家介绍一个实现比较简单的路由器框架。

二、使用

1、在AndroidManifest里面注册对应的ModuleName

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.syy.router">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name=".MyApplication"
        android:supportsRtl="true"
        android:theme="@style/Theme.Router">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".TestActivity">
            <meta-data
                android:name="moduleName"
                android:value="test|testAlias" />
        </activity>

        <activity android:name=".TestActivity1">
            <meta-data
                android:name="moduleName"
                android:value="test1" />
        </activity>

        <meta-data
            android:name="productName"
            android:value="router" />
    </application>

</manifest>

每个注册activity对应一个meta-data的,其中key为“moduleName”,value是“test”(举例),
每个Application下面注册一个meta-data的,作为这个工程的productName。

2、实现Router类

package com.syy.router;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

import com.syy.router.bean.ModuleInfo;
import com.syy.router.provider.ModuleInfoProvider;
import com.syy.router.utils.HttpHelper;

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class Router {
    public static final String TAG = "Router";

    private static Router mRouter;

    private Router() {

    }

    public synchronized static Router getInstance() {
        if (mRouter == null) {
            mRouter = new Router();
        }
        return mRouter;
    }

    public void initialize(Context context) throws Exception {
        String product = "";
        Bundle appMeta = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).metaData;
        if (appMeta != null) {
            product = appMeta.getString("productName");
        }
        PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
        for (ActivityInfo activityInfo : packageInfo.activities) {
            ActivityInfo metaInfo = context.getPackageManager().getActivityInfo(new ComponentName(activityInfo.packageName, activityInfo.name),
                    PackageManager.GET_META_DATA);
            if (metaInfo.metaData != null) {
                String moduleName = metaInfo.metaData.getString("moduleName");
                String target = metaInfo.metaData.getString("target");
                if (StringUtils.isEmpty(moduleName)) {
                    continue;
                }
                if (StringUtils.isNotEmpty(target)) {
                    if (StringUtils.isNotEmpty(product)) {
                        if (target.contains("!")) {
                            if (target.replace("!", "").equals(product)) {
                                continue;
                            }
                        } else {
                            List<String> targets = Arrays.asList(target.split("\\|"));
                            if (!targets.contains(product)) {
                                continue;
                            }
                        }
                    } else {
                        continue;
                    }
                }
                ModuleInfo moduleInfo = new ModuleInfo();
                moduleInfo.className = activityInfo.name;
                List<String> moduleNames = Arrays.asList(moduleName.split("\\|"));
                for (String moduleKey : moduleNames) {
                    if (!StringUtils.isEmpty(moduleKey)) {
                        if (ModuleInfoProvider.getInstance().isModuleExist(moduleKey)) {
                            throw new Exception("module name: " + moduleKey + " duplicated! Rename or add target is optional.");
                        }
                        ModuleInfoProvider.getInstance().addModuleInfo(moduleKey.trim(), moduleInfo);
                    }
                }
            }
        }
    }

    private void launchModule(Context context, String uriString, Bundle bundle, List<Integer> flags) {
        Log.d(TAG, "launchModule: uriString = " + uriString);
        try {
            if (TextUtils.isEmpty(uriString)) {
                return;
            }

            Uri uri = Uri.parse(uriString);
            String schema = uri.getScheme();
            String moduleName = uri.getHost();
            ModuleInfo targetModule = ModuleInfoProvider.getInstance().getModuleInfo(moduleName);
            if (targetModule != null) {
                Log.w("跳转的类","=="+targetModule.className);
                Intent intent = new Intent();
                intent.setClassName(context.getPackageName(), targetModule.className);
                intent.setData(uri);
                if (bundle != null) {
                    intent.putExtras(bundle);
                }
                if (flags != null && !flags.isEmpty()) {
                    for (Integer flag : flags) {
                        intent.addFlags(flag);
                    }
                }
                context.startActivity(intent);
            }
        } catch (Exception e) {
            Log.e(TAG, "launchModule: exception=" + e.toString());
        }

    }

    @Deprecated
    public void redirect(Context context, String uriString) {
        launchModule(context, uriString, null, null);
    }

    @Deprecated
    public void redirect(Context context, String uriString, Bundle bundle) {
        launchModule(context, uriString, bundle, null);
    }

    public void redirect(Context context, String moduleName, Map<String, String> params) {
        redirect(context, moduleName, params, null, null);
    }

    public void redirect(Context context, String moduleName, Map<String, String> params, int flag) {
        List<Integer> flags = new ArrayList<Integer>();
        flags.add(flag);
        redirect(context, moduleName, params, null, flags);
    }

    public void redirect(Context context, String moduleName, Map<String, String> params, List<Integer> flags) {
        redirect(context, moduleName, params, null, flags);
    }

    public void redirect(Context context, String moduleName, Map<String, String> params, Bundle bundle, List<Integer> flags) {
        String url;
        Uri uri = Uri.parse(HttpHelper.getAppSchema(context) + "://" + moduleName);
        if (params != null) {
            Uri.Builder builder = uri.buildUpon();
            for (Map.Entry<String, String> entry : params.entrySet()) {
                builder.appendQueryParameter(entry.getKey(), HttpHelper.getNotNullParam(entry.getValue()));
            }
            url = builder.build().toString();
        } else {
            url = uri.toString();
        }
        launchModule(context, url, bundle, flags);
    }
}

在Application里面进行Router的初始化。

 Router.getInstance().initialize(getApplicationContext());

每次app启动的时候,进行初始化,获取到所有AndroidManifest里面所有的Activity节点,进行一个遍历过程,找到每个activity对应的meta-data属性,将其放在一个hashMap里面进行保存,key为moduleName对应的值,value是包含了这个activity对应的class属性,如果过程包含了同样的moduleName的对象,则抛出异常。其中,moduleName可以对应多个名字,用"|"进行隔开,就是说同一个activity可以对应多个名称的moduleName,因为可能会有多个场景对应一个activity的情况出现。在“launchModule“的方法里面,从刚刚说的那个hashmap里面取出对应的activity的class对象,通过Intent启动的方式打开activity。

一些使用规则:
AndroidManifest里面注册:

  <activity android:name=".TestActivity">
            <meta-data
                android:name="moduleName"
                android:value="test|testAlias" />
        </activity>

TestActivity有两个对应的moduleName:test和testAlias
1、

HashMap<String, String> params = new HashMap<String, String>();
            params.put("params1", "1");
            params.put("params2", "2");
            Router.getInstance().redirect(MainActivity.this, "test", params);

2、完整的地址跳转格式:

Router.getInstance().redirect(MainActivity.this, "router://test?params1=1&params2=2");

3、

HashMap<String, String> params = new HashMap<String, String>();
            params.put("params1", "1");
            params.put("params2", "2");
            Router.getInstance().redirect(MainActivity.this, "testAlias", params);

通过

String host = getIntent().getData().getHost();

可以判断路由器中传入的host是test还是testAlias,这样我们可以区分是哪一个场景。

 String params1 = getIntent().getData().getQueryParameter("params1");
 String params2 = getIntent().getData().getQueryParameter("params2");

通过这种方式可以得到传入的参数是什么。
4、已经登入账号的情况

UserInfoProvider.getInstance().setUserId("1111111");
HashMap<String, String> params = new HashMap<String, String>();
params.put("params1", "1");
params.put("params2", "2");
Router.getInstance().redirect(MainActivity.this, "lc/test", params);                              

通过moduleName为“lc”的LoginCheckActivity的透明页面去判断是否要去跳转到登入页面。


public class LoginCheckActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login_check);

        try {
            String schema = HttpHelper.getAppSchema(this);
            String redirectUrl = schema + ":/" + getIntent().getData().getPath() + "?" + getIntent().getData().getEncodedQuery();
            if (getIntent().getData() != null) {
                if (StringUtils.isEmpty(UserInfoProvider.getInstance().getUserId())) {
                    String url = schema + "://login?redirect=" + Uri.encode(redirectUrl);
                    Router.getInstance().redirect(LoginCheckActivity.this, url);
                } else {
                    Router.getInstance().redirect(LoginCheckActivity.this, redirectUrl);
                }
            }
        } catch (Exception e) {

        }
        finish();
    }
}

5、如果没有登入账号,则需要先跳转到LoginActivity中,登入账号成功后再跳转到指定的页面


public class LoginActivity extends AppCompatActivity {
    private String redirectUrl;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_login);

        if (getIntent().getData() != null) {
            redirectUrl = getIntent().getData().getQueryParameter("redirect");
        }
        new Handler().postDelayed(() -> {
            Toast.makeText(LoginActivity.this, "已经登入完成", Toast.LENGTH_SHORT).show();
            redirectNextPage();
            finish();
        }, 4000);
    }

    private void redirectNextPage(){
        Router.getInstance().redirect(LoginActivity.this, redirectUrl);
    }
}

6、如果需要传递大的对象时

            HashMap<String, String> params = new HashMap<String, String>();
            params.put("params1", "1");
            params.put("params2", "2");
            BigBean bigBean = new BigBean();
            bigBean.setName("name");
            params.put("info", String.valueOf(ModelStore.getInstance().put(bigBean)));
            Router.getInstance().redirect(MainActivity.this, "test", params);

我们可以通过ModelStore的工具类来达到目的


public class ModelStore {
    private static ModelStore instance;
    private SparseArray<Object> models = new SparseArray<>();

    public synchronized static ModelStore getInstance() {
        if (instance == null) {
            instance = new ModelStore();
        }
        return instance;
    }

    public synchronized int put(Object obj) {
        if (obj == null) {
            return 0;
        }
        int key = obj.hashCode();
        models.put(key, obj);
        return key;
    }

    public synchronized Object pop(int key) {
        Object obj = models.get(key);
        models.remove(key);
        return obj;
    }

    public synchronized boolean fetch(int key) {
        return models.get(key) != null;
    }
}

通过传递该对象的hashcode到下一个界面,保存当前的对象到ModelStore里面去,
再在下一个页面去获取当前的对象。

params.put("info", String.valueOf(ModelStore.getInstance().put(bigBean)));
                String infoIdValue = getIntent().getData().getQueryParameter("info");
                if (StringUtils.isNotEmpty(infoIdValue)) {
                    Integer infoId = Integer.parseInt(infoIdValue);
                    BigBean bigBean = (BigBean) ModelStore.getInstance().pop(infoId);
                    Log.d(TAG, "onCreate: " +bigBean.getName());
                }

这样就可以达到传递大的对象的目的。

三、源码地址

上一篇:【设计模式从入门到精通】20-状态模式


下一篇:丰富Bean的配置