AAC - How to avoid reference to activity in viewmodel - android

The FusedLocationProviderClient that I'm using to get location information needs a reference to an activity or context. When I try to instantiate my UserLocationService - which implements FusedLocationProviderClient - inside a viewmodel, i have to pass a reference to an activity.
class UserLocationService {
public val locationUpdates: MutableLiveData<Location> = MutableLiveData()
private var fusedLocationClient: FusedLocationProviderClient
constructor(activity: Activity) {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
...
I don't want to pass any reference to the viewmodel. What is the right approach in this case? I could use the UserLocationService directly in the fragment but my understanding is, that the viewmodel should do the instantiation und initialization.

You should use an AndroidViewModel.
Edit:
So what you can do is have the view model class.
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val context = application.applicationContext
}
And then use the context for what you need.
To initialize the view model you can do something like this in your activity.
private val myViewModel: MyViewModel by lazy {
ViewModelProviders.of(this).get(MyViewModel::class.java)
}
And then use it after the onCreate()

Related

How to pass context in Repository in MVVM without DI and following the clean architecture?

I need to initiate Room in its repository. But to do that I need context. If I pass context through the viewmodel i got this message This field leaks a context object.
I have checked this answer but they init the repository object in the view layer but according to the clean architecture view layer shouldnt know anything about data layer right? So how to organize the delivery of the context to the data layer without DI?
class MainViewModel(private val context: Context) : ViewModel() {
private val roomManager : RoomManager = RoomManagerImpl(context)
private fun addItem(){
roomManager.addItem()
}
}
Here is repository code
class RoomManagerImpl(private val context: Context) : RoomManager {
private val db = Room.databaseBuilder(
context,
AppDatabase::class.java, "database-name"
)
Passing Activity Context to the Activity's ViewModel is not a good practice it will cause memory leak.
You can get the context in your ViewModel by extending the AndroidViewModel class check below code.
It will give you application level context.
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
private val roomManager: RoomManager = RoomManagerImpl(context)
fun addItem() {
roomManager.addItem()
}
}
class MainViewModal(val repository:roomManager) :ViewModel() {
fun testFunction(context: Context){
roomManager.testMethod(context) //text method is your repository method
}
}
i am telling like this I hope it's help you otherwise you can use AndroidViewModel instead of Viewmodal

Android Kotlin: Having a global Context object for all activities (best practice)

I have a global app settings class as follows:
class AppSettings : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
instance = this
resourses = applicationContext.resources
outputPathCache = cacheDir.absolutePath
}
companion object {
lateinit var instance: AppSettings
private set
val context: Context
get() { return activityContext.get()!! }
lateinit var activityContext: WeakReference<Context>
var database: SQLiteDatabase? = null
var resourses: Resources? = null
private set
lateinit var dialog: AlertDialog
const val defLanguage = Enum.Language.ENGLISH
const val defIdLanguage = Enum.LanguageId.ENGLISH
const val screenshotFilename = "xxx"
const val actionBarTitleColor = "#0D0D0D"
const val footerColor = "#8a8a8a"
const val activityBackground = "#ffffff"
... whatever
}
}
And as you see I have a static Context variable as follows:
lateinit var activityContext: WeakReference<Context>
(I use WeakReference so the IDE doesn't complain about memory leaks).
And I have a constant Context like the next:
val context: Context
get() { return activityContext.get()!! }
I assign a value for the first time to activityContext in SplashActivity as follows (I do this because the first activity is a OnBoarding class that doesn't inherit from BaseActivity):
AppSettings.activityContext = WeakReference(this)
The same in BaseActivity onCreate (most of my activities inherit from this class):
AppSettings.activityContext = WeakReference(this)
And then, in any activity which extends BaseActivity I can use the context simply like this:
AppSettings.context
For the activities that doesn't inherit from BaseActivity I just initialise the context to be used in the activity in the same way as in Base, so I can always get it as "AppSettings.context".
The reason of not simply using "this" in all activities to get context (or to use any sort of Context creation in Base) is that I'm using MVVM and there are classes outside activities (like ViewModel) with methods that may need a context, and I just don't wan't to pass it as a parameter (this is why I'm expecting to have a global context that can be accessed anywhere).
Although I have just finished and I haven't fully tested yet, it is apparently working great, but I wonder to know if this is the recommended way to deal with this, or if there is a better approach to have a global Context.
There are some ways to achieve that, but I believe the most encouraged by google is with the dependency injection library dagger-hilt, which isn't hard to set-up, but saves a lot of time and prevents possible memory-leaks.
In order to inject context into any class later you just need to do:
class ExampleClass #Inject constructor(#ApplicationContext val context: Context) {}

In Hilt, How can I Inject a context in a non activity/ fragment class using field injection?

I have a Exception Handling class
class IOException : BaseException {
#EntryPoint
#InstallIn(SingletonComponent::class)
interface AnalyticsServiceProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
private val hiltEntryPoint = EntryPointAccessors.fromApplication(**Need Context**, AnalyticsServiceProviderEntryPoint::class.java)
val analyticsService = hiltEntryPoint.analyticsService()
}
If I see this offical link, it says
In this example, you must use the ApplicationContext to retrieve the
entry point because the entry point is installed in SingletonComponent
What If I don't have the context in the class and in the function body which I will use and I don't want to use from Constructor Injection as well?
I only want to use the field injection. How can I access it, since I don't have the context.
In your application add a companion object with lateinit var applicationContext and initialize the variable when the application is initialized like:
companion object {
lateinit var applicationContext: Context
private set
then you have an static variable with the application context and you can you something like:
private val hiltEntryPoint = EntryPointAccessors.fromApplication(Application.applicationContext, AnalyticsServiceProviderEntryPoint::class.java)
I can't think of anything else to do what you want to do
Did you try to get applicationContext from dagger-hilt ApplicationContextModule? Its already provided by this module in your app and you can get that probably. You need to use the qualifer also
#Provides
#ApplicationContext
Context provideContext() {
return applicationContext;
}

Access the application context inside of a singleton

I have a singleton that I'm using to open a JSON asset and return it as a list. I need to access the application context in order to use the Asset Manager. I can't pass in the context because I'm calling it from a view model, which does not have access to the application context. I've done a lot of searching but I can't seem to find the answer.
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
object ProgramListService {
fun getProgramList(): List<ProgramList>? {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val json = context.assets.open("programs/home.json").bufferedReader().use{ it.readText() }
val listType = Types.newParameterizedType(List::class.java, ProgramList::class.java)
val adapter: JsonAdapter<List<ProgramList>> = moshi.adapter(listType)
return adapter.fromJson(json)
}
}
Context is not good in a ViewModel, not even application context, nor is AndroidViewModel good to use. So you can pass AssetManager into your ViewModel which can then pass it into your ProgramListService, avoiding the need for context, but asset manager is still a bit weird to have in view model.
So you can skip the viewmodel and pass the application context directly into your singleton ProgramListService.
object ProgramListService {
lateinit var application: Application // Add this
...
}
And then from your activity's onCreate or wherever is best for your project,
ProgramListService.application = context.applicationContext as Application
Or like #Tenfour04 suggested application is safe to make a global property
You have the Context in ViewModel
public class HomeViewModel extends AndroidViewModel {
public HomeViewModel(Application application) {
super(application);
Context context = getApplication().getApplicationContext();
}}
I would hesitate to call what you have a singleton, since it doesn't carry any state. You've used an object to organize the namespace of what would be a static method in Java.
If your ViewModel is an AndroidViewModel, it comes with an Application instance, so you can use that as your context:
class MyViewModel(val application: Application): AndroidViewModel(application) {
fun foo() {
someRepoAccessCall(application)
}
}
Since the Application is safe to use as a singleton, you can alternatively create a global property for it that you initially set in onCreate().
lateinit var application: MyApplication
class MyApplication(): Application
override fun onCreate() {
super.onCreate()
application = this
}
}
Don't forget to assign the custom Application class in the manifest:
<application
android:name=".MyApplication"
...

How to observe data from live data android on Three different Fragment

I have my Activity MainActivity.kt .
And and one ViewModel MainActivityViewModel.kt
And I want to observe my live data to my 3 different fragments.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
But my observer only work on activity not on the fragments.
Hey there you are doing everything greate except one thing you should use requireActivity() instead on this in your fragment class.
Make sure your all fragment are attached to your viewModel holding Activity.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(MainActivityViewModel::class.java)
}
This will help you solve your issue.
For further detail view this.
The ViewModelProviders.of has 2 different constructors:
of(Fragment fragment, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given fragment is alive.
of(FragmentActivity activity, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given Activity is alive.
Basically when you used this as the first parameter in your activity, you passed the context of the activity and created a viewmodel that will be alive in the scope of the activity, however your second this is the context to your fragment, meaning that the second ViewModel will be alive as long as your fragment is alive (only one fragment).
What instead you should be doing in your fragment is using the context of the activity, since activity is always alive when fragments are attached and swapped. You should change your fragments to:
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(MainActivityViewModel::class.java)
}
or you can use the requireActivity() method that was the previous answer.
To achieve what you are trying to do, you need three things. An activity/fragment that will post the value to the ViewModel, a ViewModel, and an activity/fragment that will retrieve the data from the ViewModel. Lets say your data is stored in an ArrayList, and you want to update and retrieve it from different fragments.
First, we have to implement a ViewModel. It contains the data you want to share between your activities/fragments. You declare the MutableLiveData as an ArrayList then initialize it.
class testviewmodel : ViewModel() {
val list: MutableLiveData<ArrayList<String>> = MutableLiveData()
init {
list.value = arrayListOf()
}
}
Our next step is to access and update the ArrayList using your activity:
val viewmodel = ViewModelProviders.of(this).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
If you are using a Fragment to update it, use this:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
}
Finally, to retrieve the data from the ViewModel in a fragment, put this inside your onViewCreated:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(Dbviewmodel::class.java)
observeInput(viewmodel)
}
Put this outside of your onViewCreated:
private fun observeInput(viewmodel: testviewmodel ) {
viewmodel.list.observe(viewLifecycleOwner, Observer {
it?.let {
if (it.size > 5) {
pos = it[5]
//grab it
Toast.makeText(context,pos,Toast.LENGTH_LONG).show()
//display grabbed data
}
}
})
}
Take a look at this docs for more information about ViewModels
Good Luck! I hope this helps :)
That's because you are using the fragment 'this' instance, and not the activity.
Replace
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
With
activity?.let { mainActivityViewModel = ViewModelProviders.of(it, viewModelFactory).get(MainActivityViewModel::class.java) }

Categories

Resources