1

Mobile Library vs App Development | ProAndroidDev

 1 year ago
source link: https://proandroiddev.com/mobile-library-development-its-not-another-application-82a74875ded3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Design Considerations

The first difference between libraries and applications is the target audience. The audience of applications is the general public. Libraries, on the other hand, provide services to applications. Thus, their audience is application developers. As a result, the main consideration in designing a library is the developer experience.

The main components of user experience are UI, usability and performance. The developer experience is somewhat similar. The equivalents for developers being APIs, integration and performance. Yes, performance is important in every aspect of programming 🙂. This equivalence manifests in the design considerations that every component presents.

We will now look at each of these main considerations:

“They say UI is like a joke — if you have to explain it, it’s not that good.” — Martin Leblanc

The same rule applies to APIs. They have to be clear and simple.

Variables and methods names should be expressive, with consistent terminology.

Methods should do exactly what their name suggests with no side effects.

Position the methods, variables and constants in places where a developer would expect to find them. Make sure they are available when needed. Split APIs into different objects based on their responsibilities. Expose globally available APIs on a single static object. Position specific APIs on objects with the relevant responsibility. If an API is only available in a certain context, keep it in a dedicated class.

A good API should be easy to use and hard to abuse.

APIs have to be as hard as possible to use in any way other than intended. If some of the APIs are not always available, don’t expose them on a static object. Instead, expose them in an asynchronous way only when they are actually useful. This will make it impossible to use them when they aren’t available.

Here’s an example of an API of a library that provides video players:

Another limitation that APIs may have is being constrained to a certain scope or context. To prevent abusing these types of APIs provide them as extensions to the relevant object.

One more requirement that is common to both UI and APIs design is transparency. It is important that the application UI will show when the application is busy or some functionality is unavailable. Similarly, it’s important that the library API will let the developer know exactly what’s going on inside it.

  • Expose states and events. There is a significant difference between state and events and selecting the wrong one will result in an inconvenient API. A good rule of thumb is if the developer can ask “is X currently Y?” then Y is a state of X, otherwise it’s probably an event.

State and events example.

  • Make sure to provide a result for asynchronous operations.
  • Expose Errors. Application users should be notified about errors with a polite general message. Application developers on the other hand should be notified with a message that should be as informative as possible. Explain exactly what went wrong and why. State possible actions if available. Make sure to do it in the relevant place for the application developer to be able to take action. Note that choosing your own response to an error may be unexpected. Avoid returning a default value and delegate the reaction to the application. Reflect this behavior in the API’s name.

API names example.Error messages examples.

Reduce throwing exceptions only to the most basic misuse that makes the library useless. Handle exceptions internally and avoid crashing the host application at all cost.

APIs should be easy to extend.

  • Avoid using methods with many parameters because adding another parameter is a breaking change. Instead, try to use the builder pattern that can easily be extended in a backwards compatible way. In Kotlin, use type safe builders to create a Domain Specific Language (DSL) and achieve a very convenient API that is also very easy to extend.

Kotlin DSL builder vs regular method.

** The full implementation of these APIs can be found here.

  • Use enums instead of booleans even if you only have two options because enums are extendable.

Enum instead of boolean example.

Integration

The second developer experience component is integration. The integration, especially of the most basic use case, must be as easy and intuitive as possible. Application developers will often try to evaluate any potential library by implementing the basic integration. If it is too complicated they may decide not to use the library. In addition, the more complicated the integration is, the more likely it is that the application developer will get it wrong. Therefore, always go over the integration process and see if you can perform any of the integration steps internally. Merge integration steps whenever possible. It is better to provide default settings and behavior while keeping them open for customization if needed.

Naming is also very important to help guide the process. See example below:

Good vs bad integration example.

Different development paradigms. Some developers prefer declarative programming while others prefer imperative. In practice it mostly applies to states. In the declarative programming style the developer would like to observe state changes and react to them. Developers who use imperative programming will need to be able to query the state. Thus expose state as both variables and change listeners. Kotlin’s StateFlows are a perfect way to support both paradigms. Libraries that would like to maintain Java compatibility won’t be able to avoid listeners and variables.

How to properly expose state in API.

Performance

