Android MVVM - How to reference Activity in ViewModel - android

MVVM architecture,
this is my View (Activity):
private MyApp app;
private MainActivityVM viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (MyApp) this.getApplication();
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
MainActivityVM.Factory factory = new MainActivityVM.Factory(app);
final MainActivityVM model = ViewModelProviders.of(this, factory)
.get(MainActivityVM.class);
viewModel = model;
binding.setVm(viewModel);
viewModel.onCreate();
and View Model:
public class MainActivityVM extends AndroidViewModel implements ViewModel {
public MainActivityVM(#NonNull Application application) {
super(application);
}
#Override public void onCreate() {
model = new MyService();
model.getData(); /* <-- how do i pass the activity here? */
}
#Override public void onPause() { }
#Override public void onResume() { }
#Override public void onDestroy() { }
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
public Factory(#NonNull Application application) {
mApplication = application;
}
#Override
public <T extends android.arch.lifecycle.ViewModel> T create(Class<T> modelClass) {
return (T) new MainActivityVM(mApplication);
}
}
}
and Model:
public class myService{
public getData(){
if(permissionacquired(){
getdata()
}else{
requestPermission();
}
}
private void requestPermission() {
PermissionKey permKey = new PermissionKey(HealthConstants.StepCount.HEALTH_DATA_TYPE, PermissionType.READ);
HealthPermissionManager pmsManager = new HealthPermissionManager(mStore);
try {
// Show user permission UI for allowing user to change options
/* BELOW CODE REQUIRE Activity reference to PASS */
pmsManager.requestPermissions(Collections.singleton(permKey), MainActivity.this).setResultListener(result -> {
/* ABOVE CODE REQUIRE Activity reference to PASS */
Log.d(APP_TAG, "Permission callback is received.");
Map<PermissionKey, Boolean> resultMap = result.getResultMap();
if (resultMap.containsValue(Boolean.FALSE)) {
updateStepCountView("");
showPermissionAlarmDialog();
} else {
// Get the current step count and display it
mReporter.start(mStepCountObserver);
}
});
} catch (Exception e) { Log.e(APP_TAG, "Permission setting fails.", e); }
}
}
EDIT: if you see my request permission in my Model, the API require activity to be pass - how can i pass activity reference to the request permission?
I have a get permission method that comes from Model. this get permission method from my service provider require activity e.g. requestPermission(Activity)
so in my ModelView, i have the model object which is the dataService from another source.
then, how I can reference Activity in my ViewModel so I can call: model.requestPermission(Activity); in my ViewModel?
understanding from here that:
Caution: A ViewModel must never reference a view, Lifecycle, or any
class that may hold a reference to the activity context.

As long as you require permission in onCreate() method you can just move logic with permission request into the activity, and pass request result into viewModel.

In my case I also added Activity into ViewModel for permissions and strings, but it's not a good idea. When I disabled location permission in one Fragment, an application crashed, because it restarted, then restored FragmentManager with fragment stack and later started MainActivity. So ViewModel got location status too early (in constructor) and threw an exception. But when I moved getting location status to a function, then the application restarted normally.
So, using Dagger, you can write something like:
AppModule:
#JvmStatic
#Provides
fun provideActivity(app: MainApplication): AppCompatActivity = app.mainActivity
In MainApplication hold mainActivity and in MainActivity set in onCreate:
application.mainActivity = this
In onDestroy:
application.mainActivity = null
In any ViewModel add:
class SomeViewModel #Inject constructor(
private val activity: Provider<AppCompatActivity>
)
Then use it: activity.get().getString(R.string.some_string).

Related

Android ViewModel object recreate with kotlin but not with java

