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
<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.
<FrameLayout ...>
<!-- usually a recycler view, or a fragment container -->
</FrameLayout>
ConstraintLayout: a flexible way to design views
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.
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.
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
android:text="Shown in the app"
tools:text="Shown in the DesignView"
/>
Button: a button
ImageView: display an image
<!-- 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.
- See Material 2 Documentation (currently widely used)
- See Material 3 Documentation (released in late 2022)
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.
-
RadioButton, and RadioGroup
- RadioGroup#
checkedButton
- RadioGroup#
setOnCheckedChangeListener
: parameters are radio group, and the Id of the checked button.
- RadioGroup#
-
tools:visibility
and the icon in the Design View to swap - Tint/Dark mode
- Android Basics: Adaptive Layouts, twopane, cardview
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()