Wikimedia Apps/Team/Android/Coding conventions

Views and Layouts

 * Our project uses View Bindings, which is the current recommended practice.
 * When using a View Binding in an, here is the boilerplate code:

private lateinit var binding: NameOfBindingClass ... public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = NameOfBindingClass.inflate(layoutInflater) ...


 * And when using a View Binding in a, here is the boilerplate code:

private var _binding: NameOfBindingClass? = null private val binding get = _binding!! ... override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) _binding = NameOfBindingClass.inflate(inflater, container, false) ...   return binding.root } ... override fun onDestroyView { _binding = null super.onDestroyView ... }


 * And when using a View Binding in a custom, here is the boilerplate code (if using a   node at the root of your XML):

private val binding = NameOfBindingClass.inflate(LayoutInflater.from(context), this)

And if you're not using a  node at the root of your XML layout, add the   parameter when inflating:

private val binding = NameOfBindingClass.inflate(LayoutInflater.from(context), this, true)

Other View guidelines

 * and  should be avoided. If you're finding yourself relying on   to resolve a bug, it usually means you've missed something in the lifecycle of the activity.
 * Always be mindful of left-to-right and right-to-left locales.
 * The root node of a custom View's XML should be a  node whenever possible, with   attributes to aid laying out the view in the AS designer.

Serialization

 * Our project uses the  library to transform model classes to/from JSON. This library generates serialization logic at compile time, so that it can avoid using reflection at runtime. This makes it more performant than reflection-based serialization.
 * Nevertheless, serialization should be done only when necessary:
 * When sending or receiving data over the network.
 * When sending or receiving data over the WebView javascript bridge.
 * When a class is too complex or cumbersome to be.

To make a class serializable, just add the  annotation at the top:

@Serializable class Foo(    var bar: String,     var baz: Int )

All the properties of the class will be serialized automatically, with the names as they are named in the class itself. If you need the names of the properties to be serialized differently from how they are named in the class, use the  annotation:

@Serializable class Foo(    var bar: String,     @SerialName("b_a_z") var baz: Int )

If you would like certain properties to be optional when deserializing, simply initialize the property in your class:

@Serializable class Foo(    var bar: String = "",     var baz: Int = 0,     val xyz: Long? = null )

Notice that it doesn't matter if a property is nullable or nonnull: if you need a nullable property to be optional, you must initialize it with.

List types that are optional can be initialized with an empty list, and will be populated with the correct deserialized list if it is present:

@Serializable class Foo(    var bar: String = "",     var baz: Int = 0,     val xyz: Long? = null,     val list: List = emptyList,     val map: Map = emptyMap )

Post-processing steps
There are cases when you might need to perform post-processing on a class after deserialization. For example, suppose your class contains a property that represents a status code, and if the status code represents an error, you need to throw an exception. To perform this kind of logic, simply put it into the  block of the class:

@Serializable class Foo(    val status: String = "",     var baz: Int = 0, ) { init { if (status == "error") { throw Exception("An error was received.") }    } }

Custom serializers
Custom serializers should be avoided, and are really only necessary for dealing with third-party classes. If absolutely necessary, you can specify a custom serializer for a specific property:

@Serializable class Foo(    var bar: String = "",     var baz: Int = 0,     @Serializable(with = MyUriSerializer::class) val uri: Uri )

Default deserializer settings
Our default deserializer, which we declare and build in the  class, has the following settings:


 * , which tells it to ignore incoming properties that are not declared in our corresponding model classes. This is useful for consuming APIs in which we don't care about every property that is returned. This also allows APIs to add properties in the future, without impacting our usage of them.
 * , which tells it to forgive null JSON values even if the receiving model property is nonnull, and also forgive unknown values of  types.

Polymorphism
If you are passing a subclass into a function that takes the base class as a parameter, but still want it deserialized automatically as the child class, then the base class must be.

Code style

 * New code shall be in Kotlin.
 * Indentations are 4 spaces.
 * Files must end with a newline.
 * Property annotations appear on the same line (needs updating in IDE settings); class and method annotations get their own line (the IDE default)
 * Classes should be slim, modular, of a single responsibility, and unit like; if it's difficult to test a class:
 * Ensure the class exposes its dependencies in the constructor.
 * Ensure the class has only one responsibility.
 * Member variables do not start with a little "m" (just say mNo to Hungarian Notation)

Annotations

 * Use annotations such as,  , etc. whenever a parameter or variable (usually an Int) really refers to one of these special types, so that Android Studio can provide the proper compile-time hinting.

Naming

 * All articles are pages but only some pages are articles (e.g., Talk namespace pages are not articles). Use page when referring to either unless the distinction is intended.

Tests
Write tests to improve your development speed, confidence in the submitted implementation, and maintenance costs


 * The person that writes the code, writes the test: new classes that have existing test patterns should be committed with accompanying tests
 * Model classes that have no logic may be excepted
 * View subclasses that are too complex may be excepted but it should be rare that no component of a View could be tested
 * The subject's non-private API should be tested
 * Tests should be short, pointed, and have as few assertions as possible
 * Test classes should be named SubjectTest where Subject is the class under test
 * Test methods should be named testMethodCase where Method is the API under test and Case is the specific configuration scenario
 * All paths should be covered when practical, but tests should always be encouraging.