Kotlin Coroutines

Coroutines are Kotlin’s way of dealing with asynchronous tasks. They can execute tasks sequentially.

Coroutines require a:

  • Job – cancellable background task with a lifecycle that culminates in its completion. Jobs can be arranged in a parent-child hierarchy so that cancellation of the parent cancels all child jobs
  • Dispatcher – sends off coroutines to run on various threads
  • Scope – combines information, including a job and dispatcher, to define the context in which a coroutine runs. Scopes keep track of coroutines

The keyword suspend is Kotlin’s way of marking a function, or function type, available to coroutines. When a coroutine calls a function marked suspend, instead of blocking until that function returns like a normal function call, it suspends execution until the result is ready then it resumes where it left off with the result. While it’s suspended waiting for a result, it unblocks the thread that it’s running on so other functions or coroutines can run. The suspend keyword doesn’t specify the thread code runs on. Suspend functions may run on a background thread or the main thread.

import ...

class MyViewModel(
        val database: MyDatabaseDao,
        application: Application) : AndroidViewModel(application) {

    private var viewModelJob = Job()

    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) // Set Dispatcher on the Main thread and the job

    private var myObject = MutableLiveData<MyObject?>()

    init {
        initialiseMyFunction() // When the class is instantiated call initialiseMyFunction
    }

    private fun initialiseMyFunction() {
        uiScope.launch { // This function starts by running on the Main thread as the Viewmodel usually is involved in updating the UI
            myObject.value = getObjectFromDatabase() // Then it calls the getObjectFromDatabase function
        }
    }

    private suspend fun getObjectFromDatabase(): MyObject? { // We make this a suspend functino so it won't block the UI Thread from doing work
        return withContext(Dispatchers.IO) { // This is run on the background thread as we don't eant it blocking the UI thread and messing up the user experience
            var myObject = database.getObject()
            myObject // Return the object
        }
    }

    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel() // Be sure to cancel the Job (and hence any associated coroutines) when the ViewModel is destroyed, to prevent memory leaks
    }
}

See examples in the Sleep Tracker app from the Google Udacity course: Developing Android Apps with Kotlin

Vibrate

Make the device vibrate.

Add permission:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.android.myapp">

    <uses-permission android:name="android.permission.VIBRATE" />

    <application
        ...
    </application>

</manifest>

If used in conjunction with ViewModel/LiveData then set up the variables in ViewModel:

import ...

class MyViewModel : ViewModel() {

    companion object {
        private const val DONE = 0L
        private const val ONE_SECOND = 1000L
        private const val COUNTDOWN_TIME = 6000L
        private const val PANIC_TIME = 2000L
        private val FIRST_BUZZ_PATTERN = longArrayOf(100, 100, 100, 100, 100, 100) // Define vibrate patterns
        private val SECOND_BUZZ_PATTERN = longArrayOf(0, 1000)
        private val NO_BUZZ_PATTERN = longArrayOf(0)
    }

    enum class BuzzType(val pattern: LongArray) {
        FIRST(FIRST_BUZZ_PATTERN),
        SECOND(SECOND_BUZZ_PATTERN),
        NO_BUZZ(NO_BUZZ_PATTERN)
    }

    // Buzzer state
    private val _eventBuzz = MutableLiveData<LongArray>() // Create a LiveData object for the value of the buzz LongArray
    val eventBuzz: LiveData<LongArray> // Encapsulate the data
        get() = _eventBuzz

    // Timer value
    private val _currentTime = MutableLiveData<Long>()
    val currentTime: LiveData<Long>
        get() = _currentTime

    private val timer: CountDownTimer

    init {
        timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) {
            override fun onTick(millisUntilFinished: Long) {
                _currentTime.value = millisUntilFinished/ ONE_SECOND
                if (millisUntilFinished <= PANIC_TIME) {
                    _eventBuzz.value = BuzzType.FIRST.pattern // Fire a buzz if we are 2 seconds from completion
                }
            }

            override fun onFinish() {
                _currentTime.value = DONE
            }
        }
        timer.start()

    }

    fun onPushButton() {
        _eventBuzz.value = BuzzType.SECOND.pattern
    }

    fun onBuzzComplete() {
        _eventBuzz.value = BuzzType.NO_BUZZ.pattern
    }
}

Back in our Fragment:

import ...

class MyFragment : Fragment() {

    private lateinit var viewModel: MyViewModel

    private lateinit var binding: MyFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Inflate view and obtain an instance of the binding class
        binding = DataBindingUtil.inflate(
                inflater,
                R.layout.my_fragment,
                container,
                false
        )

        viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)

        viewModel.eventBuzz.observe(this, Observer { eventBuzz ->
            if (eventBuzz.isNotEmpty()) buzz(eventBuzz)
        })

        binding.myViewModel = viewModel

        binding.setLifecycleOwner(this)

        return binding.root

    }

    private fun buzz(pattern: LongArray) {
        val buzzer = activity?.getSystemService<Vibrator>()

        buzzer?.let {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                buzzer.vibrate(VibrationEffect.createWaveform(pattern, -1))
            } else {
                //deprecated in API 26
                buzzer.vibrate(pattern, -1)
            }
        }
    }
}

 

Manipulating LiveData data in the ViewModel

Transformations.map takes the input of one LiveData, applies a transformation (a function), and outputs another LiveData.

class MyViewModel : ViewModel() {

    companion object {
        private const val DONE = 0L
        private const val ONE_SECOND = 1000L
        private const val COUNTDOWN_TIME = 6000L
    }

    // Timer value
    private val _currentTime = MutableLiveData<Long>()
    val currentTime: LiveData<Long>
        get() = _currentTime

    val currentTimeString = Transformations.map(currentTime,{time -> // This takes in the current timer value and applies a time format before outputting it. Because it is a MutableLiveData it can be observed in a layout xml via data binding
        DateUtils.formatElapsedTime(time)
    })

    private val timer: CountDownTimer

    init {
        timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) {
            override fun onTick(millisUntilFinished: Long) {
                _currentTime.value = millisUntilFinished/ ONE_SECOND
            }

            override fun onFinish() {
                _currentTime.value = DONE
                _eventGameFinish.value = true
            }
        }
        timer.start()
    }
}

 

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="myViewModel"
            type="com.example.android.myapp.MyViewModel" />
    </data>

        <TextView
            android:id="@+id/timer_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{myViewModel.currentTimeString}" /> // Call the LiveData from the ViewModel

</layout>

For more complex operations you may need to use a SwitchMap or MediatorLiveData. Allows manipulation and combination of more than one LiveData.

Combining data binding with Lifecycle components

We can use data binding along with Lifecycle components (e.g. ViewModel and LiveData) to remove the controller (Activity or Fragment) as middle-man from the process of passing data from the ViewModel to the View.

Without data binding (at least, only using data binding to avoid findViewById):

import ...

class MyFragment : Fragment() {

    private lateinit var viewModel: MyViewModel // Declare a MyViewModel object

    private lateinit var binding: MyFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Inflate view and obtain an instance of the binding class.
        val binding: ScoreFragmentBinding = DataBindingUtil.inflate(
                inflater,
                R.layout.my_fragment,
                container,
                false
        )

        viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java) // Initialise the MyViewModel object to a MyViewModel instance
        viewModel.myVar.observe(this, Observer { myVar -> // Observe a variable in the ViewModel class
            binding.myTextview.text = myVar.toString() // Use it to change our UI
        })

        binding.myButton.setOnClickListener { viewModel.changeVar() } // In this example, we have a button with an onClickListener which calls a MyViewModel function to change the variable

        return binding.root
    }
}

With data binding:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data> // Add a data tag
        <variable // With a variable
            name="myViewModel" // Give it a name
            type="com.example.android.myapp.MyViewModel" /> // Point to the ViewModel class
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/app_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/my_textview"
            android:text="@{@string/string_format(myViewModel.myVar)}" // myVar is a variable within the ViewModel with a public getter. Don't need myVar.value thanks to data binding. Defaults to empty String. This has been wrapped in a String format defined in strings.xml ( <string name="string_format">\"%s\"</string> ) which wraps the String in quotes. Customise this for other types e.g. Ints, or use String.valueOf()
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/my_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myViewModel.changeVar()}" /> // Add an onClick to provide a listener for the button

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

And our Fragment looks like:

import ...

class MyFragment : Fragment() {

    private lateinit var viewModel: MyViewModel // Declare a MyViewModel object

    private lateinit var binding: MyFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Inflate view and obtain an instance of the binding class.
        val binding: ScoreFragmentBinding = DataBindingUtil.inflate(
                inflater,
                R.layout.my_fragment,
                container,
                false
        )

        viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
        // We have removed our onClickListener and Observer

        binding.myViewModel = viewModel // Link your ViewModel with the data tag in the xml file. Allows 

        binding.setLifecycleOwner(this) // Set the Lifecycleowner of the bindingto the current Fragment. Allows us to automatically update layouts

        return binding.root
    }
}

For anything more complicated than plain text we meed to move the logic and data manipulation to the ViewModel and use the likes  of Transformations.map (see this post).

Encapsulate LiveData