The third component of the developer experience is performance. It’s pretty obvious that performance must be as good as possible. Any operation the library does will inevitably have some impact on the performance of the application. There are ways to make this impact easier to handle.

Let’s start with initialization. There are a few things to keep in mind about it.

The library must never perform any operations before it is initialized. It should provide full control over the initialization process and timing. Avoid performing actions that the application developer may not be aware of or have control over. This rule will allow application developers to integrate the library with a feature flag or some other condition knowing that when it’s off, the library has no effect. Common use cases are gradual release, A\B testing or disabling in case of an error that’s only discovered after distribution.

Do only the bare minimum during initialization. Move any other operation to a background thread as initialization is expected to be synchronous and run as early as possible — e.g. Application.onCreate() for Android.

Reduce the amount of resources that the initialization consumes. Especially mobile data as it may consume the user’s data plan. It’s common for a library to be initialized on every launch of the application but only used in certain scenarios. So initialization should be slim and light to avoid wasting resources when the library is initialized but not used.

If the library needs to perform some data or computation heavy operation, It’s better to provide a separate API for it with a clear name like “fetchImages”. Let application developers decide when to use it — they know what’s best for their application.

Sample project. It’s very important for documentation to provide not only written information and code samples but also a runnable project. This provides a much higher level of confidence in the ability to successfully integrate the library. It’s also advised to provide as many integration scenarios as possible. Provide them in the form of several projects,modules or flavors if necessary. Developers tend to follow the saying that “code never lies, comments (and documentation) sometimes do” and prefer to rely on a working sample for integration.

Key Differences And Limitations

Initialization

There are several ways in which the actual development process of a library is different from the one of an application. The first one is that unlike applications, libraries must be initialized. I’ve provided some general considerations for its design in the section about performance. Now I would like to provide some more practical advice. Initialization is important mostly to prepare the library for action. Use it to initialize services that are absolutely necessary for the library to function like dependency injection. In addition, perform operations that the library would like to perform as soon as possible like fetching configuration. That, obviously, must be executed on a separate thread and be as small as possible.

Another role of initialization is to receive global configuration from the application that may be required throughout the entire lifecycle of the library. On Android, it is also used to obtain context.
On Android, a library can initialize itself using a Content Provider as the good guys from Firebase describe in this blog post from 2016. Later on Google released the Androidx App Startup library to facilitate this method and provide some additional control. As mentioned before in the “Performance” section, I believe that even though this method exists and is very convenient, libraries shouldn’t use it and I strongly recommend libraries to use manual initialization.

Code Visibility

One thing that library developers should be more aware of than application developers is code visibility. Making big parts of application code public is a common practice and is the reason (excuse) why it’s the default access modifier in Kotlin. This is not the case for library development. Any public code in a library may be consumed by the host application. Therefore the access modifiers of every bit of code require careful consideration.
A common source of accidental exposure of internal code is utilities and extensions. Pay extra attention to the scope in which they should be valid.
One way to catch those before a release, besides lint, is to go over the documentation that is generated from the code and check for anything that doesn’t belong.

Monitoring And Configuration

The amount of control the developer has over the release cycle is different between an application and a library. When releasing an application and then realizing that changes have to be made, the developer can always release a new version. Adoption may not be very fast but clients do update eventually and applications can request or even force the user to update. This is not the case for a library. Once a library goes live, the developer has very little control over it and over the developers who embed it into their applications. They may never upgrade from version 1.0.0. And even if they do upgrade it takes much longer and is out of the library developer’s control. Keep as much control over the library as possible by providing remote configuration from the server. As mentioned before, this config should be synced to the library on initialization and cached for future runs. It should contain parameters that can be easily changed and have a significant effect on the functionality of the library such as feature flags, API URLs, etc.

In addition, just like application developers, library developers also need to monitor usage and detect issues. The first and most obvious solution that comes to mind is to simply use a library for that. But there is a catch. All of these solutions, that I know of, are designed to work as a single tennant. This means that if, for example, a library integrates Google Analytics and the application that embeds that library also uses Google Analytics, only one of them can receive data. In this situation the library is either blocking the host application from receiving their analytics data or it receives nothing. This limitation forces libraries to develop in-house solutions for both monitoring usage and detecting issues like crashes or ANRs. How to do that is an article in and of itself.
Here’s a response I got from Firebase regarding usage inside libraries:

