Content of the feed
This is a long, functional article that covers the entire process of refactoring from idea to design and landing. You can learn from reading this article:
- Glide several key features of the design principle and their thinking (interview available)
- Coding takes into account the practice of object-oriented programming at an extended level
- Similar to RxJava workflow design ideas and practices
- Some tips for Kotlin and Java calling each other
- A powerful call system camera, system album library and how it was designed
background
In view of the recent old code reconstruction of the original project, calling the system camera and photo selection module is a pain point we encounter daily, and need to write Intent in the part of calling the system camera (album) :
// Sample code
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
intent.setType("image/*");
activity.startActivityForResult(intent, getRequestCode());
Copy the code
After the photo is obtained in onActivityResult, the data obtained by asynchronous processing (direction correction, compression, etc.), and the logic to be uploaded to the back end if business needs it:
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable final Intent data) {
// Sample code
// Asynchronously handle images, compression, rotation, etcThe File result = compressPhotoFromAlbumAsy (data);// Upload to the back end, etc
uploadAsy(result);
}
Copy the code
So there are a few pain points:
- Dispersing the processing entry, triggering the photo behavior and receiving in different places, and relying on overwriting the onActivityResult method are not conducive to late modular componentization.
- Images need to be processed asynchronously to improve user experience, so the lifecycle of the container needs to be considered at this time, and a lot of judgment codes need to be added, resulting in poor readability. It takes a lot of time to sort out the logical processing of images and the image upload layer of the business
Reconstruction results:
Therefore, in view of the above pain points, I borrowed some ideas from Glide to reconstruct this module and designed CoCo library -> one line of code to flexibly complete the operations provided by the native system, such as taking pictures, selecting pictures, compression, cutting and so on, which are out of the business layer. First casually paste two functions:
- Call the system camera to take a picture:
CoCo.with(this@MainActivity)
.take(createSDCardFile())
.start(object : CoCoCallBack<TakeResult> {
override fun onSuccess(data: TakeResult) {
iv_image.setImageBitmap(Utils.getBitmapFromFile(data.savedFile!! .absolutePath)) } })Copy the code
The above will automatically fetch the original image to show our results, of course, if you need to compress or manipulate the original image, just switch to an operator:
CoCo.with(this@TakePictureActivity)
.take(createSDCardFile())
// Toggle the operator to perform the next function
.then()
// Process the image
.dispose()
.start(object : CoCoCallBack<DisposeResult> {
override fun onSuccess(data: DisposeResult) {
iv_image.setImageBitmap(data.compressBitmap)
}
})
Copy the code
Dispose operator can compress and correct the direction of the original image, and the asynchronous operation is automatically Glide bound to the life cycle of the container passed in with(), and dispose of the image file can be customized to modify the policy as needed.
Of course, CoCo also provides the system photo album selection, system cutting and other functions, and this series of operators can be combined, you can choose the figure, compression and cutting, cutting and so on, according to their own combination.
Principle and design idea:
A tribute to Glide:
Glide is the image library that we use most often on Android, and it has a few features that we are familiar with:
- One line of code completes the image loading, caching, presentation process, and the asynchronous operation automatically binds the container lifecycle
- After the load operation is performed, a number of different methods can be flexibly selected, such as image size, cache type, etc., and finally all operations can be done through into
1. The idea behind this comfortable feature is that it maintains a LifeCycle within the Fragment LifeCycle by adding a Fragment to it, so that it can sense the LifeCycle of the calling container and do some asynchronous unbinding when relevant.
public class RequestManagerFragment extends Fragment {
@Override
public void onStart(a) {
super.onStart();
lifecycle.onStart();
}
@Override
public void onDestroy(a) {
super.onDestroy(); lifecycle.onDestroy(); }}Copy the code
The object returned by the load operator is actually a Builder object. Different subclasses of Builder object are generated according to the different parameters of load. Different subclasses of Builder object encapsulate their own unique methods. DrawableTypeRequest object, which is essentially a Builder:
// Simplify the inheritance relationship just to show the core code clearly
public class DrawableTypeRequest<ModelType> extends GenericRequestBuilder<ModelType> implements DownloadOptions {
/** * own unique method... * /
public <Y extends Target<File>> Y downloadOnly(Y target) {
///}}Copy the code
Call asBitmap and return BitmapTypeRequest, also Builder:
// Simplify the inheritance relationship just to show the core code clearly
public class BitmapTypeRequest<ModelType> extends GenericRequestBuilder<ModelType.Bitmap> {
/** * own unique method... * /
public BitmapRequestBuilder<ModelType, byte[]> toBytes() {
///}}Copy the code
But they all inherit from the final base class GenericRequestBuilder, which contains the into method we often use:
public class GenericRequestBuilder<ModelType.DataType.ResourceType.TranscodeType> implements Cloneable { /** * common method of base class... * /
public <Y extends Target<TranscodeType>> Y into(Y target) {
////}}Copy the code
Through the above design, we can ensure that each sub-function module can maintain its own unique method in Builder, and finally call the common method of the parent class. On the one hand, it is in line with the design principle of object-oriented single responsibility, and on the other hand, it can improve the code readability, so that users can easily understand when using.
Summarize the core idea of Glide on a line of code to complete
- Where is it?
- What to do?
- The result is?
/ / where is it
Glide.with(this@MainActivity)
/ / why
.load("https://xxxxx.com.sxx.jpg")
/ / as a result
.into(iv_image)
Copy the code
Demand analysis
-
I need to implement the intent to start the Activity by calling the camera, album, or clipping of the system. If I use the Fragment method, I can use it as a carrier to start the Activity and rewrite the onActivityResult method to receive the returned data. The first pain point can be solved by using Glide’s life cycle simulation method to automatically sense the external container life cycle when processing images such as compression, rotation and other asynchronous operations, so as to reduce the business code
-
Glide’s function provides the base method for the base class Builder, and then extends its specific function in the subclass. This method can be used in this scenario: perform each function to generate the related function Builder, then pull it together in the parent class method, and finally get the result, so the following UML diagram was designed:
UML analysis:
- The coke. with method is passed to the current container, specifying the current workspace [Where Where], and returns a FunctionManager object that provides the basic methods available at our current workspace, Take photos, pick pictures, crop cutting, dispose and other functions are provided and expanded in this way.
- When FunctionManager calls one of these operators, the corresponding Builder object is generated. For example, when we call the take function, we return a TakerBuilder object because it is a photo function, We can provide parameters like cameraFace, fileToSave and so on. Similarly, if you choose pick, You can extend the range and so on, and eventually whatever you end up with, they all inherit from The BaseFunctionBuilder, and in this parent class you have the start method, No matter which subclass extends its method to assemble parameters, it can be implemented to the next step by the start method of the parent class, which ensures the neatness of a line of code and the isolation of methods between subclasses. It conforms to the object-oriented design idea and is convenient for subsequent expansion and maintenance.
- Through steps 1 and 2, we complete the encapsulation of where and what, and finally execute through the method of start to get the Result we want, that is, a series of results
Detailed source code design and analysis:
Below to select the function of the link as a detailed analysis of the overall design (source code has been simplified, easy to understand) :
// Where is [activity]
CoCo.with(this@MainActivity)
// What are you doing?
.pick()
// Result is [PickResult]
.start(object : CoCoCallBack<PickResult> {
override fun onSuccess(data: PickResult) {
iv_image.setImageURI(data.originUri)
}
override fun onFailed(exception: Exception){}})Copy the code
The first step is of course to define the container scenario:
object CoCo {
@JvmStatic
fun with(activity: Activity): FunctionManager {
checkStatusFirst(activity)
return FunctionManager.create(activity)
}
@JvmStatic
fun with(fragment: androidx.fragment.app.Fragment): FunctionManager {
checkStatusFirst(fragment.activity)
return FunctionManager.create(fragment)
}
@JvmStatic
fun with(fragment: Fragment): FunctionManager {
checkStatusFirst(fragment.activity)
return FunctionManager.create(fragment)
}
}
Copy the code
After defining the usage scenario, return a FunctionManager object that provides all the functionality we already have and can extend in the future:
- Tip: Use the @jVMStatic annotation to keep the experience consistent when Java calls kotlin code and when Kotlin calls itself
class FunctionManager(internal val container: IContainer) {
/** Take a photo from system camera *@param fileToSave the result of take photo to save
* @see TakeBuilder
*/
fun take(fileToSave: File): TakeBuilder =
TakeBuilder(this).fileToSave(fileToSave)
** select a photo from system gallery or file system *@see PickBuilder
*/
fun pick(a): PickBuilder =
PickBuilder(this)
Dispose an file in background thread,and will bind the lifecycle with current container *@param disposer you can also custom disposer
* @see Disposer
*/
@JvmOverloads
fun dispose(disposer: Disposer = DefaultImageDisposer.get()): DisposeBuilder =
DisposeBuilder(this)
.disposer(disposer)
/ /... expand other functions
}
Copy the code
After each function is selected, we return a Builder. The image pair should be the PickBuilder, and the photo pair should be the TakeBuilder, both inherited from its base class, BaseFunctionBuilder:
abstract class BaseFunctionBuilder<Builder, Result>(
internal val functionManager: FunctionManager
) {
/** * Starts to execute */
fun start(callback: CoCoCallBack<Result>) {
generateWorker(getParamsBuilder()).start(null,callback)
}
/**
* 获取参数Builder
*/
internal abstract fun getParamsBuilder(a): Builder
/** * generates the class */ that actually does the work
internal abstract fun generateWorker(builder: Builder): Worker<Builder, Result>
}
Copy the code
In this base class, we define two generic parameters: Builder, Result, the implementation of the first Builder parameter to encapsulate all the parameters at work, and the implementation of the PickBuilder parameter to hold the final Result.
class PickBuilder(fm: FunctionManager) :
BaseFunctionBuilder<PickBuilder, PickResult>(fm) {
internal var pickRange = Range.PICK_DICM
/** * Extend the special functions of image selection here, such as whether to select system album or *@param pickRange the range you can choose
* @see Range.PICK_DICM the system gallery
* @see Range.PICK_CONTENT the system content file
*/
fun range(@PickRange pickRange: Int = Range.PICK_DICM): PickBuilder {
this.pickRange = pickRange
return this
}
override fun getParamsBuilder(a): PickBuilder {
return this
}
override fun generateWorker(builder: PickBuilder): Worker<PickBuilder, PickResult> {
return PickPhotoWorker(functionManager.container, builder)
}
}
Copy the code
Here, the final implementation of Worker generation is to generate a PickPhotoWorker, which completes the final implementation of image selection, so now we design our Worker class:
Design interface abstraction behavior first:
interface Worker<Builder, ResultData> {
fun start( callBack: CoCoCallBack<ResultData>)
}
Copy the code
Design another abstract class. This layer holds container references (for example, we need to start the Activity to select an image, and we need to be aware of the container lifecycle) and our built Builder parameters:
abstract class BaseWorker<Builder, ResultData>(val iContainer: IContainer, val mParams: Builder) :
Worker<Builder, ResultData>
Copy the code
Look again at the final PickPhotoWorker of this selection:
class PickPhotoWorker(iContainer: IContainer, builder: PickBuilder) :
BaseWorker<PickBuilder, PickResult>(iContainer, builder) {
override fun start(
callBack: CoCoCallBack<PickResult>) {
valactivity = iContainer.provideActivity() activity ? :return
pickPhoto(activity, callBack)
}
private fun pickPhoto(activity: Activity, callBack: CoCoCallBack<PickResult>) {
val pickIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
if (null === pickIntent.resolveActivity(activity.packageManager)) {
callBack.onFailed(BaseException("activity status error"))
return
}
try {
//start activity for result
iContainer.startActivityResult(
pickIntent, Constant.REQUEST_CODE_IMAGE_PICK
) { _: Int, resultCode: Int.data: Intent? ->
handleResult(resultCode, data, callBack)
}
} catch (e: Exception) {
callBack.onFailed(e)
}
}
private fun handleResult(
resultCode: Int,
intentData: Intent? , callBack:CoCoCallBack<PickResult>) {
if (null! = intentData &&null! = intentData.data) {
val result = PickResult()
result.originUri = intentData.data!!!!! callBack.onSuccess(result) }else {
callBack.onFailed(BaseException("null result intentData"))}}}Copy the code
Look again at the container interface iContainer:
interface IContainer {
// Provide activities to determine state, or scenarios that require context-specific methods
fun provideActivity(a): Activity?
// startActivityResult Starts activty and gets the callback result
fun startActivityResult(intent: Intent, requestCode: Int,
callback: (requestCode: Int.resultCode: Int.data: Intent?). ->Unit
)
// Get the external container host
fun getLifecycleHost(a): Host
}
Copy the code
Let’s look at the concrete implementation of this, of course, which is the Fragment we’re actually using to hold everything:
class AcceptResultSupportFragment : Fragment(), IContainer,
Host {
/** * Current lifecycle status */
private var current = Host.Status.INIT
private var requestCode: Int = 0
private var callback: ((requestCode: Int, resultCode: Int.data: Intent?) -> Unit)? = null
override fun startActivityResult(
intent: Intent, requestCode: Int,
callback: (requestCode: Int.resultCode: Int.data: Intent?). ->Unit
) {
this.requestCode = requestCode
this.callback = callback
startActivityForResult(intent, requestCode)
}
override fun provideActivity(a): Activity? = activity
override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
super.onActivityResult(requestCode, resultCode, data) callback!! (requestCode, resultCode,data)}override fun onActivityCreated(savedInstanceState: Bundle?). {
super.onActivityCreated(savedInstanceState)
// When the outside world wants to know if the container is destroyed, it reads this variable
current = Host.Status.LIVE
}
override fun onDestroy(a) {
current = Host.Status.DEAD
super.onDestroy()
callback = null
}
override fun getStatus(a): Int {
return current
}
override fun getLifecycleHost(a): Host {
return this}}Copy the code
The Fragment was created when we created FunctionManager in the first CoCo. With step.
At this point, we have analyzed the basic links inside each of the operators we initially called, to summarize:
-
Create a FunctionManager based on the current state of the Activity or Fragment:
-
FunctionManager selects the specified function to generate the corresponding function pair Builder for assembling various parameters
-
After the parameter Builder is assembled, the parent class public method start is called to generate the Worker corresponding to the corresponding function, and each Worker does its own implementation
-
The Worker calls back to CallBack to get our Result**
Therefore, with the above framework, I developed, system photo, system selection, system cutting, file asynchronous processing (compression, rotation, and other picture operations), a total of four methods, we only need a line of code can be completed, such as image processing:
CoCo.with(this)
.dispose()
.origin(imageFile.path)
.start(object : CoCoAdapter<DisposeResult>() {
override fun onSuccess(data: DisposeResult) {
iv_image.setImageBitmap(Utils.getBitmapFromFile(data.savedFile!! .absolutePath)) } })Copy the code
So now you might have a question:
LifeCycle is implemented by Itself, Google already has LifeCycle components, why would they want to recreate the wheel?
2. We usually need to compress the image after taking a photo to reduce the size, so according to the above design, should not take a photo first and then process in the callback inside nested?
So to answer the first question, the reason why LifeCycle control provided by Google is not used is that this control is only available for support packages or Androidx packages, and native activities and fragments do not have the ability of lifeCycleOwner. We can’t guarantee whether the container passed in by the user supports lifeCycleOwner or not, so we implement it ourselves for better compatibility.
The second problem is the reason for the design of the switching operator, which will be mainly introduced next. By designing our switching operator, we can combine multiple operations in one line of code to meet the actual business scenarios. We often need to compress after taking photos, and the then operator can be used in one step:
For example, we choose an image to crop and then compress
CoCo.with(this@MainActivity)
/ / a selection of images
.pick()
// Toggle operators
.then()
/ / cutting
.crop()
// Toggle operators
.then()
// compress
.dispose()
.start(object : CoCoAdapter<DisposeResult>() {
override fun onSuccess(data: DisposeResult) {
iv_image.setImageBitmap(data.compressBitmap)
}
})
Copy the code
Effect:
Design the Then operator:
According to the previous design, when we call the start method, we directly generate the corresponding Worker and start executing it. Now if we want to connect multiple functions, do I just need to directly generate Woker objects and save them when we switch operators of other functions? Finally, after completing all functions, call start in sequence and throw the results of the previous Woker process to the next Woker to complete the series of functions.
Design process:
- Add a List to the FunctionManager to hold all the workers to execute:
class FunctionManager(internal val container: IContainer) {
internal val workerFlows = ArrayList<Any>()
}
Copy the code
- Then add the then method to the location of the original BaseWorker. When the then method is called, the Worker is generated and added to the list, and then the FunctionManager object is returned to ensure that after completing a job, You can switch to other functions of FunctionManager:
fun then(a): FunctionManager {
this.functionManager.workerFlows.add(generateWorker(getParamsBuilder()))
return this.functionManager
}
Copy the code
- At this time, we modify the Worker’s start method and add a parameter to continue the result of the previous Worker:
interface Worker<Builder, ResultData> {
fun start(formerResult: Any? , callBack:CoCoCallBack<ResultData>)
}
Copy the code
- At this point, our final realization of the star method of BaseFunctionBuilder is of course a recursive call:
abstract class BaseFunctionBuilder<Builder, Result>(
internal val functionManager: FunctionManager
) {
fun then(a): FunctionManager {
...
}
/** * call start to begin the workflow */
fun start(callback: CoCoCallBack<Result>) {
this.functionManager.workerFlows.add(generateWorker(getParamsBuilder()))
// loop through each Worker
val iterator = functionManager.workerFlows.iterator()
if(! iterator.hasNext()) {return
}
// The first Worker formerResult is null on the first start
realApply(null, iterator, callback)
}
private fun realApply(
formerResult: Any? , iterator:MutableIterator<Any>,
callback: CoCoCallBack<Result>) {
val worker: Worker<Builder, Result> = iterator.next() as Worker<Builder, Result>
worker.start(formerResult, object : CoCoCallBack<Result> {
override fun onSuccess(data: Result) {
if (iterator.hasNext()) {
iterator.remove()
// If there is another one, the recursion continues
realApply(data, iterator, callback)
} else {
// No next, end the process, callback to the final result
callback.onSuccess(data)}}override fun onFailed(exception: Exception) {
callback.onFailed(exception)
}
})
}
}
Copy the code
Specific implementation of each subclass is not detailed paste, strike interested can look at the source code, is the Result of the last processing in series, and finally Result layer by layer to the final Result, At this point we have a line of code that we can crop, photograph, manipulate, and do any combination of their functions, all while keeping the code simple, readable, and extensible.
conclusion
The complete UML
CoCo drew lessons from Glide’s grammar and design ideas to the system’s common functions such as taking pictures, selecting pictures, cutting to do some packaging, not only in the processing of pictures and other asynchronous operations can be aware of the external container, but also through the RxJava workflow similar to the idea, these several operations can be in series flow processing. Greatly improve the function of expansion and flexibility, but also to ensure the simple readability of the code, can reduce a lot of bloated code for our daily development, improve the development efficiency, CoCo stable version has been launched to support the stable operation of multiple applications.
Making address:
github.com/soulqw/CoCo