origin
As apps grow, we will inevitably encounter the following requirements:
- H5 jump native interface
- Notification Click the relevant interface
- Switch to different screens based on background data, for example, switch to different screens after successful login or switch to different screens based on operation requirements
- The AppLink switch is enabled
In order to solve these problems, apps generally define a scheme jump protocol, which is implemented by multiple terminals to solve various operational requirements. Today to analyze the design and implementation of the latest QMUI version QMUischaehandler.
A scheme would look something like this:
schemeName://action? param1=value1¶m2=value2Copy the code
Such as:
qmui://home? tab=2Copy the code
From a technical point of view, it is not very difficult to implement a scheme jump, which consists of the following two steps:
- Analytical scheme
- The specified page is displayed based on the resolution result
But if you don’t design it when you write code, it’s easy to have a bunch of if else’s. Such as:
if(action=="action1"){
doAction1(params)
}else if(action=="action2"){
doAction2(params)
}else{... }Copy the code
Every time a new scheme is added, an if is added until it becomes a giant piece of bad code that can’t be changed. Therefore, we should think frequently, reconstruct more, as soon as possible through the design of a good framework to liberate their hands.
For if else refactorings, a basic approach is to put all the conditions and the behavior to be performed in a map, and then query the map to get the behavior to be performed. We can build this map with annotations and code generation, reducing the amount of code we need to write. In addition, we need to consider various functional requirements:
- You can set the interceptor, for example, to jump to some interface. If you are not in the login state, you may need to jump to the login interface
- Arguments can specify some base types. The arguments carried by Scheme are all strings, but we want it to be easy to convert to the base types we need
- The same action can have different jump behaviors based on different parameters. For example, the page to jump to may be different between comic books and ordinary books
- If the current interface is the target interface, you can refresh the current interface or start a new interface
- For QMUI, both activities and fragments are supported, so Scheme should support both
- You can customize the new interface instantiation method
Interface design
For the development of any library, in order to make business users feel comfortable, it is necessary to ensure the function of the library is strong enough and the convenience of using. The QMUIScheme is mainly an entrance class, QMUehandler. And ActivityScheme and FragmentScheme annotations.
QMUISchemeHandler
QMUISchemeHandler is instantiated by Builder mode:
/ / set schemeName
val instance = QMUISchemeHandler.Builder("qmui://")
// Prevents short-time classes from triggering the same Scheme jump multiple times
.blockSameSchemeTimeout(1000)
// Scheme parameters decode
.addInterpolator(new QMUISchemeParamValueDecoder())
.addInterpolator(...)
// The fragment instantiates factory by default
.defaultFragmentFactory(...)
// The default activity instantiates factory
.defaultIntentFactory(...)
// The default scheme matcher
.defaultSchemeMatcher(...)
.build();
if(! instance.handle("qmui://xxx")) {// Scheme is not handled.
}
Copy the code
In most scenarios, QMUISchemeHandler adopts the singleton model. It can set multiple interceptors, default fragments, default instance factories for activities, and default matchers. Instance factories and matchers are provided with default implementations, and most scenarios are of no concern to the caller. This is all set to global default values, but at the Scheme annotation level, you can specify different values for each scheme to suit possible customization needs.
ActivityScheme and FragmentScheme annotations
These two annotations are very similar, but because fragments have a few more configuration items, they are separate.
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface ActivityScheme {
/ / scheme of action
String name(a);
// A list of required arguments for scenarios that support multiple schemes for the same action. Each item can be "type=4" to specify a value, or only "type" to match any value
String[] required() default {};
/ / if the current interface scheme is jump target, you can choose to refresh the current interface, of course, the current interface must implement ActivitySchemeRefreshable
boolean useRefreshIfCurrentMatched(a) default false;
// Define the matching method of current Scheme. The value is qmuematcherClass<? > customMatcher()default void.class;
// Customize the current Activity instance factory. The value is QMUISchemeIntentFactoryClass<? > customFactory()default void.class;
/ / type, of the specified argument support int/bool/long/float, double the base type, do not specify a string type
String[] keysWithIntValue() default {};
String[] keysWithBoolValue() default {};
String[] keysWithLongValue() default {};
String[] keysWithFloatValue() default {};
String[] keysWithDoubleValue() default {};
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface FragmentScheme {
// These parameters are the same as ActivityScheme
String name(a);
String[] required() default{}; Class<? > customMatcher()default void.class;
String[] keysWithIntValue() default {};
String[] keysWithBoolValue() default {};
String[] keysWithLongValue() default {};
String[] keysWithFloatValue() default {};
String[] keysWithDoubleValue() default {};
/ / with ActivityScheme, but the current UI must implement FragmentSchemeRefreshable
boolean useRefreshIfCurrentMatched(a) default false;
/ / with ActivityScheme, but the value is QMUISchemeFragmentFactory implementation classClass<? > customFactory()default void.class;
// A list of activities that can host the target Fragment. If the current activity is not in the list, start a new activity with the first item of the ActivitiesClass<? >[] activities();// Whether to force a new Activity to start
boolean forceNewActivity(a) default false;
// You can use scheme parameters to control whether to force a new Activity to start
String forceNewActivityKey(a) default "";
}
Copy the code
As you can see, the various requirements we listed earlier are embodied in SchemeHandler and both Schemes.
use
For business users, we only need to annotate the Activity or Fragment. Qmuehandler will by default parse the parameters into the intent of the Activity or the Fragment arguments, so we can get the values we care about in onCreate:
@ActivityScheme(name="activity1")
class Activity1: QMUIActivity{
override fun onCreate(...).{...if(isStartedByScheme()){
// Get the value of the parameter with intent Extra
val param1 = getIntent().getStringExtra(paramName)
}
}
}
@FragmentScheme(name="activity1", activities = {QDMainActivity.class})
class Fragment1: QMUIFragment{
override fun onCreate(...).{...if(isStartedByScheme()){
// Arguments to get the value of the argument
val param1 = getArguments().getString(paramName)
}
}
}
Copy the code
This method of passing values is consistent with the official design of Android, which requires the Fragment to follow the no-argument constructor.
For webViews, we can handle scheme jumps by overriding WebViewClient#shouldOverrideUrlLoading:
class MyWebViewClient: WebViewClient{
override fun shouldOverrideUrlLoading(view: WebView, url: String){
if(schemeHandler.handle(url)){
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest){
if(schemeHandler.handle(request.getUrl().toString())){
return true;
}
return super.shouldOverrideUrlLoading(view, request); }}Copy the code
implementation
QMUISchemeHandler uses code generation method to generate a SchemeMapImpl class at compilation time, which realizes SchemeMap class
public interface SchemeMap {
// Find SchemeItem with actions and parameters
SchemeItem findScheme(QMUISchemeHandler handler, String schemeAction, Map<String, String> params);
// Determine whether schemeAction exists
boolean exists(QMUISchemeHandler handler, String schemeAction);
}
Copy the code
And each Scheme annotation corresponds to a SchemeItem:
ActivityScheme
Instantiate oneActivitySchemeItem
Class and add it to the mapFragmentScheme
Instantiate oneFragmentSchemeItem
Class and add it to the map
The SchemeMapImpl generated by SchemeProcessor at compile time looks something like this:
public class SchemeMapImpl implements SchemeMap {
private Map<String, List<SchemeItem>> mSchemeMap;
public SchemeMapImpl(a) {
mSchemeMap = new HashMap<>();
List<SchemeItem> elements;
ArrayMap<String, String> required = null;
elements = new ArrayList<>();
required =null;
elements.add(new FragmentSchemeItem(QDSliderFragment.class,false.new Class[]{QDMainActivity.class},null.false."",required,null.null.null.null.null,SliderSchemeMatcher.class));
mSchemeMap.put("slider", elements);
elements = new ArrayList<>();
required = new ArrayMap<>();
required.put("aa".null);
required.put("bb"."3");
elements.add(new ActivitySchemeItem(ArchTestActivity.class,true.null,required,null.new String[]{"aa"},null.null.null.null));
mSchemeMap.put("arch", elements);
}
@Override
public SchemeItem findScheme(QMUISchemeHandler arg0, String arg1, Map<String, String> arg2) {
List<SchemeItem> list = mSchemeMap.get(arg1);
if(list == null || list.isEmpty()) {
return null;
}
for (int i = 0; i < list.size(); i++) {
SchemeItem item = list.get(i);
if(item.match(arg0, arg2)) {
returnitem; }}return null;
}
@Override
public boolean exists(QMUISchemeHandler arg0, String arg1) {
returnmSchemeMap.containsKey(arg1); }}Copy the code
This is the overall design and implementation of the idea, the rest is a variety of coding details. If you are interested, you can use qmueHandler # Handle () to trace this, or see how SchemeProcessor does code generation. This functionality looks simple, but it also includes the use of the Builder pattern, the chain of responsibility pattern, the factory method and other design patterns, as well as the use of SchemeMatcher, SchemeItem, object-oriented interfaces, inheritance, polymorphism, etc. Maybe you’ll get some inspiration from reading it, and maybe you’ll help me spot some potential bugs.