قالب وردپرس درنا توس
Home / Apple / Domain specific languages ​​in Kotlin: Getting started

Domain specific languages ​​in Kotlin: Getting started



In modern programming, developers are trying to write clean and readable code that is intuitive and easy to use. To achieve this, developers normally use design patterns and create specific architectural solutions. Code written in such a way is maintainable and readable by experienced developers, but what if you could write code like anyone can understand and reason about?

Kotlin gives you the tools to help create code for something that feels more natural to use, through a domain-specific language – or DSL ! :]

In this tutorial, you create DSLs to display Android dialogs, in different DSL styles and data models you need to use for the application. Your app shows a list of puppies that you can try to favorite by using the mentioned dialogs. By implementing these features, you will:

  • Learn basic reasons for building a DSL in Kotlin.
  • Learn the basics of lambdas with and without recipients.
  • Make a DSL using the construction pattern.
  • Create a DSL using Kotlin expansion features.

Before building your own DSL, it is best to take a look at some of the DSLs you already use, but may not be aware of.

Domain-Specific Language

Domain-Specific Languages, or DSLs, are languages ​​that are specialized for a particular part of an app. It is used to extract part of the code to make it reusable and more understandable. Unlike a function or method, DSL also changes the way you use and write code. Usually, DSLs make the code more readable, almost like a spoken language. This means that even people who do not understand the architecture behind the code will understand the meaning of it.

Creating a DSL means changing syntax for the particular part of the code. In Kotlin, this is achieved by using lambda and expansion functions, and expressions, to remove a lot of boiler code and hide the internal implementation for the user. One of the best examples is a crucial part of Android development – Gradle .

Popular DSLs

If you're an Android developer, chances are you've already seen a DSL before. However, would you be surprised to know that you are writing Gradle build files using a DSL? Take a look at the following:

  android {
compileSdkVersion 28
defaultConfig {
programId "android.raywenderlich.com.puppyparadise"
minSdkVersion 19
targetSdkVersion 28
version code 1
version name "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled fake
proguardFiles getDefaultProguardFile (& proguard-android-optimize.txt & # 39;), proguard-rules.pro & # 39;
}
}
}

Look at how the code is written in any build.gradle file. Objects use curly brackets to specify the content. Inside these brackets, enter the value of specific parameters by typing a parameter name and then a value.

It may seem that this file simply contains notes or chart documentation for a class, but in reality it is just a special DSL. Below you can check the same code written without DSL:

  Android (28,
DefaultConfig ("android.raywenderlich.com.puppyparadise",
19,
28,
1,
"1.0"
"Android.support.test.runner.AndroidJUnitRunner"
)
)
BuildTypes (
Drop (false,
getDefaultProguardFile (& # 39; ProGuard-android-optimize.txt & # 39;),
& # 39; Proguard-rules.pro & # 39;
)
)

At first glance, it may look unclear, mostly because each object you build has many parameters and that you do not know their name. By comparing these two approaches, you can see that the version using a DSL is easier to read. But more importantly, because you know the parameters and properties you can access, it is also easier to maintain. This is one of the requirements this DSL has, and because of that it is much easier to use than standard – to use objects.

Another popular example in Android is Anko . It is a group of Kotlin DSL libraries that cover a lot of functionality. The libraries contain not only DSLs for user interface components, but for managing network calls, background tasks, and database management. The code snippet below uses Anko to make a background call using Kotlin Coroutines :

  suspend funny getData (): Data {...}

class MyActivity: Activity () {
funny lastAndShowData () {
// Ref  uses WeakReference under the hood
selection ref: Ref  = this.asReference ()

async (ui) {
select data = getData ()

// Use ref () instead of this @ MyActivity
ref (). showData (data)
}
}

funny showData (data: Data) {...}
}

The curly brackets when calling asnyc are one of the DSL's signatures. And here you state that a function call will be made asynchronous, without really immersing you in the implementation details. Just as it should be. To see more examples of Anko Library, check the official GitHub page.

Getting Started

Before you start writing DSLs, you can download the tutorial material from Download the materials button at the top or bottom of the page. You will see the start and end projects that you want to use throughout the training.

After the project is set up, build and run the app, you get a screen like this:

You can see a scrollable screen with cute puppies. You can click on any of them to open the dialog.

After clicking on a puppy, a dialog with blurred background appears. You can choose whether you like the puppy or not. If you select the YES option, a heart icon appears in the lower right corner. If you select the NO option, the heart is removed, but you do not have to use it because all the puppies are too cute for them not to match. :]

This dialogue is the main focus of the training. Open the class DialogPopupView . If you look at the code, you can see that the class contains several properties in the dialog. These include click listeners and the methods to blur the background. At the bottom you can find DialogPopupBuilder who is responsible for building and setting these properties, finally creating a dialogue.

Open PuppyActivity.kt and see createDialogPopup () :

  DialogPopupView.builder (this)
.viewToBlur (rootView)
.titleText (titleText)
.negativeText (negativeText)
.positiveText (positiveText)
.onBackgroundClickAction (backgroundClickAction)
.onCancelClickAction (cancelActionClick)
.onPositiveClickAction (positiveClickAction)
.build()

This method creates a DialogPopupView using the build pattern . The goal is to change this conversation to look more user friendly using a DSL. Broadly speaking, clean up the code so it can be written as follows:

  buildDialog (this) {
viewToBlur (rootView)
Title (titleText)
positiveAction (positiveText) {positiveClickAction ()}
negativeAction (negativeText) {negativeClickAction ()}
backgroundAction {backgroundClickAction ()}
}

Note : If you are not familiar with the construction pattern, check out our intermediate design patterns in Swift. Although there is a tutorial written for Swift, the concepts described in this tutorial translate to all programming languages ​​including Kotlin.

As you can see, with a DSL there will be less code and no points required to connect the leased calls to the builder pattern.

DSLs in Kotlin

Now that you know what you want to achieve, you need to use two important functions in the Kotlins language to create a DSL:

  • Lambda expression
  • Lambda outside parentheses.
  • Lambdas with receivers.

<

The Lambda expression is an anonymous feature used to pack behaviors you can call any number of times, with or without parameters. You can also send them around as function parameters and save them as class properties. If you are not familiar with lambda expressions, check the official documentation.

To create or use a lambda expression, you must first specify the type of lambda you need. lambda type syntax is as follows: (parameter, parameter) -> returnType .

You can interpret this by dividing it into two parts, the left and right sides of the arrow. On the left side there are required parameters inside the brackets. You can define any number of required parameters for each lambda function so it doesn't have to be one or two. Each lambda cannot have any parameters, and it can have any number of parameters.

And on the right side you define the return type. Simply enter on the left side which parameters you want to send to the right, where you call a method, and return a result using these parameters. For a non-return lambda function, use Unit as the return type. If you were to write a lambda that takes a String and returns a Int you would write the following: (String) -> Int (String) -> Int . [19659002] However, when using lambda expressions in code, they are written differently than lambda syntax. Using lambda goes as follows: {parameter, parameter -> behavior} . You first open the curly seals and define the list of parameters you receive and must use. Then separate the code you want to run using an arrow ( -> ). After that, you can run any number of function calls between the arrow and the closing bars.

Since lambdas are extremely useful there are pretty much syntax sugar around them. Three common sugars include derive lambda parameters and wrote lambdas outside the parentheses and lambdas with receivers .

Inferring Lamda Parameters

For example, if a lambda has only one parameter, you do not need to enter it in the expression. It was then concluded with within curly braces, and called it . This is common when you use aggregate functions while writing something like: list.filter {it.isFavorite} when filtering a list of items. it in this example is then derived as each item on the list that you iterate over.

Lambdas Outside of Parentheses

Lambda's outside parenthesis is a feature of Kotlin that allows you to move the lambda argument outside parentheses if it is the last parameter. Additionally, if it is the only parameter, you can completely remove the brackets. This means that if you have a method call similar to method ({}) with lambdas outside the brackets, it can also be called method {} . This feature makes the code more readable and suggested by the Kotlin style guide.

Lambdas With Receivers

Imagine that you have a model class for a object that looks like this:

  data class Puppy (was iceLiked: Boolean = false, was imageRessource: Int = 0)

You can create a DSL by creating a function called puppy that takes a lambda as a parameter and returns a complete Puppy object:

  fun puppy (lambda: ( Puppy: (Puppy:) -> Unit): Puppy {
// 1
fall puppy = puppy ()
// 2
lambda (puppy)
// 3
return puppy
}

In this function, do the following:

  • Instantiate a Puppy .
  • Ring lambda who will use Puppy
  • Returns the puppy

You can invoke DSL by creating and editing a Puppy :

  puppy {
it.is Liked = true
it.imageResource = R.drawable.golden_retriever
}

You call the puppy function and put the properties inside the curly brackets. The current solution works, but you must use the to access the properties instead of accessing them directly. This can be fixed by implementing lambdas with receivers .

A lambda with recipients allows you to call methods of an object in the body of a lambda without any qualifications, such as it . You add the recipient with the class type, which means that the received action is a function that any object of the type can call. Furthermore, it changes this object in braces. Examine the excerpt below:

  funny puppy (lambda: puppy. () -> unit): puppy {
fall puppy = puppy ()
puppy.lambda ()
return puppy
}

Note that lambda previously gave a Puppy ( lambda: (Puppy) -> Unit ) to the caller. With recipients, however, you can call lambda as a feature directly on any Puppy . To make the code easier you can use Kotlins search extension function:

  fun pupp (lambda: Puppy. () -> Unit) = Puppy (). Apply (lambda)

Apply allows you to make the function implementation a liner by directly referring to the new object without having to create a named property. The puppy function is now called as:

  puppy {
isLiked = true
imageResource = R.drawable.golden_retriever
}

Instead of using the keyword it you can now refer to the object directly because you explicitly said it must be of the type Puppy .

Your First DSL

Armed with the knowledge above, it's time to change DialogPopupBuilder and create your first DSL!

You will use what you learned to refactor DialogPopupView and create a DSL for it. The goal is to replace the current functions to set the properties by calling a lambda. By doing this you will be able to call the builder with lambda's outside brackets, and without using a leaned function call. Replace DialogPopupBuilder with:

  class DialogPopupBuilder {
was context: context? = zero
was viewToBlur: Viewing? = zero
var titleText: String = ""
was negativeText: String = ""
was positiveText: String = ""
was onBackgroundClickAction: () -> Device = {}
var onNegativeClickAction: () -> Unit = {}
var onPositiveClickAction: () -> Device = {}

inline fun with (context: () -> context) {
this.context = context ()
}

inline fun viewToBlur (viewToBlur: () -> View) {
this.viewToBlur = viewToBlur ()
}

inline fun titleText (title: () -> String) {
this.titleText = title ()
}

inline fun negativeText (negativeText: () -> String) {
this.negativeText = negativeText ()
}

inline fun positiveText (positiveText: () -> String) {
this.positiveText = positiveText ()
}

fun onNegativeClickAction (onNegativeClickAction: () -> Unit) {
this.onNegativeClickAction = onNegativeClickAction
}

funny onPositiveClickAction (onPositiveClickAction: () -> Device) {
this.onPositiveClickAction = onPositiveClickAction
}

fun at BackgroundClickAction (onBackgroundClickAction: () -> Device) {
this.onBackgroundClickAction = onBackgroundClickAction
}

fun build () = DialogPopupView (context !!, this)
}

You removed the constructor and added a with () call, to make it cleaner in the long run. Since you are going to use a DSL to build the dialogue, you do not need the method to build it in DialogPopupView . Remove the builder method from the companion object .

Finally, you need to write a DSL that will create DialogPopupBuilder and build DialogPopupView . Create a new Kotlin file called DialogPopupViewDsl.kt and paste the following code:

  fun dialogPopupView (lambda: DialogPopupView.DialogPopupBuilder. () -> Unit) =
DialogPopupView.DialogPopupBuilder () // 1
.apply (lambda) // 2
.build () // 3

The function has a lambda with a [DialogPopupBuilder receiver as parameter. Each line breaks down as follows:

  • Instantiate an instance of DialogPopupBuilder .
  • Set all properties using lambda.
  • call build () to return a DialogPopupView .

You can use DSL instead of the old building pattern from within PuppyActivity . Change createDialogPopup () to use DSL:

  dialogPopupView {
with {this @ PuppyActivity}
viewToBlur {rootView}
titleText {titleText}
negativeText {negativeText}
positiveText {positiveText}
onPositiveClickAction {positiveClickAction ()}
onNegativeClickAction {negativeClickAction ()}
onBackgroundClickAction {backgroundClickAction ()}
}
 dialogPopupView ()  takes each dialog property directly, without the need to link feature calls or call  build () . 

Build and run the code. You see everything as before, but test the dialogue to make sure it works the same way.

DSL With Extension Functions

You just made your first DSL using a build pattern! :]

Then create a DSL using the expansion features and use the build pattern to create the data to be displayed puppies!

Extension features are features that allow you to add new functionality to an existing class type. Any object of the type you specify can use the expansion feature. If you are not familiar with expansion features, check the official documentation.

You create another DSL for the same DialogPopupView but this time you use expansion features.

Create a new Kotlin file called DialogPopupViewWithExtensionsDsl.kt . This time, you will send the context as a parameter to distinguish it from the UI properties. You will also link the title of the actions and their lambdas to a single function so that similar properties are grouped in one place.

First, create a function buildDialog that will create the entire dialog and build it:

  inline fun buildDialog (context: Context, buildDialog: DialogPopupView.DialogPopupBuilder. () -> Device): DialogPopupView {
select builder = DialogPopupView.DialogPopupBuilder ()

builder.context = context
builder.buildDialog ()
return builder.build ()
}

The function accepts a Context and a lambda with a receiver of the type DialogPopupView.DialogPopupBuilder . Building the dialog is the same as in the previous example.

Then create a method for each property group and set the properties inside:

  fun DialogPopupView.DialogPopupBuilder.title (title: String) {
this.titleText = title
}

funny DialogPopupView.DialogPopupBuilder.viewToBlur (viewToBlur: View) {
this.viewToBlur = viewToBlur
}

funny DialogPopupView.DialogPopupBuilder.negativeAction (
negativeText: String,
onNegativeClickAction: () -> Device
) {
this.onNegativeClickAction = onNegativeClickAction
this.negativeText = negativeText
}

funny DialogPopupView.DialogPopupBuilder.positiveAction (
positiveText: String,
onPositiveClickAction: () -> Device
) {
this.onPositiveClickAction = onPositiveClickAction
this.positiveText = positiveText
}

funny DialogPopupView.DialogPopupBuilder.backgroundAction (onBackgroundClickAction: () -> Unit) {
this.onBackgroundClickAction = onBackgroundClickAction
}

Each of these methods is quite similar. They set the characteristics of the corresponding groups.

Finally, you can change createDialogPopup () inside PuppyActivity.kt to use the new DSL:

  buildDialog (this) {
viewToBlur (rootView)
Title (titleText)
positiveAction (positiveText) {positiveClickAction ()}
negativeAction (negativeText) {negativeClickAction ()}
backgroundAction {backgroundClickAction ()}
}

You can see that context is now set through a parameter and not a function. Positive and negative actions are put together with their respective texts to improve readability by grouping the code.

Build and run the code to confirm that everything is working as expected. Once again you will get the same screen as before because you just changed the syntax and not the logic. But it never hurts to be safe! :]

Collections With DSL

Another interesting use of DSLs is with collections. Take a look at puppies set at the top of PuppyActivity . The code itself looks pretty clean and readable; This is because there are currently only two properties in the class Puppy . But in case you have four or more properties, it will start to look bulky. For this reason, you create a DSL that will change the way you instantiate collections.

Create a new Kotlin file called PuppiesDsl.kt . First, add a PuppyBuilder that has two features and a build method:

  class PuppyBuilder {
was isLiked: Boolean = false
var imageResourceId: Int = 0

fun build (): Puppy = Puppy (isLiked, imageResourceId)
}

The construction method returns a Puppy with the listed properties.

Then create a [Puppies] file that expands ArrayList by Puppy and has a puppy method:

  class puppies : ArrayList  () {
funny puppy (puppyBuilder: PuppyBuilder. () -> Device) {
add (PuppyBuilder (). applies (puppyBuilder) to build ())
}
}
 puppy ()  uses a lambda with the recipient of the type  PuppyBuilder . Inside, you call  add ()  from  ArrayList  to add a new  Puppy  built with  PuppyBuilder . 

Then, create a PuppyViewModelBuilder that will contain the list of all the puppies, by adding the following code:

  class PuppyViewModelBuilder {
private choice puppies = mutableListOf  ()

funny puppies (puppies List: Puppies. () -> Unit) {
puppies.addAll (puppies.) applies (puppiesList))
}

