Detailed Explanation of Design and Implementation of Android scheme Jump

  • 2021-12-04 11:24:56
  • OfStack

Origin

With the growth of App, we will inevitably encounter the following requirements:

H5 Hop Native Interface Notification click to adjust the relevant interface Jump interface according to the data returned from the background, for example, jump to different interfaces after successful login or jump to different interfaces according to operation requirements Realize the jump of AppLink

In order to solve these problems, App 1 generally customizes an scheme jump protocol, which is implemented by many terminals to solve various operational requirements. Let's analyze the latest version of QMUI today QMUISchemeHandler Design and implementation of.

The format of an scheme looks like this:

schemeName://action?param1=value1¶m2=value2

For example:

qmui://home?tab=2

From a technical point of view, it is not very difficult to realize the jump of scheme, that is, the following two steps:

Analysis of scheme Jump to the specified interface according to the analysis result

But if you don't design it when you write code, it is easy to be if else of heap 1 heap. For example:


if(action=="action1"){ 
 doAction1(params)
}else if(action=="action2"){
 doAction2(params)
}else {
 ...
}

Whenever a new scheme is added, add an if until it gradually becomes a huge long rotten code, which can't be changed. Therefore, we should think diligently, reconstruct more, and free our hands by designing an excellent framework as soon as possible.

For refactoring such as if else, a basic way is to put all the conditions and the behaviors to be executed in an map by looking up the table, and then get the behaviors to be executed by querying this map when using it. However, we can build this map by annotation and code generation, thus reducing the amount of code we write. In addition, we also need to consider various functional requirements:

You can set the interceptor interceptor, for example, skip some interfaces. If you are in a non-login state, you may need to jump to the login interface Parameter can specify 1 base type, scheme carries the parameter value is string, but we hope it can be easily converted to the base type we need The same action can have different jump behaviors according to different parameters, for example, all jump book details, and the interface to jump for comic books and ordinary books may not be the same If the current interface is already the target interface, you can choose to refresh the current interface or start a new interface For QMUI, both Activity and Fragment are supported, so scheme should also support both You can customize the instantiation method of the new interface

Interface design

For the development of any one library, in order to make the business users comfortable enough, it is necessary to ensure that the functions of the library are powerful enough and the use is convenient. QMUI Scheme is mainly QMUISchemeHandler This entry class, and ActivityScheme And FragmentScheme Two annotations.

QMUISchemeHandler

QMUISchemeHandler Instantiated by Builder mode:


//  Settings schemeName
val instance = QMUISchemeHandler.Builder("qmui://") 
 //  Prevents short-time classes from triggering the same scheme Jump 
 .blockSameSchemeTimeout(1000)
 // scheme  Parameter  decode
 .addInterpolator(new QMUISchemeParamValueDecoder())
 .addInterpolator(...)
 //  Default  fragment  Instantiation  factory
 .defaultFragmentFactory(...)
 //  Default  activity  Instantiation  factory
 .defaultIntentFactory(...)
 //  Default  scheme  Matcher 
 .defaultSchemeMatcher(...)
 .build();

if(!instance.handle("qmui://xxx")){ 
 // scheme  Not by  handle Logging? 
}

In most scenarios, QMUISchemeHandler Use singleton mode. It can set multiple interceptors, set the default instantiation plant of fragment and activity, and the default matcher. Instance factories and matchers both provide default implementations, and most scenarios do not require callers to care. Moreover, only the global default values are set here. At the level 1 of scheme annotation, different values can be specified for each scheme to meet possible customization requirements.

ActivityScheme and FragmentScheme Notes

