Before the review

  • Android componentization – Basics (I) – Componentization and integration
  • Android componentization – Basics 2 – Communication between components
  • Android componentization – Basics (3) – ARouter

In the previous article, we looked at the basic functionality of the ARouter route and its internals. So far, we have completed the componentized architecture of Android APP and solved the communication problem between components. However, as described above, we only solved the communication between components within the Android APP. In actual development, communication involves not only the internal Android APP, but also some external use scenarios, such as:

  • 1. Interface routes are delivered, dynamic pages are redirected, or functions are activated
  • 2. External APP invokes pages or functions of our APP
  • 3. Web H5 JS invokes the page or function of APP

As we know, these scenarios are not unique to Android APP, iOS, H5 and background interface are all involved, and it is necessary for all terminals to cooperate with each other to complete the related functions of the above scenarios. Therefore, unified communication rules between all terminals are particularly important, and URL Scheme may be the only choice.

URL Scheme

We know that a normal Url link looks like this:

https://www.baidu.com?key=hello
Copy the code

It consists of scheme protocol header, host domain name, path path and Query parameter

[scheme:][host][path][?query] 
Copy the code

Both Android and iOS support developers to register a custom URL Scheme for their own apps, which is convenient for other apps to communicate with them.

How to design a URL that meets the requirements of our business scene is the first problem we need to solve. Through the analysis of the requirements of the above three scenes, we know that the designed URL is nothing more than to efficiently or significantly express the two requirements of “page jump” and “function call”.

ARouter的URL Scheme

In the previous article, ARouter mentioned that it supports the jump of standard URL Scheme. We first try to use ARouter directly to see if it can meet our requirements. Taking the second scenario as an example, the external APP calls up the UserMainActivity and isLogin() functions of the User module.

  • ARouter – Standard page jump URL definition
Page routing: example://www.demo.com/user/UserMainActivityCopy the code

Above is the jump UserMainActivity route that I defined. Just register the Scheme in androidmanifest.xml and let ARouter do the jump.

        <activity android:name=".AppMainActivity">
          
            ...

            <intent-filter>
                <data
                    android:host="www.demo.com"
                    android:scheme="example"/>

                <action android:name="android.intent.action.VIEW"/>

                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>

        </activity>
Copy the code
// Activity class AppMainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) handUri(); } private fun handUri() { val uri: Uri? = intent.data if (uri ! Arouter.getinstance ().build(uri).navigation()}}}Copy the code

Externally, I use the browser to invoke our APP

< a href = "example://www.demo.com/user/UserMainActivity" > jump APP < / a >Copy the code

Under test:

  • ARouter – Standard function call URL definition
The function of routing: example://www.demo.com/user/isLoginCopy the code

But when I define the function call URL according to the standard URL specification, ARouter doesn’t know what the URL means. Looking at the source code, we can see that ARouter handles the following logic using the PATH part of the URL as its own routing part.

final class _ARouter { ... protected Postcard build(Uri uri) { if (null == uri || TextUtils.isEmpty(uri.toString())) { throw new HandlerException(Consts.TAG + "Parameter invalid!" ); } else { PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); if (null ! = pService) { uri = pService.forUri(uri); Return new Postcard(uri.getPath(), extractGroup(uri.getPath()), URI, null); }}}Copy the code

Obviously, the /user/isLogin function does not match the route. Remember whose route was provided by the ARouter cross-module function call? Yes Servcie! There may be several functions under a Service, and isLogin() is just one. We can only get Service objects through routing, and if we want to call to a specific Service internal function through url, we have to write a lot of code to match, and this code is very poor maintenance.

Therefore, I personally do not suggest handing the URL Scheme to ARouter directly. ARouter only needs to do a good job in internal communication of its own APP. For URL processing, the upper layer can build the wheel and give it to parse, and then ARouter will complete the jump or function call.

User-defined URL Scheme

Since the Url needs to be handed to the upper wheel to deal with, then the definition of THE Url does not need to refer to the ARouter specification constraints, the actual development of the DEFINITION of the Url is often not idealized in accordance with the requirements of ARouter to design, after all, suitable for multi-terminal collaboration Url is a good Url, the following I define the Url format as an example, Start making the wheel.

example://www.demo.com/openApp?action= {" action_type ":" jump ", / / action type Jump, jump) or function (call) "page_type" : "native", // Native Web RN flutter "path":"/user/UserMainActivity", // actual route address "params":"{\"key ": \"value\"}"Copy the code

