How to send data between activities using ViewModel - android

I used the same process for sending data between fragments and it works but now I'm not getting data in Receiver Activity. Even the Log message Tag is not showing as I click on submit button. I checked in Sender Activity Log message and it is showing data but can't get those data in Receiver Activity.
Please help me to get data. Thank you!!
ViewModel Class:
public class ViewModelClass extends ViewModel {
private final MutableLiveData message = new MutableLiveData();
public void setMessage(HomeModelClass data){
message.setValue(data);
}
public MutableLiveData getMessage() {
return message;
}
}
Sender Activity:
public class EditHomeData extends AppCompatActivity {
private ViewModelClass viewModelClass;
HomeModelClass homeModelClassData = new HomeModelClass();
#Override
protected void onCreate(Bundle savedInsatancestate) {
super.onCreate(savedInsatancestate);
setContentView(R.layout.first_page);
viewModelClass = ViewModelProviders.of(this).get(ViewModelClass.class);
setValues();
});
public void setValues() {
if (yes.isChecked()) {
rent_value = String.valueOf(1);
} else if (no.isChecked()) {
rent_value = String.valueOf(0);
}
homeModelClassData.setWard_id(ward_id + "");
homeModelClassData.setToleName(tole_name.getText().toString());
homeModelClassData.setHouseAge(house_age.getText().toString());
homeModelClassData.setRadio(rent_value);
homeModelClassData.setTotal_tenant(editText1.getText().toString());
homeModelClassData.setMale_tenant(editText2.getText().toString());
homeModelClassData.setFemale_tenant(editText3.getText().toString());
homeModelClassData.setHouse_stroyes(spi1);
homeModelClassData.setRoof_types(spi2);
homeModelClassData.setLatitude(lati.getText().toString());
homeModelClassData.setLongitude(longi.getText().toString());
viewModelClass.setMessage(homeModelClassData);
}
Receiver Activity:
public class EditHomeData3 extends AppCompatActivity {
Button submit, cancel;
String ward_id, houseNumber, toleName, house_age, radio, total_tenant, male_tenant, female_tenant, house_stroyes,
roof_types, latitude, longitude, value_updateby;
#Override
protected void onCreate(Bundle savedInsatancestate) {
super.onCreate(savedInsatancestate);
setContentView(R.layout.third_page);
submit = findViewById(R.id.submit_btn);
submit.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
getDatafromField();
}
});
private void getDatafromField() {
final ViewModelClass model = ViewModelProviders.of(this).get(ViewModelClass.class);
model.getMessage().observe(this, new Observer() {
#Override
public void onChanged(#Nullable Object o) {
if (o instanceof HomeModelClass) {
HomeModelClass homedata = (HomeModelClass) o;
ward_id = homedata.getWard_id();
houseNumber = homedata.getHouseNumber();
toleName = homedata.getToleName();
house_age = homedata.getHouseAge();
radio = homedata.getRadio();
total_tenant = homedata.getTotal_tenant();
male_tenant = homedata.getMale_tenant();
female_tenant = homedata.getFemale_tenant();
house_stroyes = homedata.getHouse_stroyes();
roof_types = homedata.getRoof_types();
latitude = homedata.getLatitude();
longitude = homedata.getLongitude();
value_updateby = String.valueOf("1");
Log.i("GetMessage", houseNumber +"");
}
}
});
}

ViewModels are not shared across Activities - since you pass a different object to ViewModelProviders.of(), you'll get different ViewModel instances.
This was specifically called out in the Single Activity: Why, When, and How talk as a reason to prefer a single Activity architecture in your app.

Yes Indeed, ViewModel are not shared across activities,So either you create different viewmodel for different activies or you could use different fragment with same viewmodel. Because in fragment you can achieved using SharedViewModel

Related

Recyclerview data disappears when device is rotated

Even though I am using ViewModel, whenever the device is rotated, the data in the Recyclerview disappears. I had to put the makeSearch() method inside the onClick() method because I need to get the text that the button grabs and use it as the search parameter. Is there a better way I can handle this to avoid this problem? My code is right here:
SearchActivity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
// What happens when the search button is clicked
materialButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (Objects.requireNonNull(textInputEditText.getText()).toString().isEmpty()) {
textInputEditText.setError("Type a search query");
} else {
mSearchInput = Objects.requireNonNull(textInputEditText.getText()).toString();
textInputEditText.setText("");
makeSearch();
}
}
});
}
// Gets the ViewModel, Observes the Question LiveData and delivers it to the Recyclerview
private void makeSearch() {
final SearchAdapter searchAdapter = new SearchAdapter();
SearchViewModel mSearchViewModel = new ViewModelProvider(this,
new CustomSearchViewModelFactory(new SearchRepository())).get(SearchViewModel.class);
mSearchViewModel.setQuery(mSearchInput);
mSearchViewModel.getQuestionLiveData().observe(this, new Observer<List<Question>>() {
#Override
public void onChanged(List<Question> questions) {
mQuestions = questions;
searchAdapter.setQuestions(questions);
}
});
mRecyclerView.setAdapter(searchAdapter);
searchAdapter.setOnClickListener(mOnClickListener);
}
SearchViewModel:
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<>();
private LiveData<List<Question>> mQuestionLiveData = Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
SearchViewModel(SearchRepository searchRepository) {
this.mSearchRepository = searchRepository;
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
}
SearchRepository:
public class SearchRepository {
//private String inTitle;
private MutableLiveData<List<Question>> mQuestions = new MutableLiveData<>();
public SearchRepository() {
//getQuestionsWithTextInTitle();
}
private void getQuestionsWithTextInTitle(String inTitle) {
ApiService apiService = RestApiClient.getApiService(ApiService.class);
Call<QuestionsResponse> call = apiService.getQuestionsWithTextInTitle(inTitle);
call.enqueue(new Callback<QuestionsResponse>() {
#Override
public void onResponse(Call<QuestionsResponse> call, Response<QuestionsResponse> response) {
QuestionsResponse questionsResponse = response.body();
if (questionsResponse != null) {
mQuestions.postValue(questionsResponse.getItems());
//shouldShowData = true;
} else {
Log.d("SearchRepository", "No matching question");
//shouldShowData = false;
}
}
#Override
public void onFailure(Call<QuestionsResponse> call, Throwable t) {
//shouldShowData = false;
t.printStackTrace();
}
});
}
public LiveData<List<Question>> getQuestions(String inTitle) {
getQuestionsWithTextInTitle(inTitle);
return mQuestions;
}
}
Your approach of passing the search input in through your CustomSearchViewModelFactory and into the constructor for the ViewModel and into the constructor for your SearchRepository isn't going to work in any case. While the first time you search your CustomSearchViewModelFactory creates the ViewModel, the second time you hit search, your SearchViewModel is already created and your factory is not invoked a second time, meaning you never get the second query.
Instead, you should file the ViewModel Overview documentation, and use Transformations.switchMap() to convert your input (the search string) into a new LiveData<List<Question>> for that given query.
This means that your ViewModel would look something like
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<String>();
private LiveData<List<Question>> mQuestionLiveData =
Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
public SearchViewModel() {
mSearchRepository = new SearchRepository();
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
}
You'd then update your Activity to:
Always observe the getQuestionLiveData() (note that you won't get a callback to your Observer until you actually set the first query)
Call setQuery() on your SearchViewModel in your makeSearch()
Remove your CustomSearchViewModelFactory entirely (it would no longer be needed).

Loading data in ViewModel that have been retrieved in SplashActvity

I'm new with the ViewModel and I understand that it's a powerful and easy way to communicate with fragments.
My problem is the following : How to load the data retrieved in the SplashActivity in the ViewModel of the mainActivity ?
My app achitecture is the following :
SplashActivity : retrieve data with retrofit and store it into a List
Main Activity : contains two fragments displaying the data in different ways
Here is a piece of code showing my implementation.
SplashActivity
public class SplashActivity extends AppCompatActivity {
private final String TAG = "TAG.SplashActivity";
public static List<Toilet> toiletList = new ArrayList<>(); // HERE IS THE DATA I WANT TO
RETRIEVE IN THE MAIN ACTIVITY
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*Create handle for the RetrofitInstance interface*/
GetDataService service = ...;
// MY STUFF RETROFIT including
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra("toiletList", (Serializable) toiletList);
startActivity(intent);
finish();
}
}
MainActivity
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
private final String TAG = getClass().getName();
private List<Toilet> toiletList = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent= getIntent();
Serializable s = intent.getSerializableExtra("toiletList");
// Check type and cast
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
// SETTING UP FRAGMENTS
}
}
FragmentExample
public class MainFragment extends Fragment {
public static List<Toilet> toiletArrayList = new ArrayList<>();
private final String TAG = this.getClass().getName();
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
// SETTING UP UI
return view;
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ToiletListViewModel model = ViewModelProviders.of(this).get(ToiletListViewModel.class);
model.getToiletList().observe(this, new Observer<List<Toilet>>() {
#Override
public void onChanged(#Nullable List<Toilet> toilets) {
// update UI
}
});
}
}
ToiletListViewModel
public class ToiletListViewModel extends ViewModel {
private final String TAG = getClass().getName();
private MutableLiveData<List<Toilet>> toiletList;
public LiveData<List<Toilet>> getToiletList() {
if (toiletList == null) {
toiletList = new MutableLiveData<>();
loadToilets();
}
return toiletList;
}
private void loadToilets() {
// asynchronously fetch toilets
// HERE IS MY PROBLEM : How to access the toiletList retrieved
in the SplashActivity ?
toiletList.setValue(SplashActivity.toiletList);
}
#Override
protected void onCleared() {
super.onCleared();
Log.d(TAG, "onCleared() called");
}
}
I hope that's clear. If you want any further info, fell free to ask !
Best
You can share your ToiletListViewModel between the MainActivity and its Fragments.
So what you need is to provide your ViewModel with MainActivity scope (It means you bound the lifecycle of your ViewModel to your Activity) and call initToilets then child fragments can easily retrieve this ViewModel and observe on its LiveData.
ToiletListViewModel:
public class ToiletListViewModel extends ViewModel {
private MutableLiveData<List<Toilet>> toiletList = new MutableLiveData();
public LiveData<List<Toilet>> getToiletList() {
return toiletList;
}
private void initToilets(List<Toilet> toilets) {
toiletList.setValue(toilets);
}
}
MainActivity:
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
private final String TAG = getClass().getName();
private List<Toilet> toiletList = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent= getIntent();
Serializable s = intent.getSerializableExtra("toiletList");
// Check type and cast
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
ToiletListViewModel vm = ViewModelProviders.of(this).get(ToiletListViewModel.class);
vm.initToilets(toiletList);
// SETTING UP FRAGMENTS
}
}
So, when setValue is called, Fragments that listen to the toiletList live data will be notified.
Note:
You can create a shared ViewModel without providing it on MainActivity, instead of calling
ViewModelProviders.of(this).get(ToiletListViewModel.class);
in your Fragment do
ViewModelProviders.of(getActivity()).get(ToiletListViewModel.class);
In order to get use out of the a view model, you need to store a reference to it's instance in your activities and then interface with them to modify data.
I would first of all suggest to you that you read the developer guide on View Model.
When you are set-up and storing a reference to the model in your activities and fragments, you could add a method to the model, like setToilets(List<Toilet>), which updates the toilets in the View Model, calls loadToilets() or stores the raw toilets so loadToilets() can later access it and now what toilets to load.
Then you can access all the data that you want to expose from other classes by writing the respective methods, just like you did with the getToiletList(LiveData<Toilet>) -method.
There are two suggestions:
You can add data to list directly (Off Topic):
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
use this instead of:
if (s instanceof List<?>) {
toiletList.addAll((List<Toilet>)s);
}
Back to main topic:
You can take ViewModel instance of Activity instead of this in Fragment. How?
Take ViewModel in activity as below,
ToiletListViewModel model = ViewModelProviders.of(this).get(ToiletListViewModel.class);
& for Fragment share it like this,
ToiletListViewModel model = ViewModelProviders.of(getActivity()).get(ToiletListViewModel.class);
This will share your ViewModel between fragments inside of activity & observe your livedata.

