Null vs noop implementation for api design

Here are 3 examples of api design, which is more natural or which I should prefer, or maybe a kotlin way to design api?


    class SomeView1 {
        
        interface Listener {
            fun onItemSelected(index: Int)
        }
        
        var listener: Listener? = null
        
        protected fun notifyListener(index: Int) {
            listener?.onItemSelected(index) 
        }    
    }
    
    class SomeView2 {
    
        interface Listener {
            fun onItemSelected(index: Int) { }
        }
    
        var listener: Listener = object : Listener { }
    
        protected fun notifyListener(index: Int) {
            listener.onItemSelected(index)
        }
    }
    
    class SomeView3 {
    
        interface Listener {
            fun onItemSelected(index: Int) 
            
            companion object {
                val NoOp = object : Listener {
                    override fun onItemSelected(index: Int) {
                        // noop
                    }
                }
            }
        }
    
        var listener: Listener = Listener.NoOp
    
        protected fun notifyListener(index: Int) {
            listener.onItemSelected(index)
        }
    }

I would personally do:

     class SomeView2 {
    
        fun interface Listener {
            fun onItemSelected(index: Int) { }
        }
    
        var listener: Listener = Listener { }
    
        protected fun notifyListener(index: Int) {
            listener.onItemSelected(index)
        }
    }

Or even forego the interface completely and just use (Int) -> Unit

I often prefer SomeView1, because the presence or the absence of the listener is clear.

var listener: ((Int) -> Unit)? = null
1 Like

the problem with nullable listener is that this scenario is never used de facto, so null checks on every call to the listener are useless, and the absense of question marks is a plus to readability. When SomeView created in code we can enforce to pass listener inside of the constructor, but if SomeView created for example by LayoutInflater(android) a late binding is unavoidable and leads to use a single stub noop listener or one stub per every SomeView instance - it’s like a matter of taste but I asked to know opinions of other people

1 Like

You can use lateinit if listener is always initialized before first notifyListener() call.

No, there is no guarantee that listener will be set. And I think lateinit in kotlin is a hack because it breaks nullability checks and primarily used for interopability and dependency injection libraries where the guarantee of initialization is on the DI library not user code

Frame challenge: why support only one listener?

Obviously it depends on the context, but most places I’ve used listeners, there was the possibility of having more than one listener for a given object.

And in that case, what you want isn’t a single listener, but a collection* of them.

And of course, that collection can be empty — so no null reference/object/type is needed!


But if there must be only a single listener, then the first implementation with a simple null seems by far the best to me: it’s safe, idiomatic, flexible, easy to read, concise, efficient, and has no issues with initialisation order etc.


(* A set would make sense, but I’ve only seen it done with a list. You probably want something that has low overhead for iteration, and ideally which is thread-safe. Because informing listeners tends to be much more frequent than adding/removing them, CopyOnWriteArray can be a good solution.)

1 Like

I mean, given his listener design, creating a delegating listener (and hence a linked list of listeners) would be very simple, or even creating a MutableListItemBlahListener. I’d prefer the simple solution of one listener (yes linked lists are slow, but there’s no need to over-complicate the design until necessary

A lot of components usually work with a single controller/handler/processor so in many cases creating a list of listeners/sharedflow is overengineering. I have not seen a button with more than listener to onClick

1 Like

As I said, it depends upon the context. But back when I did FEs for a living, there were many situations where e.g. an incoming price update could potentially affect many different components.

(And as for complicating a design, CopyOnWriteArrayList has already done most of the heavy lifting, and iterating it is dead easy. Whereas delegation doesn’t easily handle e.g. listener removal without more work — and yes, unless your UI is simple and/or completely static, removing listeners is probably needed too; you wouldn’t want lots of no-longer-visible components hanging around just so they could keep listening for e.g. price updates.)

There is no over-engineering if you use ready-to-use implementation: Flow (MutableSharedFlow as implementation)

We never use standard callbacks anymore in our code, because they are the main source of memory leaks, do not work well with other async code (flow, suspend functions), do not support multiple subscribers (and if one day we need to support, it requires rewrite implementation completely)

If you use Flow (or even RxJava or other reactive framework), I would suggest to do the same

It’s also true for cases like button’s onClick, where it’s important to avoid memory leaks (when observer lives longer than button) and where having an option to combine with other sources of events is very useful