This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
preface
At Google I/O 2019, Google announced that Kotlin would be a priority for Android development in the future, and it has stuck to that promise. Android development using Kotlin is less code, more readable, and compatible with Java code.
After learning some of Kotlin’s grammar candy, I wanted to apply it to my own tool classes. I was the only one in the company learning Kotlin, so the project was a mix of Kotlin and Java. Then when I was writing the tool class, a problem occurred to me. What should I do if the result of calling the tool class I wrote with Kotlin was inconsistent with that of the original Java tool class? For example, if I write regex in Kotlin and someone else writes it in Java, the match is not the same, then I’m not trying to blame the teacher. Or deliberately ensure that the implementation logic is consistent with the Java utility class. Why not just call the Java utility class? AndroidUtilCode, the Java utility class library the company was using for the project, was used to encapsulate the extension library.
Since most of the functionality is already there, the main thing to do is to supplement the missing functionality and design a good Kotlin API. Designing an API seems simple, but actually it’s hard. Because Kotlin’s gameplay is too much, and not always use grammar sugar will be easy to use, usage will bring a certain learning cost, may not be easy to use. Personal comparison obsessive-compulsive disorder, in this regard to think a lot, there are some encapsulated experience can be shared with you.
Later, the company’s new project was basically Kotlin development, and it was possible to start writing a pure Kotlin tool class library that was as lightweight as possible, regardless of compatibility with Java code. Thanks to a lot of thinking before, the current implementation is relatively satisfactory.
Next, I will share with you some personal experience of encapsulating Kotlin utility classes and a useful Kotlin utility class library.
Packaging ideas
I’ve seen a lot of people write Kotlin’s utility classes and basically just translate the original Java utility classes into Kotlin. It’s just like when C++ was first introduced, some people still wrote code with a process-oriented mentality. It’s not that it can’t be used, but it can be done better.
So here are some of the less common syntax sugars in Java and some tips to help you use them better in your utility classes.
Top-Level
This is one of the big differences between Kotlin and Java, where all properties and methods need to be written inside the class, whereas Kotlin has top-level properties and top-level functions that can write methods and properties outside the class. In other classes, it is possible to call top-level attributes or top-level methods directly.
What’s the use? For example, to get an Application object, the Java utility class calls apputils.getApplication (), whereas the Kotlin utility class can get the Application properties directly. It’s nice to be able to get an Application property anywhere at any time. Why call apputils.getApplication () when we can get an application property? Most of the time using Kotlin to write an XXXUtils to call a static method is unnecessary, when it is better to write a top-level property or top-level method.
This is a very simple feature, but there is also a place to note, is the name to describe the function clearly, I think it is important. For example, we wrote an immersive status bar feature earlier, which can be used as follows:
StatusBarUtils.immerse(this)
Copy the code
Move a method outside the class to become a top-level method:
immerse(this)
Copy the code
Some people might change this and leave it at that, but this is not a good situation, someone calling a global immersion method will be confused as to what the immersion is about. The reason we used the word “immersed” as a method name was that the name of the utility class also had information, which could be used to infer that the status bar was to be immersed. So it’s better to change it to:
immerseStatusBar(this)
Copy the code
The name of a top-level method or property should describe the function, because it can be called globally, rather than simply removing the class name from the original Java utility class.
expand
Kotlin can easily extend an existing class and add additional methods or attributes to it without having to inherit the class or use the decorator pattern.
We can further refine the use of the immersion bar above and change the method to an extension of the Activity. Add a receiver before the method:
fun Activity.immerseStatusBar(a){... }Copy the code
We can use this to get the Activity object inside the method, so the original Activity parameter can be removed. This can then be called in the Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
// ...
immerseStatusBar()
}
}
Copy the code
Java implements this usage by writing an immerseStatusBar() method in the Activity base class, whereas Kotlin can use extensions directly without writing the base class.
This is a very simple feature, but I have some suggestions for you:
- Be careful about extending Any or common base data types.
- The usage should be consistent with the original usage habits or intuition, not too different.
What does that mean? Through the expansion can play some SAO operation, but not what function is appropriate. For example, when I first learned about extensions, I wanted to use extensions to encapsulate all functions. When I saw that it was troublesome to pass two arguments to the print log, I used extension functions to reduce one argument and add a print extension method to String:
"Downloaded progress is $progress".logd("http")
Copy the code
The usage is really cool, but it doesn’t work well after a while. The usage is so different from the original print log usage that it is awkwardly written. The code is not readable until you get to the end of the String and you don’t realize that this line is used to print logs. And a confusing code prompt pops up when calling the string method.
DrawableRes = Drawable; drawableRes = Drawable; drawableRes = Drawable; drawableRes = Drawable;
val drawable = R.drawable.ic_back_icon_black.drawableRes
Copy the code
Both of these examples are functionally fine, but the difference in usage makes the code less readable and takes a while to get used to. It also adds strange code hints for common types that are personally discouraged. Sloppy usage doesn’t mean easy use. Don’t use grammar sugar just to use grammar sugar.
Add dp to Int and convert dp to px.
paint.strokeWidth = 1.dp
Copy the code
Usage is also very different, but intuitive. When we read this line of code, it’s easy to think that the code is more readable by setting the length of the property to 1 dp, which is recommended.
Higher-order functions
Higher-order functions are functions that use functions as arguments or return values. I will write a blog post about the nature of higher-order functions, but I don’t want to spend too much time on how to use them, and share a scenario. Most people use higher-order functions for event callbacks, but higher-order functions are also handy for DSL usage. For example Anko Layout’s DSL:
verticalLayout {
editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!")}}}Copy the code
This DSL usage is more comfortable than chain calls and can be layered in a way that chain calls are not.
So how does that work? In fact, there are optional configurations that can be considered, the most common is the Builder mode, such as the familiar Glide:
Glide.with(context)
.load(url)
.placeholder(placeholder)
.fitCenter()
.into(imageView)
Copy the code
Let’s encapsulate it a little bit:
fun ImageView.load(url: String? , block:RequestBuilder<Drawable>. () - >Unit) =
Glide.with(context).load(url).apply(block).into(this)
Copy the code
This simple encapsulation transforms the chain call into DSL usage.
imageView.load(url) {
placeholder(placeholder)
fitCenter()
}
Copy the code
This is the same as Coil, but there are some yellow warnings that need to be dealt with, so MY personal recommendation is to use the Coil. DSL usage is a bit cleaner and more comfortable than chain calls and allows for multiple levels of nesting.
Attribute to entrust
Attribute delegation is the use of the BY keyword to delegate the get and set methods of an attribute to the expression following by. Such as:
private val viewModel: LoginViewModel by viewModels()
Copy the code
This is the official ViewModel delegate usage, and the ViewModel instance is obtained through the ViewModelProvider when a ViewModel property is obtained. With the delegate we don’t have to worry about how to get the ViewModel and can focus on writing the logical code.
How to write a commission will take some space to explain, and I will write a blog post later. So here’s a little bit of a usage scenario, some of you might have learned about property delegate, but don’t know where to use it. When we get or set properties in a certain way, we can consider whether it is appropriate to use property delegates. For example, to get a value passed through an intent:
private var id: String? = null
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
val id = intent.getStringExtra("id")}Copy the code
Here we can simplify the code with attribute delegates:
private val id: String? by intentExtras("id")
Copy the code
Property delegates let us not worry about how to get and set the property, the code is more concise, is a good syntax candy, you can think about whether or not to use property delegates.
Experience in other
Do not duplicate the wheel
View.visible (), view.invisible(), view.gone().
It is indeed more convenient to use than before, but there is still room for optimization, show and hide is often a judgment operation, such as:
if (isShowed) {
view.visible()
} else {
view.gone()
}
Copy the code
It’s a bit of a hassle to do this every time, so a better way to wrap it is to add an extended property to the view.isVisible Boolean value, which optimizes the above code to look like this:
view.isVisible = isShowed
Copy the code
This extended property can be used not only to modify the display hidden state, but also to determine whether it is currently displayed on the layout, making it easier to use.
However, when you call the extension attribute, you will find that you need to choose which attribute to use with the same name. If you look closely, the official Core-KTX library already implements this extension attribute, so there is no need to duplicate the wheel.
Therefore, it is best to check whether the official Android KTX library and Kotlin’s standard library implement the same functions before encapsulating the tool classes. The positioning of the packaged tool classes should be to supplement the functions that do not exist. There is no point in reinventing the wheel, and it may not be as good as the official one.
Naming Suggestions
As mentioned above, we should supplement the functions that the official library does not have, so we recommend to refer to the official library naming and usage when designing usage.
For example, the official name of a creation operation with parameters is xxxOf(), such as listOf() and mapOf(). You are advised to use the same name as the official name. Do not use createXXX() or newXXX().
There are also methods that listen to events that some people like to call onXXX, for example:
btnLogin.onClick {
// ...
}
Copy the code
It’s a strange way to start with a preposition, because most method names start with a verb. So I suggest referring to the official name doOnXXX:
view.doOnAttach {
// ...
}
Copy the code
The advantage of conforming to the naming conventions of the official library is that there is no ambiguity, and people may be tempted to guess, based on past usage, whether your utility class has a particular function or not. For example, if you want to see if there is a certain listening event, you might first type do to see if there is any association with the corresponding function method. Therefore, I suggest not to add too many personal naming rules, more reference to learn the official library naming and usage.
Final plan
The above experience is mainly to share with some people who have written Kotlin tool class, but more people are not good at writing, so I share a long time to polish Kotlin tool class library – Longan.
Why is it called Longan? Personally, I wanted to use a fruit as the name of the database. At first, I thought of Guava (pomegranate), which felt very suitable. However, I found a database with the same name on Google, so I changed it to Longan (Longan), which is also a fruit with many seeds.
Add dependencies:
allprojects {
repositories {
// ...
maven { url 'https://www.jitpack.io'}}}Copy the code
dependencies {
implementation 'com. Making. DylanCaiCoding. Longan, Longan, 1.0.0'
/ / is optional
implementation 'com. Making. DylanCaiCoding. Longan, Longan - design: 1.0.0'
}
Copy the code
Preserved and improved some useful Anko usages, such as:
startActivity<SomeOtherActivity>("id" to 5)
logDebug(5)
toast("Hi there!")
snackbar(R.string.message)
alert("Hi, I'm Roy"."Have you tried turning it off and on again?")
Copy the code
It has also added many common development features, such as the following usages:
You can get the Application or topActivity properties directly when you need the Context or Activity.
TabLayout + ViewPager2 custom style bottom navigation bar with less code:
private val titleList = listOf(R.string.home, R.string.shop, R.string.mine)
private val iconList = listOf(
R.drawable.bottom_tab_home_selector
R.drawable.bottom_tab_shop_selector,
R.drawable.bottom_tab_mine_selector
)
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
...
viewPager2.adapter = FragmentStateAdapter(HomeFragment(), ShopFragment(), MineFragment())
tabLayout.setupWithViewPager2(viewPager2, enableScroll = false) { tab, position ->
tab.setCustomView(R.layout.layout_bottom_tab) {
findViewById<TextView>(R.id.tv_title).setText(titleList[position])
findViewById<ImageView>(R.id.iv_icon).apply {
setImageResource(iconList[position])
contentDescription = getString(titleList[position])
}
}
}
}
Copy the code
Create a Fragment with parameters and use attribute delegate to get parameters in the Fragment:
class SomeFragment : Fragment() {
private val viewModel: SomeViewModel by viewModels()
private val id: String by safeArguments(KEY_ID)
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
super.onViewCreated(view, savedInstanceState)
/ /...
viewModel.loadData(id)
}
companion object {
fun newInstance(id: String) = SomeFragment().withArguments(KEY_ID to id)
}
}
Copy the code
val fragment = SomeFragment.newInstance(id)
Copy the code
Double click the back button to exit the App or click the back button to return to the desktop without exiting the App:
pressBackTwiceToExitApp("Click exit app again")
// pressBackToNotExitApp()
Copy the code
To achieve immersive status bar, and increase the height of the status bar to the top margin of the title bar, which can be adapted to the bangs screen:
immerseStatusBar()
toolbar.addStatusBarHeightToMarginTop()
// toolbar.addStatusBarHeightToPaddingTop()
Copy the code
Quick realization of the countdown to obtain the verification code:
btnSendCode.startCountDown(this,
onTick = {
text = "${it}SEC. ""
},
onFinish = {
text = "Obtain captcha code"
})
Copy the code
The Settings button can only be clicked when certain input fields have content:
btnLogin.enableWhenOtherTextNotEmpty(edtAccount, edtPwd)
Copy the code
Click event can set the click interval to prevent repeated clicking within a period of time:
btnLogin.doOnClick(clickIntervals = 500) {
// ...
}
Copy the code
Custom control to obtain custom properties of the code is more simplified:
withStyledAttrs(attrs, R.styleable.CustomView) {
textSize = getDimension(R.styleable.CustomView_textSize, 12.sp) textColor = getColor(R.styleable.CustomView_textColor, getCompatColor(R.color.text_normal)) icon = getDrawable(R.styleable.CustomView_icon) ? : getCompatDrawable(R.drawable.default_icon) iconSize = getDimension(R.styleable.CustomView_iconSize,30.dp)
}
Copy the code
Custom control to draw centered or vertically centered text:
canvas.drawCenterText(text, centerX, centerY, paint)
canvas.drawCenterVerticalText(text, centerX, centerY, paint)
Copy the code
Switch to the main thread, used with thread {… } maintains unity:
mainThread {
// ...
}
Copy the code
Listen for lifecycle operations:
lifecycleOwner.doOnLifecycle(
onCreate = {
// ...
},
onDestroy = {
// ...})Copy the code
Automatically display an empty layout when RecyclerView data is empty:
recyclerView.setEmptyView(this, emptyView)
Copy the code
RecyclerView’s smoothScrollToPosition() method is to slide to the item visible, if the slide from the top will stop at the bottom, generally does not meet the requirements. So we added an extension that always slides to the top.
recyclerView.smoothScrollToStartPosition(position)
Copy the code
Textview.text.tostring ().isnotempty () is a very long textView.toString ().
if (textView.isTextNotEmpty()) {
// ...
}
Copy the code
KunMinX’s plan for message event transmission is recommended, which uses LiveData held by shared ViewModel for distribution to avoid problems such as difficult traceability of message push and unreliable and inconsistent message synchronization. Because LiveData has dependency flooding problems, EventLiveData is generally self-encapsulated for the event scenario. But without Java, just use SharedFlow for coroutines.
class SharedViewModel : ViewModel() {
val saveNameEvent = MutableSharedFlow<String>()
}
Copy the code
Get the application-level ViewModel by applicationViewModels() and implement the shared ViewModel:
private val sharedViewModel: SharedViewModel by applicationViewModels()
// Send events
sharedViewModel.saveNameEvent.tryEmit(name)
// Listen on events, providing an observe usage similar to LiveData, simplifying the code of collect
sharedViewModel.saveNameEvent.launchAndCollectIn(this) {
finish()
}
Copy the code
There are also a number of useful apis, such as The Android 10 partitioned storage API for adding and deleting urIs to media files, which can simplify a lot of code, but I won’t cover all of them here. For more information, see GitHub, which has over 300 common methods or attributes that can greatly improve development efficiency.
Personal long-term maintenance, any problems can be raised issues, I will deal with as soon as possible. Any desired function can also be mentioned.
Future articles on encapsulation
- Elegantly encapsulating and using ViewBinding, time to replace Kotlin Synthetic and ButterKnife
- ViewBinding cleverly encapsulates ideas and ADAPTS BRVAH in this way
- Elegantly encapsulating the Activity Result API, a perfect alternative to startActivityForResult()
conclusion
This article provides tips on using Some of Kotlin’s features, including top-levels, extensions, higher-order functions, and delegates, as well as sharing some personal experience with utility classes. In the end, I share the Kotlin tool class library — Longan, which has more than 300 common methods or attributes, which can effectively improve the development efficiency. I recommend you to use Kotlin now.
If you find it helpful, please click star to support it. I will share more articles related to encapsulation with you later.