These two annotations are very similar, but because Fragment has 1 more configuration items, it is independent.


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface ActivityScheme { 
 // scheme action  Name 
 String name();
 //  Required parameter list for supporting the same 1 A  action  Corresponding to multiple  scheme  The scene, every 1 Items can be "type=4"  To specify a value, or just pass "type" To match any value 
 String[] required() default {};
 //  If the current interface is  scheme  For the target value of jump, you can choose to refresh the current interface. Of course, the current interface must be implemented  ActivitySchemeRefreshable
 boolean useRefreshIfCurrentMatched() default false;
 //  Customize the current  scheme  The matching implementation method of,   Passed value is  QMUISchemeMatcher  Implementation of 
 Class<?> customMatcher() default void.class;
 //  Customize the current  Activity  Instance factory, passing value is  QMUISchemeIntentFactory
 Class<?> customFactory() default void.class;
 //  Specifies the type of parameter, supports  int/bool/long/float/double  These underlying types, if not specified, are  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  ActivityScheme
 String name();
 String[] required() default {};
 Class<?> customMatcher() default void.class;
 String[] keysWithIntValue() default {};
 String[] keysWithBoolValue() default {};
 String[] keysWithLongValue() default {};
 String[] keysWithFloatValue() default {};
 String[] keysWithDoubleValue() default {};

 // Same as  ActivityScheme But currently UI Must be realized  FragmentSchemeRefreshable
 boolean useRefreshIfCurrentMatched() default false;

 //  Same as  ActivityScheme,  But the passing value is  QMUISchemeFragmentFactory  Implementation class of 
 Class<?> customFactory() default void.class;
 //  You can host the target  Fragment  Adj.  activity  List, if the current  activity  Is not in the list, use the  activities  The first part of 1 Item to start a new  activity
 Class<?>[] activities();
 //  Whether to force the start of a new  Activity
 boolean forceNewActivity() default false;
 //  It can be passed through  scheme  Control whether to force a new  Activity
 String forceNewActivityKey() default ""; 
}

It can be seen that all the requirements listed above are reflected in SchemeHandler and two scheme.

Use

For business users, we only need to set the Activity Or Fragment Add notes to it. QMUISchemeHandler By default, the parameters are parsed and placed in the Activity In intent or Fragment In arguments, so we can find the onCreate Take out the values we care about in:


@ActivityScheme(name="activity1")
class Activity1: QMUIActivity{

 override fun onCreate(...){
 ...
 if(isStartedByScheme()){
  //  Pass  intent extra  Gets the value of the parameter 
  val param1 = getIntent().getStringExtra(paramName)
 }
 }
}

@FragmentScheme(name="activity1", activities = {QDMainActivity.class})
class Fragment1: QMUIFragment{ 
 override fun onCreate(...){
 ...
 if(isStartedByScheme()){
  //  Pass  arguments  Gets the value of the parameter 
  val param1 = getArguments().getString(paramName)
 }
 }
}

This method of value transfer is in line with the official design of Android, which also requires Fragment Follow the way parameterless constructors are used.

For WebView, we can rewrite the WebViewClient#shouldOverrideUrlLoading To handle scheme jumps:


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);
 }
}

Realization

QMUISchemeHandler Adopt the method of code generation, and generate 1 at compile time SchemeMapImpl Class, which implements the SchemeMap Class


public interface SchemeMap {

 //  Pass  action  And parameter search  SchemeItem
 SchemeItem findScheme(QMUISchemeHandler handler, String schemeAction, Map<String, String> params);
 //  Judge  schemeAction  Does it exist 
 boolean exists(QMUISchemeHandler handler, String schemeAction);
}

And the annotation of each scheme corresponds to 1 SchemeItem :

ActivityScheme Corresponding instantiation of 1 ActivitySchemeItem Class and added to map FragmentScheme Corresponding instantiation of 1 FragmentSchemeItem Class and added to map

At compile time through SchemeProcessor Generated SchemeMapImpl It probably looks like this:


public class SchemeMapImpl implements SchemeMap { 
 private Map<String, List<SchemeItem>> mSchemeMap;

 public SchemeMapImpl() {
 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)) {
  return item;
  }
 }
 return null;
 }

 @Override
 public boolean exists(QMUISchemeHandler arg0, String arg1) {
 return mSchemeMap.containsKey(arg1);
 }
}

This is the overall design and implementation idea, and the rest is all kinds of coding details. Those who are interested can pass QMUISchemeHandler#handle() Track it, or see SchemeProcessor How to do code generation. This function looks simple, but it also includes the application of design patterns such as Builder pattern, responsibility chain pattern and factory method, and the application of object-oriented interface, inheritance and polymorphism such as SchemeMatcher and SchemeItem. Read 1 Read may enlighten you, and maybe you can help me discover some potential Bug.

Summarize


Related articles: