Skip to content

Commit

Permalink
Add View Binding sample (#11)
Browse files Browse the repository at this point in the history
* Create ViewBinding sample app

* Implement ViewBinding sample app

* Add MainActivity instrumentation tests

* Add FragmentBind and FragmentInflate instrumentation tests

* Create README.md
  • Loading branch information
husaynhakeem authored Nov 1, 2020
1 parent d49d4ad commit 1cd8f1f
Show file tree
Hide file tree
Showing 29 changed files with 815 additions and 0 deletions.
10 changes: 10 additions & 0 deletions ViewBindingSample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea/*
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
14 changes: 14 additions & 0 deletions ViewBindingSample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# View Binding

Android sample app to learn about view binding in Android. It mainly showcases:
- Enabling view binding through the app's build file.
- Setting up binding instances in activities.
- Setting up binding instances in Fragments using both the `inflate(LayoutInflater)` and `bind(View)` static methods.
- Accessing and interacting with views through the binding instances.
- Setting up layout files differently for different configurations, and accessing them through the binding instances with their nullable references.

![View binding sample - inflate](https://github.com/husaynhakeem/android-playground/blob/master/ViewBindingSample/art/view_binding_portrait.png)
![View binding sample - bind](https://github.com/husaynhakeem/android-playground/blob/master/ViewBindingSample/art/view_binding_landscape.png)

### Resources to learn about view binding
- [The official documentation on View Binding](https://developer.android.com/topic/libraries/view-binding)
1 change: 1 addition & 0 deletions ViewBindingSample/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
39 changes: 39 additions & 0 deletions ViewBindingSample/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
id "com.android.application"
id "kotlin-android"
}

android {
compileSdkVersion 30

defaultConfig {
applicationId "com.husaynhakeem.viewbindingsample"
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
viewBinding {
enabled = true
}
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "com.google.android.material:material:1.2.1"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
androidTestImplementation "androidx.test:core-ktx:1.3.0"
debugImplementation("androidx.fragment:fragment-testing:1.2.5") {
exclude group: 'androidx.test', module: 'core'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.husaynhakeem.viewbindingsample

import android.content.pm.ActivityInfo
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class FragmentBindTest {

@Test
fun doNotShowSubtitle_inPortraitOrientation() {
// Arrange
val scenario = launchFragmentInContainer { FragmentBind() }

// Act
scenario.onFragment { fragment ->
fragment.requireActivity().requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}

// Assert
onView(withId(R.id.subtitle)).check(doesNotExist())
}

@Test
fun showSubtitle_inLandscapeOrientation() {
// Arrange
val scenario = launchFragmentInContainer { FragmentBind() }

// Act
scenario.onFragment { fragment ->
fragment.requireActivity().requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}

// Assert
onView(withId(R.id.subtitle)).check(matches(isDisplayed()))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.husaynhakeem.viewbindingsample

import android.os.Build
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class FragmentInflateTest {

@Test
fun doNotShowSubtitle_onApisBelow23() {
// Arrange
assumeTrue(Build.VERSION.SDK_INT < 23)
launchFragmentInContainer { FragmentInflate() }

// Assert
onView(withId(R.id.subtitle)).check(doesNotExist())
}

@Test
fun showSubtitle_onApis23AndAbove() {
// Arrange
assumeTrue(Build.VERSION.SDK_INT >= 23)
launchFragmentInContainer { FragmentInflate() }

// Assert
onView(withId(R.id.subtitle)).check(matches(isDisplayed()))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.husaynhakeem.viewbindingsample

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

@get:Rule
val rule = ActivityScenarioRule(MainActivity::class.java)

@Test
fun showBindFragment_whenBindFragmentButtonClicked() {
// Act
onView(withId(R.id.bindFragmentButton)).perform(click())

// Assert
onView(withId(R.id.bindFragmentRoot)).check(matches(isDisplayed()))
}

@Test
fun showInflateFragment_whenInflateFragmentButtonClicked() {
// Act
onView(withId(R.id.inflateFragmentButton)).perform(click())

// Assert
onView(withId(R.id.inflateFragmentRoot)).check(matches(isDisplayed()))
}
}
19 changes: 19 additions & 0 deletions ViewBindingSample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.husaynhakeem.viewbindingsample">

<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.ViewBindingSample">
<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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.husaynhakeem.viewbindingsample

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import com.husaynhakeem.viewbindingsample.databinding.FragmentBindBinding

class FragmentBind : Fragment(R.layout.fragment_bind) {

// Instance of the view binding. Should only be accessed while the view is valid, i.e. between
// onViewCreated and onDestroyView.
private var _binding: FragmentBindBinding? = null
private val binding: FragmentBindBinding
get() = _binding!!

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// The layout has already been inflated. Get an instance of the binding class using the
// static [bind] method.
_binding = FragmentBindBinding.bind(view)

// Get access to the title TextView, available in all layouts.
binding.title.text = getString(R.string.bind_fragment_title)

// [subtitle] is only available in landscape, and should be null in portrait orientation.
binding.subtitle?.text = getString(R.string.bind_fragment_subtitle_landscape)
}

override fun onDestroyView() {
super.onDestroyView()

// Set the binding to null, since it should no longer be accessed after the view has been
// destroyed.
_binding = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.husaynhakeem.viewbindingsample

import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.husaynhakeem.viewbindingsample.databinding.FragmentInflateBinding

class FragmentInflate : Fragment() {

// Instance of the view binding. Should only be accessed while the view is valid, i.e. between
// onCreateView and onDestroyView.
private var _binding: FragmentInflateBinding? = null
private val binding: FragmentInflateBinding
get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Set up instance of the binding class to use with this fragment by using the static
// [inflate] method.
_binding = FragmentInflateBinding.inflate(inflater)

// Return the root of the binding, which references the LinearLayout.
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Get access to the title TextView, available in all layouts.
binding.title.text = getString(R.string.inflate_fragment_title)

// [subtitle] is only available in api levels 23 and above, and should be null in devices
// running on older versions.
val apiLevel = Build.VERSION.SDK_INT
binding.subtitle?.text = getString(R.string.inflate_fragment_subtitle_pre_api_23, apiLevel)
}

override fun onDestroyView() {
super.onDestroyView()

// Set the binding to null, since it should no longer be accessed after the view has been
// destroyed.
_binding = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.husaynhakeem.viewbindingsample

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.husaynhakeem.viewbindingsample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Set up instance of the binding class to use within this Activity.
val binding = ActivityMainBinding.inflate(layoutInflater)

// Set the root view of the binding as the active view on the screen.
setContentView(binding.root)

// Set OnClickListeners on the buttons
binding.bindFragmentButton.setOnClickListener {
switchFragment(FragmentBind())
}
binding.inflateFragmentButton.setOnClickListener {
switchFragment(FragmentInflate())
}

// Initially show Bind fragment
if (savedInstanceState == null) {
setFragment(FragmentBind())
}
}

private fun setFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.add(R.id.content, fragment)
.commit()
}

private fun switchFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.replace(R.id.content, fragment)
.commit()
}
}
24 changes: 24 additions & 0 deletions ViewBindingSample/app/src/main/res/layout-land/fragment_bind.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bindFragmentRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
tools:text="Bind Fragment" />

<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
tools:text="Landscape" />

</LinearLayout>
Loading

0 comments on commit 1cd8f1f

Please sign in to comment.