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.
android
element so that ViewBinding is available:buildFeatures {
viewBinding = true
}
private lateinit var binding: ActivityMainBinding
setContentView()
in the onCreate method with the following binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
<?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>
<?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>
manifest
element: <uses-permission android:name="android.permission.INTERNET" />
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
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 manuallyimport java.net.URLEncoder
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result ->
//todo: handle activity result
}
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
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")
}
onCreate
method of the WebSearchActivity classval url = intent.getStringExtra("url")
binding.webView.loadUrl(url!!)
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 statementimport android.webkit.WebViewClient
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!onCreate
is called again). To resolve this, we need to do two things:
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)
}
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
)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.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 worksgetString
, 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 futureAs 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
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 oneonBackPressedDispatcher.addCallback(this, backPressedCallback)
//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 promptednull
. You will need to specify the type as it cannot be inferredonCreate
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 nullAnother 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.
private fun toggleButtonsState(enabled: Boolean){
binding.btnAndroid.isEnabled = enabled
binding.btnKotlin.isEnabled = enabled
binding.btnStackOverflow.isEnabled = enabled
binding.btnGoogle.isEnabled = enabled
}
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'toggleButtonState
when the activity is created, so that the buttons are disabledYou can view a completed version of this project on GitHub
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