Views

Each activity or fragment is associated with one layout file. It's an XML file stored in app/res/layout. Inside there is a layout with children views defining the appearance of the user interface.

<?xml version="1.0" encoding="utf-8"?>
<XXXLayout 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" >
  <!-- NESTED TAGS -->
</XXXLayout>

XXXLayout is called the root. <XXXLayout>...</XXXLayout> is called a tag. They can have attributes such as <XXX attribute='value'> and may contain nested tags, which could be views or layouts.

A few things to know

βš’οΈ tools:context points to the associated Fragment/Activity in YOUR code, so you must give it an appropriate value.

✨ xmlns: are very important. You can't use android: if you didn't add the matching xmlns:android. They are added to the root.

πŸš€ Layouts are usually populated using the Layout Editor as doing so manually can be complicated.


Layouts

Layouts are a category of ViewGroups. They are Views with predefined settings to arrange children views. For instance, a GridLayout will arrange its nested views in a grid-like format.

They are also convenient to apply a style. For instance, to add some margin to the left, you may group elements in a layout, and apply the margin to the layout instead of each view.

LinearLayout: Horizontal/Vertical

Documentation

<LinearLayout
    ...
    android:orientation="vertical"
    android:orientation="horizontal"
    >
      <!-- optional weight (responsive width/height) -->
      <XXXView
           android:layout_weight="1"
           />
</LinearLayout>

FrameLayout: a layout with only one child.

Documentation

<FrameLayout ...>
  <!-- usually a recycler view, or a fragment container -->
</FrameLayout>

ConstraintLayout: a flexible way to design views

Documentation

implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

A constraint layout is rendering elements relatively to another component (which could be the screen). You will set margins which are the gap with the other component.

Constraint Layout Margins Relative

On a screen of 226px, if you define that the component should have a margin-left of 140, and a margin-right of 26. The component will be centered within the 60 remaining px called free space.

To change the behavior of how the free space is handled, simply edit the layout width/height OR you can use a shortcut by clicking on arrows such as ">>" right where you added margins.

Constraint Layout Margins Kind


Views

In Android, every component such as an Image is called a View. Every view must have at least these two attributes

  • android:layout_width: wrap, match_parent, a fixed width
  • android:layout_height: wrap, match_parent, a fixed height

And these attributes are available on any View

  • android:padding: internal gap (ex: 10dp)
  • android:layout_margin: external gap (ex: 10dp)
  • android:visibility: View.VISIBLE / View.INVISIBLE / View.GONE

TextView: display a text

TextView extends View.

<TextView
    android:text="Shown in the app"
    tools:text="Shown in the DesignView"
    />

Button: a button

Button extends TextView.

ImageView: display an image

ImageView extends View

<!-- scaleType="centerCrop" is used for 9patches scaling -->
<ImageView 
    tools:srcCompat="YOUR_IMAGE"
    android:scaleType="centerCrop"
    />

EditText: an input field

EditText extends TextView. See also InputType and Autofill.

<EditText
    android:inputType="text"
    android:autofillHints="username"
    android:hint="Placeholder in the app"
    tools:hint="Placeholder in the DesignView"
    />

Switch: check or uncheck

Switch extends Button (indirectly) and SwitchCompat.

<Switch android:checked="true" />
<!-- βœ… better -->
<androidx.appcompat.widget.SwitchCompat
    android:checked="true" />

πŸ”— Get access to a view from the code πŸ–‡οΈ

You can then use findViewById(some_id) to get a view.

<SomeViewHere
+        android:id="@+id/someUniqIdHere"
// ➑️ In Activity#onCreate
val x = findViewById<SomeViewHere>(R.id.someUniqIdHere)
// ➑️ In Fragment#onViewCreated
val x = view.findViewById<SomeViewHere>(R.id.someUniqIdHere)
val x = requireView().findViewById<SomeViewHere>(R.id.someUniqIdHere)

TextView

var t = findViewById<TextView>(...)
t.text = "Some text"
t.setText(R.string.some_string)

Button

val b = findViewById<Button>(...)
b.setOnClickListener {
    println("Clicked on myButton")
}

ImageView

var i = findViewById<ImageView>(...)
// set image from the code
i.setImageResource(R.drawable.some_drawable_here)

EditText

val e = findViewById<EditText>(...)
// handle key events
e.setOnKeyListener { v, keyCode, keyEvent ->
    if (keyCode == KeyEvent.KEYCODE_A) {
        return@setOnKeyListener true
    }
    return@setOnKeyListener false
}

Switch/SwitchCompact

var s = findViewById<SwitchCompact>(...)
if(s.isChecked) {}

πŸ”₯ Accessibility πŸ”₯

If something is only here to decorate the screen, you should mark it as not important for accessibility.

<ImageView
  android:importantForAccessibility="no" />

For images, if they are important for accessibility, you should provide a content description. ⚠️If the image is modified from the code, the content description should be updated.

<ImageView
  android:contentDescription="Describe this image" />

✨ View Binding ✨

ViewBinding is a new alternative to findViewById.

Ids declared in activity_main.xml will be available via a generated class called ActivityMainBinding (matching the XML filename).

First, add the viewBinding build feature.

android {
    ...

+    buildFeatures {
+        viewBinding = true
+    }
}

Assume that we have an XML file with

<SomeViewHere
    android:id="@+id/someUniqIdHere"
    ...
/>

This is how you could adapt your previous code with findViewById.

Ex: activity_main.xml in an Activity
class MainActivity : AppCompatActivity() {
+    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
+        binding = ActivityMainBinding.inflate(layoutInflater)

-        setContentView(R.layout.activity_main)
+        setContentView(binding.root)

-        val x = findViewById<SomeViewHere>(R.id.someUniqIdHere)
+        val x = binding.someUniqIdHere
    }
}
Ex: fragment_blank.xml in a Fragment
class BlankFragment : Fragment() {
+    private lateinit var binding: FragmentBlankBinding

    override fun onCreateView(...): View? {
-        return inflater.inflate(R.layout.fragment_blank, container, false)
+        binding = FragmentBlankBinding.inflate(layoutInflater, container, false)
+        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
+        val x = binding.someUniqIdHere
    }
}

🎨 Material Design 🎨

Material design is a library of pre-made components. Google recommends using Material UI components as much as possible.

Material design provides both

  • πŸ‘‰ Guidelines (padding, sizes...) to make a nice UI
  • πŸ‘‰ Pre-made Components (padding, sizes...)

Manually edit the XML and replace AndroidX classes with MaterialUI classes. Aside from the name of the class, and new attributes being available, nothing much will change.

See the list here + detailed instructions

  • EditText β†’ TextInputLayout+TextInputEditText
  • SwitchCompat β†’ SwitchMaterial
  • ...

⚑ Data Binding ⚑

DataBinding is an extension of ViewBinding. It allows us to directly connect the data and the view directly inside the XML.

With LiveData, the view is automatically updated when the data has changed, allowing us to get rid of observers.

android {
    ...
    buildFeatures {
        viewBinding = true
+        dataBinding = true
    }
}

Prepare your XML

DataBinding is a bit hard to set up. You need to edit your XML first. To wrap your root inside a tag layout. You don't have to move xmlns: attributes, or change anything else.

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

<!-- Your previous root here (unchanged) -->
</layout>

➑️ Indents will be messed up. Right-click on the file > Reformat code, keep everything checked, and run it by pressing "ok".


Adapt the code

The code is the same as ViewBinding, with a minor change.

Ex: activity_main.xml in an Activity
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
-        binding = ActivityMainBinding.inflate(layoutInflater)
+        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        setContentView(binding.root)

        val x = binding.someUniqIdHere
    }
}
Ex: fragment_blank.xml in a Fragment
class BlankFragment : Fragment() {
    private lateinit var binding: FragmentBlankBinding

    override fun onCreateView(...): View? {
-        binding = FragmentBlankBinding.inflate(layoutInflater, container, false)
+        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_blank, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val x = binding.someUniqIdHere
    }
}

πŸ”₯ It's worth mentioning that the change above is unneeded if we're using ViewBinding's code as it automatically binds data.

Passing data to the XML

First, you must set the Lifecycle owner.

// ➑️ In Activity#onCreate
binding.lifecycleOwner = this
// ➑️ OR; In Fragment#onViewCreated
binding.lifecycleOwner = viewLifecycleOwner

