RxJava/Retrofit2/Java - generic NetworkBoundResource not fired - java

I'm new in RxJava so my deep apology if this subject has already been covered or seems pretty easy for some of you. I've thoroughly been searching for a solution without any luck.
Many of my API calls are are based on the original Google LiveData NetworkBoundResource class. Because of the complexity of my business logic I decided to move from LiveData to RxJava. I converted a Kotlin version https://android.jlelse.eu/networkboundresource-with-rxjava-and-kotlin-sealed-classes-1574bc516f82 into a Java one and adapted the rest of the implementation accordingly.
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final String TAG = NetworkBoundResource.class.getSimpleName();
private Flowable<Resource<ResultType>> result;
protected NetworkBoundResource() {
// Lazy db observable.
Flowable<ResultType> dbObservable = Flowable.defer(() ->
loadFromDb()
// Read from disk on Computation Scheduler
.subscribeOn(Schedulers.computation())
);
// Lazy network observable.
Flowable<ResultType> networkObservable = Flowable.defer(() ->
createCall()
// Request API on IO Scheduler
.subscribeOn(Schedulers.io())
// Read/Write to disk on Computation Scheduler
.observeOn(Schedulers.computation())
.doOnNext (request -> {
if (request.isSuccessful()) {
saveCallResult(processResponse(request));
} else {
processInternalError(request);
}
})
.onErrorReturn (throwable -> {
throw Exceptions.propagate(throwable);
})
.flatMap(__ -> loadFromDb())
);
result = NetworkUtils.isNetworkStatusAvailable()
? networkObservable
.map(Resource::success)
.onErrorReturn (t -> Resource.error(t.getMessage(), null))
.observeOn(AndroidSchedulers.mainThread())
.startWith(Resource::loading)
: dbObservable
.map(Resource::success)
.onErrorReturn (t -> Resource.error(t.getMessage(), null))
.observeOn(AndroidSchedulers.mainThread())
.startWith(Resource::loading);
}
public Flowable<Resource<ResultType>> asFlowable() {
return result;
}
private RequestType processResponse(Response<RequestType> response) {
return response.body();
}
private void processInternalError(Response<RequestType> response){
String error = null;
if (response.errorBody() != null) {
try {
error = response.errorBody().string();
} catch (java.io.IOException ignored) {
android.util.Log.e(TAG, "Error while parsing response: " + ignored.getMessage());
}
throw Exceptions.propagate(new Throwable(response.code() + ": " + error));
}
}
protected abstract void saveCallResult(#NonNull RequestType item);
protected abstract Flowable<ResultType> loadFromDb();
protected abstract Flowable<Response<RequestType>> createCall();
}
My View model which subscribes to the Steam
public abstract class ItemListViewModel<T extends IListItem> extends AndroidViewModel {
private MutableLiveData<Resource<List<T>>> itemListObservable;
private Disposable disposable;
protected ItemListViewModel(Application application) {
super(application);
itemListObservable = new MutableLiveData<>();
subscribe();
}
public LiveData<Resource<List<T>>> getItemListObservable() {
return itemListObservable;
}
private void subscribe() {
disposable = loadListItem()
.subscribe(
list -> itemListObservable.setValue(list),
(Throwable t) -> itemListObservable.setValue(Resource.error(t.getMessage(), null))
);
DisposableManager.add(disposable);
}
#Override
protected void onCleared() {
disposable.dispose();
}
protected abstract void init();
protected abstract Flowable<Resource<List<T>>> loadListItem();
}
My generic fragment is handling the resource status
public abstract class ListItemFragment<T extends IListItem> extends Fragment {
protected ItemListAdapter<T> adapter;
protected RecyclerViewBinding binding;
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, R.layout.recycler_view, container, false);
binding.recyclerView.setAdapter(adapter);
binding.recyclerView.setHasFixedSize(true);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
binding.setIsLoading(true);
return binding.getRoot();
}
protected void observeViewModel(ItemListViewModel<T> viewModel) {
// Observe and update the list when the data changes
viewModel.getItemListObservable().observe(this, (#Nullable Resource<List<T>> iObservables) -> {
if (iObservables != null) {
processResponse(iObservables);
}
});
}
private void processResponse(Resource<List<T>> resource) {
switch (resource.status) {
case LOADING:
renderLoadingState();
break;
case SUCCESS:
renderDataState(resource.data);
break;
case ERROR:
renderErrorState(resource.message);
break;
}
}
private void renderLoadingState(){
binding.setIsLoading(true);
adapter.replace(Collections.emptyList());
}
private void renderDataState(List<T> data){
binding.setIsLoading(false);
adapter.replace(data);
}
private void renderErrorState(String error){
Toast.makeText(this.getContext(),error, Toast.LENGTH_SHORT).show();
}
}
The createCall() is never fired though I subscribed to it as explained. Can't see it in my logging interceptor as it was before
My guess was Flowable> was not correctly recognized by Retrofit. I made sure to add the CallAdapterFactory from the squareup repo to my HttpClient
BDRepository(String URL) {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.enableComplexMapKeySerialization();
Gson gson = gsonBuilder.create();
retrofit = new Retrofit
.Builder()
.baseUrl(URL)
.client(CustomTrust.getHttpClient())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
My gradle conf
//API request
implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofit"
implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofit"
implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.retrofit"
The API call returns data when trying it in a browser.
When breaking down the code my network is always available so networkObservable is supposed to be the actual stream.
My retrofit service
#GET("items")
Flowable<Response<List<Items>>> getItemList(#Query("lang") String
language);
Any help would be much appreciated

Related

Flutter Plugin access native objects

I'm developing a Flutter plugin to implements an iOS SDK and an Android SDK in Flutter. In both native SDKs, there is an object called Peripheral, which is a complexe object extending and implementing other objects. If I want to use theses Objects, do I have to implement them in Flutter too ? Or can I just create an manipulate instances of those objects from dart.
Right now, I'm trying to manipulate instances by have a PeripheralObject that calls a function in constructor that will create an instance in native Java (for Android) of a peripheral, place it in a hash map, and return it's memory adress to dart. In dart, I keep the memory adress of the Java object and when I call a function, like getName, I pass to the method channel the java memory adress and with that, I can retrieve from the map my instance of the native object, call the method and send back the answer. Is it a good way of resolving the problem or is there other better way to do so ?
Here is my dart object:
class Peripheral {
late String _objectReference;
late String _localName, _uuid;
Peripheral({required String localName, required String uuid}) {
_uuid = uuid;
_localName = localName;
_newPeripheralInstance(localName, uuid);
}
Future<void> _newPeripheralInstance(String localName, String uuid) async {
_objectReference = (await PeripheralPlatform.instance.newPeripheralInstance(localName, uuid))!;
return;
}
String get objectReference => _objectReference;
Future<String?> getModelName() async {
return PeripheralPlatform.instance.getModelName(_objectReference);
}
Future<String?> getUuid() async {
return PeripheralPlatform.instance.getUuid(_objectReference);
}
}
Here is my Dart Method Channel :
class MethodChannelPeripheral extends PeripheralPlatform {
/// The method channel used to interact with the native platform.
#visibleForTesting
final methodChannel = const MethodChannel('channel');
#override
Future<String?> newPeripheralInstance(String localName, String uuid) async {
String? instance = await methodChannel.invokeMethod<String>('Peripheral-newPeripheralInstance', <String, String>{
'localName': localName,
'uuid': uuid
});
return instance;
}
#override
Future<String?> getModelName(String peripheralReference) {
return methodChannel.invokeMethod<String>('Peripheral-getModelName', <String, String>{
'peripheralReference': peripheralReference
});
}
#override
Future<String?> getUuid(String peripheralReference) {
return methodChannel.invokeMethod<String>('Peripheral-getUuid', <String, String>{
'peripheralReference': peripheralReference
});
}
}
And here is my Android Java file :
public class PluginPeripheral {
private static Map<String, Peripheral> peripheralMap = new HashMap<>();
public static void handleMethodCall(String method, MethodCall call, MethodChannel.Result result) {
method = method.replace("Peripheral-", "");
switch (method) {
case "newPeripheralInstance":
newPeripheralInstance(call, result);
break;
case "getModelName":
getModelName(call, result);
break;
case "getUuid":
getUuid(call, result);
break;
default:
result.notImplemented();
break;
}
}
private static void newPeripheralInstance(MethodCall call, MethodChannel.Result result) {
if (call.hasArgument("uuid") && call.hasArgument("localName")) {
String uuid = call.argument("uuid");
String localName = call.argument("localName");
if (localName == null || uuid == null) {
result.error("Missing argument", "Missing argument 'uuid' or 'localName'", null);
return;
}
Peripheral peripheral = new Peripheral(localName, uuid);
peripheralMap.put(peripheral.toString(), peripheral);
result.success(peripheral.toString());
}
}
private static void getModelName(MethodCall call, MethodChannel.Result result) {
if (call.hasArgument("peripheralReference")) {
String peripheralString = call.argument("peripheralReference");
if (peripheralString == null) {
result.error("Missing argument", "Missing argument 'peripheral'", null);
return;
}
Peripheral peripheral = peripheralMap.get(peripheralString);
if (peripheral == null) {
result.error("Invalid peripheral", "Invalid peripheral", null);
return;
}
result.success(peripheral.getModelName());
} else {
result.error("Missing argument", "Missing argument 'peripheralReference'", null);
}
}
private static void getUuid(MethodCall call, MethodChannel.Result result) {
if (call.hasArgument("peripheralReference")) {
String peripheralString = call.argument("peripheralReference");
if (peripheralString == null) {
result.error("Missing argument", "Missing argument 'peripheral'", null);
return;
}
Peripheral peripheral = peripheralMap.get(peripheralString);
if (peripheral == null) {
result.error("Invalid peripheral", "Invalid peripheral", null);
return;
}
result.success(peripheral.getUuid());
} else {
result.error("Missing argument", "Missing argument 'peripheralReference'", null);
}
}
}
The alternative way is to convert an object to a map in Android and back to an object in Flutter. Something like this:
Flutter/Dart:
class Device {
String? id;
String? name;
...
Device.fromMap(Map<String, dynamic> map) {
id = map['id'];
name = map['name'];
}
}
final map = await methodChannel.invokeMethod('requestDevice');
final device = Device.fromMap(map.cast<String, dynamic>());
Android/Kotlin:
data class Device(
val id: String,
val name: String?
) {
...
fun toMap(): Map<String, Any?> {
return mapOf(
"id" to id,
"name" to name
)
}
}
override fun onMethodCall(#NonNull call: MethodCall, #NonNull result: Result) {
...
result.success(device.toMap())
...
}

PlayIntegrity API Calls: How to handle GoogleServerUnavailable Error

I am developing an Android security app and have decided to implement the PlayIntegrity API as an alternative to SafetyNet API. I have already completed the necessary setup steps such as enabling the Play and Cloud console, however, I am encountering an issue where I am getting an error 'GOOGLE SERVER UNAVAILABLE' when trying to obtain a token. Can anyone provide any insight into why this might be happening and possible solutions? Any help would be greatly appreciated.
Please see below code:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// playIntegritySetup.lol();
getToken();
}
private void getToken() {
String nonce = Base64.encodeToString(generateNonce(50).getBytes(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
// Create an instance of a manager.
IntegrityManager integrityManager = IntegrityManagerFactory.create(getApplicationContext());
// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse = integrityManager.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce)
.build());
integrityTokenResponse.addOnSuccessListener(new OnSuccessListener<IntegrityTokenResponse>() {
#Override
public void onSuccess(IntegrityTokenResponse integrityTokenResponse) {
String integrityToken = integrityTokenResponse.token();
SplashActivity.this.doIntegrityCheck(integrityToken);
Log.e("Integrity Token", "integrity token from the app" + integrityToken);
}
});
integrityTokenResponse.addOnFailureListener(e -> showErrorDialog("Error getting token from Google. Google said: " + getErrorText(e)));
}
private void doIntegrityCheck(String token) {
AtomicBoolean hasError = new AtomicBoolean(false);
Observable.fromCallable(() -> {
OkHttpClient okHttpClient = new OkHttpClient();
Response response = okHttpClient.newCall(new Request.Builder().url("money control url" + "token from backend server" + token).build()).execute();
Log.e("Token", "token from the app" + token);
if (!response.isSuccessful()) {
hasError.set(true);
return "Api request error. Code: " + response.code();
}
ResponseBody responseBody = response.body();
if (responseBody == null) {
hasError.set(true);
return "Api request error. Empty response";
}
JSONObject responseJson = new JSONObject(responseBody.string());
if (responseJson.has("error")) {
hasError.set(true);
return "Api request error: " + responseJson.getString("error");
}
if (!responseJson.has("deviceIntegrity")) {
hasError.set(true);
}
return responseJson.getJSONObject("deviceIntegrity").toString();
}) // Execute in IO thread, i.e. background thread.
.subscribeOn(Schedulers.io())
// report or post the result to main thread.
.observeOn(AndroidSchedulers.mainThread())
// execute this RxJava
.subscribe(new Observer<String>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(String result) {
if (hasError.get()) {
if (result.contains("MEETS_DEVICE_INTEGRITY") && result.contains("MEETS_BASIC_INTEGRITY")) {
//Here goes my other code
}
}
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
});
}
private String getErrorText(Exception e) {
String msg = e.getMessage();
if (msg == null) {
return "Unknown Error";
}
//the error code
int errorCode = Integer.parseInt(msg.replaceAll("\n", "").replaceAll(":(.*)", ""));
switch (errorCode) {
case IntegrityErrorCode.API_NOT_AVAILABLE:
return "API_NOT_AVAILABLE";
case IntegrityErrorCode.NO_ERROR:
return "NO_ERROR";
case IntegrityErrorCode.INTERNAL_ERROR:
return "INTERNAL_ERROR";
case IntegrityErrorCode.NETWORK_ERROR:
return "NETWORK_ERROR";
case IntegrityErrorCode.PLAY_STORE_NOT_FOUND:
return "PLAY_STORE_NOT_FOUND";
case IntegrityErrorCode.PLAY_STORE_ACCOUNT_NOT_FOUND:
return "PLAY_STORE_ACCOUNT_NOT_FOUND";
case IntegrityErrorCode.APP_NOT_INSTALLED:
return "APP_NOT_INSTALLED";
case IntegrityErrorCode.PLAY_SERVICES_NOT_FOUND:
return "PLAY_SERVICES_NOT_FOUND";
case IntegrityErrorCode.APP_UID_MISMATCH:
return "APP_UID_MISMATCH";
case IntegrityErrorCode.TOO_MANY_REQUESTS:
return "TOO_MANY_REQUESTS";
case IntegrityErrorCode.CANNOT_BIND_TO_SERVICE:
return "CANNOT_BIND_TO_SERVICE";
case IntegrityErrorCode.NONCE_TOO_SHORT:
return "NONCE_TOO_SHORT";
case IntegrityErrorCode.NONCE_TOO_LONG:
return "NONCE_TOO_LONG";
case IntegrityErrorCode.GOOGLE_SERVER_UNAVAILABLE:
return "GOOGLE_SERVER_UNAVAILABLE";
case IntegrityErrorCode.NONCE_IS_NOT_BASE64:
return "NONCE_IS_NOT_BASE64";
default:
return "Unknown Error";
}
}
private String generateNonce(int length) {
String nonce = "";
String allowed = getNonce();
for (int i = 0; i < length; i++) {
nonce = nonce.concat(String.valueOf(allowed.charAt((int) Math.floor(Math.random() * allowed.length()))));
}
return nonce;
}
public native String getNonce();
static {
System.loadLibrary("all-keys");
}
I ran into the same problem and I found a solution for this.
You need to specify cloudProjectNumber() when you are working on outside of Google Play, which can be found in google cloud console.
Quote from the doc:
Important: In order to receive and decrypt Integrity API responses,
apps not available on Google Play need to include their Cloud project
number in their requests. You can find this in Project info in the
Google Cloud Console.
So the code should be like this:
IntegrityTokenRequest.builder()
.setNonce(nonce)
.cloudProjectNumber(100004676) // your cloud project number here for dev build
.build());

How can I use MVVM with the UI components of the App/activity and AsyncTask

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

Unexpected behaviour with paging library

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) :)

Recycler View Load Next Page / Edit / Delete Records Using MVVM+ROOM

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/

Categories