Properly initialize heavy objects - android

I am trying to convert some Java code to Kotlin. I have a "heavy" object that I cannot reason about how to initialize properly in the app. The object can take some time to create and I don't want to block except for when the functionality is actually required. I wrote some code that meets my requirements, but it doesn't seem like a good pattern and I was hoping someone tell me what the proper pattern here (will list what I don't like about it after the code):
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.*
import javax.inject.Provider
import kotlinx.coroutines.channels.Channel
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = initHeavyObject()
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.get()
}
}
fun initHeavyObject(): Provider<HeavyInitObject> {
val cnl = Channel<HeavyInitObject>(Channel.BUFFERED)
//purposely block to ensure to ensure it is initialized
val provider = Provider { runBlocking { cnl.receive().also { cnl.close() }}}
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener{
override fun onReady(heavyInitObject: HeavyInitObject) = runBlocking {
cnl.send(heavyInitObject)
}
})
return provider
}
// Mocked library I am using (i.e. I don't have control over the implementation)
class HeavyInitObject {
companion object {
fun get(listener: HeavyInitObjectListener) {
val heavyInitObject = HeavyInitObject()
listener.onReady(heavyInitObject)
}
}
interface HeavyInitObjectListener {
fun onReady(heavyInitObject: HeavyInitObject)
}
}
What I don't like
Should be a val
It naturally really be a val, because the value should never change once initialized.
class MainActivity : AppCompatActivity() {
val heavyInitObject: Provider<HeavyInitObject> = initHeavyObject()
// OR...
val heavyInitObject: HeavyInitObject by lazy {
initHeavyObject().get()
}
The first option seems like it could do too much too fast. Depending on how someone would add MainActivity to the object graph it could really affect startup performance.
The second one is too slow. If we haven't requested the heavy object to be created before it is needed, there will be definite jank in the application when the heavy object is queried the first time.
Is there a good way to have the object be a val while requesting the object to be created in onCreate (understanding that I don't have control over implementation of the underlying library)?
Is channel the right data structure here?
Maybe this is bareable, but I wanted to see if there is a better option. A RENDEZVOUS channel makes more sense, but send suspends until receive is called and I don't want to block anything on thread initializing the object (i.e. since i can't convert the implementation to a suspend function). Switching to a bufferend channel won't block since I only send one element through, but that seems like a hack. What is the best data structure for this task?
Edit:
Thanks to some help in the comments I have improved the second condition (eliminate akward use of channel). I have a couple ideas for how to improve the first condition...
Code for getting rid of channel
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override suspend fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = lifecycleScope.async {deferredHeavyObject()}
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.await()
}
}
suspend fun initHeavyObject(): HeavyInitObject = suspendCancellableCoroutine { continuation ->
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener {
override fun onReady(heavyInitObject: HeavyInitObject) {
continuation.resume(heavyInitObject)
}
})
}
Code to finalize heavyInitObject
class MainActivity : AppCompatActivity() {
val heavyInitObject by lazy { heavyInitObjectBackingField }
private lateinit var heavyInitObjectBackingField: Deferred<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObjectBackingField = lifecycleScope.async { deferredHeavyObject()}
}}
I basically get a lateinit val that way... and can be confident I won't get an error for it not being initialized. Ideally, it makes me realize this is overcomplicated, because I can't get under the hood and easily seperate the object initialization from the call back initialization.. Unless anyone else has a better idea?

Channel is kind of weird for returning a single item. I would load it in a Deferred privately and publicly expose a suspend getter that awaits the Deferred result. Once the object is ready, it won’t have to be waited for. And since it’s a suspend function, you can access it via a coroutine without unlocking your main thread.
object HeavyObjectCreator {
private val heavyObject: Deferred<HeavyObject> = GlobalScope.async {
// Long running actions to generate the object…
HeavyObject(params)
}
suspend fun getInstance(): HeavyObject =
heavyObject.await()
}
In your activity you can use lifecycleScope.launch to start a coroutine when you need to do a task that uses the object and it can call the above function to get it in a suspending way. If you want to preload the heavy object, you can put the statement HeavyObjectCreator in onCreate of your Application class or your Activity so the creator object will be instantiated and start the coroutine to load the heavy object.
This is just one example of a way to do it for an object that you’ll be reusing on multiple screens. If you intend to load a new heavy object only on screens that need it, I’d consider putting the contents of the class above directly in a ViewModel and use viewModelScope instead of GlobalScope.

Related

Can pass data from View to ViewModel?

