After reading MANY posts that seem similar, these were all about JSON requests, not StringRequests.
I am using volley API for my Android application, and I am following a tutorial on interaction between my app using volley and my server which is handled with php.
For some reason however, my data is not sent to the php part, because when I try to access the data on the webserver, it states that the variables are empty.
Here is my project. First off is my Singleton class which sets up ONE requestqueue:
import android.content.Context;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
public class Server_singleton
{
private static Server_singleton anInstance;
private RequestQueue requestQueue;
private static Context aCtx;
private Server_singleton(Context context)
{
aCtx = context;
requestQueue = getRequestQueue();
}
public static synchronized Server_singleton getInstance(Context context)
{
if(anInstance == null)
{
anInstance = new Server_singleton(context);
}
return anInstance;
}
public RequestQueue getRequestQueue()
{
if(requestQueue == null)
{
requestQueue = Volley.newRequestQueue(aCtx.getApplicationContext());
}
return requestQueue;
}
public <T> void addToRequestQueue(Request<T> request)
{
requestQueue.add(request);
}
}
This above class should be all nice and fine I believe(99% certain), since I follow a general design approach recommended by Android/Google using volley.
Secondly, the next file which uses Server_singleton. Here is where the magic happens, and most likely the mistake is in here some place:
import android.content.Context;
import android.util.Log;
import com.android.volley.AuthFailureError;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
import java.util.HashMap;
import java.util.Map;
/**
*
*
* This class handles requests to web server by using Google Volley API
* Google Volley API is very powerful and abstracts many low-level details when establishing
* connection with a web server.
* Volley API does not run on the main thread, which is the correct way of doing it in android.
* If it was not doing work in a background thread, the main thread would be blocked(perhaps).
* This is all done in an asynchronous way, which means that methods may behave somewhat
* different than you would expect. A method which returns a string for example
* may return a null object, before it is actually done waiting on the response from server
* This means that we have to introduce callback methods with for instance interfaces.
*/
public class Server_interaction
{
String server_url = "http://hiddenfromyou/update_location.php"; //correct ip in my code, but hidden here
String response_string;
RequestQueue queue;
Context context;
public Server_interaction(Context context)
{
this.context = context;
queue = Server_singleton.getInstance(context).getRequestQueue();
}
public static final String TAG = Server_interaction.class.getSimpleName();
public void post_request(final VolleyCallback callback)
{
StringRequest stringRequest = new StringRequest(Request.Method.POST, server_url,
new Response.Listener<String>()
{
#Override
public void onResponse(String response)
{
response_string = response;
callback.onSuccess(response_string);
//requestQueue.stop();
Log.i(TAG, "the response is: "+ response_string);
}
}
, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error){
response_string = "Something went wrong";
//error.printstacktrace()
//requestQueue.stop();
Log.i(TAG, "something went wrong. Is the server up and running?");
}
})
{
#Override
protected Map<String, String> getParams() throws AuthFailureError {
String the_name = "olaf";
String the_mail = "lalalal";
String the_country = "Norway";
String the_latitude = "33";
String the_longitude = "99";
Map<String, String> params = new HashMap<String, String>();
params.put("name", the_name);
params.put("email", the_mail);
params.put("country", the_country);
params.put("latitude", String.valueOf(the_latitude));
params.put("longitude", String.valueOf(the_longitude));
Log.i(TAG, "inside getparams : "+params);
return params;
}
};//stringrequest parameter end
//add request to requestqueue
Log.i(TAG, "the stringrequest: "+ stringRequest);
Server_singleton.getInstance(context).addToRequestQueue(stringRequest);
Log.i(TAG, "the response again:: "+ response_string);
}
}
The above code WORKS. But it should POST country, latitute etc to my webserver...
Here is my PHP script:
<?php
$email = isset($_POST["email"]) ? $_POST["email"] : print("received nothing!"); //receive from android app
$phonenumber = $_POST["phonenumber"]; //receive from android app
$country = $_POST["country"]; //receive from android app
$latitude = $_POST["latitude"]; //receive from android app
$longitude = $_POST["longitude"];
$username_for_localhost = "root";
$password_for_localhost = "";
$host = "localhost";
$db_name = "exigentia_location_db";
$con = mysqli_connect($host, $username_for_localhost, $password_for_localhost, $db_name);
if($con)
{
echo "Connection succeded";
}
else
{
echo "Connection failed";
}
$sql = "insert into person values('".$email."', '".$phonenumber."', '".$country."', '".$location."', '".$latitude."', '".$longitude."');";
if(mysqli_query($con, $sql))
{
echo "data insertion succeeded";
}
else
{
echo "data insertion failed";
}
mysqli_close($con);
?>
I check only the first var if its set, and else print out. It prints out the text, which means it is not set...Also the other ones give me index errors, since they obviously are empty...
What am I doing wrong? I have been fiddling with this problem for days, and I cannot figure out where I am wrong.
Finally a pic of what happens when I refresh my page with the php script after running the app:
try this:
Server_singleton.getInstance().addToRequestQueue(request, method);
where method is your tag: like "Register", "login" ..etc.You can use without Tag method also.
Now in your Server_singleton write this code:
public class Server_singleton extends Application {
public static final String TAG = Server_singleton.class.getSimpleName();
private RequestQueue mRequestQueue;
private static Server_singleton mInstance;
#Override
public void onCreate() {
super.onCreate();
mInstance = this;
}
public static synchronized Server_singleton getInstance() {
return mInstance;
}
public RequestQueue getRequestQueue() {
if (mRequestQueue == null) {
mRequestQueue = Volley.newRequestQueue(getApplicationContext());
}
return mRequestQueue;
}
public <T> void addToRequestQueue(Request<T> req, String tag) {
req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
getRequestQueue().add(req);
}
public <T> void addToRequestQueue(Request<T> req) {
req.setTag(TAG);
getRequestQueue().add(req);
}
public void cancelPendingRequests(Object tag) {
if (mRequestQueue != null) {
mRequestQueue.cancelAll(tag);
}
}
}
Make sure you set the permission in manifest:
<uses-permission android:name="android.permission.INTERNET" />
And in build.gradle use:
compile 'com.mcxiaoke.volley:library-aar:1.0.0'
put("Content-Type", "application/x-www-form-urlencoded")
http://www.itworld.com/article/2702452/development/how-to-send-a-post-request-with-google-volley-on-android.html
You need to also override the getBodyContentType() like getParams() and put the following code in it.
#Override
public String getBodyContentType() {
return "application/x-www-form-urlencoded; charset=UTF-8";
}
The picture you have shown depicts that there may be errors in your PHP script. The $ sign may not be present with the variables used in the script, or any other scripting errors cause such UI to appear as a result of running php script.
Related
I am developing a client-server application, where I wanted to have a persistent connection between client-server, and I chose the CometD framework for the same.
I successfully created the CometD application.
Client -
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.client.BayeuxClient;
import org.cometd.client.transport.LongPollingTransport;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import com.synacor.idm.auth.LdapAuthenticator;
import com.synacor.idm.resources.LdapResource;
public class CometDClient {
private volatile BayeuxClient client;
private final AuthListner authListner = new AuthListner();
private LdapResource ldapResource;
public static void main(String[] args) throws Exception {
org.eclipse.jetty.util.log.Log.getProperties().setProperty("org.eclipse.jetty.LEVEL", "ERROR");
org.eclipse.jetty.util.log.Log.getProperties().setProperty("org.eclipse.jetty.util.log.announce", "false");
org.eclipse.jetty.util.log.Log.getRootLogger().setDebugEnabled(false);
CometDClient client = new CometDClient();
client.run();
}
public void run() {
String url = "http://localhost:1010/cometd";
HttpClient httpClient = new HttpClient();
try {
httpClient.start();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
client = new BayeuxClient(url, new LongPollingTransport(null, httpClient));
client.getChannel(Channel.META_HANDSHAKE).addListener(new InitializerListener());
client.getChannel(Channel.META_CONNECT).addListener(new ConnectionListener());
client.getChannel("/ldapAuth").addListener(new AuthListner());
client.handshake();
boolean success = client.waitFor(1000, BayeuxClient.State.CONNECTED);
if (!success) {
System.err.printf("Could not handshake with server at %s%n", url);
return;
}
}
private void initialize() {
client.batch(() -> {
ClientSessionChannel authChannel = client.getChannel("/ldapAuth");
authChannel.subscribe(authListner);
});
}
private class InitializerListener implements ClientSessionChannel.MessageListener {
#Override
public void onMessage(ClientSessionChannel channel, Message message) {
if (message.isSuccessful()) {
initialize();
}
}
}
private class ConnectionListener implements ClientSessionChannel.MessageListener {
private boolean wasConnected;
private boolean connected;
#Override
public void onMessage(ClientSessionChannel channel, Message message) {
if (client.isDisconnected()) {
connected = false;
connectionClosed();
return;
}
wasConnected = connected;
connected = message.isSuccessful();
if (!wasConnected && connected) {
connectionEstablished();
} else if (wasConnected && !connected) {
connectionBroken();
}
}
}
private void connectionEstablished() {
System.err.printf("system: Connection to Server Opened%n");
}
private void connectionClosed() {
System.err.printf("system: Connection to Server Closed%n");
}
private void connectionBroken() {
System.err.printf("system: Connection to Server Broken%n");
}
private class AuthListner implements ClientSessionChannel.MessageListener{
#Override
public void onMessage(ClientSessionChannel channel, Message message) {
Object data2 = message.getData();
System.err.println("Authentication String " + data2 );
if(data2 != null && data2.toString().indexOf("=")>0) {
String[] split = data2.toString().split(",");
String userString = split[0];
String passString = split[1];
String[] splitUser = userString.split("=");
String[] splitPass = passString.split("=");
LdapAuthenticator authenticator = new LdapAuthenticator(ldapResource);
if(authenticator.authenticateToLdap(splitUser[1], splitPass[1])) {
// client.getChannel("/ldapAuth").publish("200:success from client "+user);
// channel.publish("200:Success "+user);
Map<String, Object> data = new HashMap<>();
// Fill in the structure, for example:
data.put(splitUser[1], "Authenticated");
channel.publish(data, publishReply -> {
if (publishReply.isSuccessful()) {
System.out.print("message sent successfully on server");
}
});
}
}
}
}
}
Server - Service Class
import java.util.List;
import java.util.concurrent.BlockingQueue;
import org.cometd.bayeux.MarkedReference;
import org.cometd.bayeux.Promise;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.AbstractService;
import org.cometd.server.ServerMessageImpl;
import com.synacor.idm.resources.AuthenticationResource;
import com.synacor.idm.resources.AuthenticationResource.AuthC;
public class AuthenticationService extends AbstractService implements AuthenticationResource.Listener {
String authParam;
BayeuxServer bayeux;
BlockingQueue<String> sharedResponseQueue;
public AuthenticationService(BayeuxServer bayeux) {
super(bayeux, "ldapagentauth");
addService("/ldapAuth", "ldapAuthentication");
this.bayeux = bayeux;
}
public void ldapAuthentication(ServerSession session, ServerMessage message) {
System.err.println("********* inside auth service ***********");
Object data = message.getData();
System.err.println("****** got data back from client " +data.toString());
sharedResponseQueue.add(data.toString());
}
#Override
public void onUpdates(List<AuthC> updates) {
System.err.println("********* inside auth service listner ***********");
MarkedReference<ServerChannel> createChannelIfAbsent = bayeux.createChannelIfAbsent("/ldapAuth", new ConfigurableServerChannel.Initializer() {
public void configureChannel(ConfigurableServerChannel channel)
{
channel.setPersistent(true);
channel.setLazy(true);
}
});
ServerChannel reference = createChannelIfAbsent.getReference();
for (AuthC authC : updates) {
authParam = authC.getAuthStr();
this.sharedResponseQueue= authC.getsharedResponseQueue();
ServerChannel channel = bayeux.getChannel("/ldapAuth");
ServerMessageImpl serverMessageImpl = new ServerMessageImpl();
serverMessageImpl.setData(authParam);
reference.setBroadcastToPublisher(false);
reference.publish(getServerSession(), authParam, Promise.noop());
}
}
}
Event trigger class-
public class AuthenticationResource implements Runnable{
private final JerseyClientBuilder clientBuilder;
private final BlockingQueue<String> sharedQueue;
private final BlockingQueue<String> sharedResponseQueue;
private boolean isAuthCall = false;
private String userAuth;
private final List<Listener> listeners = new CopyOnWriteArrayList<Listener>();
Thread runner;
public AuthenticationResource(JerseyClientBuilder clientBuilder,BlockingQueue<String> sharedQueue,BlockingQueue<String> sharedResponseQueue) {
super();
this.clientBuilder = clientBuilder;
this.sharedQueue = sharedQueue;
this.sharedResponseQueue= sharedResponseQueue;
this.runner = new Thread(this);
this.runner.start();
}
public List<Listener> getListeners()
{
return listeners;
}
#Override
public void run() {
List<AuthC> updates = new ArrayList<AuthC>();
// boolean is = true;
while(true){
if(sharedQueue.size()<=0) {
continue;
}
try {
userAuth = sharedQueue.take();
// Notify the listeners
for (Listener listener : listeners)
{
updates.add(new AuthC(userAuth,sharedResponseQueue));
listener.onUpdates(updates);
}
updates.add(new AuthC(userAuth,sharedResponseQueue));
System.out.println("****** Auth consume ******** " + userAuth);
if(userAuth != null) {
isAuthCall = true;
}
} catch (Exception err) {
err.printStackTrace();
break;
}
// if (sharedQueue.size()>0) {
// is = false;
// }
}
}
public static class AuthC
{
private final String authStr;
private final BlockingQueue<String> sharedResponseQueue;
public AuthC(String authStr,BlockingQueue<String> sharedResponseQueue)
{
this.authStr = authStr;
this.sharedResponseQueue=sharedResponseQueue;
}
public String getAuthStr()
{
return authStr;
}
public BlockingQueue<String> getsharedResponseQueue()
{
return sharedResponseQueue;
}
}
public interface Listener extends EventListener
{
void onUpdates(List<AuthC> updates);
}
}
I have successfully established a connection between client and server.
Problems -
1- When I am sending a message from the server to the Client, the same message is sent out multiple times. I only expecting one request-response mechanism.
In my case- server is sending user credentila I am expecting result, whether the user is authenticated or not.
you can see in image how it is flooding with same string at client side -
2- There was other problem looping up of message between client and server, that I can be able to resolve by adding, but still some time looping of message is happening.
serverChannel.setBroadcastToPublisher(false);
3- If I change the auth string on sever, at client side it appears to be old one.
For example -
1 request from server - auth string -> user=foo,pass=bar -> at
client side - user=foo,pass=bar
2 request from server - auth string user=myuser,pass=mypass ->
at client side - user=foo,pass=bar
this are the three problems, please guide me and help me to resolve this.
CometD offer a request/response style of messaging using remote calls, both on the client and on the server (you want to use annotated services on the server).
Channel /ldapAuth has 2 subscribers: the remote client (which subscribes with authChannel.subscribe(...)), and the server-side AuthenticationService (which subscribes with addService("/ldapAuth", "ldapAuthentication")).
Therefore, every time you publish to that channel from AuthenticationService.onUpdates(...), you publish to the remote client, and then back to AuthenticationService, and that is why calling setBroadcastToPublisher(false) helps.
For authentication messages, it's probably best that you stick with remote calls, because they have a natural request/response semantic, rather than a broadcasting semantic.
Please read about how applications should interact with CometD.
About other looping, there are no loops triggered by CometD.
You have loops in your application (in AuthenticationService.onUpdates(...)) and you take from a queue that may have the same information multiple times (in AuthenticationResource.run() -- which by the way it's a spin loop that will likely spin a CPU core to 100% utilization -- you should fix that).
The fact that you see stale data it's likely not a CometD issue, since CometD does not store messages anywhere so it cannot make up user-specific data.
I recommend that you clean up your code using remote calls and annotated services.
Also, clean up your own code from spin loops.
If you still have the problem after the suggestions above, look harder for application mistakes, it's unlikely that this is a CometD issue.
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 have build a hybrid-app using flutter's web_view_plugin(webview).
One of our paying methods require to open a 3rd party app(in this case kakaotalk). But the flutter webview plugin does not provide this function and returned net::ERR_UNKNOWN_URL_SCHEME. I did some research and I understand that the problem lies in the url. If the url does not start with http or https, it will cause this error.
So, to solve this problem I had to change native java code. Now I have no experience at all with java and android, so fixing the native code was very difficult. I understand that I have to modify shouldOverrideUrlLoading part, in order to allow the url that starts with intent:// and also I have to put in some validation to check if the app is installed or not.(If not installed the user should be redirected to playstore)
The code which I added is in shouldOverrideUrlLoading.
I also did three imports. The rest is code, generated by flutter
package com.flutter_webview_plugin;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.os.Build;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.Intent; //added import
import android.net.Uri; //added import
import android.content.ActivityNotFoundException; //added import
/**
* Created by lejard_h on 20/12/2017.
*/
public class BrowserClient extends WebViewClient {
private Pattern invalidUrlPattern = null;
public BrowserClient() {
this(null);
}
public BrowserClient(String invalidUrlRegex) {
super();
if (invalidUrlRegex != null) {
invalidUrlPattern = Pattern.compile(invalidUrlRegex);
}
}
public void updateInvalidUrlRegex(String invalidUrlRegex) {
if (invalidUrlRegex != null) {
invalidUrlPattern = Pattern.compile(invalidUrlRegex);
} else {
invalidUrlPattern = null;
}
}
#Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Map<String, Object> data = new HashMap<>();
data.put("url", url);
data.put("type", "startLoad");
FlutterWebviewPlugin.channel.invokeMethod("onState", data);
}
#Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Map<String, Object> data = new HashMap<>();
data.put("url", url);
FlutterWebviewPlugin.channel.invokeMethod("onUrlChanged", data);
data.put("type", "finishLoad");
FlutterWebviewPlugin.channel.invokeMethod("onState", data);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// returning true causes the current WebView to abort loading the URL,
// while returning false causes the WebView to continue loading the URL as usual.
String url = request.getUrl().toString();
boolean isInvalid = checkInvalidUrl(url);
Map<String, Object> data = new HashMap<>();
data.put("url", url);
data.put("type", isInvalid ? "abortLoad" : "shouldStart");
FlutterWebviewPlugin.channel.invokeMethod("onState", data);
return isInvalid;
}
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// returning true causes the current WebView to abort loading the URL,
// while returning false causes the WebView to continue loading the URL as usual.
if (url.startsWith(INTENT_PROTOCOL_START)) {
final int customUrlStartIndex = INTENT_PROTOCOL_START.length();
final int customUrlEndIndex = url.indexOf(INTENT_PROTOCOL_INTENT);
if (customUrlEndIndex < 0) {
return false;
} else {
final String customUrl = url.substring(customUrlStartIndex, customUrlEndIndex);
try {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(customUrl)));
} catch (ActivityNotFoundException e) {
final int packageStartIndex = customUrlEndIndex + INTENT_PROTOCOL_INTENT.length();
final int packageEndIndex = url.indexOf(INTENT_PROTOCOL_END);
final String packageName = url.substring(packageStartIndex, packageEndIndex < 0 ? url.length() : packageEndIndex);
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(GOOGLE_PLAY_STORE_PREFIX + packageName)));
}
return true;
}
} else {
return false;
}
// boolean isInvalid = checkInvalidUrl(url);
// Map<String, Object> data = new HashMap<>();
// data.put("url", url);
// data.put("type", isInvalid ? "abortLoad" : "shouldStart");
// FlutterWebviewPlugin.channel.invokeMethod("onState", data);
// return isInvalid;
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
Map<String, Object> data = new HashMap<>();
data.put("url", request.getUrl().toString());
data.put("code", Integer.toString(errorResponse.getStatusCode()));
FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data);
}
#Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
Map<String, Object> data = new HashMap<>();
data.put("url", failingUrl);
data.put("code", errorCode);
FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data);
}
private boolean checkInvalidUrl(String url) {
if (invalidUrlPattern == null) {
return false;
} else {
Matcher matcher = invalidUrlPattern.matcher(url);
return matcher.lookingAt();
}
}
}
The code compiles, but it still returns the same error net::ERR_UNKNOWN_URL_SCHEME when I try to pay with the "3rd party app(kakaotalk)"
I encountered a similar error on Android before when Firebase Dynamic Links are being forced to be loaded in a WebView. In my case, FDL is expected to be handled by Google Play Services in Android. But since the WebView doesn't know what to do with the link it's forced to display, the WebView returns "net::ERR_UNKNOWN_URL_SCHEME" error. I'm unsure if this is the same case as yours since I'm unable to verify the link that you're trying to load apart from "intent://kakaopay..."
You can try opening the link externally by using url_launcher. Use RegEx to filter intent URLs and check if the URL can be launched and be handled externally (outside the app).
var yourURL = "URL goes here";
// Check if URL contains "intent"
yourURL.contains(RegExp('^intent://.*\$')){
// Check if the URL can be launched
if (await canLaunch(yourURL)) {
await launch(yourURL);
} else {
print('Could not launch $yourURL');
}
}
Also, the plugin (web_view_plugin) that you're using seems to be outdated, and I can't find it here https://pub.dev/packages?q=web_view_plugin. Flutter has its official WebView plugin (webview_flutter) that has been released and I suggest checking it out if it fits your use case.
Listen, In some cases using #omatt's answer might not work, especially for webview_flutter. I struggled to find a solution so I did this:
_launchURL(url) async {
var link = "https://hiddenwords.page.link/deposit";
if (await canLaunch(link)) {
await launch(link,
forceWebView: false, enableJavaScript: true, forceSafariVC:
false);
} else {
throw 'Could not launch $link';
}
}
I manually put the url/link I wanted it to open in the _launch function... Dont mind the url in the _launch parenthesis.
I also added this to the Webview widget:
navigationDelegate: (NavigationRequest request) {
if (request.url.contains(RegExp('^intent://.*\$'))) {
_launchURL(request.url);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
Hope this works for you. It works for me...
1.Use your APP open other app in flutter with parameter(in your dynamic link);
2.use: url_launcher: ^6.1.6;
First, their app must support dynamic link;
Second, they provide you with dynamic links for their deals;
In this way, we can click the dynamic link in your APP and jump to the specified page of their APP.
Code:
final Uri toLaunch = Uri(scheme: 'https', host: 'link.fitstop.com', path: 'link/qbvQ/');
//https://link.fitstop.com/link/qbvQ is dynamic link
Future<void>? _launched;
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInBrowser(toLaunch);
}),
child: Text(
'url_launcher',
),
)
Future<void> _launchInBrowser(Uri url) async {
if (!await launchUrl(
url,
mode: LaunchMode.externalApplication,
)) {
throw 'Could not launch $url';
}
}
I have created an android application according to the tutorial on the following link https://www.youtube.com/watch?v=M4VTi5-Aw20 and these are the files,
FetchUserData.php
<?php
//database connection
$host = 'localhost';
$user1 = 'root';
$password1 = '';
$database = 'users';
$con = mysqli_connect($host, $user1, $password1, $database);
//checking the validity of the database
// if(!$con){
//die("connection Failed" . mysqli_connect_error());}
//echo "connected Successfully";
$password=$_POST["password"];
$userName=$_POST["userName"];
$statement=mysqli_prepare($con,"SELECT * FROM usersLogged WHERE userName=? AND password=?");
//to prevent from sql injection
mysqli_stmt_bind_param($statement,"ss",$userName,$password);
mysqli_stmt_execute($statement);
//after executing the command we need to get the results that were selected
mysqli_stmt_store_result($statement); //storing the results in a buffer for temporary
//we need to bind the results
mysqli_stmt_bind_result($statement, $userId, $userName, $firstName, $lastName, $password, $age);
//now we need to store them into an array
$user=array();
//this while loop is going to run only once
while(mysqli_stmt_fetch($statement)){
//storing the values which are fetched from the database are kept in the array(#user)
$user[userName]=$userName;
$user[firstName]=$firstName;
$user[lastName]=$lastName;
$user[password]=$password;
$user[age]=$age;
//now we need to pass the content to the phone,we send the array in a json
json_encode($user);
mysqli_stmt_close($statement);
mysqli_close($con);
}
?>
here is my RequestServer.java
package com.example.kasun.timetable;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.widget.ProgressBar;
import org.apache.http.HttpConnection;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import java.util.ArrayList;
/**
* Created by kasun on 1/7/16.
*/
public class RequestServer {
ProgressDialog progressDialog; // loads the pregress dialog
private static final int CONNECTION_TIMEOUT = 1000 * 5;
private static final String SERVER_ADDRESS = "http://10.111.97.94/Timetable/";
//constructor
//we can't initialize progress dialog as this is not a java class that is associcated with an activity
public RequestServer(Context context) {
progressDialog = new ProgressDialog(context); // progressDialog is initialized
progressDialog.setTitle("Processing");
progressDialog.setMessage("Please wait");
progressDialog.setCancelable(false);
//users can't cancel the fetch
}
public void storeUserDataInBackground(User user, GetUserCallBack getUserCallBack) {
progressDialog.show();
new storeUserDataAsyncTask(user, getUserCallBack).execute();
/* here we need to create an object for it as it is a class, from this code the constructor
of the AsyncClass will be run
*/
}
public void fetchUserDataInBackground(User user, GetUserCallBack callBack) {
progressDialog.show();
new fetchUserDataAsyncTask(user,callBack).execute();
}
/*we need t create inner class, which is the background class which runs, backgrounds classes in android are known as
Async tasks
*/
public class storeUserDataAsyncTask extends AsyncTask<Void, Void, Void>
{
/* void = we are not sending anything to this task when it is being executed
void= we are not asking any progress from the AsyncTask as we are running the
progressDialog( how we need the AsyncTask show the progress to the user)
void= what this should return
*/
User user;
GetUserCallBack getUserCallBack;
public storeUserDataAsyncTask(User user, GetUserCallBack getUserCallBack) {
this.user = user;
this.getUserCallBack = getUserCallBack;
}
#Override
protected Void doInBackground(Void... params) {
ArrayList<NameValuePair> dataToSend = new ArrayList<>();
dataToSend.add(new BasicNameValuePair("userName", user.userName));
dataToSend.add(new BasicNameValuePair("firstName", user.firstName));
dataToSend.add(new BasicNameValuePair("lastName", user.lastName));
dataToSend.add(new BasicNameValuePair("password", user.password));
dataToSend.add(new BasicNameValuePair("age", user.age + ""));
/*setting the attributes for the httpRequest we are going to make( attributes for the
http_post
*/
HttpParams httpRequestParams = new BasicHttpParams();
//going to set the attributes
HttpConnectionParams.setConnectionTimeout(httpRequestParams, CONNECTION_TIMEOUT);
//waiting for the changes to be made to the localhost
HttpConnectionParams.setSoTimeout(httpRequestParams, CONNECTION_TIMEOUT);
//waiting till data is received from the server
//-----------setting up the http request is finished
//setting up a client, client can only do is make a request to the server0
HttpClient client = new DefaultHttpClient(httpRequestParams);
//client must perform duties according to the attributes mentioned in the httpRequestParams
//setting the post method, post is used to store the data that is used to send to the database
HttpPost post = new HttpPost(SERVER_ADDRESS + "Register.php"); // sets the server address
try {
post.setEntity(new UrlEncodedFormEntity(dataToSend));
client.execute(post);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
//this method runs after the saving data to the server is completed
progressDialog.dismiss();
getUserCallBack.done(null);
super.onPostExecute(aVoid);
}
}
//--------------- for fetching the user data from the database
public class fetchUserDataAsyncTask extends AsyncTask<Void, Void, User>
{
/* Void = we are not sending anything to this task when it is being executed
Void= we are not asking any progress from the AsyncTask as we are running the
progressDialog( how we need the AsyncTask show the progress to the user)
user= what this should return
*/
User user;
GetUserCallBack getUserCallBack;
public fetchUserDataAsyncTask(User user, GetUserCallBack getUserCallBack) {
this.user = user;
this.getUserCallBack = getUserCallBack;
}
#Override
protected User doInBackground(Void... voids) {
ArrayList<NameValuePair> dataToSend = new ArrayList<>();
dataToSend.add(new BasicNameValuePair("userName", user.userName));
dataToSend.add(new BasicNameValuePair("password", user.password));
HttpParams httpParams = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParams, CONNECTION_TIMEOUT);
HttpConnectionParams.setSoTimeout(httpParams, CONNECTION_TIMEOUT);
HttpClient client = new DefaultHttpClient(httpParams);
HttpPost post = new HttpPost(SERVER_ADDRESS + "FetchUserData.php");
User returnedUser=null;
try {
post.setEntity(new UrlEncodedFormEntity(dataToSend));
HttpResponse httpResponse = client.execute(post);
/*after excuting this
will return the detials of the user
*/
// we need to convert them so that we can use them
HttpEntity entity= httpResponse.getEntity();
// now details are stored as entities
// now we need to conevert them to string
String result= EntityUtils.toString(entity);
/* we passed data to the phone from the servse through a Json object, so we need to
take the details send through the json object
*/
JSONObject jsonObject= new JSONObject(result);
// if there are no details are send through the JSON object,
if(jsonObject.length()==0){
returnedUser=null;
}
else{
String firstName=jsonObject.getString("firstName");
String lastName=jsonObject.getString("lastName");
int age= jsonObject.getInt("age");
returnedUser= new User(firstName,lastName,user.userName,user.password,age);
}
} catch (Exception e) {
e.printStackTrace();
}
return returnedUser;
}
#Override
protected void onPostExecute(User returnedUser) {
//this method runs after the saving data to the server is completed
progressDialog.dismiss();
getUserCallBack.done(returnedUser);
super.onPostExecute(returnedUser);
}
}
}
this is UserLocalDatabase.java
package com.example.kasun.timetable;
import android.content.Context;
import android.content.SharedPreferences;
/**
* Created by kasun on 12/31/15.
*/
public class UserLocalDatabase {
private static final String DB_name = "userDetails";
// this is the name of the database which is used to store the data in the phone
SharedPreferences userDataBase;
/*SharedPreferences is the thing that stores data in the phone, here userDatabase is the object
of the SharedPreference, DB_name is the file which is used by that object for storing data
*/
//constructor
public UserLocalDatabase(Context context) {
/* SharedPreference can only be initialize from an activity, since UserLocalDatabase is a seperate java
class, it can't initialize the SharedPreference, so we want all the activities that uses the
SharedPreference to send their Context to this method so that, then we can initialize the Shared Preference
*/
//initializing the SharedPreference through the context passed by the activity
userDataBase = context.getSharedPreferences(DB_name, 0);
//DB_name is the file where the SharedPreferences values are coming from,
//0 is the default value
}
//initializing the SharedPreference through the context passed by the activity is finished
// methods for the adding and substracting
public void storeUserData(User user) {
// and object of User class is being passed to here
// adding the editable function in SharedPreference
SharedPreferences.Editor spEditor = userDataBase.edit();
spEditor.putString("firstName", user.firstName);
spEditor.putString("lastName", user.lastName);
spEditor.putString("userName", user.userName);
spEditor.putString("password", user.password);
spEditor.putInt("age", user.age);
spEditor.commit();
// commit is need to delete or add a data to the SharePreference
/*spEditor is an object of the SharedPreference and it uses the SharedPreference edit()
method through the object
*/
/*
here the values are obtained from the User class attributes and they are stored in the SharedPreference
*/
}
public User getLoggedInUser() {
// getting data from the SharedPreference
SharedPreferences.Editor spEditor = userDataBase.edit();
String firstName = userDataBase.getString("firstName", "");
String lastName = userDataBase.getString("lastName", "");
String userName = userDataBase.getString("userName", "");
String password = userDataBase.getString("password", "");
int age = userDataBase.getInt("age", -1);
// here -1 is needed as this is an integer values
// here values are being passed to the User class
User userLoggedIn = new User(firstName, lastName, userName, password, age);
return userLoggedIn;
// any method calls this method get the details of the user logged
}
// to check whether the user has logged into the system
public void setUserStatus(Boolean status) {
SharedPreferences.Editor spEditor = userDataBase.edit();
spEditor.putBoolean("status", status);
}
public boolean getUserStatus() {
if (userDataBase.getBoolean("status", false) == true) {
//this checks the status field in the userDatabase.
//("status",false) this boolean value is the default values for the "status" field
return true;
} else {
return false;
}
}
public void clear() {
SharedPreferences.Editor spEditor = userDataBase.edit();
spEditor.clear(); //after clearing also commit should be done
spEditor.commit();
}
}
this is User.java
package com.example.kasun.timetable;
/**
* Created by kasun on 12/31/15.
*/
public class User {
//user details
String firstName,lastName,userName,password;
int age;
public User(String firstName, String lastName,String username, String password,int age){
this.firstName=firstName;
this.lastName=lastName;
this.userName=username;
this.password=password;
this.age=age;
}
public User(String username, String password){
this.userName=username;
this.password=password;
this.age=-1;
this.firstName="";
this.lastName="";
}
}
these errors are poped up when Iam going to log in
W/System.err: at com.example.kasun.timetable.RequestServer$fetchUserDataAsyncTask.doInBackground(RequestServer.java:195)
which is the line
JSONObject jsonObject= new JSONObject(result);
W/System.err: at com.example.kasun.timetable.RequestServer$fetchUserDataAsyncTask.doInBackground(RequestServer.java:140)
which is the line
public class fetchUserDataAsyncTask extends AsyncTask<Void, Void, User>
please help me to solve this
I solved the problem, the problem was the app could not read the FetchUserData.php which was in htdocs directory. I change the permission to 777 by using
sudo chmod 777 -R /opt/lammp/htdocs
now it is ok.
com.android.volley.NoConnectionError: java.net.ProtocolException: Unknown method 'PATCH'; must be one of [OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE]
StringRequest putRequest = new StringRequest(Request.Method.PATCH, url,
new Response.Listener<String>()
{
#Override
public void onResponse(String response)
{
Log.d("Response", response);
}
},
new Response.ErrorListener()
{
#Override
public void onErrorResponse(VolleyError error)
{
Log.d("Error.Response", error.toString());
}
}
) {
#Override
protected Map<String, String> getParams()
{
Map<String, String> params = new HashMap<String, String> ();
params.put("name", "My Name");
params.put("age", "11");
return params;
}
};
Are you sure you are using correct version of Volley Library? I just tried your code in Lollipop and it is working OK.
If you are using Volley library as external project, check the Method interface of Request class in com.android.volley package. It should have a PATCH variable in it.
public interface Method {
int DEPRECATED_GET_OR_POST = -1;
int GET = 0;
int POST = 1;
int PUT = 2;
int DELETE = 3;
int HEAD = 4;
int OPTIONS = 5;
int TRACE = 6;
int PATCH = 7;
}
If not, use the latest version of Volley library.
UPDATE:
You are correct, it is showing this error in Kitkat, but not in Lollipop. I guess the main problem is that HTTPUrlConnection of Java does not support PATCH.
(I guess it works in Lollipop because it is using Java 7 and HTTPUrlConnection of Java 7 supports PATCH method?)
Anyhow, You can use the OkHttp Library to correct this problem. The okhttp-urlconnection module implements the java.net.HttpURLConnection
Add the following jar to your libs folder:
okhttp-2.2.0.jar
okhttp-urlconnection-2.2.0.jar
okio-1.2.0.jar
Create a OkHttpStack class:
package com.example.temp;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import com.android.volley.toolbox.HurlStack;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkUrlFactory;
public class OkHttpStack extends HurlStack {
private final OkUrlFactory mFactory;
public OkHttpStack() {
this(new OkHttpClient());
}
public OkHttpStack(OkHttpClient client) {
if (client == null) {
throw new NullPointerException("Client must not be null.");
}
mFactory = new OkUrlFactory(client);
}
#Override protected HttpURLConnection createConnection(URL url) throws IOException {
return mFactory.open(url);
}
}
Use the following constructor to create a Volley RequestQueue:
Volley.newRequestQueue(getApplicationContext(),new OkHttpStack()).add(putRequest);
It is working for me on Kitkat now.
While sending request use POST.
In headers just override http method to PATCH.
For me now its working in volley even in kitkat version.
header.put("X-HTTP-Method-Override", "PATCH");