It can be seen that [Scheme :][host][path] is fixed, and the business logic is determined by the action field. The action consists of four parts:

  • Action_type: Identify route action, jump(page jump), call(function call)
  • Page_type: when the route action is jump, identify the page jump target type, native (App native page), Web (H5 page), RN (RN page), and flutter(page).
  • Path: indicates the actual routing address
  • Params: parameters required by the target page or function

These four parts can basically meet the requirements of page skipping and function setting. The Bean object corresponding to action is as follows:

data class RouterAction( var action_type: String? Var page_type: String? Var page_type: String? Var page_type: String? ", // Page types: native, web (H5), RN (rn), flutter(flutter) var path: String? Var params: JsonObject? "{key: value}")Copy the code

Customize the page redirect Url

OK, now that we have routing rules, let’s see what the page jump route looks like now.

Page routing:  example://www.demo.com/openApp?action={"action_type":"jump" ,"page_type":"native","path":"/user/UserMainActivity" , "params":"{}" }Copy the code

With routing in place, begin to complete the three scenarios mentioned at the beginning of this article!

Scenario 1: Interface routes are delivered and dynamic pages are redirected

This is a very common scenario, for example: Order details button at the bottom of the page, in a different order status will show different business logic button (to apply for a refund, cancel the order, evaluation, and contact customer service, etc.), in the past these buttons of display rules are written by mobile developers in the client code, once the order business adjustment, the original display rules change, we have to modify the client code, The process has too much influence, and even need to open a stronger version. Now with routing, these display rules can be moved to the server and dynamically returned by the backend developer.

Below, I will simulate the interface return routing scenario, involving JSON data as follows:

{" btnBgColor ":" # FF0000 ", "btnColor" : "# FFFFFF", "btnTxt" : "jump User page", "openUrl":"example://www.demo.com/openApp?action\u003d{\"action_type\":\"jump\" ,\"page_type\":\"native\",\"path\":\"/user/UserMainActivity\" , \"params\":{} }" }Copy the code

Mock request code:

private fun getBtnForNet() { Thread { Thread.sleep(1500) val json = "{\" btnBgColor \ ": \" # FF0000 \ ", \ "btnColor \" : \ "# FFFFFF \", \ "btnTxt \" : \ "jump User page \", \ "openUrl \" : \ "example://www.demo.com/openA pp?action\\u003d{\\\"action_type\\\":\\\"jump\\\" ,\\\"page_type\\\":\\\"native\\\",\\\"path\\\":\\\"/user/UserMainActivity\\\" , \\\"params\\\":{} }\"}" runOnUiThread { showButton(JsonParser.fromJsonObj(json, ButtonBean::class.java)) } }.start() } private fun showButton(btnBean: ButtonBean) { val button = Button(this) btnBean.btnColor?.run { button.setTextColor(Color.parseColor(this)) } btnBean.btnBgColor?.run { button.setBackgroundColor(Color.parseColor(this)) } button.text = btnBean.btnTxt button.setOnClickListener { RouterManager.jumpUrl(this, btnBean.openUrl) } findViewById<ViewGroup>(R.id.layoutParent).addView(button) }Copy the code

As you can see, I’ve handed the routing to the RouterManager’s jumpUrl() function, which is really just my wrapper management class for the ARouter API, and I’ve handed the HANDLING of the URL Scheme to the aforementioned wheel: SchemeHelper.

/** * RouterManager {private val SchemeHelper by lazy {SchemeHelper()} /** * jump Activity ** / fun goActivity(context: Context? , path: String, bundle: Bundle? = null) { ARouter.getInstance().build(path) .with(bundle) .navigation(context) } ... /** * Scheme route jump ** / fun jumpUrl(context: context, jumpUrl: String? , callBefore: ((context: Context, action: RouterAction) -> Unit)? = null, callAfter: ((context: Context, json: String?) -> Unit)? = null ) { mSchemeHelper.jumpUrl(context, jumpUrl, callBefore, callAfter) } ... }Copy the code

What SchemeHepler does, of course, is parse the Url, pull out the four parts of the action parameter, and jump to the page based on those four parts of the data.

Class SchemeHelper {/** * Scheme route jump ** / fun jumpUrl(context: context, jumpUrl: String? , callBefore: ((context: Context, action: RouterAction) -> Unit)? = null, callAfter: ((context: Context, json: String?) -> Unit)? = null ) { try { XLog.i("jumpUrl:: If (textutils.isempty (jumpUrl)) return val schemeUrl = commRouter.scheme. run {"$Scheme $HOST$PATH"} if (jumpUrl!! .startswith (schemeUrl)) {// Parse Action var actionJson = urlutils.geturlParam (jumpUrl, CommRouter.Scheme.ACTION) actionJson = URLDecoder.decode(actionJson) val routerAction = Jsonparser.fromjsonobj (actionJson, RouterAction::class.java) dispatchAction(context, RouterAction, callBefore, CallAfter)}} Catch (e: Exception) {xlog.e (e)}} /** * dispatchAction(context: Context, routerAction: RouterAction, callBefore: ((context: Context, action: RouterAction) -> Unit)? = null, callAfter: ((context: Context, json: String?) -> Unit)? = null ) { if (TextUtils.isEmpty(routerAction.action_type)) return when (routerAction.action_type) { ACTION_TYPE_JUMP -> {// Jump page jumpAction(context, routerAction, callBefore, ACTION_TYPE_CALL -> {callAfter)} commrouter.scheme. ACTION_TYPE_CALL -> { CallAfter)} else -> {xlog. e(" Unknown behavior Type (action_type) : : ${routerAction.action_type}")}}... }Copy the code

Through route distribution, we have resolved the Url into jumpAction (Route jump) and callAction (function call). We know that the interface now delivers the “/User /UserMainActivity” page jump route. This route is the ARouter route configured on UserMainActivity, and the rest is up to ARouter.

Private fun jumpAction(Context: context, routerAction: routerAction, callBefore: (context: Context, action: RouterAction) -> Unit)? = null, callAfter: ((context: Context, json: String) -> Unit)? = null ) { if (TextUtils.isEmpty(routerAction.page_type) || TextUtils.isEmpty(routerAction.path)) return callBefore? .invoke(context, routerAction) when (routerAction.page_type) { CommRouter.Scheme.PAGE_TYPE_NATIVE, Commrouter.scheme. PAGE_TYPE_WEB -> {// Native pages & H5 handle the same, Routermanager. goActivity(Context, RouterAction. path!! , bundle)} commRoute.scheme. PAGE_TYPE_RN -> {// RN} else -> {xlog. e(" Unknown page type (page_type) : : ${routerAction.page_type}") } } callAfter? .invoke(context, "") }Copy the code

Let’s run it and see what it looks like:

Custom function invokes Url

Page jump complete, next look at the function of the route and how to implement.

Functional routing:  example://www.demo.com/openApp?action={"action_type":"call" ,"page_type":"","path":"/user/isLogin" , "params":"{}" }Copy the code
Scenario 1: Interface routes are delivered and dynamic functions are enabled

** Consider: In the beginning, we mentioned that ARouter is not able to call a function directly through the route, but needs to use the Service to do the transfer, and “/user/isLogin” ** belongs to which Service. To the developer of the user module, it is known that this function belongs to his module, but not to other developers. If each business module provides a function, it is not appropriate to write code in the SchemeHelper wheel to invoke its own module’s Service function. How to do?

Remember how we implemented the routing framework manually in the chapter “Android componentization — Basics 2 — Communication between components”? Similarly, you need to register the route to SchemeHelper manually. The wheels don’t have to worry about which Service to use and which function to invoke. The registered developer can just call back to implement it.

Here is the callback interface I defined:

Interface IRouterCall {/** * @param Context * @param path function routing * @param bundle parameters * @return This function can return data, * */ Fun handleCall(Context: context, path:String, bundle: bundle): String? }Copy the code

After writing external functions, the service developer implements a subclass of IRouterCall and registers the subclass object and its routes with SchemeHelper.

ARouter Service ** / @route (path = "/ User /UserService") class IUserServiceImpl2: IUserService2 { override fun init(context: Context?) {} /** * override fun isLogin(): Boolean {// Whether to log in service logic return true}} /** */ user/isLogin function route processing class */ class IsLoginCall: IRouterCall { override fun handleCall(context: Context, path: String, bundle: Bundle): String? { val ret = RouterManager.getService(IUserService2::class.java)? .isLogin() ? : False toast. makeText(context, "user login status: $ret", toast.length_short).show() return null}} /** * UserInit {fun init(context: The Context) {initRouter ()} private fun initRouter () {/ / registered function routing/user/isLogin RouterManager. AddRouterCall ("/user/isLogin"  , IsLoginCall()) } }Copy the code

As I did when I implemented the routing framework manually, I stored the routing paths and Call objects in the Map container. In the jump section of the routing page, I distributed the routing processing to the jumpAction() and callAction() functions, where I matched the routes in the callAction() function, Call back to the developer.

object RouterManager { private val mSchemeHelper by lazy { SchemeHelper() } ... ** / Fun addRouterCall(path: String, call: IRouterCall ) { mSchemeHelper.registerCall(path, call) } ... }Copy the code
Class SchemeHelper {// actionType: Call Route container private val mCallGroup = HashMap<String, IRouterCall? >() /** * registerCall function ** / funregistercall (path: String, Call: IRouterCall) {mCallGroup[path] = call} /** * Private fun callAction(context: context, routerAction: RouterAction, callBefore: ((context: Context, action: RouterAction) -> Unit)? = null, callAfter: ((context: Context, json: String?) -> Unit)? = null) {if (texTutils.isEmpty (RouterAction.path)) return call before? .invoke(context, routerAction) var resultJson: String? = null val call = mCallGroup[routerAction.path] if (call ! ResultJson = Call.handlecAll (Context, RouterAction.path!! , bundle)} else {when (RouterAction.page_type) {// TODO implements handleCommCall(context, RouterAction.path!! , bundle) } } callAfter? .invoke(context, resultJson) } }Copy the code

To test whether the App module can enable the isLogin function of the User module:

private fun getBtnForNet() { Thread { Thread.sleep(1500) val json = "{\" btnBgColor \ ": \" # FF0000 \ ", \ "btnColor \" : \ "# FFFFFF \", \ "btnTxt \" : \ "isLogin \", \ "openUrl \" : \ "example://www.demo.com/open App?action\\u003d{\\\"action_type\\\":\\\"call\\\" ,\\\"page_type\\\":\\\"\\\",\\\"path\\\":\\\"/user/isLogin\\\" , \\\"params\\\":{} }\"}" runOnUiThread { showButton(JsonParser.fromJsonObj(json, ButtonBean::class.java)) } }.start() }Copy the code

So far, the SchemeHelper wheel is basically formed, and we have successfully got through the dynamic page jump and function call scenarios. As for the implementation of scenario 2 and scenario 3, change the communication data into Scheme Url, get the Url and throw it to the wheel. I will paste the core code below and not demonstrate it separately.

Scenario 2: External APP invokes pages or functions of our APP
  • Listing file configuration Scheme:
    <application>

        <activity android:name=".AppMainActivity">
            ...

            <intent-filter>
                <data
                    android:host="www.demo.com"
                    android:scheme="example"/>

                <action android:name="android.intent.action.VIEW"/>

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

    </application>
Copy the code
  • The Activity picks up the Url and hands it to the wheel
@Route(path = "/app/AppMainActivity") class AppMainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) handUri(); } private fun handUri() { RouterManager.jumpUrl(this, intent? .dataString) } }Copy the code
Scenario 3: Web H5 Js invokes APP native pages or functions
  • WebView exposes JS interface
MWebView. AddJavascriptInterface (JsApi (mContext), "androidJs")/Js interface for H5 call * * * * * / class JsApi (private val mContext: OpenApp (openUrl: String) {RouterManager.jumpurl (mContext, openUrl)}}Copy the code
  • H5 Js calls
window.androidJs.openApp("example://www.demo.com/openApp?action={\"action_type\":\"call\" ,\"page_type\":\"\",\"path\":\"/user/isLogin\" , \"params\":\"{}\" }")
Copy the code

summary

In this paper, we learn the use of URL Scheme in the componentalized scenario, it provides a unified standard for communication in the development of multiple scenarios, making business implementation more flexible, a well-designed URL route can make developers know its role at a glance, thus reducing the cost of code maintenance; Imagine scenarios 1 and 3 where, without the use of Url routing for communication, the client developer would not have to write a lot of code to improve these features, which involve process changes that are often accompanied by publishing. Using routing for communication would reduce the publishing frequency.

Although we have completed the communication function of Url routing, manual registration is still adopted when dealing with Call routing. We learned from the previous two articles that manual registration will be done by startup when the APP starts. This route may be loaded into memory before being used, resulting in extra memory overhead. In the process of learning ARouter, we found that it is through APT (Annotation processor) scheme to complete the registration related work.

In the next article, we’ll look at ARouter’s APT implementation and write an annotation handler that does Call routing registration. See you next time