Issue
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;
}
}
Solution
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.
Answered By - yosriz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.