Room: Cannot access database on the main thread since it may potentially lock the UI for a long period of time

In the main activity, I have LiveData which contains members and a click listener. If I click on a member, then his ID is passed with intent.putExtra. That ID is later passed on to the method open in this activity. With this activity, I want to see the details of a member. In my MemberInfo activity, I marked a line where my problem lies.
It shows me this error: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
My DAO consists this code:
#Query("SELECT * FROM member_table WHERE MemberID=:id")
Member getMemberInfo(long id);
This is my main activity:
public class MemberMainActivity extends AppCompatActivity implements MemberListAdapter.MemberClickListener{
private MemberViewModel mMemberViewModel;
private List<Member> mMember;
void setMember(List<Member> members) {
mMember = members;
}
public static final int NEW_MEMBER_ACTIVITY_REQUEST_CODE = 1;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_member);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent(MemberMainActivity.this, NewMemberActivity.class);
startActivityForResult(intent, NEW_MEMBER_ACTIVITY_REQUEST_CODE);
}
});
RecyclerView recyclerView = findViewById(R.id.recyclerviewcard_member);
final MemberListAdapter adapter = new MemberListAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
mMemberViewModel = ViewModelProviders.of(this).get(MemberViewModel.class);
mMemberViewModel.getAllMember().observe(this, new Observer<List<Member>>() {
#Override
public void onChanged(#Nullable final List<Member> members) {
mMember = members;
// Update the cached copy of the words in the adapter.
adapter.setMember(members);
}
});
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == NEW_MEMBER_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
Member member = new Member(data.getStringExtra(NewMemberActivity.EXTRA_REPLY), data.getStringExtra(NewMemberActivity.EXTRA_REPLY2));
mMemberViewModel.insert(member);
} else {
Toast.makeText(
getApplicationContext(),
R.string.empty_not_saved,
Toast.LENGTH_LONG).show();
}
}
public void onMemberClick(int position) {
Member member = mMember.get(position);
Intent intent = new Intent(getApplicationContext(),MemberInfo.class);
intent.putExtra("MemberID", member.getId());
MemberInfo.open(this, member.getId());
}
}
This is my activity:
public class MemberInfo extends AppCompatActivity {
public static void open(Activity activity, long memberid) {
Intent intent = new Intent(activity, MemberInfo.class);
intent.putExtra("MemberID", memberid);
activity.startActivity(intent);
}
private List<Member> mMember;
private MemberViewModel mMemberViewModel;
void setMember(List<Member> members){
mMember = members;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memberinfo);
Log.i("okay", "memberinfo");
Intent intent = getIntent();
if (intent != null && intent.hasExtra("MemberID")) {
long memberid = intent.getLongExtra("MemberID", -1);
// TODO: get customer details based on customer id
TextView firstname = findViewById(R.id.layout_memberfirstname);
TextView surname = findViewById(R.id.layout_membersurname);
TextView balance = findViewById(R.id.layout_memberbalance);
-------------Member member = MemberRoomDatabase.getDatabase().memberDao().getMemberInfo(memberid);-------------
firstname.setText(member.getFirstname());
surname.setText(member.getSurname());
}
else {
Toast.makeText(
getApplicationContext(),
R.string.empty_not_saved,
Toast.LENGTH_LONG).show();
}
}
}
I thought that maybe it is because I'm missing a AsyncTask method. I tried this, but this also didn't work:
private static class insertMemberInfoAsyncTask extends AsyncTask<Member, Void, Void> {
private MemberDao mAsyncTaskDao;
insertMemberInfoAsyncTask(MemberDao dao) {
mAsyncTaskDao = dao;
}
#Override
protected Void doInBackground(Member... params) {
Member member = params[0];
mAsyncTaskDao.getMemberInfo(member.getId());
return null;
}
}
public Member getMemberInfo(long id) {
mAllMember = mMemberDao.getAllMember();
Member member = mMemberDao.getMemberInfo(id);
new insertMemberInfoAsyncTask(mMemberDao).execute(member);
return member;
}
I think I use the method wrong. Can anybody help me?
One option is to update your query to this:
#Query("SELECT * FROM member_table WHERE MemberID=:id")
LiveData<Member> getMemberInfo(long id);
(or similar, using Flowable). This avoids the need to manually create your own AsyncTask.
Returning the LiveData wrapper around the Member type automatically signals to Room that the query can/should be performed asynchronously. Per https://developer.android.com/training/data-storage/room/accessing-data (my emphasis):
Note: Room doesn't support database access on the main thread unless you've called allowMainThreadQueries() on the builder because it might lock the UI for a long period of time. Asynchronous queries—queries that return instances of LiveData or Flowable—are exempt from this rule because they asynchronously run the query on a background thread when needed.
You can use Future and Callable. So you would not be required to write a long asynctask and can perform your queries without adding allowMainThreadQueries() or using LiveData.
My dao query:-
#Query("SELECT * from user_data_table where SNO = 1")
UserData getDefaultData();
My repository method:-
public UserData getDefaultData() throws ExecutionException, InterruptedException {
Callable<UserData> callable = new Callable<UserData>() {
#Override
public UserData call() throws Exception {
return userDao.getDefaultData();
}
};
Future<UserData> future = Executors.newSingleThreadExecutor().submit(callable);
return future.get();
}
In my case, it works if you add Dispatcher.IO when you use coroutines:
viewModelScope.launch(Dispatchers.IO) {
//your database call
}
For me allowMainThreadQueries() works.
This allows room to support database access on the main thread.
See the following code
#Database(entities = [Word::class ],version = 1)
abstract class VocabularyDatabase:RoomDatabase() {
companion object {
private lateinit var INSTANCE:VocabularyDatabase
fun getInstance(context:Context):VocabularyDatabase= Room.databaseBuilder(
context,
VocabularyDatabase::class.java,
"vocabulary"
)
.createFromAsset("vocabulary.db")
.allowMainThreadQueries()
.build()
}
abstract fun dao():WordDao
}
Using Future and Callables can be an alternative here. By using Future and Callable you can get rid of AsyncTask and forcing your queries to the main thread.
The syntax would be as follow -
#Throws(ExecutionException::class, InterruptedException::class)
private fun canContinue(id: String): UserData{
val callable = Callable { userDao.getDefaultData() }
val future = Executors.newSingleThreadExecutor().submit(callable)
return future!!.get()
}
And, don't forget the null check for the data returned. Because it might be null