Then, you must declare variables in the XML data tag.

<data>
+  <!-- example with a viewModel variable -->
+  <variable name="viewModel" type=".YourViewModelTypeHere" />
</data>

Then, right below binding.lifecycleOwner, pass the variables.

// binding.yourVariableName = yourVariableValue
binding.viewModel = viewModel // an attribute in my class

Then, in your XML, you can use your variables and basic expressions inside @{}. Here are some examples:

<example>
  <!-- user is a LiveData -->
  <TextView android:text="@{viewModel.user.username}" />
  <!-- some_key is a string taking a parameter "%s" -->
  <TextView android:text="@{@string/some_key(viewModel.string)}" />
  <!-- examples with booleans -->
  <TextView android:text='@{viewModel.boolean ? "x" : "y" }' />
  <TextView android:text='@{viewModel.boolean ? @string/toto : "" }' />
  <!-- state / events -->
  <Switch android:checked="@{viewModel.xxx.equals(yyy)}" />
  <Button android:onClick="@{() -> viewModel.xxx()}" />
</example>

You can't do complex calculations. For instance, you can't convert an Int to a String. Instead, either use BindingAdapters, or add a function, for instance, in User here, returning an appropriate value.

BindingAdapters: complex, but powerful
plugins {
+    id 'kotlin-kapt'
}

Either create a specific class, or use a companion object.

companion object { // inside any appropriate class
  // ➑️ Add an attribute "app:xxx" on a TextView
  //  taking a value of type "Type"
  @BindingAdapter("app:xxx") @JvmStatic
  fun bindXXXText(textView: TextView, value: Type) {
     // do what you want the attribute to do
  }

  // πŸ‘» -- never used this
  @InverseBindingAdapter(attribute = "app:xxx", event = "android:textAttrChanged") @JvmStatic
  fun getText(textView: TextView) = textView.text.toString()
}

You can use the attribute as long as app was imported (see the appropriate xmlns: at the top of this page if needed).

<TextView
    app:xxx="@{viewModel.aValueMatchingTheSelectedType}"
    />

πŸ‘» To-do πŸ‘»

Stuff that I found, but never read/used yet.

Theming?

Theming and Colors and ColorTools. Dark Theme

style="?attr/materialButtonOutlinedStyle"
android:textAppearance="?attr/textAppearanceHeadline6"
name="Theme.XXX"

<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>

By convention, each style should have a parent, and be named as the parent, while replacing the "MaterialComponents" with "YOUR_APP_NAME".

<examples>
  <style name="Widget.YOUR_APP_NAME.TextView" parent="Widget.MaterialComponents.TextView">
  </style>
  <style name="Widget.xxx.CompoundButton.RadioButton" parent="Widget.MaterialComponents.CompoundButton.RadioButton">
    <item name="android:paddingStart">8dp</item>
    <item name="android:textAppearance">?attr/textAppearanceBody1</item>
  </style>
  <style name="Widget.xxx.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
    <item name="android:minHeight">48dp</item>
    <item name="android:gravity">center_vertical</item>
    <item name="android:textAppearance">?attr/textAppearanceBody1</item>
  </style>
  <style name="Widget.xxxx.TextView" parent="Widget.MaterialComponents.TextView">
    <item name="android:minHeight">48dp</item>
    <item name="android:gravity">center_vertical</item>
    <item name="android:textAppearance">?attr/textAppearanceBody1</item>
  </style>
</examples>
Icons

Different manufacturers may show app icons in different shapes than the circular icon shape: square shape, rounded square, or squircle (between a square and circle)...

https://developer.android.com/training/multiscreen/screendensities#TaskProvideAltBmp

Adaptive icons since v26 => background => foreground => + new image asset => new assets should be moved to the same folder v26

https://developer.android.com/codelabs/basic-android-kotlin-training-display-list-cards (3 => easy way to import icon).

android:textAlignment="textStart"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
Vertical alignment
android:gravity="center"
android:gravity="bottom"
???
LinearLayout => android:baselineAligned="false"
Tint black icon to white
android:tint="@color/white"
android:background="#B95EB17C"

TableLayout TableRow

val input = EditText(requireContext())
.setView(input)
input.text.toString().toFloatOrNull()