“Firebase SDKs are not intended for library projects. The features available on Firebase were integrated in an application level and not on a per module or per library basis so, the use case for having this integrated on a library project is not possible or not supported.”

Dependencies

Unlike applications that follow the “don’t reinvent the wheel” principle, library developers should adopt the opposite approach and keep dependencies to a bare minimum. On Android the main reason is that the application that will be using the library may rely on the same dependencies. In this case, only one instance of the dependency will actually be present at runtime. Gradle will perform a dependency resolution process that will most likely end up in either the app or the library running a different version of that dependency than the one it was developed with. This may lead to unexpected behavior and crashes if the versions aren’t compatible, especially in the case of two different major versions. The exception to this rule are libraries that generate their product at build time and don’t actually run any of their own code at runtime. This is usually the case with libraries that provide database abstractions or libraries that perform code manipulation using annotation processing.

Adding dependencies also increases the size of the library, sometimes significantly. A large size is a common reason for application developers to decide not to use a certain library.

Modularization (Android)

We are constantly being told to modularize our applications. There are many advantages to modularization, like shorter build times, testability, code reuse, and more. Google even prepared different types of modules to make the lives of application developers easier. But what about library development? There seem to be no official guidelines or support for modularization of libraries.

In practice, there is a big difference between modules in applications and libraries. While in applications modules are being packaged automatically into the apk (or bundle), this is not the case with jars (or aars). There are two ways to include more than one module in a compiled library.

One is to compile every dependency module to a jar (or aar) and import it as a file during the build process. This will result in the code of the dependencies being compiled as part of the original module. This approach makes sharing dependencies between two modules a big problem. It causes namespace collisions if the same classes are present in two libraries at the same time. This problem may be resolved by renaming the dependencies using a process called shadowing but it results in more than one instance of the same code.

Another way to import a module is to publish it as a separate library and use it as a dependency. In this case, Gradle will handle the version resolution and the code may be shared. Unfortunately it creates additional challenges for version management and prolongs the publishing process.

Neither way is convenient and both require a customized project structure to be able to depend on the code of the module in development and on the compiled module for publishing (depending on a file or a remote library). Also, they both complicate the publishing process. In addition, adding a new module requires more work and the more modules the project contains the more complicated and fragile it becomes.
The bottom line is that modularization of a library adds much more work and complexity than modularization of an application. I would suggest using modules for libraries only in order to share code between different services that can be used separately. In this case there’s no other choice and each service should be a separate library and therefore a separate module in the project. They can share code by using a shared module that is published and consumed as a dependency by the services (the second option presented above). Such a module is usually called “basement”.

1*3uUqvBW5__EAii62dhCgGw.png

A basement library dependency in Google Play Services SDK.

As mentioned, this will also require the project to have at least two flavors — one for development and one for publishing. In the development flavor the services depend on the shared code as a module in the project. In the publishing one they will depend on the shared module as a Gradle dependency.

Gradle flavors for development and publishing.

Notice that versioning of a library that has many modules and shares at least one of them can be tricky. They should either use the same version and be published all together or use a Bill of Materials (BOM) to support different versions of the services.
Use custom Gradle plugins to share configuration between the modules to avoid duplicating the Gradle configuration.

My early attempts at library modularization can be found in this article.

Compatibility

When developing an application, it is obvious that staying up to date with the latest versions of the programming language, build tools, and dependencies is a best practice. It is much less obvious when developing a library. In library development it is important that the code remains compatible with as many applications as possible. Using the latest versions may create compatibility issues with older ones. For example, targeting a higher Android SDK version than the host application will result in a build exception.

Another compatibility issue that may arise is language compatibility. For example, Kotlin’s suspending functions and other features won’t be available in a Java application. Therefore, every release must be tested against the lowest supported setup and all supported languages.

Summary

I hope that this article sums up what in my opinion are the most important things that application developers should keep in mind when approaching library development. I tried to give a brief presentation of the main ones. Note that every one of them can be the topic of a separate article. Before writing a library for the first time, every developer should get a deeper understanding of the differences in order to avoid making mistakes and deliver a quality product. Hope you find it useful.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK