Lambdas with Receivers

Lambdas with receivers in Kotlin.

fun main() {

    println(countTo100())
    println(countTo99().toString()) // Call toString() here, on the returned StringBuilder object

    val employee = KotlinEmployee2("Jim", "Johnson", 2011)
    val employees = listOf(KotlinEmployee2("John", "Smith", 2012), KotlinEmployee2("Jane", "Wilson", 2015), KotlinEmployee2("Mary", "Johnson", 2010), KotlinEmployee2("Mike", "Jones",2002))

    // Let's pretend we don't know about the find() function in Collections
    findByLastName(employees, "Wilson")
    findByLastName(employees, "Watson")
    findByFirstName(employees, "Jane")
    findByFirstName(employees, "Joan")

    with (employee.firstName) { // Regular usage of 'with' function
        println(capitalize())
    }

    myWith(employee.firstName) {// Demo: This is a high-order function. It passes a function into a function
        println(capitalize())
    // Every time this is called a lambda object is created. This could result in excessive memory consumption. To avoid this, make the myWith function inline

        println(employee.run { firstName }) // Another example of an extension lambda which works with all data types. Takes one lambda as argument and returns the result of executing the lambda. Sends in 'this' as an argument, but the 'this' can be excluded when calling e.g. 'name' instead of 'this.name'.

        DbConnection.getConnection().let { connection -> ..... } // Connection will no longer be available outside the block, limiting its scope.
// .let can also be used as an alternative to testing against null:
variable?.let { someFunction } // This block will be executed if 'variable' is not null. 'it' (variable) has now also been cast to not-null
// It is also particularly useful for chaining manipulations together:
        println((employee.let{it.firstName.capitalize()} // Capitalise first name
                         .let{it + " Snr"} // Concatenate some text
                         .let{it.length} // Get its length
                         .let{it + 31}) // And add 31. Returns 38

        println(employee.apply {}) // Another example of an extension lambda which works with all data types. Returns the object it's applied to rather than the result of the execution. Really useful for calling functions on a newly created object:
        val employee2 = KotlinEmployee2(name = "Jack").apply{ name = "James" } // Immediately call a function on a newly created object
        println(employee2.name)

    "Some String".apply somestring@{ // Add label so we can apply an operation to it
        "Another String".apply {
            println(toLowerCase()) // Operates on "Another String"
            // If we want to perform an operation on "Some String" use labels:
            println(this@somestring.toUpperCase())
        }
    }


}

// The long way
//fun countTo100(): String {
//    val numbers = StringBuilder()
//    for (i in 1..99) {
//        numbers.append(i)
//        numbers.append(",")
//    }
//    numbers.append(100)
//    return numbers.toString() // numbers variable is used a lot
//}

fun countTo100() = with(StringBuilder()) { // 'with' converts the instance you pass to it into a RECEIVER
        for (i in 1..99) {
            append(i) // We can exclude the instance reference
            append(",") // Could also type this.append but less concise
        }
        append(100)
        toString()
    }

Further explanation:

// EXTENSION LAMBDA - this is the equivalent of the built-in 'with' function:
fun myWith(name: String, block: String.() -> Unit) { // block (common convention name, can be anything) is the name of the function definition for the operation. We use this name in the body of the myWith function
    // String.() specifies the class we are extending (the Receiver object), so that we can use myWith on it. Unit is the return type of this particular function i.e. no return type
    // So block is now an extension function on a String object, and it can be applied to a String. We can now apply the passed-in function to the passed-in argument.
    name.block()
    // When we call the myWith function on fish.name (above), fish.name is the name argument and capitalize() is the block function
    // capitalize() returns a copy of the passed-in String, and does not change the original String. Wrapping it in a printLine shows us the result
}

// 'apply' does the same as 'with' but always returns the receiver object

fun countTo99() = StringBuilder().apply {
    for (i in 1..98) {
        append(i) // We can exclude the instance reference
        append(",") // Could also type this.append but less concise
    }
    append(99) // Don't call toString() here as the whole StringBuilder object is returned
} // You could add .toString() here as well

// The long way
fun findByLastName(employees: List<KotlinEmployee2>, lastName: String) {
    for (employee in employees) {
        if (employee.lastName == lastName) {
            println("We have an employee with last name $lastName")
            return
        }
    }
    println("Nobody here with the last name $lastName")
}

fun findByFirstName(employees: List<KotlinEmployee2>, firstName: String) {
    employees.forEach { // We can use a lambda as this is a functional interface
        if (it.firstName == firstName) {
            println("we have an employee with first name $firstName")
            return // Returns from both the lambda and the function i.e. a non-local return. This will only work when the function that is taking the lambda is inlined
            // If we want to make it a local return we can label the lambda: employees.forEach returnBlock@ {     followed by    return@returnBlock. In this case it would result in the "Nobody..." String being printed
            // This is useful when using nested 'with' or 'apply' statements (see "Some String" example in main function)
        }
    }
    println("Nobody here with first name $firstName")
}

data class KotlinEmployee2(val firstName: String, val lastName: String, val startYear: Int) { // Data class overrides our toString for a legible output

}

 

Setting Preference Summary/Label for ListPreference

ListPreference items in Preference screens do not automatically provide a label showing their current value. You can create labels for all list Preference items as so:

In the Settings Fragment onCreatePreferences override, get the Shared Preferences and the Preference screen. Then iterate through all of the Preferences in the screen, checking whether they are tickboxes. Any that aren't will have their value stored and passed to a setPreferenceSummary function along with the Preference itself:

    @Override
    public void onCreatePreferences(Bundle bundle, String s) {

        addPreferencesFromResource(R.xml.pref_xxxxx); // Reference to xml resource
        SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); // Retrieve the Shared Preferences from the Preference screen
        PreferenceScreen preferenceScreen = getPreferenceScreen(); // Retrieve the Preference screen
        int count = preferenceScreen.getPreferenceCount(); // The number of Preferences within the screen

        for (int i = 0; i < count; i++ ) { // For each Preference...
            Preference p = preferenceScreen.getPreference(i); // ...retrieve the Preference...
            if (!(p instanceof CheckBoxPreference)) { // ...make sure it isn't a checkbox...
                String value = sharedPreferences.getString(p.getKey(), ""); // ...then retrieve its value (can't use getString on a Boolean, so another reason to exclude tickboxes)...
                setPreferenceSummary(p, value); // ...and send the Preference and its value to the setPreferenceSummary function
            }
        }
    }

The setPreferenceSummary function is as follows:

    private void setPreferenceSummary(Preference preference, String value) {
        if (preference instanceof ListPreference) { // If it is a ListPreference...
            ListPreference listPreference = (ListPreference) preference; // ...cast it to an object of type ListPreference...
            int prefIndex = listPreference.findIndexOfValue(value); // ...retrieve the index of that value...
            if (prefIndex >= 0) {
                listPreference.setSummary(listPreference.getEntries()[prefIndex]); // ...and get the corresponding label for it, setting it as the Preference Summary
            }
        } else if (preference instanceof EditTextPreference) {
            // For EditTextPreferences, set the summary to the value's simple string representation.
            preference.setSummary(value);
        }
    }

Implement an OnSharedPreferenceChangeListener to make sure changes are implemented without having to force an onCreate (e.g. device rotation). Within the required onSharedPreferenceChanged override, again check to see if the preference is not a checkbox, and if so send the preference to the setPreferenceSummary function along with its value:

public class SettingsFragment extends PreferenceFragmentCompat implements OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        Preference preference = findPreference(key);
        if (preference != null) {
            if (!(preference instanceof CheckBoxPreference)) {
                String value = sharedPreferences.getString(preference.getKey(), "");
                setPreferenceSummary(preference, value);
            }
        }
    }
}

Register and unregister the OnSharedPreferenceChangeListener with the following overrides:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

ud851-Exercises-student\Lesson06-Visualizer-Preferences\T06.08