Is there mistake if I pass data from View to ViewModel? For example, pass url from onPageFinished event of WebView. I am confused because all source tell that ViewModel mustn't have any link to View. In this case will be such link or not? Or if type of argument will be custom data class than just string?
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.webView.settings.javaScriptEnabled = true
binding.webView.webViewClient = object : WebViewClient(){
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
viewModel.onPageFinished(url) // this line
}
}
viewModel.url.observe(this) { url ->
binding.webView.loadUrl(url)
}
}
}
class MainViewModel: ViewModel() {
private val _cookieManager: CookieManager
private lateinit var _url: MutableLiveData<String>
val url: LiveData<String> = _url
init {
_url.value = "google.com"
_cookieManager = CookieManager.getInstance()
}
fun onPageFinished(url: String) {
val cookies = _cookieManager.getCookie(url)
Log.i("MainViewMovel", url)
Log.i("MainViewMovel", cookies)
}
}
The View is the UI, it has to pass some data to the View Model, like button presses, typed search queries, requests for data when the user scrolls etc. It's absolutely a two-way thing - here's the docs talking about the recommended way of designing your app. There's more on the side, including a section on the UI Layer (the View)
The terminology might be a little confusing here, because the View in Model-View-ViewModel is referring to your UI layer. But a View in Android is a layout component, which is generally tied to a specific Fragment or Activity which have shorter lifetimes than a ViewModel (one of the main points of a VM is to retain data even when Activity and Fragment instances are destroyed and recreated).
So for that reason, your ViewModel shouldn't hold any references to any Views. That's why you expose data through things like LiveData which are observed by passing a LifecycleOwner - the observers are automatically removed when they're destroyed, so the LiveData in the VM isn't keeping them alive
As far as your question goes, I don't think it hugely matters - your WebViewClient is a little bit of wiring between the View and the ViewModel, and there's always a bit of that! But if you wanted, you could always put the WebViewClient in the ViewModel, there's nothing obviously tying it to a particular view
I think that makes more sense in general - if you look at the other callbacks in WebViewClient, a lot of them are about program logic, handling events and situations. The UI layer shouldn't really be concerned with any of that, so it makes more sense to me to have the VM take care of that, decide what needs to happen, and just push any resulting UI state updates to the UI.
An alternative to keeping a singleton WebViewClient in the VM would be to have a function in there that creates a new one and configures it. That way you don't need to worry about it having any state relating to the old WebView, but all the code is still internal to the ViewModel:
class MainViewModel: ViewModel() {
...
fun getWebViewClient() = object : WebViewClient(){
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
onPageFinished(url) // calling the internal VM method
}
}
}
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.webView.webViewClient = viewModel.getWebViewClient()

Make Retrofit API call in Activity using Kotlin coroutines

I want to load some data inside activity after the button is clicked. I came up with the following solution and it works as I expect. But I just started learning kotlin coroutines and I want someone else to comment on my code. For example, is it okay that I update the UI using lifecycleScope.launch? I could probably use withContext(Dispatchers.Main) instead but is there a difference?
Is my implementation good in general? Is there something that could be optimzed/refactored?
I understand that it's better to use ViewModel and make API calls there but in this case I want all action to happen inside the activity.
class MainActivity : AppCompatActivity() {
var apiCallScope: CoroutineScope? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.btn_load_content).setOnClickListener {
// Cancel previous API call triggered by the click.
// I don't want to have multiple API calls executing at the same time.
apiCallScope?.cancel()
showProgress(true)
apiCallScope = CoroutineScope(Dispatchers.IO)
apiCallScope!!.launch {
// Execute Retrofit API call
val content = api.loadContent().await()
// Update UI with the content from API call on main thread
lifecycleScope.launch {
showProgress(false)
drawContent(content)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
apiCallScope?.cancel()
}
private fun showProgress(show: Boolean) {
// TODO implement
}
private fun drawContent(content: String) {
// TODO implement
}
}
It's preferable to use ViewModel to make such types of operations and not perform them inside Activity, especially in the onCreate method.
ViewModel gives you the viewModelScope property, any coroutine launched in this scope is automatically canceled if the ViewModel is cleared to avoid consuming resources.

Why companion object for newStartIntent in Android+Kotlin?

I have seen quite a few examples in Kotlin where an activity class has a companion object to encapsulate the creation of a start intent like the following. It seems particularly Java inspired.
class HomeActivity : AppCompatActivity() {
companion object {
fun newStartIntent(context: Context): Intent {
val intent = Intent(context, HomeActivity::class.java)
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.setContentView(R.layout.activity_home)
// ...
}
// ...
}
Since Kotlin has top level functions, why not skip the companion object and just have a top level function?
fun newHomeActivityStartIntent(context: Context): Intent {
val intent = Intent(context, HomeActivity::class.java)
return intent
}
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.setContentView(R.layout.activity_home)
// ...
}
// ...
}
There is nothing wrong in your approach actually. I thought of a few reasons why I would choose a companion object over top-level functions:
Top-level functions visible for everyone, thus every time you start typing new... you will get a list of partially irrelevant results;
Companion objects can hold private values that you would not like to make open to the public and keep them visible only within your class but still make them static. Maybe there are some arguments that are calculated under this function invocation and passed with intent, and you would like to hide these calculations or arguments keys;
This is not your case but still relevant: using companion object you can make all constructors private and control all arguments passed to object initialization. This is how Singleton can be created in Kotlin;
Opinionated For me personally it makes things look tidy. I usually extract only simple and relatively vastly used functions. Like Date conversion functions, or math function calculations.
It is a matter of style. Just pick one and be consistent!