Android ViewModel LiveData update view on button click

I am following this tutorial to learn ViewModel and LiveData. In my case, instead of getting data from network, I am simply generating random string on button click and trying to update a textview. The problem is that the textview does not get updated when the data is changed by button click, but only gets updated when orientation is toggled.
Activity Class (extends LifecycleActivity)
public class PScreen extends BaseActivity {
#Override protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.places_screen);
final UserModel viewModel = ViewModelProviders.of(this).get(UserModel.class);
viewModel.init();
viewModel.getUser().observe(this, new Observer<User>() {
#Override public void onChanged(#Nullable User user) {
((TextView) findViewById(R.id.name)).setText(user.getName());
}
});
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View v) {
final MutableLiveData<User> data = new MutableLiveData<>();
User user = new User();
user.setName(String.valueOf(Math.random() * 1000));
data.postValue(user);
viewModel.setUser(data); // Why it does not call observe()
}
});
}
}
ViewModel Class
package timsina.prabin.tripoptimizer.model;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.ViewModel;
public class UserModel extends ViewModel {
private LiveData<User> user;
public void init() {
if (this.getUser() != null) {
return;
}
this.user = new LiveData<User>() {
#Override protected void setValue(User value) {
value.setName("Fresh New Name");
super.setValue(value);
}
};
}
public LiveData<User> getUser() {
return user;
}
public void setUser(LiveData<User> user) {
this.user = user;
}
}
You are creating a new LiveData instance each time! You are not supposed to do that. If you do that all previous observers will be ignored.
In this case you could replace your setUSer(LiveData<User>) method on your ViewModel to setUser(User u) (taking a User instead of a LiveData) and then do user.setValue(u) inside it.
Of course, will have to initialize the LiveData member in your ViewModel class, like this:
final private LiveData<User> user = new MutableLiveData<>();
It will work then because it will notify the existing observers.
I was somehow able to resolve this by using MutableLiveData instead of LiveData.
Model class
private MutableLiveData<User> user2;
public void init() {
if (user2 == null) {
user2 = new MutableLiveData<>();
}
}
public MutableLiveData<User> getUser2() {
return user2;
}
public void setUser2(final User user) {
user2.postValue(user);
}
Activity
viewModel.getUser2().observe(this, new Observer<User>() {
#Override public void onChanged(#Nullable User user) {
((TextView) findViewById(R.id.name)).setText(user.getName());
}
});
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View v) {
User user = new User();
viewModel.getUser().postValue(user);
}
});
You replace the reference to the object inside UserModel, try to swap the lines of code
data.postValue(user);
viewModel.setUser(data); // Why it does not call observe()
replace on
viewModel.setUser(data); // Why it does not call observe()
data.postValue(user);
Try to modify your code as #niqueco mentioned, set your updated method inside setUser() method and change your onclick() listener in the activity to send the new user data info only. Other works the LiveData will help u.
public class UserModel extends ViewModel {
private LiveData<User> user;
public void init() {
if (this.getUser() != null) {
return;
}
this.user = new LiveData<User>() {
#Override protected void setValue(User value) {
value.setName("Fresh New Name");
super.setValue(value);
}
};
}
public LiveData<User> getUser() {
return user;
}
public void setUser(LiveData<User> user) {
this.user.setValue(user); //the live data will help u push data
}
}
Activity Class
public class PScreen extends BaseActivity {
#Override protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.places_screen);
final UserModel viewModel = ViewModelProviders.of(this).get(UserModel.class);
viewModel.init();
viewModel.getUser().observe(this, new Observer<User>() {
#Override public void onChanged(#Nullable User user) {
((TextView) findViewById(R.id.name)).setText(user.getName());
}
});
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View v) {
//final MutableLiveData<User> data = new MutableLiveData<>();
User user = new User();
user.setName(String.valueOf(Math.random() * 1000));
//data.postValue(user);
viewModel.setUser(user); // Why it does not call observe()
}
});
}
}

