Începeți cu un client HTTP Retrofit 2

Ce veți crea

Ce este Retrofit?

Retrofit este un client HTTP sigur pentru aplicații Android și Java. Retrofit-ul facilitează conectarea la un serviciu Web REST prin traducerea API-ului în interfețe Java. În acest tutorial, vă vom arăta cum să utilizați una dintre cele mai populare și adesea recomandate biblioteci HTTP disponibile pentru Android. 

Această bibliotecă puternică facilitează consumarea datelor JSON sau XML care sunt apoi analizate în obiecte obișnuite Java vechi (POJOs). OBȚINEPOSTA PUNEPLASTURE, și ȘTERGE toate cererile pot fi executate. 

Ca majoritatea software-urilor open source, Retrofit a fost construit pe lângă alte biblioteci și instrumente puternice. În spatele scenei, Retrofit folosește OkHttp (de la același dezvoltator) pentru a gestiona cererile de rețea. De asemenea, Retrofit nu are un convertor JSON încorporat pentru a analiza obiectele JSON de la Java. În schimb, livrează suport pentru următoarele biblioteci de conversie JSON pentru a rezolva următoarele probleme: 

  • Gson: com.squareup.retrofit: convertor-gson
  • Jackson: com.squareup.retrofit: convertor-jackson
  • Moshi: com.squareup.retrofit: convertor-Moshi

Pentru tampoanele de protocol, Retrofit suporta:

  • Protobuf: com.squareup.retrofit2: convertor-Protobuf
  • Sârmă: com.squareup.retrofit2: convertor fire

Și pentru XML, Retrofit sprijină:

  • Cadru simplu: com.squareup.retrofit2: convertor-simpleframework

Deci, de ce folosiți Retrofit?

Dezvoltarea propriei biblioteci HTTP de tip pentru a interfața cu un API REST poate fi o adevărată durere: trebuie să vă ocupați de multe funcționalități cum ar fi crearea conexiunilor, cache-ul, reîncercarea solicitărilor nereușite, filetarea, parsarea răspunsurilor, tratarea erorilor și multe altele. Retrofit, pe de altă parte, este foarte bine planificat, documentat și testat - o bibliotecă testată de luptă care vă va economisi mult timp prețios și dureri de cap.

În acest tutorial, vă voi explica cum să utilizați Retrofit 2 pentru a gestiona solicitările de rețea construind o aplicație simplă pentru a interoga răspunsurile recente din API-ul Stack Exchange. Vom efectua OBȚINE solicitări prin specificarea unui obiectiv final-/ răspunsuri, atașat la URL-ul de bază https://api.stackexchange.com/2.2/- apoi obțineți rezultatele și afișați-le într-un ecran de reciclare. De asemenea, vă voi arăta cum să faceți acest lucru cu RxJava pentru o gestionare ușoară a fluxului de stare și date.

1. Creați un proiect Android Studio

Activați Android Studio și creați un nou proiect cu o activitate goală numită Activitate principala.

2. Declararea dependențelor

După crearea unui nou proiect, declarați următoarele dependențe în dvs. build.gradleDependențele includ o vizualizare a reciclării, biblioteca Retrofit și, de asemenea, biblioteca Google a lui Gson pentru a converti JSON în POJO (obiecte simple Java obișnuite), precum și integrarea Gson a Retrofit. 

// Retrofit compile 'com.squareup.retrofit2: modernizare: 2.1.0' // JSON Parsing compile 'com.google.code.gson: gson: 2.6.1' compilați com.squareup.retrofit2: convertor-gson: 2.1 .0 '// reciclerview compile' com.android.support:recyclerview-v7:25.0.1 ' 

Nu uitați să sincronizați proiectul pentru a descărca aceste biblioteci. 

3. Adăugarea permisului Internet

Pentru a efectua operațiuni de rețea, trebuie să includeți INTERNETpermisiune în manifestarea cererii: AndroidManifest.xml.

           

4. Modelele de generare automată

Vom crea automat modelele noastre din datele noastre de răspuns JSON utilizând un instrument foarte util: jsonschema2pojo. 

Obțineți datele de probă JSON

