Android Activities

Few apps rely on one single interface, so in Android we need to ensure we can present, and transition between multiple views. As you may have deduced from the Hello World tutorial, Android uses Activities to present a screen to a user. This tutorial builds a simple app which transitions between two Activities and passes data between then. The final application will make it easy to quickly search relevant websites for Kotlin / Android related queries.

  1. Create a new Android Application using the Empty Activity as the starting point.
  2. Name the project "Kotlin Help" and target API 24 and later
  3. Open the module level build.gradle file and add the following code inside the android element so that ViewBinding is available:
    buildFeatures {
        viewBinding = true
    }
  4. Setup view binding in Main Activity by adding the following variable:
    private lateinit var binding: ActivityMainBinding
  5. Replace the call to setContentView() in the onCreate method with the following
    binding = ActivityMainBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
  6. Delete the TextView from the activity
  7. Using the screenshot below as a guide, add the following components to the activity_main.xml file. Do not change the default layout style (which should be ConstraintLayout) app with EditText followed by five buttons
    1. EditText (named Plain text in the Palette) with an id of 'etSearch', and the inputType property set to 'text'
    2. Five Buttons named btnGoogle, btnStackOverflow, btnKotlin, btnAndroid and btnPrevious
    There should be 8dp between the buttons and 16dp above and below the EditText control
  8. Using the button text from the screenshot, set the titles and create String resources for the text for each button
  9. Set the hint property for the EditText field to 'Search term' and create a corresponding String resource
  10. Remove the default value of 'Text' from the EditText's 'text' property
  11. Set the visibility property of the 'Previous' button to 'invisible'
  12. Verify the XML representation matches that shown here (click to expand)
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <EditText
            android:id="@+id/etSearch"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginRight="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:ems="10"
            android:hint="@string/search_terms"
            android:inputType="text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/btnGoogle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="16dp"
            android:text="@string/google"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/etSearch" />
    
        <Button
            android:id="@+id/btnStackOverflow"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="@string/stackoverflow"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnGoogle" />
    
        <Button
            android:id="@+id/btnKotlin"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="@string/kotlin_docs"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnStackOverflow" />
    
        <Button
            android:id="@+id/btnAndroid"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="@string/android_developer"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnKotlin" />
    
        <Button
            android:id="@+id/btnPrevious"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="@string/previous"
            android:visibility="invisible"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnAndroid" />
    
    </android.support.constraint.ConstraintLayout>

Adding another Activity

  1. Right click on the app/res/layout folder, and from the context menu choose New -> Activity -> Empty Activity
  2. Name it WebSearchActivity, leave the other fields unchanged and click finish
  3. Modify the code so it using view binding in the same way you did for the Main Activity, but note that the binding class will be called ActivityWebSearchBinding
  4. In design view for the web search activity, using the widgets section of the Palette, drag a WebView onto the page, so that it fills the blank area of the activity, add constraints to all sides of the Webview
  5. Set the id of the WebView to 'webView'
  6. Check the activity's XML matches that shown here (Click to expand)
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".WebSearchActivity">
    
        <WebView
            android:layout_width="368dp"
            android:layout_height="495dp"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </android.support.constraint.ConstraintLayout>
  7. In order to ensure that the WebView can show web pages, we need to specify the permissions that the app requires. To do this, Open the AndroidManifest.xml file (from within the manifests folder). Place the following directly inside the manifest element:
    <uses-permission android:name="android.permission.INTERNET" />

Transitioning to the Activity with WebView

In order to make our application useful, when the user clicks a button in MainActivity (having entered a search term), they should be directed to a web page showing the corresponding search results. We therefore need to construct the URL with the search term appended (as it would be if we carried out the search from the appropriate search page). To do this, we will HTML encode the search term and add it at the appropriate place in the URL for each site

