1. 程式人生 > >Android 元件化之路 路由設計

Android 元件化之路 路由設計

基於公司業務發展,公司的APP需求不斷增加,應用也略顯“臃腫”。想著趁現在不那麼“糟糕”,時間也比較寬裕,把專案結構整整,因而走上了元件化之路。

模組化 VS 元件化

模組化: 將一個程式按照其功能做拆分,分成相互獨立的模組,以便於每個模組只包含與其功能相關的內容。

元件化: 基於可重用的目的,將一個大的軟體系統按照分離關注點的形式,拆分成多個獨立的元件。

區別:
模組化和元件化本質思想是一樣的,都是“大化小”,兩者的目的都是為了重用和解耦,只是叫法不一樣。如果非要說區別,那麼可以認為模組化粒度更小,更側重於重用;而元件化粒度稍大於模組,更側重於業務解耦。

為什麼需要路由

路由主要的功能是實現模組化之間介面(Activity,網頁)的跳轉,那為什麼不直接調顯式跳轉呢?

據我總結,主要有以下3點原因:

  • 減少模組之間的依賴。因為顯式跳轉需要引用其他模組。

  • 跳轉時,引數不明確,溝通需要時間。因為顯式呼叫時,需要知道目標Activity需要哪些引數,如果引數改了,如果溝通不到位或者文件沒有及時更新,會出現錯誤等問題。

  • 簡化H5頁面與應用頁面之間的跳轉。

雖然網上有很多庫,如阿里的ARouter。我不是有輪子不用,只是想著能學習多點元件化的知識,出於這個目的,決定自定義路由。

跳轉頁面的方式

Activity之間的跳轉有2種方式:顯式和隱式,顯式顯然不行,我們來看看隱式(如果不知道,請參考Android開發藝術探索筆記(2)- Activity的啟動模式

)。

隱式跳轉的條件是Intent要麼匹配action,要麼匹配data。我這裡選擇匹配data方式,理由是匹配data方式是利用URL Scheme頁面跳轉協議,可以非常方便跳轉app中的各個頁面,也可以通過H5頁面跳轉頁面等

什麼是URL Scheme

Android中的scheme是一種頁面內跳轉協議,通過定義自己的scheme協議,可以非常方便跳轉app中的各個頁面;通過scheme協議,伺服器可以定製化告訴App跳轉那個頁面,可以通過通知欄訊息定製化跳轉頁面,可以通過H5頁面跳轉頁面等。

URL Scheme格式

URL Scheme和Http的Uri很像,包括scheme,host,port,path,query。
例子:

app://user:8888/login?username=xiaoming
  • app:代表scheme,協議的名稱
  • user:代表host,地址域
  • 8888:代表port,埠號
  • login:代表path,路徑
  • username:代表query,傳遞的引數

使用URL Scheme

manifest宣告activity:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.johan.demo">
    <application
        <activity android:name=".SecondActivity">
            <intent-filter>
                <data android:scheme="app" android:host="user" android:port="8888" android:path="/login" />
                <!-- 一下幾行必須設定 -->
                <action android:name="android.intent.action.VIEW" /> <!-- 隱式呼叫必須宣告 -->
                <category android:name="android.intent.category.DEFAULT" /> <!-- 隱式呼叫必須宣告 -->
                <category android:name="android.intent.category.BROWSABLE" /> <!-- BROWSABLE的意思就是瀏覽器在特定條件下可以開啟你的Activity -->
            </intent-filter>
        </activity>
        ...
    </application>
</manifest>    

activity跳轉:

Intent intent = new Intent();
Uri uri = Uri.parse("app://user:8888/login?username=xiaoming");
intent.setData(uri);
startActivity(intent);

這種方式跳轉Activity有個致命的缺點,如果找不到對應的Uri,程式就會Crash,所以在跳轉之前要判斷一下Uri是否存在:

PackageManager packageManager = getPackageManager();
Intent intent = new Intent();
Uri uri = Uri.parse("app://user:8888/login?username=xiaoming");
intent.setData(uri);
List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0);
boolean isValid = !activities.isEmpty();
if (isValid) {
    startActivity(intent);
}

設計路由

既然跳轉方式選定了,但是還存在問題,就是引數不明確問題。如果能像retrofit那樣,方法決定引數,這樣就不用擔心引數不明確問題了。

稍微看過retrofit原始碼都知道,retrofit運用的動態代理模式,so我們模仿一下,Like This,程式碼如下:(dome,僅供參考)

public class SimpleRouter {

    // ApplicationContext
    private static Context routerContext;

    /**
     * 初始化
     * @param context
     */
    public static void init(Context context, int cacheSize) {
        routerContext = context.getApplicationContext();
    }

    @SuppressWarnings("unchecked")
    public static <T> T create(final Class<T> iRouter) {
        return (T) Proxy.newProxyInstance(iRouter.getClassLoader(), new Class[]{iRouter}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 開始解析
                // 域名,可以不寫
                String domain = null;
                if (iRouter.isAnnotationPresent(RouterDomain.class)) {
                    RouterDomain routerDomain = iRouter.getAnnotation(RouterDomain.class);
                    domain = routerDomain.value();
                }
                // 路徑,必須有,否則報錯
                if (!method.isAnnotationPresent(RouterPath.class)) {
                    throw new RuntimeException(iRouter.getName() + " " + method.getName() + " should has RouterPath Annotation");
                }
                RouterPath uriPath = method.getAnnotation(RouterPath.class);
                String path = uriPath.value();
                StringBuilder urlBuilder = new StringBuilder();
                if (domain != null) urlBuilder.append(domain);
                urlBuilder.append(path);
                // 引數
                Annotation[][] paramAnnotations = method.getParameterAnnotations();
                for (int index = 0; index < paramAnnotations.length; index++) {
                    Annotation[] paramAnnotation = paramAnnotations[0];
                    if (paramAnnotation == null || paramAnnotation.length == 0) {
                        continue;
                    }
                    if (paramAnnotation[0] instanceof RouterParam) {
                        RouterParam uriParam = (RouterParam) paramAnnotation[0];
                        String param = uriParam.value();
                        String connector = index == 0 ? "?" : "&";
                        urlBuilder.append(connector).append(param).append("=").append(args[index]);
                    }
                }
                // 開啟Activity
                openActivity(urlBuilder.toString());
                return null;
            }
        });
    }

    /**
     * 開啟Activity
     * @param url
     * @return
     */
    private static boolean openActivity(String url) {
        PackageManager packageManager = routerContext.getPackageManager();
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0);
        boolean isValid = !activities.isEmpty();
        if (isValid) {
            routerContext.startActivity(intent);
        }
        return isValid;
    }

}

應用動態代理模式,由介面生成代理類,呼叫介面方法時,就會執行代理類的invoke方法,開啟對應的Activity!

使用方法:比如我們有2個module,app和user-module。

先在user-module的manifest檔案宣告activity:

<activity android:name=".UserLoginActivity">
    <intent-filter>
        <data android:scheme="app" android:host="user" android:path="/login" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
</activity>

然後在app跳轉到user-module的介面:

// 我在Application初始化SimpleRouter,也可以在Activity中初始化
public class MainApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        SimpleRouter.init(this);
    }
}
// UserRouter介面
@RouterDomain("app://user")
public interface UserRouter {
    @RouterPath("/login")
    void goLogin();
}
// MainActivity跳轉
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    public void goToUserLogin(View view) {
        // 跳轉
        SimpleRouter.create(UserRouter.class).goLogin();
    }
}

使用就這麼簡單!!!

當然,目前只是一個實現頁面跳轉的路由,以後慢慢改善!!

參考資料