Create internal and external version of the variables that need to be passed out of our class, so that other classes cannot modify the internal data.

import ...

class MyViewModel : ViewModel() {

    // The variable to be passed out of the class
    private val _variable = MutableLiveData<String>() // Private internal class, mutable
    val variable: LiveData<String> // Public external class, immutable
    get() = _variable // With a custom setter to provide the value from the internal class

    init {
        _variable.value = "" // Initialise the internal version of the variable if needed. All uses of the variable in this class should use the underscore version i.e. _variable
    }
}

 

Add Timber logging library

Add dependency to build.gradle:

dependencies {
    implementation ...
    implementation "com.jakewharton.timber:timber:4.7.1" // Update version if necessary
}

Build an application class (a base class that contains global application state for your entire app):

class PusherApplication: Application() {
    override fun onCreate() {
        Timber.plant(Timber.DebugTree())
        super.onCreate()
    }
}

Register the application in AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.dessertpusher">

    <application
        android:allowBackup="true"
        ...
        android:name=".PusherApplication"> // <- Here!
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Use Timber.x instead of Log.x when calling logs

Adding and using animations with the Navigation component

For a new animation, create a new resource file in the res/anim folder:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="@android:integer/config_mediumAnimTime" // Sets the duration of the animation
        android:fromAlpha="0.0" // Begins fully transparent
        android:toAlpha="1.0" // And ends in fully opaque
        />
</set>

Another example:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_shortAnimTime"
        android:fromXDelta="-100%" // Begins fully offscreen left
        android:fromYDelta="0%" // No change in vertical positioning
        android:toXDelta="0%" // Ends centred (no offest)
        android:toYDelta="0%" />
</set>

Apply your animations by clicking on the link between Fragments in the Design view and choosing in the Animations section.

Navigation Listener

An example of using a Navigation Listener would be to limit the screens on which your Navigation Drawer will be available. In this instance the drawer is locked on all screens other than the start destination (the opening Fragment):

import ...
class MainActivity : AppCompatActivity() {
    lateinit var drawerLayout: DrawerLayout
    lateinit var navController: NavController
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        @Suppress("UNUSED_VARIABLE")
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        navController = this.findNavController(R.id.myNavHostFragment)
        drawerLayout = binding.drawerLayout
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)

        // Add a listener which will determine if the destination is the start destination. If not, the nav drawer will be locked
        navController.addOnNavigatedListener { nc: NavController, nd: NavDestination -> // This currently doesn't work with Navigation version above 1.0.0-alpha07
            if (nd.id == nc.graph.startDestination) {
                drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
            } else {
                drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
            }
        }

        NavigationUI.setupWithNavController(binding.navView, navController)
    }

    override fun onSupportNavigateUp(): Boolean {
//        return navController.navigateUp()
        return NavigationUI.navigateUp(navController, drawerLayout)
    }
}

 

Navigation Drawer (with Navigation component?)

Add dependency:

dependencies {
    ...
    implementation "com.google.android.material:material:$version_supportlib" // Add version number to build.gradle also
}

Make sure any Fragments you wish to navigate to have been added to the Navigation Design window.

Create a new menu (e.g. navdrawer_menu.xml) in the res/menu folder.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> // All of this can be done through the GUI also

    <item
        android:id="@+id/firstFragment"
        android:icon="@drawable/first"
        android:title="@string/first" />
    <item
        android:id="@+id/secondFragment"
        android:icon="@drawable/second"
        android:title="@string/second" />
</menu>

In your main Activity layout file:

<?xml version="1.0" encoding="utf-8"?><!--

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    
    <androidx.drawerlayout.widget.DrawerLayout // Wrap the main ViewGroup in the DrawerLayout tag and give it an id
        android:id="@+id/drawerLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <fragment
                android:id="@+id/myNavHostFragment"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:defaultNavHost="true"
                app:navGraph="@navigation/navigation" />
        </LinearLayout>
        
        <com.google.android.material.navigation.NavigationView // Create the NavDrawer element and give it an id
            android:id="@+id/navView"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="start" // Aligns it to the left of the screen for LtR languages
            app:headerLayout="@layout/nav_header" // Point to a layout file to define a custom header for your menu
            app:menu="@menu/navdrawer_menu" /> // Point it at the menu resource we just created

    </androidx.drawerlayout.widget.DrawerLayout>

</layout>

And in the main Activity file:

import ...