Im new to kotlin, and mvvm, but i was able to make it work in java, but when i made a new example mvvm-retrofit-corutines in kotlin, the view model gets called all the time on the OnCreate function is called, (which shouldn't happen according to docs and works fine in java).
MainActivity:
lateinit var viewModel : MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Here we can see the logs in every orientation changed in the emulator.
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.getMutableLiveDataModel().observe(this, Observer {
Log.d("zzzz","lamda executes onChanged method -> "+ it.otherValues). //element from model
})
}
MyViewModel:
class MyViewModel : ViewModel() {
private lateinit var objectTypeModel: MutableLiveData<MyTestModel>
fun getMutableLiveDataModel():MutableLiveData<MyTestModel>{
//Gets the model from a retrofit service call
objectTypeModel = MyRepository.getModelFromService()
return objectTypeModel
}
}
Am i doing something wrong? already tried convert 'viewModel' into local variable as suggested in other post.
Java Code, MainActivity
MyViewModel model;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
model = new ViewModelProvider(this).get(MyViewModel.class);
model.getUsers().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer users) {
Log.d("zzzz","updated value..")
}
});
}
Model
public class MyViewModel extends ViewModel {
private MutableLiveData<Integer> users;
public LiveData<Integer> getUsers() {
if (users == null) {
users = new MutableLiveData<Integer>();
users.setValue(10);
}
return users;
}
}
If you don't want to recreate view model declare your view model like this
private val model: MyViewModel by activityViewModels()
for more details refer ViewModel
I think the issue lies in your kotlin viewmodel class, if you are not getting the value(unless you have few more issues in other classes)
Fix your kotlin viewmodel class in which data is not set in MutableLiveData, you forgot to add a piece of code.
//Here it is like this
objectTypeModel.value= MyRepository.getModelFromService()
AFAIK onCreate() only gets called when activity is created. So its natural if your viewmodel is getting created again. You can also check it by init{} method in your viewmodel class.
Still if you are not satisfied move your api call from activity's onCreate() method to viewmodels init{} method and just observe the changes from Activity. Your getMutableLiveDataModel() will called once when viewmodel object gets created.
If your java viewmodel example is running as you expected. Then,try to convert the java class to kotlin and run it again(just paste the java code to a kotlin file, it will ask you to convert it), it should work.
I've tried the same concept and as expected, the functionality in Java and Kotlin is identical. In the LogCat, I expected that the log should be printed on every rotation and it does. Now, let me tell you why it happens.
So, as per the documentation ViewModel instance stays alive after the configuration change. Basically, ViewModel uses the same instance if your activity is re-creating numerous times but it's not getting destroyed (calling finish()). But it's not the magic of the ViewModel it's the magic of LiveData.
LiveData is an observable data view holder so it sends the latest preserved value to the active observers on every configuration change which you're observing in the onCreate().
Let me present you my code.
Java
// Activity
public class JavaActivity extends AppCompatActivity {
private static final String TAG = "JavaActivity";
private JavaViewModel javaViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_java);
// Ignore this listener
findViewById(R.id.go_to_kotlin_activity).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
finish();
}
});
// Main
javaViewModel = new ViewModelProvider(this).get(JavaViewModel.class);
javaViewModel.getJavaLiveData().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer integer) {
Log.d(TAG, "onChanged: " + integer);
}
});
}
}
// ViewModel
public class JavaViewModel extends ViewModel {
private MutableLiveData<Integer> javaLiveData;
public LiveData<Integer> getJavaLiveData() {
if(javaLiveData == null) {
javaLiveData = new MutableLiveData<>();
javaLiveData.setValue(10);
}
return javaLiveData;
}
}
Kotlin
// Activity
class KotlinActivity : AppCompatActivity() {
companion object {
private const val TAG = "KotlinActivity"
}
private lateinit var kotlinViewModel: KotlinViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_kotlin)
// Ignore this listener
findViewById<Button>(R.id.go_to_java_activity_btn).setOnClickListener {
startActivity(Intent(this, JavaActivity::class.java))
}
// Main
kotlinViewModel = ViewModelProvider(this).get(KotlinViewModel::class.java)
kotlinViewModel.getKotlinLiveData().observe(this, Observer {
Log.d(TAG, "onCreate: $it")
})
}
}
// ViewModel
class KotlinViewModel : ViewModel() {
private lateinit var kotlinLiveData: MutableLiveData<Int>
fun getKotlinLiveData(): LiveData<Int> {
if (!::kotlinLiveData.isInitialized) {
kotlinLiveData = MutableLiveData()
kotlinLiveData.value = 10
}
return kotlinLiveData
}
}
If you have any follow-up questions, leave them in comments.
Thanks!
References
LiveData - Official Documentation
ViewModel - Official Documentation
This is a great article on how ViewModel works internally.
Do read this article as well
Try
MainActivity
lateinit var viewModel : MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.objectTypeModel.observe(this, Observer {
Log.d("zzzz","lamda executes onChanged method -> "+ it.otherValues).
//element from model
})
}
ViewModel
class MyViewModel : ViewModel() {
val objectTypeModel= MutableLiveData<MyTestModel>()
init {
objectTypeModel.value = MyRepository.getModelFromService()
}
}

unable to create ViewModel - my_package.App cannot be cast to android.arch.lifecycle.LifecycleOwner