Getting the encoded search term

  1. Add the following code to the MainActivity class to create a read only property representing the text from the EditText element encoded appropriately:
    private val encodedSearchTerm : String
            get() = URLEncoder.encode(binding.etSearch.text.toString(),"UTF-8")
    You should be prompted to add the following import statement, but if not, add it manually
    import java.net.URLEncoder
  2. In order to handle transitions between different Activities, we make use of an 'ActivityResultLauncher'. This object allows us to both launch a new activity, and also handle any data provided by that activity when it finishes being in use. Add an ActivityResultLauncher as follows:
    private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 
            result ->
        //todo: handle activity result
    }
  3. Next we need a method which will use the resultlauncher to launch the WebSearchActivity and pass across the URL (the URL will vary depending on the button clicked, but we'll get to that shortly). Add the following method:
    private fun loadWebActivity(url: String) {
        val intent = Intent(this, WebSearchActivity::class.java)
        intent.putExtra("url", url)
        resultLauncher.launch(intent)
    }

    Ensure the following import statement appears near the top of the file:

    import android.content.Intent

    This will create an 'intent' - a description of an operation to be performed. The weird looking WebSearchActivity::class.java code represents a reference to the Java class, when we start the Activity, the Android system will search for an activity with the matching class reference to launch.

    The second line will provide a URL (as a String), which will eventually be used to load the page in the WebView of the next activity

  4. To piece it together, we need to specify an action for each of the buttons, so we'll add an on click listener to the first four buttons. Within the onCreate method, add the following:
    binding.btnGoogle.setOnClickListener {
        loadWebActivity("https://google.co.uk/search?q=$encodedSearchTerm")
    }
    binding.btnStackOverflow.setOnClickListener {
        loadWebActivity("https://stackoverflow.com/search?q=$encodedSearchTerm")
    }
    binding.btnKotlin.setOnClickListener{
        loadWebActivity("https://kotlinlang.org/?q=$encodedSearchTerm&p=0")
    }
    binding.btnAndroid.setOnClickListener{
        loadWebActivity("https://developer.android.com/s/results/?q=$encodedSearchTerm")
    }
  5. Build and run the application. Click a button and all that will happen at this stage is a new activity will appear with no visible content. Use the back button to verify you can return to the Main activity.

Getting the WebView to load the URL

  1. Add the following code to the onCreate method of the WebSearchActivity class
    val url = intent.getStringExtra("url")
    binding.webView.loadUrl(url!!)
  2. Run the application again and try out each of the buttons. You should find that it works, but not as we would like, for example, the Google link will try and open a new browser window, and the Kotlin and Android links don't show any search results.
  3. To resolve the issue where a new browser is opened, add the following code inside the onCreate method in the WebSearchActivity class before you call loadUrl:
    binding.webView.webViewClient = WebViewClient() //prevents opening in browser app
    Leave the comment in as it's not overly clear why this is needed otherwise. Note that you will need the following import statement
    import android.webkit.WebViewClient
  4. Also add the following line (before the call to loadUrl) to ensure that the pages that rely on JavaScript for the search to function also work:
    binding.webView.settings.javaScriptEnabled = true // required for search functionality on Kotlin and Android sites
    Again, leave the comment, as it's worth knowing why you've introduced a potential security vulnerability into your app!
  5. Test the app again, and this time you should find that the links work as expected

Handling rotation issues

  1. Carry out a search and follow a few links on one of the pages, then rotate the emulator or your device 90 degrees. You should notice that the web view returns to the first on you visited (i.e. after you pressed the button). This is because the Activity is recreated upon rotation (i.e. onCreate is called again). To resolve this, we need to do two things:
    • Save the url when the app is rotated
    • Depending on whether the activity is being created afresh, or following a rotation, reload the appropriate URL
  2. To save the url, we can override the onSaveInstanceState method. This allows state to be stored in something called a Bundle. Add the following method:
    override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)
        outState?.putString("url",binding.webView.url)
    }
  3. To ensure the correct URL is loaded, we need to check if onCreate is being called with, or without a Bundle (without means it's being created having been started from the Main Activity, with means it's being recreated having been terminated (e.g. by rotation, or by the user having navigated away from the app). A first step to doing this would be to replace the following line:
    val url = intent.getStringExtra("url")
    with this code:
    var url = intent.getStringExtra("url")
    if (savedInstanceState != null) {
        url = savedInstanceState.getString("url")
    }
    (note the change of the url from val to var)
  4. Test the application and you should see that the problem is eliminated, the code however is less than satisfactory - we assign url one value, then reassign it. In Java we might get around this by instantiating it separately from its declaration, however that would leave us open to the risk if it being null (maybe not now but if we refactored our code later, we could do it by accident). Either way, Kotlin wont allow us to write similar code without making url nullable which we don't want to do.
  5. The solution in other languages would be to use a ternary statement in the form val x = someBoolean ? y : z, however Kotlin does not support ternary statements, instead it has something very similar. Replace the code above with the code shown here:
    val url = if (savedInstanceState != null) savedInstanceState.getString("url").toString() else intent.getStringExtra("url")
    Test the application again to ensure it works
  6. We can actually improve this code further, as saved instance state is nullable, we can make a safe call to getString, which, if savedInstanceState is null will result in null, and if not, will result in the result of calling getString (which is either null or a String), we can then use the Elvis operator ?: to assign a different string value, if the left side of the operator returns null. The use of the Elvis operator is as follows:
    val x = someNullableValueOrExpression ?: someNonNullableValueOrExpression
    whereby if the left side returns null, the right side is assigned. In our case, the simplified code is as follows:
    val url = savedInstanceState?.getString("url") ?: intent.getStringExtra("url")
    Replace the code with this and test again. Add comments to your code so you understand what's happening if you revisit it in future

Passing the data back so we can persist the previous page

As it stands, when the user presses the back button, they return to the menu. If they've followed a few links they would have to follow the same set of links again to find the page. The previous button (which we've hidden) will be used to allow them to return to the most recently viewed page

  1. We need to modify the code in the WebSearchActivity so that the URL of the current page can be passed back when the user presses the back button. Create a variable that extends the OnBackPressedCallback abstract class as follows (Adding imports for Activity and Intent as prompted):
    private val backPressedCallback = object: OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                val intent = Intent()
                intent.putExtra("url",binding.webView.url)
                setResult(Activity.RESULT_OK, intent)
                finish()
        }
    }
    As before, we use an Intent to pass data, though rather than starting an Activity, we finish the current one
  2. At the end of the onCreate method add the following line of code:
    onBackPressedDispatcher.addCallback(this, backPressedCallback)
  3. Next we need to return to the MainActivity and implement logic in the result handler to handle the result from the WebActivity. Return to the //todo: ... statement you added earlier and replace it as follows:
    if (result.resultCode == Activity.RESULT_OK) {
        val url = result.data?.getStringExtra("url")
        if (url != null) {
            prevUrl = url
            binding.btnPrevious.visibility = View.VISIBLE
        } else {
            binding.btnPrevious.visibility = View.INVISIBLE
        }
    }
    Add import statements for Activity and View when prompted
  4. You will see an error in the above line of code indicating that prevUrl does not exist. Add a nullable string property named prevUrl to the MainActivity class to resolve this, instantiating it with a value of null. You will need to specify the type as it cannot be inferred
  5. Finally add the following code to the onCreate method to set the on click listener for the 'Previous' button:
    binding.btnPrevious.setOnClickListener {
        loadWebActivity(prevUrl?: "")
    }
    Note that this will set the parameter to an empty string if prevUrl is null
  6. Test the application again, follow a couple of links then press the back button. Ensure that the 'Previous' button returns you to that page