class MainActivity : AppCompatActivity() {
    lateinit var drawerLayout: DrawerLayout
    lateinit var navController: NavController
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        @Suppress("UNUSED_VARIABLE")
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        navController = this.findNavController(R.id.myNavHostFragment)
        drawerLayout = binding.drawerLayout // Get the DrawerLayout
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout) // Add the DrawerLayout to the ActionBar
        NavigationUI.setupWithNavController(binding.navView, navController) // Link the navigation view with the UI
    }

    override fun onSupportNavigateUp(): Boolean {
//        return navController.navigateUp() // Get rid of this return statement
        return NavigationUI.navigateUp(navController, drawerLayout) // Use this instead which includes the DrawerLayout
    }
}

safe-args to pass Bundles safely between Activities/Fragments

Previously there was no way or guaranteeing that the data handed across in Bundles was what was expected. The safe-args plugin seeks to address this:

dependencies {
  ...
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"

   // NOTE: Do not place your application dependencies here; they belong
   // in the individual module build.gradle files
}
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-kapt'

// Add the safe-args plugin after any others:
apply plugin: 'androidx.navigation.safeargs'

android {
    compileSdkVersion 28
    ...

In the Navigation component select the Fragment which is passing on arguments and add Arguments using the + symbol next to the Arguments section of the GUI. Give each a name and specify type if necessary.

import ...

class TitleFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding: FragmentTitleBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_title, container, false)
        binding.playButton.setOnClickListener {v: View ->
 //               v.findNavController().navigate(R.id.action_titleFragment_to_nextFragment) // Before safe-args
          v.findNavController().navigate(TitleFragmentDirections.actionTitleFragmentToNextFragment(firstParameter, secondParameter)) // With safe-args. Creates a class *Directions based on Fragment name with associated actions which take the parameters to be passed on
        }
        setHasOptionsMenu(true)
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.overflow_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        return NavigationUI.onNavDestinationSelected(item!!, view!!.findNavController())
                || super.onOptionsItemSelected(item)
    }
}

In the receiving fragment:

import ...

class NextFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding: FragmentTitleBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_next, container, false)
        val args = NextFragmentArgs.fromBundle(arguments!!) // Get arguments.
        Toast.makeText(context, "First Parameter: ${args.firstParameter}, Second Parameter: ${args.secondParameter}", Toast.LENGTH_SHORT).show() // Use the Bundled parameters
        return binding.root
    }
}

 

Add overflow menu with Navigation component in Kotlin

Create a menu resource file in the res/menu folder:

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/aboutFragment"
        android:title="@string/about" />

</menu>

In the relevant Activity/Fragment:

import ...

class TitleFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding: FragmentTitleBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_title, container, false)
        setHasOptionsMenu(true) // State that we want an overflow menu
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { // Override onCreateOptionsMenu
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.overflow_menu, menu) // Feed in the xml resource you created
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean { // Override onOptionsItemSelected 
        return NavigationUI.onNavDestinationSelected(item!!, view!!.findNavController()) // Use the NavigationUI class to determine if you clicked an item in your list
                || super.onOptionsItemSelected(item) // If not, defer to super
    }
}

 

Up Navigation support in Navigation component

In Launcher Activity:

import ...

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        @Suppress("UNUSED_VARIABLE")
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        val navController = this.findNavController(R.id.myNavHostFragment) // Find the NavController
        NavigationUI.setupActionBarWithNavController(this, navController) // Setup the ActionBar
    }

    override fun onSupportNavigateUp(): Boolean { // Override the Up Navigation
        val navController = this.findNavController(R.id.myNavHostFragment)
        return navController.navigateUp()
    }
}

 

Adding Navigation to an Android Project

Adding the Navigation component to an Android project in Kotlin

buildscript {
    ext {
        ...
        version_navigation = "1.0.0-beta01" // Update based on version at https://developer.android.com/jetpack/androidx/releases/navigation
    }
    ...
}
dependencies {
    ...
    implementation "android.arch.navigation:navigation-fragment-ktx:$version_navigation"
    implementation "android.arch.navigation:navigation-ui-ktx:$version_navigation"
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

                <fragment
                    android:id="@+id/myNavHostFragment" // ID for Navigation fragment
                    android:name="androidx.navigation.fragment.NavHostFragment" // Add this. Don't ask, just do it. This is the host for all the other fragments
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    app:navGraph="@navigation/navigation" // And this. Points to navigation.xml in res/navigation folder which is where the Navigation Graph lives
                    app:defaultNavHost="true" /> // And this. Allows Navigation component to control system back key
        </LinearLayout>

</layout>

Create a new Resource file called navigation.xml in the res/navigation folder and switch to the Design view. You can now pull in Activities and Fragments and link them together. To link the Navigation Graph action (the link between two destinations) to a UI component use the built-in setOnClickListener:

class TitleFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding: FragmentTitleBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_title, container, false
        )
        binding.playButton.setOnClickListener (
            Navigation.createNavigateOnClickListener(R.id.action_titleFragment_to_gameFragment) // Built-in onClickListener. The resource pointed to here is auto-generated when a link is drawn between the 2 Fragments in the Navigation Graph
        )
        return binding.root
    }
}

 