Get data from JSON API in my MainActivity from a fragment

My project use MVP architecture and RxJava to get data from a remote JSON Api.
I have a MainActivity, it has 2 roles. The first one is to be a fragment container, the second one is to get data from the JSON api and transmit it to my fragment (I only have one fragment for now but will have another one later using the same data).
For now, I'm getting the data in my MainActivity. I'm trying to get the data from my fragment by calling a method in my MainActivity (using an interface for decoupling).
The problem is the data in my fragment is always empty, I suppose it's because my activity inflate my fragment so fast that when my fragment calls my activity method to get the data this data is still empty since the request didn't receive the answer yet and this request is called asynchronously using RxJava.
So I want to wait for the data being loaded to open my fragment,or open my fragment and wait the data being loaded in the activity before get it (showing a visual progress to the user). The problem is not really how to do this but when and where. Thank you for your help.
I moved my loadData() method and the transaction to open my fragment several times in different positions in the lifecycle, nothing worked. For now everything is in in MainActivity.onStart() :
#Override
protected void onStart() {
super.onStart();
presenter.setView(this);
// Load data from JSON API
presenter.loadData(city, authToken);
// Load fragments
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
fm.beginTransaction()
.add(R.id.ll_container, fragment)
.commit();
}
}
The data is retrieve in the loadData() method of my presenter :
public class MainPresenter implements MainActivityMVP.Presenter {
final static String TAG = MainPresenter.class.getCanonicalName();
private MainActivityMVP.View view;
private MainActivityMVP.Model model;
private Subscription subscription = null;
public MainPresenter(MainActivityMVP.Model model) { this.model = model; }
#Override
public void loadData(String city, String authToken) {
subscription = model.result(city, authToken)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Aqicn>() {
#Override
public void onCompleted() {
Log.i(TAG,"completed");
}
#Override
public void onError(Throwable e) {
e.printStackTrace();
}
#Override
public void onNext(Aqicn aqicn) {
Data data = aqicn.getData();
Iaqi iaqi = data.getIaqi();
ViewModel viewModel = new ViewModel(data.getAqi(),
data.getDominentpol(),
iaqi.getCo().getV(),
iaqi.getH().getV(),
iaqi.getNo2().getV(),
iaqi.getO3().getV(),
iaqi.getP().getV(),
iaqi.getPm10().getV(),
iaqi.getPm25().getV(),
iaqi.getR().getV(),
iaqi.getSo2().getV(),
iaqi.getT().getV(),
iaqi.getW().getV());
Log.d(TAG,data.getCity().getName());
if (view != null) {
view.updateData(viewModel);
}
}
});
}
#Override
public void rxUnsubscribe() {
if (subscription != null) {
if (!subscription.isUnsubscribed()) {
subscription.unsubscribe();
}
}
}
#Override
public void setView(MainActivityMVP.View view) {
this.view = view;
}
}
When the response to the request is received the presenter call the updateData() method in MainActivity (see in my presenter code above). This is where I initialize the ArrayList pollutionLevels that is supposed to contain the data I try to get from my fragment :
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
pollutionLevels = viewModel.getAllPolluants();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
}
This is the method in my MainActivity called from my fragment to get data :
#Override
public ArrayList<PollutionLevel> getPollutionLevels() {
return pollutionLevels;
}
In my fragment I try to get the data in onAttach() but it's always empty :
public interface PollutionLevelsListener{
ArrayList<PollutionLevel> getPollutionLevels();
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
pollutionLevelsListener = (PollutionLevelsListener) context;
ArrayList<PollutionLevel> levels = pollutionLevelsListener.getPollutionLevels();
for(PollutionLevel l:levels) {
Log.d(TAG,l.getName());
}
} catch (ClassCastException castException){
castException.printStackTrace();
}
}
EDIT : add ViewModel.getAllPolluants() method
This is the method in my ViewModel that returns the ArrayList :
public ArrayList<PollutionLevel> getAllPolluants() {
ArrayList<PollutionLevel> allLevels = new ArrayList();
allLevels.add(new PollutionLevel("Co",Double.toString(co)));
allLevels.add(new PollutionLevel("H",Double.toString(h)));
allLevels.add(new PollutionLevel("No2",Double.toString(no2)));
allLevels.add(new PollutionLevel("o3",Double.toString(o3)));
allLevels.add(new PollutionLevel("p",Double.toString(p)));
allLevels.add(new PollutionLevel("o3",Double.toString(o3)));
allLevels.add(new PollutionLevel("pm10",Integer.toString(pm10)));
allLevels.add(new PollutionLevel("pm25",Integer.toString(pm25)));
allLevels.add(new PollutionLevel("r",Double.toString(r)));
allLevels.add(new PollutionLevel("so2",Double.toString(so2)));
allLevels.add(new PollutionLevel("t",Double.toString(t)));
allLevels.add(new PollutionLevel("w",Double.toString(w)));
return allLevels;
}
EDIT : Add new modified MainActivity class and PollutionLevelListener interface, trying to apply #cricket_007 answer
public class MainActivity extends AppCompatActivity implements MainActivityMVP.View, PollutionLevelsListener {
final static String TAG = MainActivity.class.getCanonicalName();
#BindString(R.string.city)
String city;
#BindString(R.string.aqicn_token)
String authToken;
#Inject
MainActivityMVP.Presenter presenter;
ArrayList<PollutionLevel> pollutionLevels;
PollutionLevelsListener pollutionListener;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
((App) getApplication()).getComponent().injectPollutionLevels(this);
}
#Override
public void updateData(ViewModel viewModel) {
pollutionLevels = viewModel.getAllPolluants();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
//===== NullPointerException
pollutionListener.onPollutionLevelsLoaded(pollutionLevels);
}
#Override
protected void onStart() {
super.onStart();
presenter.setView(this);
presenter.loadData(city, authToken);
}
#Override
public void onPollutionLevelsLoaded(List<PollutionLevel> levels) {
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
};
// Load fragments
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
fm.beginTransaction()
.add(R.id.ll_container, fragment)
.commit();
}
}
#Override
protected void onDestroy() {
super.onDestroy();
presenter.rxUnsubscribe();
}
}
Interface
public interface PollutionLevelsListener {
void onPollutionLevelsLoaded(List<PollutionLevel> levels);
}
#################### EDIT ########################
After a lot of doubt with what solution to adopt I follow the answer and recommendations of #yosriz. This is the code I ended with. Be aware that I still need to implement a cache management feature as for now the JSON resquest is made for both fragment.
As a result I have a common repository used by my both fragment. The MainActivity became only a fragment container, it doesn't get any data. it doesn't even have a MVP structure since I think It's now useless.
My both fragment (so my both features) get their data from PollutionLevelRepository :
public interface Repository {
Observable<Aqicn> getPollutionLevelsFromNetwork(String city, String authToken);
Observable<Aqicn> getPollutionLevels(String city, String authToken);
}
public class PollutionLevelsRepository implements Repository {
private PollutionApiService pollutionApiService;
private static Observable<Aqicn> pollutionData = null;
public PollutionLevelsRepository(PollutionApiService pollutionApiService) {
this.pollutionApiService = pollutionApiService;
}
#Override
public Observable<Aqicn> getPollutionLevelsFromNetwork(String city, String authToken) {
pollutionData = pollutionApiService.getPollutionObservable(city, authToken);
return pollutionData;
}
#Override
public Observable<Aqicn> getPollutionLevels(String city, String authToken) {
return getPollutionLevelsFromNetwork(city, authToken);
}
}
The Model of my first fragment (Donut feature) :
public class DonutModel implements DonutFragmentMVP.Model {
final static String TAG = DonutModel.class.getSimpleName();
private Repository repository;
public DonutModel(Repository repository) {
this.repository = repository;
}
#Override
public Observable<Aqicn> getPollutionLevels(String city, String authToken) {
Observable<Aqicn> aqicnObservable = repository.getPollutionLevels(city, authToken);
return aqicnObservable;
}
}
The Model of my second fragment (Pollution level feature) :
public class PollutionLevelsModel implements PollutionLevelsFragmentMVP.Model {
private Repository repository;
public PollutionLevelsModel(Repository repository) {
this.repository = repository;
}
#Override
public Observable<Aqicn> result(String city, String authToken) {
Observable<Aqicn> aqicnObservable = repository.getPollutionLevels(city, authToken);
return aqicnObservable;
}
}
Well, you probably have timing issue, the model.result is async IO operation that will update data on activity in async fashion when it will finish, while your fragment call to get the data is happening as soon as the fragment attached the activity (which is still async as you call fragment commit() and not commitNow()) but if you compare it to the probably network call of model.result it will be probably always faster.
Actually I think your approach is wrong, when you're using reactive fashion with Rx you should push the data, here at the end, you're pulling it at the fragment side from the Activity, while you don't know if this data is already available.
The data that is loaded from the presenter should update immediately the fragment, meaning either your Activity.updateData() will update the fragment, or more correct approach to my opinion is that the presenter will be tied to the fragment itself as this is the actual View it's updating, so the view.UpdateData() at the presenter will notify the fragment directly.
Did you tried to make an method inside the fragment and you can hit it once updateData(ViewModel viewModel) called ?
for example (try to add this method in you fragment):
public class YourFragmentName extends Fragment {
public YourFragmentName(StepsHandler stepsHandler){
this.stepsHandler = stepsHandler;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_supplier_registrtion_first, container, false);
return rootView;
}
public void dataLoaded() {
// Do what you need after data finish loading..
}
}
From your Activity :
public class MainActivity extends Activity implements StepsHandler {
YourFragmentName fragmentName;
//onCreate ()
fragmentName = new YourFragmentName(this);
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
pollutionLevels = viewModel.getAllPolluants();
fragmentName.dataLoaded();
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
}
}
}
I'm trying to get the data from my fragment by calling a method in my MainActivity
It seems your interface is only returning the field, which could very possibly be before the request has finished. Which you seem to understand...
didn't receive the answer yet and this request is called asynchronously using RxJava
I wouldn't suggest you wait, and instead do
open my fragment and wait the data being loaded in the activity before get it (showing a visual progress to the user).
However you want to implement that, you can try a new ProgressDialog() and show / hide that.
Your issue is that onAttach gets immediately called and the request is still going on indefinitely.
You need to "subscribe" for that data from the Fragment.
A "listener" is not typically written to implement a "getter", so let's rewrite that
public interface PollutionLevelsListener {
void onPollutionLevelsLoaded(List<PollutionLevel> levels);
}
Then, you can use that instead to start your Fragment rather than immediately when the Activity starts
// The Activity
class ... implements PollutionLevelsListener {
#Override
public void onPollutionLevelsLoaded(List<PollutionLevel> levels) {
for(PollutionLevel p : pollutionLevels) {
Log.d(TAG,p.getName());
};
// Moved this section here
// Load fragments
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment fragment = fm.findFragmentById(R.id.ll_container);
if (fragment == null) {
fragment = new PollutionLevelsFragment();
// If your object is Parcelable
/*
* Bundle args = new Bundle();
* args.putParcelableArrayList(levels);
* fragment.setArguments(args);
*/
ft.add(R.id.ll_container, fragment).commit();
}
}
And now that you have that method,
the presenter call the updateData() method in MainActivity
Well, there's where the list comes from, so just pass it to that new method where the Fragment is then loaded
#Override
public void updateData(ViewModel viewModel) {
this.pollutionData = viewModel;
if (pollutionLevels == null) {
pollutionsLevels = new ArrayList<>();
}
pollutionLevels.clear();
pollutionLevels.addAll(viewModel.getAllPolluants());
this.onPollutionLevelsLoaded(pollutionsLevels);
}

Categories

Resources