Wikimedia Apps/Team/Android/Coding conventions

From mediawiki.org

Views and Layouts[edit]

  • Our project uses View Bindings, which is the current recommended practice.
  • When using a View Binding in an Activity, 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 Fragment, 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 View, here is the boilerplate code (if using a merge node at the root of your XML):
private val binding = NameOfBindingClass.inflate(LayoutInflater.from(context), this)

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

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

Other View guidelines[edit]

  • View.post() and View.postDelayed() should be avoided. If you're finding yourself relying on post() 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 merge node whenever possible, with tools:* attributes to aid laying out the view in the AS designer.

Serialization[edit]

  • Our project uses the kotlinx.serialization 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 Parcelable.

To make a class serializable, just add the @Serializable 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 @SerialName 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 null.

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<String> = emptyList(),
    val map: Map<Int, String> = emptyMap()
)

Post-processing steps[edit]

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 init 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[edit]

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[edit]

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

  • ignoreUnknownKeys = true, 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.
  • coerceInputValues = true, which tells it to forgive null JSON values even if the receiving model property is nonnull, and also forgive unknown values of enum types.

Polymorphism[edit]

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 sealed.

Code style[edit]

  • 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)

Kotlin specifics[edit]

  • Try to minimize usage of nullable properties, and instead use lateinit whenever possible, i.e. when an Activity is created, or when a View is bound.
  • Use by lazy sparingly. This should be used only for very heavy objects, since by lazy itself introduces some overhead.
  • Use data class where appropriate, but not for everything.

Annotations[edit]

  • Use annotations such as @ColorInt, @AttrRes, 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.

Terminology[edit]

Naming[edit]

  • 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[edit]

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.