fun build (): ArrayList  = ArrayList (puppies)
}

The class contains a MutableList by Puppy . puppies () have a lambda with the recipient of the type Puppies and it adds all the elements that were returned by calling puppiesList () to the collection. In addition, you added build () which returns a ArrayList by Puppy with puppies as their data.

Finally, create a puppyViewModel method inside PuppiesDsl :

  fun puppyViewModel (puppies: PuppyViewModelBuilder. () -> Device): ArrayList  =
PuppyViewModelBuilder (). (Puppies) to build ()

The function uses a lambda with the recipient of the type PuppyViewModelBuilder as a parameter that you use to build ArrayList by Puppy .

Now, you can replace the code in PuppyActivity.kt which creates puppies using the DSL you just created:

  private var puppies: List  = puppyViewModel {
puppies {
puppy {
isLiked = false
imageResourceId = R.drawable.samoyed
}
puppy {
isLiked = false
imageResourceId = R.drawable.shiba
}
puppy {
isLiked = false
imageResourceId = R.drawable.siberian_husky
}
puppy {
isLiked = false
imageResourceId = R.drawable.akita
}
puppy {
isLiked = false
imageResourceId = R.drawable.german_shepherd
}
puppy {
isLiked = false
imageResourceId = R.drawable.golden_retriever
}
}
}

First, call puppyViewModel () to begin the data build. Inside, you call puppies () where you call puppy () for each puppy you need to make. Each of these conversations will create a new Puppy and you can customize their properties as you like.

The current syntax for creating a collection looks like a JSON structure, which is very user-friendly. The benefits of this DSL would grow, the bigger and more complex the puppy would be. And we all know how puppies can grow! :]

Build and run the app to check the current state. You should get the same home screen with a list of puppies in the same order.

DSL Markers

Try adding a new Puppy to an existing Puppy or a new ArrayList by Puppy inside in the existing list. You will see that you are able to do it even if you are not, since it can destroy the data. Because you make lambdas in other lambdas, you can still access the recipients of the outer lambda's! To prevent this, you must create a DSL marker, which was created specifically to resolve this issue. Inside PuppiesDsl at the bottom, create a new note class called PuppyDslMarker :

  @DslMarker
note class PuppyDslMarker
 @DslMarker  specifies that classes labeled with it, or  PuppyDslMarker  define a DSL. 

Then you comment on all the classes inside PuppiesDsl.kt file with @PuppyDslMarker . Try adding a new Puppy to an existing one and you will get an error stating that "cannot be called by implicit recipient." Problem solved!

Where to Go From Here? [19659010] You can download the final project using Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned how to create different types of DSL for different situations. You can now create your own specific DSLs that will facilitate daily programming. To learn more, check the official documentation.

I hope you enjoyed this tutorial! If you have questions or comments, you can participate in the forum discussion below! And try not to look at the puppies too long! :]


Source link