kotlinjvm

How to implement an attribute accessible by everything within a scope


I want to know if it is possible in Kotlin to create a "Context" from which objects can get data from. Something like the following:

MyScope(myGlobalObject) {
    val objectA = MyClassA()
    objectA.doSomething()
}

ClassA() {
   fun doSomething() {
       val resultA = ...
       myGlobalObject.add(resultA)
       val objectB = ClassB()
       objectB.doSomethingElse()
   }
}

ClassB() {
   fun doSomethingElse() {
      val resultB = ...
      myGlobalObject.add(resultB)
   }
}

I wouldn't want MyScope to be a singleton because this is multi-threaded environment (server request) where I wouldn't want other threads messing up with context within local instances of MyScope

Code above is in the spirit of avoiding passing around myGlobalObject several levels down as follows:

val objectA = MyClassA(myGlobalObject)
objectA.doSomething()

ClassA constructor(val myGlobalObject) {
   fun doSomething() {
       val resultA = ...
       myGlobalObject.add(resultA)
       val objectB = ClassB(myGlobalObject)
       objectB.doSomethingElse()
   }
}

ClassB constructor(val myGlobalObject) {
   fun doSomethingElse() {
      val resultB = ...
      myGlobalObject.add(resultB)
   }
}

In other words, similar to other frameworks like React with ReactContexts. Been searching the internet and seems possible, JetPack compose has the feel of it but not sure if it possible to pass down data like above, or if there is a way to "reach" into a scope from any part of the code... I know it might return null or non-initialized but that is something I would be perfectly fine with


Solution

  • I believe you are looking for the experimental feature of Context Receivers. You can enable this by passing the -Xcontext-receivers compiler option.

    Suppose you want your functions to access such an object:

    // as per your requirements, not a singleton
    class SomeGlobalObject {
        fun foo() {
            println("foo")
        }
        fun bar() {
            println("bar")
        }
    }
    

    You can prefix your function declaration with context(SomeGlobalObject):

    context(SomeGlobalObject)
    fun thisNeedsAGlobalObject() {
        // here you can use members of SomeGlobalObject without qualification
        foo()
        bar()
    
        // if there is ambiguity, you can qualify it with 'this@SomeGlobalObject'
        this@SomeGlobalObject.foo()
    }
    

    Functions that need a SomeGlobalObject can call other functions that need a SomeGlobalObject:

    context(SomeGlobalObject)
    fun somethingElseThatNeedsAGlobalObject() {
        thisNeedsAGlobalObject()
    }
    

    You can use with to start a scope where SomeGlobalObject exists:

    fun main() {
        with(SomeGlobalObject()) {
            thisNeedsAGlobalObject()
        }
    }
    

    Adapting this to your ClassA and ClassB example, it would look like this:

    fun main() {
        val globalObject = SomeGlobalObject()
        with(globalObject) {
            val objectA = ClassA()
    
            // globalObject is implicitly passed to doSomething here
            objectA.doSomething()
        }
        println(globalObject.list)
    }
    
    class ClassA {
        context(SomeGlobalObject)
        fun doSomething() {
            val resultA = "A..."
            // 'this@SomeGlobalObject' is redundant here, but just to be clear
            this@SomeGlobalObject.add(resultA)
            val objectB = ClassB()
    
            // globalObject is implicitly passed to doSomething here
            objectB.doSomethingElse()
        }
    }
    
    class ClassB {
        context(SomeGlobalObject)
        fun doSomethingElse() {
            val resultB = "B..."
    
            // 'this@SomeGlobalObject' is redundant here, but just to be clear
            this@SomeGlobalObject.add(resultB)
        }
    }
    
    class SomeGlobalObject {
        val list = mutableListOf<Any>()
    
        fun add(x: Any) {
            println("Adding $x...")
            list.add(x)
        }
    }
    

    You can also make a function require multiple context receivers of different types. See the KEEP proposal for more features.