ViewModel object attribute uninitialized?

I sometimes get an error of UninitializedPropertyAccessException (from analytics), but the application has never crashed during my experience of using it. I think after application is dropped in background and process is killed then this happens - but I've no way of reproducing this error.
In my Activity, I do following:
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.xxx)
... // doing other stuff
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
viewModel.init(
...,
...,
...,))
setUpObservables()
}
and my MyViewModel looks like:
class MyViewModel : ViewModel() {
var lateinit car: String
fun init(
car: String,
...: String?,
...: Boolean
) {
if (isInitialized) {
return
}
this.car = car
...
isInitialized = true
}
Later on I try to access car object an get an error if it's uninitialized. Question is - how can this happen? car object is initialized when MyViewModel is. Any ideas how to reproduce this? Any ideas how to avoid this?
Ok, So I've added your code like this in a ViewModel on one of my working projects.
lateinit var car: String
fun init(car: String) {
this.car = car
}
And called:
viewModel.init("car")
Log.d("CAR_DEBUG",viewModel.car)
And received in my console the expected output: D/CAR_DEBUG: car. So it works but bugs might appear depending on your implementation.
Also, this is not the correct way to do this, I would suggest using the MutableLiveData observer pattern so that you make sure that any unexpected behaviour won't happen due to screen rotation or activity/fragment recreation. With this in mind change your code to this:
var carObservable: MutableLiveData<String> = MutableLiveData()
fun init(car: String) {
carObservable.value = car
}
And in Activity/Fragment:
viewModel.carObservable.observe(this, Observer {
Log.d("CAR_DEBUG",it)
})
// doesnt matter where your viewModel.init this will still work
viewModel.init("car")
This way even if you call the init function after the observe invocation you are sure that you are notified only when the value changes. This is actually the recommended way of using ViewModels (the reactive way through the observer pattern). And doing this you make sure that even if you rotate the screen or recreate the Activity/Fragment your car variable will be available.

how do you get an Idlingresource to work in Kotlin with coroutines

My Espresso Idling Resource is not working - it compiles and runs but no longer waits long enough for the result to be returned from the 'net.
Start with https://github.com/chiuki/espresso-samples/tree/master/idling-resource-okhttp
Convert the main activity to Kotlin - test (which is still in java) still works with OKHttpIdlingResource
Convert to anko coroutine call instead of retrofit.enqueue - test no longer works.
Here is the new code for MainActivity in its entirety
import android.app.Activity
import android.os.Bundle
import android.widget.TextView
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import org.jetbrains.anko.coroutines.experimental.bg
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
doCallAsync()
}
private fun doCallAsync() = async(UI) {
val user = bg { getUser() }
val name = user.await().name
val nameView = findViewById(R.id.name) as TextView
nameView.text = name;
}
private fun getUser(): User {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpProvider.getOkHttpInstance())
.build()
val service = retrofit.create(GitHubService::class.java)
val response = service.getUser("chiuki").execute().body()
return response!!
}
}
Convert to anko coroutine call instead of retrofit.enqueue - test no longer works.
retrofit.enqueue uses OkHttp's dispatcher. This is what the "idling-resource-okhttp" recognizes and communicates to the idlingresource manager.
However by using retrofit.execute and anko's bg you are using a different execution mechanism that the idlingresource manager does not know about, so while it might be executing the application is idle from the view of the manager, thus ending the test.
To fix this you need to register an IdlingResource for whatever execution mechanism bg uses, so it can recognize when there is something happening on that thread of execution.
You have to create an IdlingResource to tell Espresso whether the app is currently idle or not (as Kiskae wrote). AFAIK for coroutines there does not exist a central registry that can tell you whether there is a coroutine running.
So you have to keep track of them yourself as suggested in the documentation by using a CountingIdlingResource. Add this convenience async-wrapper to your project:
public fun <T> asyncRes(
idlingResource: CountingIdlingResource,
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> T
): Deferred<T> = async(context, start, parent) {
try {
idlingResource.increment()
block()
} finally {
idlingResource.decrement()
}
}
Add an instance of CountingIdlingResource inside your Activity, call asyncRes instead of async and pass in the CountingIdlingResource.
class MainActivity : Activity() {
// Active coroutine counter
val idlingResource = CountingIdlingResource("coroutines")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
doCallAsync()
}
private fun doCallAsync() = asyncRes(idlingResource, UI) {
...
}
...
}

Categories

Resources