When I complete a project using Kotlin which I had also completed with Kotlin five years ago
Recently, there has been an increasing frequency of Kotlin usage, prompting me to do a simple reorganization of my Kotlin code in recent years. While reviewing my work, I stumbled upon a project that I completed five years ago using Kotlin(Bezier Curve), Out of interest, I decided to use Kotlin and Compose to re-implement the project. This also gives me a chance to reflect on what I have learned in Kotlin over these years.
Regarding Bézier curves, we won’t go into too much detail here. Simply put, for each line segment, a certain point’s proportion to its two endpoints is consistent. Bézier curves are a collection of line segments (points) during this process with the mid-segment of each line segment located at the same position.
As shown in the figure, the ratio of AD to AB, BE to BC, and DF to DE are all the same. This ratio ranges from 0 to 1, and when the positions of point F are connected by a line, it forms the Bézier curve of the three points ABC.
The feeling of having completed it twice.
Although it has been five years, I still have a deep impression of this project(after all, it was difficult to find relevant information at that time).
This project used Kotlin Synthetic for data binding(although it’s now deprecated), and it was a surprise for me who had previously used findeViewById and @BindView. Yes, the biggest surprise for me when using Kotlin was Synthetic. I also found other aspects of its syntax to be useful. Now, I can utilize Compose to complete the layout of the page. The most noticeable result is the significant reduction in code volume. The first code(including XML) was around 800 lines, whereas the entire functionality can be completed in only around 450 lines of code.
In the process of using Compose, I have gained a deeper understanding of the “Compose is function” concept. Data is just data, and by bypassing data as a parameter to the Compose function, we can update the UI whenever the data changes. The beauty of this approach is that we no longer need to keep track of any additional UI objects or consider how certain elements should respond to specific actions. All we need to focus on is how a particular Compose function should display itself given certain parameters.
For example, when the “Change Point” button is clicked, it will change the contents of `mInChange`, which will affect many other elements. If we were to do this using View, we would need to listen for the “Change Point” button’s click event and modify all the affected elements in turn (which would require holding many other View objects in memory). However, with Compose, we still need to listen to the “Change Point” button’s click event, but for the corresponding listener action, we only need to modify the value of `mInChange`. The listener doesn’t need to do anything else or even be aware of what happens when this value is modified. Compose will handle the actual effects of this change(we can think of it like the parameters changing, which then re-invoke the function).
The usage of certain features is not very high, as the project is relatively small. Many of the features have not been fully utilized.
I am most happy about the fact that I was able to complete this same functionality in just half a day this time around, whereas it took me over a week to accomplish a similar task five years ago. I am unsure whether it is me or Kotlin that has undergone greater changes in these five years 😆.
Bezier Curves Tools
Let’s take a look at the functionality of this tool. The main function is to draw Bezier curves (which can be of any degree), adjust key points, and manually adjust the draw progress. To maintain the authenticity of the drawing results, the final result point has not been optimized for display. As a result, there may be a discontinuity in cases where there are sudden large changes in position over a short period.
Compare Code
Since it’s the same functionality with different codes, comparing them with each other would be meaningful even if they were completed at different times. Of course, we should focus on the parts that are implemented similarly when making the comparison.
Screen touch event monitoring
The main focus is on detecting the touch events that occur on the screen
First Code:
override fun onTouchEvent(event: MotionEvent): Boolean {
touchX = event.x
touchY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
toFindChageCounts = true
findPointChangeIndex = -1
if (controlIndex < maxPoint || isMore == true) {
addPoints(BezierCurveView.Point(touchX, touchY))
}
invalidate()
}
MotionEvent.ACTION_MOVE ->{
checkLevel++
if (inChangePoint){
if (touchX == lastPoint.x && touchY == lastPoint.y){
changePoint = true
lastPoint.x = -1F
lastPoint.y = -1F
}else{
lastPoint.x = touchX
lastPoint.y = touchY
}
if (changePoint){
if (toFindChageCounts){
findPointChangeIndex = findNearlyPoint(touchX , touchY)
}
}
if (findPointChangeIndex == -1){
if (checkLevel > 1){
changePoint = false
}
}else{
points[findPointChangeIndex].x = touchX
points[findPointChangeIndex].y = touchY
toFindChageCounts = false
invalidate()
}
}
}
MotionEvent.ACTION_UP ->{
checkLevel = -1
changePoint = false
toFindChageCounts = false
}
}
return true
}
Second Code:
Canvas(
...
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
model.pointDragStart(it)
},
onDragEnd = {
model.pointDragEnd()
}
) { _, dragAmount ->
model.pointDragProgress(dragAmount)
}
}
.pointerInput(Unit) {
detectTapGestures {
model.addPoint(it.x, it.y)
}
}
)
...
/**
* change point position start, check if have point in range
*/
fun pointDragStart(position: Offset) {
if (!mInChange.value) {
return
}
if (mBezierPoints.isEmpty()) {
return
}
mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}
}
/**
* change point position end
*/
fun pointDragEnd() {
bezierPoint = null
}
/**
* change point position progress
*/
fun pointDragProgress(drag: Offset) {
if (!mInChange.value || bezierPoint == null) {
return
} else {
bezierPoint!!.x.value += drag.x
bezierPoint!!.y.value += drag.y
calculate()
}
}
We can see that Compose provides detailed events for Tap and Drag, which results in fewer marked variables in the new code.
I was once amazed by this feature, which I thought was just syntax sugar.
For instance, here is a method that finds the closest valid point to the touch position.
First Code:
private fun findNearlyPoint(touchX: Float, touchY: Float): Int {
Log.d("bsr" , "touchX: ${touchX} , touchY: ${touchY}")
var index = -1
var tempLength = 100000F
for (i in 0..points.size - 1){
val lengthX = Math.abs(touchX - points[i].x)
val lengthY = Math.abs(touchY - points[i].y)
val length = Math.sqrt((lengthX * lengthX + lengthY * lengthY).toDouble()).toFloat()
if (length < tempLength){
tempLength = length
if (tempLength < minLength){
toFindChageCounts = false
index = i
}
}
}
return index
}
Secode Code:
mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}
Similar to Java Stream, a chain structure looks more understandable.
Bezier curves drawing layer.
Bezier curves are mainly implemented using recursion.
First Code:
private fun drawBezier(canvas: Canvas, per: Float, points: MutableList<Point>) {
val inBase: Boolean
if (level == 0 || drawControl){
inBase = true
}else{
inBase = false
}
if (isMore){
linePaint.color = 0x3F000000
textPaint.color = 0x3F000000
}else {
linePaint.color = colorSequence[level].toInt()
textPaint.color = colorSequence[level].toInt()
}
path.moveTo(points[0].x , points[0].y)
if (points.size == 1){
bezierPoints.add(Point(points[0].x , points[0].y))
drawBezierPoint(bezierPoints , canvas)
val paint = Paint()
paint.strokeWidth = 10F
paint.style = Paint.Style.FILL
canvas.drawPoint(points[0].x , points[0].y , paint)
return
}
val nextPoints: MutableList<Point> = ArrayList()
for (index in 1..points.size - 1){
path.lineTo(points[index].x , points[index].y)
val nextPointX = points[index - 1].x -(points[index - 1].x - points[index].x) * per
val nextPointY = points[index - 1].y -(points[index - 1].y - points[index].y) * per
nextPoints.add(Point(nextPointX , nextPointY))
}
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
if (isMore && level != 0){
canvas.drawText("0:0", points[0].x, points[0].y, textPaint)
}else {
canvas.drawText("${charSequence[level]}0", points[0].x, points[0].y, textPaint)
}
for (index in 1..points.size - 1){
if (isMore && level != 0){
canvas.drawText( "${index}:${index}" ,points[index].x , points[index].y , textPaint)
}else {
canvas.drawText( "${charSequence[level]}${index}" ,points[index].x , points[index].y , textPaint)
}
}
}
}
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
canvas.drawPath(path, linePaint)
}
}
path.reset()
level++
drawBezier(canvas, per, nextPoints)
}
Second Code:
{
lateinit var preBezierPoint: BezierPoint
val paint = Paint()
paint.textSize = mTextSize.toPx()
for (pointList in model.mBezierDrawPoints) {
if (pointList == model.mBezierDrawPoints.first() ||
(model.mInAuxiliary.value && !model.mInChange.value)
) {
for (point in pointList) {
if (point != pointList.first()) {
drawLine(
color = Color(point.color),
start = Offset(point.x.value, point.y.value),
end = Offset(preBezierPoint.x.value, preBezierPoint.y.value),
strokeWidth = mLineWidth.value
)
}
preBezierPoint = point
drawCircle(
color = Color(point.color),
radius = mPointRadius.value,
center = Offset(point.x.value, point.y.value)
)
paint.color = Color(point.color).toArgb()
drawIntoCanvas {
it.nativeCanvas.drawText(
point.name,
point.x.value - mPointRadius.value,
point.y.value - mPointRadius.value * 1.5f,
paint
)
}
}
}
}
...
}
/**
* calculate Bezier line points
*/
private fun calculateBezierPoint(deep: Int, parentList: List<BezierPoint>) {
if (parentList.size > 1) {
val childList = mutableListOf<BezierPoint>()
for (i in 0 until parentList.size - 1) {
val point1 = parentList[i]
val point2 = parentList[i + 1]
val x = point1.x.value + (point2.x.value - point1.x.value) * mProgress.value
val y = point1.y.value + (point2.y.value - point1.y.value) * mProgress.value
if (parentList.size == 2) {
mBezierLinePoints[mProgress.value] = Pair(x, y)
return
} else {
val point = BezierPoint(
mutableStateOf(x),
mutableStateOf(y),
deep + 1,
"${mCharSequence.getOrElse(deep + 1){"Z"}}$i",
mColorSequence.getOrElse(deep + 1) { 0xff000000 }
)
childList.add(point)
}
}
mBezierDrawPoints.add(childList)
calculateBezierPoint(deep + 1, childList)
} else {
return
}
}
During the initial development phase, due to my limitations, the recursive method included both the drawing and calculation of the next level. However, during the secondary coding phase, influenced by the design of Compose, an attempt was made to transform all the point states into input information for the Canvas. This made the code-writing process more streamlined.
Of course, my development abilities have certainly changed in the past five years. Despite this, with the continous development of Kotlin, even for project accomplished using Kotlin, as new concepts are introduced and more suitable development technologises emerge, we still gain more from Kotlin and Compose.
My Story with Kotlin
My first encounter with Kotlin was in May of 2017. At that time, Kotlin was not yet the recommended language for Android development by Google. For me, Kotlin was more of a new technology that couldn’t be used practically in my work.
Nevertheless, I tried to use Kotlin to accomplish more tasks. Fortunately, without doing so, I wouldn’t have been able to finish this article and would have missed the chance to learn more about Kotlin in depth.
However, even when Google recommended Kotlin as the language for Android development in 2018, Kotlin still wasn’t a mainstream choice at the time. For me, some factors made me less inclined to use Kotlin at the time. Firstly, it was a new language, and the community was still under development. Many things still needed to be completed by the community, which resulted in various problems when using it in practice. There weren’t many feasible solutions for these problems online. Secondly, one of Kotlin’s features is its compatibility with Java, which is a double-edged sword. Although it allows me to use Kotlin with less burden (if I can’t do it in Kotlin, I could always use Java), it also led me to think that Kotlin is either Java++ or Java — . Thirdly, Kotlin didn’t bring any new content or special features. Anything Kotlin can do, Java can do as well (things like null values and data classes are more like syntactic sugar). So, why bother using an unfamiliar language to achieve what Java can already do?
Fortunately, more and more people are continually promoting and building Kotlin. This has attracted more people to join in. In recent years, Kotlin can be found in more and more projects. Adding Kotlin to existing projects is also becoming more widely accepted. I look forward to helping even more people with Kotlin.
Code Url