Copiați și lipiți https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow în bara de adrese a browserului dvs. (sau puteți utiliza Postman dacă sunteți familiarizat cu instrumentul respectiv). Apoi apăsați introduce-aceasta va executa o solicitare GET pentru obiectivul dat. Ce veți vedea în răspuns este o serie de obiecte JSON. Imaginea de mai jos este răspunsul JSON folosind Postman.

"items": ["proprietar": "reputație": 1, "user_id": 6540831, "user_type": "înregistrat", "profile_image": "https://www.gravatar.com/avatar/6a468ce8a8ff42c17923a6009ab77723 ? s = 128 & d = identiticon & r = PG & f = 1 "," display_name ":" bobolafrite "," link ":" http://stackoverflow.com/users/6540831/bobolafrite " : 0, "last_activity_date": 1480862271, "create_date": 1480862271, "answer_id": 40959732, "question_id": 35931342, "owner": reputation ": 629," user_id ": 3054722; "înregistrat", "profile_image": "https://www.gravatar.com/avatar/0cf65651ae9a3ba2858ef0d0a7dbf900?s=128&d=identicon&r=PG&f=1", "display_name": "jeremy-denis", "link": "http : //www.accountflow.com/indeers/3054722/jeremy-denis "," is_accepted ": false," score ": 0," last_activity_date ": 1480862260," creation_date ": 1480862260," answer_id " : 40959661, ...], "has_more": true, "backoff": 10, "quota_max": 300, "quota_remaining": 241

Copiați acest răspuns JSON fie din browserul dvs., fie din Postman. 

Mapați datele JSON către Java

Acum vizitați jsonschema2pojo și inserați răspunsul JSON în caseta de introducere.

Selectați un tip de sursă de JSON, stilul de adnotare al Gson, și debifați Permiteți proprietăți suplimentare

Apoi faceți clic pe previzualizare pentru a genera obiectele Java. 

S-ar putea să te întrebi ce @SerializedName și @Expune se fac adnotări în acest cod generat. Nu-ți face griji, o să explic totul!

 @SerializedName este nevoie de adnotări pentru ca Gson să găsească cheile JSON cu câmpurile noastre. În conformitate cu convenția Java de numire camelCase pentru proprietățile membrilor de clasă, nu se recomandă utilizarea sublinierii pentru separarea cuvintelor într-o variabilă. @SerializedName ajută la traducerea între cele două.

@SerializedName ("quota_remaining") @ Expune cota intregului privatRemaining;

În exemplul de mai sus, îi spunem lui Gson cheia noastră JSON quota_remaining ar trebui să fie mapate în câmpul Java quotaRemaining.  Dacă ambele valori au fost aceleași, adică dacă cheia noastră JSON a fost quotaRemaining la fel ca domeniul Java, atunci nu ar fi nevoie de @SerializedName adnotări pe teren deoarece Gson le-ar cartografia automat.

@Expune adnotarea indică faptul că acest membru ar trebui expus pentru serializare sau deserializare JSON. 

Modificați modelele de date pentru Android Studio

Acum, să revenim la Android Studio. Creați un nou sub-pachet în interiorul pachetului principal și denumiți-l date. În noul pachet de date creat, creați un alt pachet și denumiți-l model. În interiorul pachetului de modele, creați o nouă clasă Java și denumiți-o Proprietar. Acum copiați Proprietar clasa care a fost generată de jsonschema2pojo și inserați-o în interiorul Proprietar clasa pe care ați creat-o. 

importați com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; proprietarul clasei publice @SerializedName ("reputația") @ Expune reputația privată integrată; @SerializedName ("user_id") @Expose private UserId; @SerializedName ("user_type") @Expose privateType String; @SerializedName ("profile_image") @Exprimarea profilului privat StringImage; @SerializedName ("display_name") @Expongeți numele de afișare privat displayName; @SerializedName ("link") @ Expune link-ul privat String; @SerializedName ("accept_rate") @Export privat integer acceptRate; public Integer getReputation () retur reputație;  public void setReputație (reputația întregului) this.reputation = reputație;  Integer public getUserId () return userId;  public void setUserId (Integer userId) this.userId = userId;  public String getUserType () retur userType;  public void setUserType (String userType) this.userType = userType;  public String getProfileImage () returnare profilImage;  void public setProfileImage (String profilImage) this.profileImage = ProfilImage;  public String getDisplayName () retur displayName;  void public setDisplayName (String displayName) this.displayName = displayName;  public String getLink () return link;  public void setLink (link-ul de șir) this.link = link;  Integer public getAcceptRate () return acceptate;  void public setAcceptRate (Integer acceptRate) this.acceptRate = acceptRate; 

Faceți același lucru pentru un nou produs Articol clasă, copiat de la jsonschema2pojo. 

importați com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; element public de clasă @SerializatName ("proprietar") @ Expune proprietarul de proprietar privat; @SerializedName ("is_accepted") @Expose boolean privat esteAccepted; @SerializedName ("scorul") @Expune scorul privat Integer; @SerializedName ("last_activity_date") @Exploacă integer privat lastActivityDate; @SerializedName ("creation_date") @Expune crearea intregului privatDate; @SerializedName ("answer_id") @Export privat Integer answerId; @SerializedName ("question_id") @Expose private Identificator întrebare; @SerializedName ("last_edit_date") @Exploți întregul privat lastEditDate; public Proprietar getOwner () retur proprietar;  public void setOwner (proprietar de proprietar) this.owner = proprietar;  boolean public getIsAccepted () return este Accepted;  public void setIsAccepted (boolean esteAccepted) this.isAccepted = esteAccepted;  Integer public getScore () return score;  public void setScore (scor întreg) this.score = scor;  Integer public getLastActivityDate () return lastActivityDate;  void public setLastActivityDate (Integer lastActivityDate) this.lastActivityDate = lastActivityDate;  Integer public getCreationDate () return creationDate;  public void setCreationDate (integer createDate) this.creationDate = creationDate;  Integer public getAnswerId () return answerId;  void public setAnswerId (Integer răspunsId) this.answerId = answerId;  Integer public getQuestionId () returnIndicationId;  public void setQuestionId (intrebare interogareId) this.questionId = questionId;  Integer public getLastEditDate () return lastEditDate;  void public setLastEditDate (Integer lastEditDate) this.lastEditDate = lastEditDate; 

În cele din urmă, creați o clasă numităSOAnswersResponse pentru răspunsurile StackOverflow returnate. Veți găsi codul pentru această clasă în jsonschema2pojo as Exemplu. Asigurați-vă că actualizați numele clasei la SOAnswersResponse oriunde se întâmplă. 

importați com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import java.util.List; clasa publică SOAnswersResponse @SerializedName ("items") @ Expune lista privată elemente = null; @SerializedName ("has_more") @Expose boolean privat hasMore; @SerializedName ("backoff") @Exponați privat Integer backoff; @SerializedName ("quota_max") @Exploți cota privată privatăMax; @SerializedName ("quota_remaining") @ Expune cota intregului privatRemaining; Lista publică getItems () return items;  public void setItems (Listă elemente) this.items = items;  boolean public getHasMore () return hasMore;  public void setHasMore (Boolean are mai mult) this.hasMore = hasMore;  Integer public getBackoff () return backoff;  void publice setBackoff (Integer backoff) this.backoff = backoff;  Integer public getQuotaMax () return quotaMax;  void public setQuotaMax (cota întreguluiMax) this.quotaMax = quotaMax;  Integer public getQuotaRemaining () return cotaRemaining;  public void setQuotaRemaining (cota intreguluiRemaining) this.quotaRemaining = cotaRemaining; 

5. Crearea instanței Retrofit

Pentru a emite cereri de rețea către un API REST cu Retrofit, trebuie să creați o instanță folosind Retrofit.Builder clasați și configurați-o cu o adresă URL de bază. 

Creați un nou pachet sub-pachet în interiorul date pachet și numele acestuia la distanta. Acum înăuntru la distanta, creați o clasă Java și denumiți-o RetrofitClient. Această clasă va crea un singleton de Retrofit. Retrofit-ul are nevoie de o adresă URL de bază pentru a-și construi instanța, așa că vom transmite un URL atunci când sunăm RetrofitClient.getClient (String baseUrl). Această adresă URL va fi apoi utilizată pentru a construi instanța în rândul 13. De asemenea, specificăm convertorul JSON de care avem nevoie (Gson) în rândul 14. 

import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; clasa publică RetrofitClient private static Retrofit retrofit = null; public static Retrofit getClient (String baseUrl) if (retrofit == null) retrofit = nou Retrofit.Builder () .baseUrl (baseUrl) .addConverterFactory (GsonConverterFactory.create ()) .build ();  retrofit retur;  

6. Crearea interfeței API

În interiorul pachetului de la distanță, creați o interfață și apelați-o SOService. Această interfață conține metode pe care le vom folosi pentru a executa cereri HTTP, cum ar fi OBȚINEPOSTA PUNE, PLASTURE, și ȘTERGE. Pentru acest tutorial, vom executa a OBȚINE cerere. 

import com.chikeandroid.retrofittutorial.data.model.SOAnswersResponse; import java.util.List; import retrofit2.Call; import retrofit2.http.GET; interfața publică SOService @GET ("/ answers? order = desc & sort = activity & site = stackoverflow") (); obțineți răspunsuri @GET ("/ answers? Order = desc & sort = activity & site = stackoverflow") Apel getAnswers (@Query ("tagged") Etichete de coarde);  

 @OBȚINE adnotarea specifică în mod explicit acest lucru OBȚINE cererea care va fi executată odată ce metoda va fi apelată. Fiecare metodă din această interfață trebuie să aibă o adnotare HTTP care oferă metoda de solicitare și adresa URL relativă. Există cinci adnotări încorporate disponibile: @OBȚINE@POST, @A PUNE@ȘTERGE, și @CAP.

În a doua definiție a metodei, am adăugat un parametru de interogare pentru a filtra datele de pe server. Retrofit are funcția @Query ( "cheie") adnotare de utilizat în loc de codificare greu în punctul final. Valoarea cheie reprezintă numele parametrului din adresa URL. Acesta va fi adăugat la adresa URL de către Retrofit. De exemplu, dacă depășim valoarea "Android" ca argument pentru getAnswers (etichete cu coarde) , adresa URL completă va fi:

https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow&tagged=android

Parametrii metodelor de interfață pot avea următoarele adnotări:

@Cale substituție variabilă pentru obiectivul API
@Query specifică numele cheii de interogare cu valoarea parametrului adnotat
@Corp sarcină utilă pentru apelul POST 
@Antet specifică antetul cu valoarea parametrului adnotat

7. Crearea utilitatilor API

Acum se va crea o clasă de utilități. O vom numi ApiUtils. Această clasă va avea adresa URL de bază ca variabilă statică și va furniza și SOService interfață cu aplicația noastră prin intermediul getSOService () metoda statică.

clasa publică ApiUtils String public static final BASE_URL = "https://api.stackexchange.com/2.2/"; serviciul public static SOService getSOService () întoarcere RetrofitClient.getClient (BASE_URL) .create (SOService.class);  

8. Afișarea la un RecyclerView

Deoarece rezultatele vor fi afișate într-o vizualizare de reciclare, avem nevoie de un adaptor. Următorul fragment de cod afișează AnswersAdapter clasă.

clasa publică AnswersAdapter extinde RecyclerView.Adapter listă privată mItems; Context privat mContext; private PostItemListener mItemListener; clasa publică ViewHolder extinde RecyclerView.ViewHolder implementează View.OnClickListener public TextView titleTv; PostItemListener mItemListener; public ViewHolder (Vizualizare itemView, PostItemListener postItemListener) super (itemView); titluTv = (TextView) itemView.findViewById (android.R.id.text1); this.mItemListener = postItemListener; itemView.setOnClickListener (aceasta);  @Override public void onClick (Vizualizare vizualizare) Item item = getItem (getAdapterPosition ()); this.mItemListener.onPostClick (item.getAnswerId ()); notifyDataSetChanged ();  public AnswersAdapter (context context, lista posturi, PostItemListener itemListener) mItems = posturi; mContext = context; mItemListener = itemListener;  @Override publice AnswersAdapter.ViewHolder onCreateViewHolder (parentă ViewGroup, int viewType) Context context = parent.getContext (); LayoutInflater inflater = LayoutInflater.from (context); Afișați postView = inflater.inflate (android.R.layout.simple_list_item_1, parent, false); ViewHolder viewHolder = ViewHolder nou (postView, this.mItemListener); retur ViewHolder;  @Overide public void onBindViewHolder (Titluri răspunsuriAdapter.ViewHolder, poziție int) Item item = mItems.get (position); TextView textView = titular.titleTv; textView.setText (item.getOwner () getDisplayName ().);  @Override public int getItemCount () retur mItems.size ();  public void updateAnswers (Listă elemente) mItems = elemente; notifyDataSetChanged ();  element privat getItem (int adapterPosition) return mItems.get (adapterPosition);  interfață publică PostItemListener void onPostClick (id lung); 

9. Executarea cererii

În interiorul onCreate () metodă a Activitate principala, inițializăm o instanță a SOService interfața (linia 9), vizualizarea reciclării și, de asemenea, adaptorul. În cele din urmă, numim loadAnswers () metodă. 

 private AnswersAdapter mAdapter; privat RecyclerView mRecyclerView; privat SOService mService; @Override protejate void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); mService = ApiUtils.getSOService (); mRecyclerView = (RecyclerView) findViewById (R.id.rv_answers); mAdapter = new AnswersAdapter (acest nou ArrayList(0), noul AnswersAdapter.PostItemListener () @Override public void onPostClick (id lung) Toast.makeText (MainActivity.this, "ID-ul postului este" + id, Toast.LENGTH_SHORT) .show (); ); RecyclerView.LayoutManager layoutManager = nou LinearLayoutManager (acest lucru); mRecyclerView.setLayoutManager (layoutManager); mRecyclerView.setAdapter (mAdapter); mRecyclerView.setHasFixedSize (true); RecyclerView.ItemDecoration itemDecoration = noua DividerItemDecoration (aceasta, DividerItemDecoration.VERTICAL_LIST); mRecyclerView.addItemDecoration (itemDecoration); loadAnswers (); 

 loadAnswers () metoda face o solicitare de rețea prin apelare Puneți în coadă (). Când răspunsul se întoarce, Retrofit ne ajută să analizăm răspunsul JSON la o listă de obiecte Java. (Acest lucru este posibil prin utilizarea GsonConverter.)  