I'm trying to create a ViewModel in MainActivity, which observes to some data changes in some singleton component. The goal is to use that ViewModel in a few fragments of this activity. But so far even without involving the fragment yet it doesn't work. The app gets stuck at launch, printing :
java.lang.RuntimeException: Unable to start activity ComponentInfo{my_package.MainActivity}: java.lang.RuntimeException: Cannot create an instance of class my_package.MyViewModel
my_package.App cannot be cast to android.arch.lifecycle.LifecycleOwner
the problem seems to be in the line : MyCustomSingletonComponent.getInstance().getSomeDataLiveData().observe......
The Code :
public class MyCustomSingletonComponent
{
public MutableLiveData<CustomClass> someData = new MutableLiveData<>();
private static final MyCustomSingletonComponent instance = new MyCustomSingletonComponent();
private MyCustomSingletonComponent() {
someData = new MutableLiveData<>();
}
public static MyCustomSingletonComponent getInstance() {
return instance;
}
public LiveData<CustomClass> getDataLiveData()
{
return someData;
}
}
public class MyViewModel extends AndroidViewModel {
public MyViewModel(#NonNull Application application)
{
super(application);
MyCustomSingletonComponent.getInstance().getSomeDataLiveData().observe(getApplication(), myData -> {
...
});
}
}
public class MainActivity extends AppCompatActivity
{
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
....
}
}
First, an Application is not a LifecycleOwner, so you cannot pass it to observe() on a LiveData. Activities and fragments are standard lifecycle owners.
Second, IMHO, a ViewModel should not be observing anything. The thing that uses the ViewModel does the observing. MyViewModel might hold onto the LiveData, but then MainActivity is the one that observes.

Should I make AsyncTask member of LiveData or Repository class - As replacement of Loader

LiveData/ ViewModel is a good replacement for complicated Loader.
Based on https://medium.com/google-developers/lifecycle-aware-data-loading-with-android-architecture-components-f95484159de4 ,
AsyncTask is member of LiveData.
public class JsonLiveData extends LiveData<List<String>> {
public JsonLiveData(Context context) {
loadData();
}
private void loadData() {
new AsyncTask<Void,Void,List<String>>() {
}.execute();
}
}
However, based on the presentation from Lyla Fujiwara:
Should I make AsyncTask member of Repository class?
You should avoid running your AsyncTask in LiveData. LiveData should really only be concerned with the observation of data. Not the act of changing the data.
The best way of dealing with this situation is to use the ViewModel / Repository pattern.
Activity / Fragment observes LiveData from ViewModel, ViewModel observes LiveData from Repository. Changes are made in the repository, which are pushed to its LiveData. Those changes are delivered to the Activity / Fragment (through the ViewModel).
I would avoid using AsyncTask in this situation. The bonus of AsyncTask is that you can get results on the UI thread after doing work. In this case though, that isn't necessary. LiveData will do that for you.
Here is an (untested) example:
Activity
public class MyActivity extends AppCompatActivity {
private MyViewModel viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set up your view model
viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
// Observe the view model
viewModel.getMyLiveData().observe(this, s -> {
// You work with the data provided through the view model here.
// You should only really be delivering UI updates at this point. Updating
// a RecyclerView for example.
Log.v("LIVEDATA", "The livedata changed: "+s);
});
// This will start the off-the-UI-thread work that we want to perform.
MyRepository.getInstance().doSomeStuff();
}
}
ViewModel
public class MyViewModel extends AndroidViewModel {
#NonNull
private MyRepository repo = MyRepository.getInstance();
#NonNull
private LiveData<String> myLiveData;
public MyViewModel(#NonNull Application application) {
super(application);
// The local live data needs to reference the repository live data
myLiveData = repo.getMyLiveData();
}
#NonNull
public LiveData<String> getMyLiveData() {
return myLiveData;
}
}
Repository
public class MyRepository {
private static MyRepository instance;
// Note the use of MutableLiveData, this allows changes to be made
#NonNull
private MutableLiveData<String> myLiveData = new MutableLiveData<>();
public static MyRepository getInstance() {
if(instance == null) {
synchronized (MyRepository.class) {
if(instance == null) {
instance = new MyRepository();
}
}
}
return instance;
}
// The getter upcasts to LiveData, this ensures that only the repository can cause a change
#NonNull
public LiveData<String> getMyLiveData() {
return myLiveData;
}
// This method runs some work for 3 seconds. It then posts a status update to the live data.
// This would effectively be the "doInBackground" method from AsyncTask.
public void doSomeStuff() {
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException ignored) {
}
myLiveData.postValue("Updated time: "+System.currentTimeMillis());
}).start();
}
}

Avoid activity leak using scope dagger 2

