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();
}
}
使用就這麼簡單!!!
當然,目前只是一個實現頁面跳轉的路由,以後慢慢改善!!