public void loadAnswers () mService.getAnswers (). enqueue (nou apel invers() @Override public void onResponse (Apel apel, răspuns răspuns) if (answer.isSuccessful ()) mAdapter.updateAnswers (răspuns.body (). getItems ()); Log.d ("MainActivity", "posturi încărcate de la API");  altceva int statusCode = answer.code (); // preluați erorile de solicitare în funcție de codul de stare @Override public void onFailure (Apel apel, Throwable t) showErrorMessage (); Log.d ("MainActivity", "eroare de încărcare din API"); ); 

10. Înțelegerea Puneți în coadă ()

Puneți în coadă () asynchronously trimite cererea și notifică aplicația dvs. cu un apel invers atunci când un răspuns vine înapoi. Deoarece această solicitare este asincronă, Retrofit se ocupă de un fir de fundal astfel încât firul principal al UI să nu fie blocat sau interferat cu.

A folosi Puneți în coadă (), trebuie să implementați două metode de apel invers:

  • onResponse ()
  • onFailure ()

Numai una dintre aceste metode va fi apelată ca răspuns la o cerere dată. 

  • onResponse (): invocat pentru un răspuns HTTP primit. Această metodă este solicitată pentru un răspuns care poate fi gestionat corect, chiar dacă serverul returnează un mesaj de eroare. Deci, dacă primiți un cod de stare de 404 sau 500, această metodă va fi în continuare apelată. Pentru a obține codul de stare pentru a putea face față situațiilor bazate pe acestea, puteți folosi metoda response.code (). De asemenea, puteți utiliza funcția este de succes() pentru a afla dacă codul de stare este în intervalul 200-300, indicând succesul.
  • onFailure (): invocată atunci când a apărut o excepție de rețea care a comunicat serverului sau când a apărut o excepție neașteptată care gestiona solicitarea sau procesarea răspunsului. 

Pentru a efectua o cerere sincronă, puteți utiliza funcția a executa() metodă. Rețineți că metodele sincrone pe firul principal / interfața de utilizare vor bloca orice acțiune a utilizatorului. Deci, nu executați metode sincrone pe firul principal al Android / UI! În schimb, rulați-le pe un fir de fundal.

11. Testarea aplicației

Acum puteți rula aplicația. 

12. Integrarea RxJava

Dacă sunteți un fan al RxJava, puteți implementa cu ușurință Retrofit cu RxJava. În Retrofit 1 a fost integrat în mod implicit, dar în Retrofit 2 trebuie să includeți unele dependențe suplimentare. Retrofitează navele cu un adaptor implicit pentru execuție Apel instanțe. Deci, puteți schimba mecanismul de execuție al Retrofit pentru a include RxJava prin includerea RxJava CallAdapter

Pasul 1

Adăugați dependențele.

compilați 'io.reactivex: rxjava: 1.1.6' compilați 'io.reactivex: rxandroid: 1.2.1' compile 'com.squareup.retrofit2: adapter-rxjava: 2.1.0'

Pasul 2

Adăugați noul CallAdapter RxJavaCallAdapterFactory.create () când construiți o instanță Retrofit.  

public static Retrofit getClient (String baseUrl) if (retrofit == null) retrofit = nou Retrofit.Builder () .baseUrl (baseUrl) .addCallAdapterFactory (RxJavaCallAdapterFactory.create ()) .addConverterFactory (GsonConverterFactory.create ();  retrofit retur; 

Pasul 3

Actualizați acum () obțineți răspunsuri  metode de returnare Observabils:

@GET ("/ answer? Order = desc & sort = activity & site = stackoverflow") Observabil (); obțineți răspunsuri @GET ("/ answer? Order = desc & sort = activity & site = stackoverflow") Observabil getAnswers (@Query ("tagged") Etichete de coarde); 

Pasul 4

Când facem cererile, abonatul nostru anonim răspunde la fluxul observabil care emite evenimente, în cazul nostru SOAnswersResponse.  onNext este apelată atunci când abonatul nostru primește orice eveniment emis, care este apoi trecut la adaptorul nostru. 

@Override public void loadAnswers () mService.getAnswers (). SubscribeOn (Schedulers.io ()) observeOn (AndroidSchedulers.mainThread ()) .subscribe (noul abonat() @Override public void onCompleted ()  @Override public void peError (Throwable e)  @Override publice void onNext (SOAnswersResponse soAnswersResponse) mAdapter.updateAnswers (soAnswersResponse.getItems ()); ); 

Verificați începutul cu ReactiveX pe Android de Ashraff Hathibelagal pentru a afla mai multe despre RxJava și RxAndroid. 

Concluzie

În acest tutorial, ați aflat despre Retrofit: de ce ar trebui să o utilizați și cum. De asemenea, am explicat cum să adăugați integrarea RxJava cu Retrofit. În următoarea mea postare, vă voi arăta cum să efectuați POSTA PUNE, și ȘTERGE, cum să trimită Formular-urlencoded datele și modul de anulare a cererilor. 

Pentru a afla mai multe despre Retrofit, consultați documentația oficială. Între timp, verificați câteva dintre celelalte cursuri și tutoriale ale dezvoltării aplicațiilor Android.

  • Comunicare în cadrul unei aplicații Android cu EventBus

    Greenrobot EventBus este o populară bibliotecă open-source care folosește modelul de publicare / abonare pentru comunicarea dintre componentele sistemului Android. În…
    Chike Mgbemena
    Android
  • Concurs practic pe Android cu HaMeR

    În acest tutorial vom explora cadrul HaMeR (Handler, Message and Runnable), unul dintre cele mai puternice modele de concurență disponibile pe Android. Cu…
    Tin Megali
    Android SDK
  • Android From Scratch: Utilizarea API-urilor REST

    În acest tutorial, vă voi arăta cum să utilizați clasele și metodele disponibile în Android SDK pentru a vă conecta la serverele web la distanță și pentru a interacționa cu ...
    Ashraff Hathibelagal
    Android SDK
  • Începeți cu un șablon de aplicație Android în 60 de secunde

    CodeCanyon are sute de șabloane de aplicații Android pe care le puteți utiliza pentru a vă lansa dezvoltarea. Acest videoclip vă va arăta cum să instalați și să personalizați ...
    Ashraff Hathibelagal
    Android SDK
Cod