Avoiding blank or irrelevant searches

Another issue you may have identified by now, is that the buttons can be clicked even if the search term is empty. This can be resolved by ensuring that there are at least two characters in the search term before the buttons are enabled.

  1. First add code which will allow the buttons' 'enabled' state to be toggled as follows:
    private fun toggleButtonsState(enabled: Boolean){
        binding.btnAndroid.isEnabled = enabled
        binding.btnKotlin.isEnabled = enabled
        binding.btnStackOverflow.isEnabled = enabled
        binding.btnGoogle.isEnabled = enabled
    }
  2. In order to detect changes in the EditText element etSearch we have to provide it with an instance of a class that implements the TextWatcher interface. Because this interface declares multiple methods, we can't implement it as simply as we can with an onClickListener, instead we can either make the MainActivity could implement the interface, or we could create an anonymous object that does. We'll take the second approach, though there's nothing wrong with the first. Add the following code at the end of MainActivity's onCreate method:
    binding.etSearch.addTextChangedListener(object: TextWatcher {
        override fun afterTextChanged(s: Editable?) {
    
        }
    
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    
        }
    
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            toggleButtonsState(binding.etSearch.text.length > 1)
        }
    
    })
    You'll need to add import statements for 'Editable' and 'TextWatcher'
  3. Finally, add a line of code that calls the toggleButtonState when the activity is created, so that the buttons are disabled

You can view a completed version of this project on GitHub

Task

Further develop the application so that the most recent url can be accessed from the 'previous' button, even if the application has been terminated and restarted