Kotlin Data Binding in Fragments

If necessary see Data Binding in Activities post for other files.

import ...
import androidx.databinding.DataBindingUtil
import com.example.android.navigation.databinding.FragmentTitleBinding

class TitleFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding: FragmentTitleBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_title, container, false
        )
        return binding.root
    }

}

 

Kotlin Data Binding

Data binding avoids repeatedly traversing of the view hierarchy by findViewById (an expensive transaction) by generating a helper class at compile-time which the Activity/Fragment can refer to. It can also be used to bind a data class to a View so the View can access the data easily.

The idea behind data binding is to create an object that connects/maps/binds two pieces of distant information together at compile time, so that you don’t have to look for it at runtime. The object that surfaces these bindings to you is called the Binding object. You create an instance of the binding object, and then reference views through the binding object with no extra overhead.

See how you can combine this with Lifecycle components (ViewModel and LiveData) by going to this post.

In build.gradle (Module: app)

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.aboutme"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    dataBinding {
        enabled = true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

In each layout file wrap in <layout> tag, and optionally bind to a data class if you want to:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
                name="myName" // Alias for this data variable
                type="com.example.aboutme.MyName" /> // Points to the Kotlin data class
    </data>
    
    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingStart="@dimen/padding"
            android:paddingEnd="@dimen/padding"
            tools:context=".MainActivity">

        <TextView
                android:id="@+id/name_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@={myName.name}" // Refers to the variable declared above, with the 'name' property specified
                android:textAlignment="center"/>

        <TextView
                android:id="@+id/nickname_text"
                style="@style/NameStyle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@={myName.nickname}" // Refers to the variable declared above, with the 'nickname' property specified
                android:textAlignment="center"
                android:visibility="visible"/>

    </LinearLayout>
</layout>

In Activity:

import ...
import com.example.aboutme.databinding.ActivityMainBinding // import this auto-generated class based on Activity name
import android.databinding.DataBindingUtil // This should be imported automatically
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding // Declare this class based on Activity name. A rebuild is required for this to be generated and remove error warning

    private val myName: MyName = MyName("Al", "AldeZu") // Create an instance of the data class

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.activity_main) // Initialise the binding variable with the view

        binding.myName = myName // Set the value of the myName variable used in the layout file to the data class

//        findViewById<Button>(R.id.done_button).setOnClickListener { // Don't do this any more
        binding.doneButton.setOnClickListener { // Use auto-generated view name instead of invoking costly findViewById
            addNickname(it)
        }
    }

    private fun addNickname(view: View) {
        binding.apply { // Use Kotlin's apply function to make the code easier to read when multiple views are referenced
            myName?.nickname = nicknameEdit.text.toString()
            invalidateAll() // After changing data we need to invalidate all the references to force a refresh
            nicknameEdit.visibility = View.GONE
            doneButton.visibility = View.GONE
            nicknameText.visibility = View.VISIBLE
        }

        val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(view.windowToken, 0)
    }
}

 

data class MyName(var name: String = "", var nickname: String = "")

 

Lateinit

Normally, properties declared as having a non-null type must be initialised in the constructor. However, fairly often this is not convenient. For example, properties can be initialised through dependency injection, or in the setup method of a unit test. In this case, you cannot supply a non-null initialiser in the constructor, but you still want to avoid null checks when referencing the property inside the body of a class.

To handle this case, you can mark the property with the lateinit modifier.

The modifier can be used on var properties declared inside the body of a class (not in the primary constructor, and only when the property does not have a custom getter or setter) and, since Kotlin 1.2, for top-level properties and local variables. The type of the property or variable must be non-null, and it must not be a primitive type.

Accessing a lateinit property before it has been initialised throws a special exception that clearly identifies the property being accessed and the fact that it hasn’t been initialised.

class MainActivity : AppCompatActivity() {

    private lateinit var tvDescription: TextView // We don't want to set the value yet but we'd also prefer to not set to nullable. Use lateinit to let compiler know you will initialise it later
    // N.B. If you don't initialise it the IDE will not warn you and you'll get a kotlin.UninitializedPropertyAccessException at runtime

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tvDescription = findViewById(R.id.tv_description)
        tvDescription.text = "Refreshing glass of water gets you hydrated"


    }
}