First to start with the my project architecture, I am using MVP and Dagger 2 for dependency injection.
I have been exploring scopes in dagger and my question is to better understand scope in context with Activity.
I have an activity(view) leak through presenter despite using activity scope.
As I am new to dagger and I feel I am missing something.
I am assuming that scope should handle my view to null when activity is destroyed(though right now do not understand how it will).Is my assumption right? if yes what I am doing wrong, else is it possible to avoid view leak using dagger? I know about the detachView approach, just curious if we can achieve the same thing using dagger 2.
P.S: I came to know about the leak through leakCanary.
Following is my code
LoginActivity.class
public class LoginActivity extends BaseActivity implements LoginContract.View {
private static final String TAG = "LoginActivity";
#Inject
LoginPresenter presenter;
private LoginComponent loginComponent;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
createComponent();
initViews();
}
private void createComponent() {
loginComponent = ((MyApplication) getApplication()).getRepositoryComponent()
.COMPONENT(new LoginPresenterModule(this));
loginComponent.inject(this);
}
#Override
protected void onDestroy() {
super.onDestroy();
loginComponent = null;
}
LoginPresenterModule.class
#Module
public class LoginPresenterModule {
private final LoginContract.View view;
public LoginPresenterModule(LoginContract.View view) {
this.view = view;
}
#Provides
#ActivityScoped
public LoginContract.View providesView(){
return view;
}
}
LoginComponent.class
#ActivityScoped
#Subcomponent(modules = LoginPresenterModule.class)
public interface LoginComponent {
void inject(LoginActivity loginActivity);
}
LoginPresenter.class
#ActivityScoped
public class LoginPresenter implements LoginContract.Presenter {
private static final String TAG = "LoginPresenter";
private LoginContract.View view;
private DataRespository dataRespository;
#Inject
LoginPresenter(LoginContract.View view, DataRespository dataRespository) {
this.view = view;
this.dataRespository = dataRespository;
}
#Override
public void initTest(String testNo) {
view.showProgressIndicator();
dataRespository.sendTest(testNo, new DataSource.onResponseCallback<Void>() {
#Override
public void onSuccess(Void obj) {
Log.d(TAG, "onSuccess: ");
}
#Override
public void onError(#NotNull ErrorWrapper error) {
Log.d(TAG, "onError: ");
}
});
}
#Override
public void start() {
}
}
DataRespositoryComponent.class
#ApplicationScoped
#Component(dependencies = ApplicationComponent.class,modules =
DataRespositoryModule.class)
public interface DataRepositoryComponent {
LoginComponent COMPONENT(LoginPresenterModule loginPresenterModule);
}
Basically, view is leaked while making network call.
My leakcanary stack:
This activity leaking has nothing to do with Dagger nor can Dagger help to prevent it.
The problem here lies with dataRespository.sendTest(..anonymousCallback..) where you add a callback to receive a result.
Anonymous classes as well as non-static inner classes will keep a reference to their enclosing object. In your case the callback keeps a reference to the presenter, which keeps a reference to the view, which keeps a reference to the Activity (this is what LeakCanary shows).
Since the callback is alive until it receives a response or an error, if your Activity were to be destroyed before your callback finishes, it will leak.
To fix your issue you need to either stop or unregister your callbacks, or remove the reference to the Activity. In this case it would probably be enough to set view = null in your presenter when the Activity gets destroyed to prevent the Activity from leaking.
Just make sure to check if the view is null before accessing it in your callback.

Cant inject classes using Dagger on Android

I am beggining with Dagger, I am using 1.2 version of it, and I have the following scenario:
Module:
#Module(injects = {
AuthenticationService.class
})
public class ServiceModule {
#Provides
AuthenticationService provideAuthenticationService() {
return ServiceFactory.buildService(AuthenticationService.class);
}
}
On my Application class I create the ObjectGraph:
public class FoxyRastreabilidadeApplication extends Application {
private static FoxyRastreabilidadeApplication singleton;
#Override
public void onCreate() {
super.onCreate();
createObjectGraph();
singleton = this;
}
private void createObjectGraph() {
ObjectGraph.create(ServiceModule.class);
}
}
and finally, at my LoginActivity, I try to inject my AuthenticationService:
public class LoginActivity extends Activity implements LoaderCallbacks<Cursor> {
private UserLoginTask mAuthTask = null;
#Inject
AuthenticationService authenticationService;
}
At this point, when I try to access my AuthenticationService instance it is always null, meaning it wasnt injected at all, I debugged my provider method to be sure of it, so, the question is, am I missing something? If so, what is it?
You need to hold on to the ObjectGraph and ask it to inject your objects directly.
Add an instance variable to your custom Application subclass to hold on to the created ObjectGraph:
this.objectGraph = ObjectGraph.create(ServiceModule.class);
Add a convenience method to your Application to do injection:
public void inject(Object object) {
this.objectGraph.inject(object);
}
Call that method wherever you need injection to happen, for example in your Activity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((MyApplication)getApplication()).inject(this);
...
}

Categories

Resources