I'm making a call using Retrofit's enqueue() method. I'm calling my refreshImages() in my MainActivity's onCreate(), refreshImages() then calls a method refreshImagesIds() which is supposed to make a call out to Flickr's API and return back a PhotosList object, I'll then pull out the Photos from there which will contain a list of Photo objects. My issue is that for some reason the onResponse() inside my enqueue() method is never getting called. When I use the debugger it skips right over it, and when I put Log statements inside they never get written out. I know the endpoint it is hitting is correct because I can see it using OkHttp's logger, and my POJOs all look to be correct for the data being returned.
Any idea why this isn't working? Below are my refreshImages and refreshImagesId. These are both contained in my MainAcitivty and modify class-level variables.
private void refreshImages() {
// make api call
//imageUrls = FlickrServiceManager_withinterface.getKittenImages(8);
refreshImageIds();
List<Photo> photos = photosList.getPhotos().getPhoto();
imageIds = new ArrayList<String>();
for(Photo photo : photos) {
Log.d("TAG", "It is pringint imageIds: " + photo.getId());
imageIds.add(photo.getId());
}
}
private void refreshImageIds() {
Retrofit retrofit = Api.getRestAdapter();
FlickrServiceInterface flickrService = retrofit.create(FlickrServiceInterface.class);
Call<PhotosList> call = flickrService.getPhotos(API_KEY, FORMAT, "1");
imageIds = new ArrayList<String>();
call.enqueue(new Callback<PhotosList>(){
#Override
public void onResponse(Call<PhotosList> call, Response<PhotosList> response) {
photosList = response.body();
}
#Override
public void onFailure(Call<PhotosList> call, Throwable t) {
// TODO: Clean up
Log.d("TEMP_TAG", "Call failed");
}
});
}
And my FlickrServiceInterface:
public interface FlickrServiceInterface {
#GET("?method=flickr.photos.getSizes")
Call<PhotoSizes> getPhotoSizes(#Query("api_key") String apiKey, #Query("format") String format, #Query("nojsoncallback") String jsonCallback, #Query("photo_id") String photoId);
#GET("?method=flickr.photos.getRecent")
Call<PhotosList> getPhotos(#Query("api_key") String apiKey, #Query("format") String format, #Query("nojsoncallback") String jsonCallback);
}
Change your call to the synchronous retrofit API :
public static List<String> getImageIds(int size) {
Call<PhotosList> call = flickrService.getPhotos(apiKey, format, "1");
photoIds = new ArrayList<String>();
PhotosList photosList = call.execute().body();
List<Photo> photos = photosList.getPhotos().getPhoto();
for(Photo photo : photos) {
Log.d("TEMP_TAG", "adding photo id to list: " + photo.getId());
photoIds.add(photo.getId());
}
Log.d("TEMP_TAG", "it's getting here too");
return photoIds;
}
Please note that you need to call this method on an AsyncTask
EDIT
You could also continue to use enqueue, but you need to provide an "onFinish" hook, so you know when your data has been received and then you "notify" the client with the data:
//interface por communication
public interface ImageIdsCallBack {
public void onFinish( List<String> photoIds );
}
Then you receive this interface and send data:
public static List<String> getImageIds(int size, final ImageIdsCallBack callback) {
Call<PhotosList> call = flickrService.getPhotos(apiKey, format, "1");
photoIds = new ArrayList<String>();
call.enqueue(new Callback<PhotosList>(){
#Override
public void onResponse(Call<PhotosList> call, Response<PhotosList> response) {
PhotosList photosList = response.body();
List<Photo> photos = photosList.getPhotos().getPhoto();
for(Photo photo : photos) {
Log.d("TEMP_TAG", "adding photo id to list: " + photo.getId());
photoIds.add(photo.getId());
}
//send the data to the caller
callback.onFinish(photoIds);
}
#Override
public void onFailure(Call<PhotosList> call, Throwable t) {
// TODO: Clean up
Log.d("TEMP_TAG", "Call failed");
}
});
Log.d("TEMP_TAG", "it's getting here too");
return photoIds;
}
calling the method :
getImageIds( 50 , new ImageIdsCallBack() {
public void onFinish( List<String> photoIds ) {
//update UI with photoIds
}
} );
I typically use a library like EventBus to make it easier, I really recommend it to you.
Correct me if I'm wrong, is this on the main thread? That would pose the problem of not waiting for a response.
Consider using async
Related
As I know that the ViewModel should be secluded from the UI/View and contains only the logic that observes the data that's coming from the server or database
In my App, I used REST API "retrofit" and blogger API and I tried to migrate/upgrade the current code to MVVM but there are a few problems, let's go to the code
BloggerAPI Class
public class BloggerAPI {
private static final String BASE_URL =
"https://www.googleapis.com/blogger/v3/blogs/4294497614198718393/posts/";
private static final String KEY = "the Key";
private PostInterFace postInterFace;
private static BloggerAPI INSTANCE;
public BloggerAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
postInterFace = retrofit.create(PostInterFace.class);
}
public static String getBaseUrl() {
return BASE_URL;
}
public static String getKEY() {
return KEY;
}
public static BloggerAPI getINSTANCE() {
if(INSTANCE == null){
INSTANCE = new BloggerAPI();
}
return INSTANCE;
}
public interface PostInterFace {
#GET
Call<PostList> getPostList(#Url String url);
}
public Call<PostList>getPosts(String url){
return postInterFace.getPostList(url);
}
}
this getData method I used in the Mainctivity to retrieve blog posts
public void getData() {
if (getItemsByLabelCalled) return;
progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
final Call<PostList> postList = BloggerAPI.getINSTANCE().getPosts(url);
postList.enqueue(new Callback<PostList>() {
#Override
public void onResponse(#NonNull Call<PostList> call, #NonNull Response<PostList> response) {
if (response.isSuccessful()) {
progressBar.setVisibility(View.GONE);
PostList list = response.body();
Log.d(TAG, "onResponse: " + response.body());
if (list != null) {
token = list.getNextPageToken();
items.addAll(list.getItems());
adapter.notifyDataSetChanged();
for (int i = 0; i < items.size(); i++) {
items.get(i).setReDefinedID(i);
}
if (sqLiteItemsDBHelper == null || sqLiteItemsDBHelper.getAllItems().isEmpty()) {
SaveInDatabase task = new SaveInDatabase();
Item[] listArr = items.toArray(new Item[0]);
task.execute(listArr);
}
}
} else {
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
int sc = response.code();
switch (sc) {
case 400:
Log.e("Error 400", "Bad Request");
break;
case 404:
Log.e("Error 404", "Not Found");
break;
default:
Log.e("Error", "Generic Error");
}
}
}
#Override
public void onFailure(#NonNull Call<PostList> call, #NonNull Throwable t) {
Toast.makeText(MainActivity.this, "getData error occured", Toast.LENGTH_LONG).show();
Log.e(TAG, "onFailure: " + t.toString());
Log.e(TAG, "onFailure: " + t.getCause());
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
});
}
I created the PostsViewModel to trying to think practically how to migrate the current code to use MVVM
public class PostsViewModel extends ViewModel {
public MutableLiveData<PostList> postListMutableLiveData = new MutableLiveData<>();
public void getData() {
String token = "";
// if (getItemsByLabelCalled) return;
// progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
BloggerAPI.getINSTANCE().getPosts(url).enqueue(new Callback<PostList>() {
#Override
public void onResponse(Call<PostList> call, Response<PostList> response) {
postListMutableLiveData.setValue(response.body());
}
#Override
public void onFailure(Call<PostList> call, Throwable t) {
}
});
}
}
and it's used thus in MainActivity
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.postListMutableLiveData.observe(this, postList -> {
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
});
now there are two problems using this way of MVVM "ViewModel"
first in the current getData method in the MainActivity it's contains some components that should work only in the View layer like the items list, the recyclerView needs to set View.GONE in case of response unsuccessful, progressBar, emptyView TextView, the adapter that needs to notify if there are changes in the list, and finally I need the context to used the create the Toast messages.
To solve this issue I think to add the UI components and other things into the ViewModel Class and create a constructor like this
public class PostsViewModel extends ViewModel {
Context context;
List<Item> itemList;
PostAdapter postAdapter;
ProgressBar progressBar;
TextView textView;
public PostsViewModel(Context context, List<Item> itemList, PostAdapter postAdapter, ProgressBar progressBar, TextView textView) {
this.context = context;
this.itemList = itemList;
this.postAdapter = postAdapter;
this.progressBar = progressBar;
this.textView = textView;
}
but this is not logically with MVVM arch and for sure cause memory leaking also I will not be able to create the instance of ViewModel with regular way like this
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.postListMutableLiveData.observe(this, postList -> {
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
});
and must be used like this
postsViewModel = new PostsViewModel(this,items,adapter,progressBar,emptyView);
so the first question is How to bind these UI components with the ViewModel?
second in the current getata I used the SaveInDatabase class use the AsyncTask way to save all items in the SQLite database the second question is How to move this class to work with ViewModel? but it also needs to work in the View layer to avoid leaking
the SaveInDatabase Class
static class SaveInDatabase extends AsyncTask<Item, Void, Void> {
#Override
protected Void doInBackground(Item... items) {
List<Item> itemsList = Arrays.asList(items);
// runtimeExceptionDaoItems.create(itemsList);
for (int i = 0 ; i< itemsList.size();i++) {
sqLiteItemsDBHelper.addItem(itemsList.get(i));
Log.e(TAG, "Size :" + sqLiteItemsDBHelper.getAllItems().size());
}
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
}
}
Actually the question is too broad to answer because there are many ways to implement for this case. First of all, never pass view objects to viewModel. ViewModel is used to notify changes to ui layer with LiveData or rxJava without retaining the view instance. You may try this way.
class PostViewModel extends ViewModel {
private final MutableLiveData<PostList> postListLiveData = new MutableLiveData<PostList>();
private final MutableLiveData<Boolean> loadingStateLiveData = new MutableLiveData<Boolean>();
private String token = "";
public void getData() {
loadingStateLiveData.postValue(true);
// if (getItemsByLabelCalled) return;
// progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
BloggerAPI.getINSTANCE().getPosts(url).enqueue(new Callback<PostList>() {
#Override
public void onResponse(Call<PostList> call, Response<PostList> response) {
loadingStateLiveData.postValue(false);
postListLiveData.setValue(response.body());
token = response.body().getNextPageToken(); //===> the token
}
#Override
public void onFailure(Call<PostList> call, Throwable t) {
loadingStateLiveData.postValue(false);
}
});
}
public LiveData<PostList> getPostListLiveData(){
return postListLiveData;
}
public LiveData<Boolean> getLoadingStateLiveData(){
return loadingStateLiveData;
}
}
and you may observe the changes from your activity like this.
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.getPostListLiveData().observe(this,postList->{
if(isYourPostListEmpty(postlist)) {
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
}else {
recyclerView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
});
postsViewModel.getLoadingStateLiveData().observe(this,isLoading->{
if(isLoading) {
progressBar.setVisibility(View.VISIBLE);
}else {
progressBar.setVisibility(View.GONE);
}
});
For my personal prefer, I like using Enum for error handling, but I can't post here as it will make the answer very long. For your second question, use Room from google. It will make you life a lot easier. It work very well with mvvm and it natively support liveData. You can try CodeLab from google to practise using room.
Bonus: You don't need to edit the url like this:
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
You can use #Path or #query based on your requirements.
As your question is bit broad , I am not giving any source code for the same, Rather mentioning samples which clearly resolves issues mentioned with MVVM.
Clean Code Architecture can be followed which will clearly separate the responsibilities of each layer.
First of all application architecture needs to be restructured so that each layer has designated role in MVVM. You can follow the following pattern for the same.
Only View Model will have access to UI layer
View model will connect with Use Case layer
Use case layer will connect with Data Layer
No layer will have cyclic reference to other components.
So now for Database, Repository will decide, from which section the data needs to be fetched
This can be either from Network or from DataBase.
All these points (except Database part) are covered over Medium Article, were each step is covered with actual API's .
Along with that unit test is also covered.
Libraries used are in this project are
Coroutines
Retrofit
Koin (Dependency Injection) Can be replaced with dagger2 is required
MockWebServer (Testing)
Language: Kotlin
Full Source code can be found over Github
Edit
Kotlin is the official supported language for Android Development now. I suggest you should lean and migrate your java android projects to Kotlin.
Still for converting Kotlin to Java, Go to Menu > Tools > Kotlin > Decompile Kotlin to Java Option
I started to learn about the paging library, And I've a problem.
I'm able to fetch the data and show inside my recyclerView, But I've really weird beahviors.
I Put logs on loadInitial, loadBefore and loadAfter and the first time loadInitial and loadAfter call one after another immediatly.
When I scroll down, I log getPage from the response and it give me the right page number after 20 item, but I really suspect it just load ALL the pages for the first time, I mean, I can literly scroll 500 item without wait to load even one time.
The first problem as I said - it called loadInitial and loadAfter one after another immediatly at the first time.
second problem - when I scroll up, loadBefore NEVER triggered.
I don't sure which code I should share, but I suspect the problem is somewhere inside the data source, If you need more let me know in the comments
CODE:
public class MoviesDataSource extends PageKeyedDataSource<Integer, Results> {
private static final int FIRST_PAGE = 1;
#Override
public void loadInitial(#NonNull LoadInitialParams<Integer> params, #NonNull LoadInitialCallback<Integer, Results> callback) {
Log.i(TAG, "loadInitial: ");
ApiService.getAllMovies().getAllMovies(API_KEY, FIRST_PAGE).enqueue(
new Callback<AllMovies>() {
#Override
public void onResponse(Call<AllMovies> call, Response<AllMovies> response) {
if (response.body() != null) {
Log.i(TAG, "onResponse: " + response.body().getPage());
List<Results> results = Arrays.asList(response.body().getResults());
callback.onResult(results, null, FIRST_PAGE + 1 );
}
}
#Override
public void onFailure(Call<AllMovies> call, Throwable t) {
}
}
);
}
#Override
public void loadBefore(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, Results> callback) {
Log.i(TAG, "loadBefore: ");
ApiService.getAllMovies().getAllMovies(API_KEY, params.key).enqueue(
new Callback<AllMovies>() {
#Override
public void onResponse(Call<AllMovies> call, Response<AllMovies> response) {
Log.i(TAG, "onResponse: " + response.body().getPage());
Integer key = (params.key > 1) ? params.key -1 : null;
if (response.body() != null) {
List<Results> results = Arrays.asList(response.body().getResults());
callback.onResult(results, key );
}
}
#Override
public void onFailure(Call<AllMovies> call, Throwable t) {
}
}
);
}
#Override
public void loadAfter(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, Results> callback) {
Log.i(TAG, "loadAfter: ");
ApiService.getAllMovies().getAllMovies(API_KEY, params.key).enqueue(
new Callback<AllMovies>() {
#Override
public void onResponse(Call<AllMovies> call, Response<AllMovies> response) {
Log.i(TAG, "onResponse: " + response.body().getPage());
Integer key = params.key + 1; // calculate until there is no data
if (response.body() != null) {
List<Results> results = Arrays.asList(response.body().getResults());
callback.onResult(results, key );
}
}
#Override
public void onFailure(Call<AllMovies> call, Throwable t) {
}
}
);
}
}
MainActivity
#Override
public void onChanged(PagedList<Results> results) {
adapter.submitList(results);
}
ViewModel
First problem: loadAfter gets called based on PagedList.Config prefetch distance and page size configuration.To control how and when a PagedList queries data from its DataSource, see PagedList.Config
second: you load data in one direction so loadBefore never gets called.
Also, you should be using synchronous retrofit calls, you can find reference here: network only paging
Im developing app based on same api while learning about jetpack stuff, am little ahead of you (caching db + network) :)
View model has been initialized by the following code inside fragment.
viewModel.getContacts(pageNumber, AppConstants.DIRECTION).observe(getActivity(), list -> {
adapter.submitList(list);
});
where viewModel.getContacts() method calls a repository method which in turn makes the web request and brings the response back.
public MutableLiveData<List<Contact>> getAllContacts(int page, String sortedBy) {
return repository.getAllContacts(page, sortedBy);
}
where repository.getAllContacts() method is
public MutableLiveData<List<Contact>> getAllContacts(int page, String orderBy) {
if (allContacts == null) {
allContacts = new MutableLiveData<>();
}
//we will load it asynchronously from server in this method
loadContacts(page, orderBy);
return allContacts;
}
private void loadContacts(int page, String orderBy) {
Call<ContactsResponse> call = bearerApiInterface.getContacts(page, orderBy);
call.enqueue(new Callback<ContactsResponse>() {
#Override
public void onResponse(Call<ContactsResponse> call, Response<ContactsResponse> response) {
Timber.e("Contacts Response => " + new GsonBuilder().setPrettyPrinting().create().toJson(response.body()));
//finally we are setting the list to our MutableLiveData
allContacts.setValue(response.body().getResult().getData());
}
#Override
public void onFailure(Call<ContactsResponse> call, Throwable t) {
}
});
}
And here is my recycler view scroll listener
recyclerView.setOnScrollListener(new EndlessRecyclerOnScrollListener(linearLayoutManager) {
#Override
public void onLoadMore(int current_page) {
loadNextPage();
}
});
Upon scrolling when loadNextPage() gets called, how viewModel.getContacts() could be triggered from loadNextPage() method.
What are the options to send the call again with incremented page number and observe it with same viewModel.getContacts() method. Paging list adapter is not an option for now as the response needs to be updated, deleted & customized while paging list adapter isn't doing that without datasource and snapshot inclusion which isn't working (any help with that would be very helpful if it is possible).
And below is the code for deleting any item from recycler view.
#Override
public void onItemDelete(RecyclerView.ViewHolder viewHolder, int position) {
mActivity.showProgressBar(true);
Timber.e("Delete the contact at position " + position);
viewModel.deleteContact(adapter.getContactAt(viewHolder.getAdapterPosition()).getId(), adapter.getContactAt(viewHolder.getAdapterPosition())).observe(this, new Observer<Boolean>() {
#Override
public void onChanged(Boolean isSuccess) {
if (isSuccess) {
mActivity.showErrorDialog("Contact Deleted Successfully", null, null);
listAdapter.notifyItemRemoved(viewHolder.getAdapterPosition());
} else {
mActivity.showErrorDialog("Something went wrong, please try again", null, null);
}
}
});
}
The view model delete method is
public MutableLiveData<Boolean> deleteContact(int id, Contact contact) {
return repository.deleteThisContact(id, contact);
}
And the repository delete method is
public MutableLiveData<Boolean> deleteThisContact(int contactId, Contact contact) {
if (deleteContact == null)
deleteContact = new MutableLiveData<>();
callDeleteContact(contactId, contact);
return deleteContact;
}
private void callDeleteContact(int contactId, Contact contact) {
Call<JsonObject> call = bearerApiInterface.deleteContact(contactId);
call.enqueue(new Callback<JsonObject>() {
#Override
public void onResponse(Call<JsonObject> call, Response<JsonObject> response) {
if (response.isSuccessful() && response.code() == 200) {
Timber.e("***** Contact Deleted Successfully => " + new GsonBuilder().setPrettyPrinting().create().toJson(response.body()));
delete(contact);
deleteContact.setValue(true);
} else {
try {
deleteContact.setValue(false);
String errorMessage = new APIError().extractMessage(new JSONObject(response.errorBody() != null ? response.errorBody().string().trim() : null));
Timber.e("***** Error message is => " + errorMessage);
} catch (JSONException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
#Override
public void onFailure(Call<JsonObject> call, Throwable t) {
deleteContact.setValue(false);
Timber.e("***** onFailure" + "response: " + t.getMessage());
}
});
}
Any related code which might be worthy of sharing can be asked. Skipped for simplicity.
You will need to implement the android paging:
First, you have to add on gradle the paging lib:
implementation 'androidx.paging:paging-runtime:2.1.0'
Your data source must extend the PageKeyedDataSource, so, you have to implement 3 methods, loadInitial, loadAfter and loadBefore
On your view model you must create a pager config variable, like:
private val config: PagedList.Config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(PAGE_SIZE_HINT)
.setEnablePlaceholders(false)
.build()
It will set up how the pager must be executed, and do you have to create an executor to load the data:
private val executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE)
And after all, create a livedata to receive the list:
val your_source: LiveData<PagedList<YourSource>> = LivePagedListBuilder(dataFactory, config)
.setFetchExecutor(executor)
.build()
Your recycler view adapter must be changed to a PagedListAdapter instead.
I recommend this article:
https://androidwave.com/pagination-in-recyclerview/
I am using the Facebook graph api to find out what pages a user is apart of. When the query comes back with a json object it has what I need but, for some reason it doesn't want to add to my array list. The correct value is printed in log.d it seems to skip my arraylist for some reason. Any ideas?
Find page function
private ArrayList<String> foundPages;
private JSONObject jsonObject;
public ArrayList<String> findPages()
{
accessToken = AccessToken.getCurrentAccessToken();
foundPages = new ArrayList<>();
GraphRequest request = GraphRequest.newGraphPathRequest(
accessToken,
"/me/accounts",
new GraphRequest.Callback() {
#Override
public void onCompleted(GraphResponse response) {
try {
jsonObject = response.getJSONObject();
for(int i=0; i < jsonObject.getJSONArray("data").length(); i++)
{
page = response.getJSONObject().getJSONArray("data").getJSONObject(i).getString("name");
Log.d("viewmodel",page);
foundPages.add(page);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
request.executeAsync();
return foundPages;
}
There is a common way to solve this problem, which is to define a callback method which will return these values to you, AFTER they have been populated by the call, which goes something like this (my java is rusty, bear with me...)
define an interface :
interface Callback{
void apiResponseCallback(ArrayList<Page> result);//whatever your model is, make the array of that type
}
then, in your normal findPages method, change it to this:
public void findPages(Callback callback) {
//
//
........
for(int i=0; i < jsonObject.getJSONArray("data").length(); i++)
{
page = response.getJSONObject().getJSONArray("data").getJSONObject(i).getString("name");
Log.d("viewmodel",page);
foundPages.add(page);
}
callback.apiResponseCallback(foundPages);//here we are returning the data when it is done
}
then, when you call findPages
findPages(new Callback() {
#Override
public void apiResponseCallback(ArrayList<Page> result) {
here, this result parameter that comes through is your api call result to use, so result will be your populated pages to use.
}
});
}
sake of completeness:
public void findPages(Callback callback)
{
accessToken = AccessToken.getCurrentAccessToken();
foundPages = new ArrayList<>();
GraphRequest request = GraphRequest.newGraphPathRequest(
accessToken,
"/me/accounts",
new GraphRequest.Callback() {
#Override
public void onCompleted(GraphResponse response) {
try {
jsonObject = response.getJSONObject();
for(int i=0; i < jsonObject.getJSONArray("data").length(); i++)
{
page = response.getJSONObject().getJSONArray("data").getJSONObject(i).getString("name");
Log.d("viewmodel",page);
foundPages.add(page);
}
callback.apiResponseCallback(foundPages);
} catch (JSONException e) {
e.printStackTrace();
}
}
});
request.executeAsync();
}
Yep. This here:
request.executeAsync();
triggers an asynchronous request. But your "current" thread simply continues to do:
return foundPages;
and it returns an empty list.
That list gets later filled, but at the moment in time when that method returns, that list is still empty. Or just gets filled. Who knows, as it gets filled asynchronously, at some unknown point in the future.
A solution could be to have some other variable/field that tells you the data has arrived and pushed into the list.
Alternatively, that method could just make a synchronous request, simply block the caller from progressing until the data has arrived.
You see, you can't have it both ways: when you don't wait for your results to arrive, you shouldn't expect them to be available immediately.
I'm making a call using Retrofit's enqueue() method. I'm calling my refreshImages() in my MainActivity's onCreate(), refreshImages() then calls a method refreshImagesIds() which is supposed to make a call out to Flickr's API and return back a PhotosList object, I'll then pull out the Photos from there which will contain a list of Photo objects. My issue is that for some reason the onResponse() inside my enqueue() method is never getting called. When I use the debugger it skips right over it, and when I put Log statements inside they never get written out. I know the endpoint it is hitting is correct because I can see it using OkHttp's logger, and my POJOs all look to be correct for the data being returned.
Any idea why this isn't working? Below are my refreshImages and refreshImagesId. These are both contained in my MainAcitivty and modify class-level variables.
private void refreshImages() {
// make api call
//imageUrls = FlickrServiceManager_withinterface.getKittenImages(8);
refreshImageIds();
List<Photo> photos = photosList.getPhotos().getPhoto();
imageIds = new ArrayList<String>();
for(Photo photo : photos) {
Log.d("TAG", "It is pringint imageIds: " + photo.getId());
imageIds.add(photo.getId());
}
}
private void refreshImageIds() {
Retrofit retrofit = Api.getRestAdapter();
FlickrServiceInterface flickrService = retrofit.create(FlickrServiceInterface.class);
Call<PhotosList> call = flickrService.getPhotos(API_KEY, FORMAT, "1");
imageIds = new ArrayList<String>();
call.enqueue(new Callback<PhotosList>(){
#Override
public void onResponse(Call<PhotosList> call, Response<PhotosList> response) {
photosList = response.body();
}
#Override
public void onFailure(Call<PhotosList> call, Throwable t) {
// TODO: Clean up
Log.d("TEMP_TAG", "Call failed");
}
});
}
And my FlickrServiceInterface:
public interface FlickrServiceInterface {
#GET("?method=flickr.photos.getSizes")
Call<PhotoSizes> getPhotoSizes(#Query("api_key") String apiKey, #Query("format") String format, #Query("nojsoncallback") String jsonCallback, #Query("photo_id") String photoId);
#GET("?method=flickr.photos.getRecent")
Call<PhotosList> getPhotos(#Query("api_key") String apiKey, #Query("format") String format, #Query("nojsoncallback") String jsonCallback);
}
Change your call to the synchronous retrofit API :
public static List<String> getImageIds(int size) {
Call<PhotosList> call = flickrService.getPhotos(apiKey, format, "1");
photoIds = new ArrayList<String>();
PhotosList photosList = call.execute().body();
List<Photo> photos = photosList.getPhotos().getPhoto();
for(Photo photo : photos) {
Log.d("TEMP_TAG", "adding photo id to list: " + photo.getId());
photoIds.add(photo.getId());
}
Log.d("TEMP_TAG", "it's getting here too");
return photoIds;
}
Please note that you need to call this method on an AsyncTask
EDIT
You could also continue to use enqueue, but you need to provide an "onFinish" hook, so you know when your data has been received and then you "notify" the client with the data:
//interface por communication
public interface ImageIdsCallBack {
public void onFinish( List<String> photoIds );
}
Then you receive this interface and send data:
public static List<String> getImageIds(int size, final ImageIdsCallBack callback) {
Call<PhotosList> call = flickrService.getPhotos(apiKey, format, "1");
photoIds = new ArrayList<String>();
call.enqueue(new Callback<PhotosList>(){
#Override
public void onResponse(Call<PhotosList> call, Response<PhotosList> response) {
PhotosList photosList = response.body();
List<Photo> photos = photosList.getPhotos().getPhoto();
for(Photo photo : photos) {
Log.d("TEMP_TAG", "adding photo id to list: " + photo.getId());
photoIds.add(photo.getId());
}
//send the data to the caller
callback.onFinish(photoIds);
}
#Override
public void onFailure(Call<PhotosList> call, Throwable t) {
// TODO: Clean up
Log.d("TEMP_TAG", "Call failed");
}
});
Log.d("TEMP_TAG", "it's getting here too");
return photoIds;
}
calling the method :
getImageIds( 50 , new ImageIdsCallBack() {
public void onFinish( List<String> photoIds ) {
//update UI with photoIds
}
} );
I typically use a library like EventBus to make it easier, I really recommend it to you.
Correct me if I'm wrong, is this on the main thread? That would pose the problem of not waiting for a response.
Consider using async