1. التعديل التحديثي
1.1. ما هو التعديل التحديثي؟
التحديث التحديثي هو عميل REST لـ Java و Android. إنه يسهل الحصول على JSON (أو البيانات المنظمة الأخرى) وتحميلها من خلال خدمة الويب المستندة إلى REST. في التحديث التحديثي ، تقوم بتكوين المحول المستخدم لإجراء تسلسل للبيانات. عادةً ما يتم استخدام GSon في JSON ، ولكن يمكنك إضافة محولاتك الخاصة لمعالجة XML أو البروتوكولات الأخرى. يستخدم التحديثية مكتبة OkHttp لطلبات HTTP.
يمكنك إنشاء كائنات Java المستندة إلى JSON باستخدام الأداة التالية: www.jsonschema2pojo.org يمكن أن يكون هذا مفيدًا لإنشاء هياكل بيانات Java معقدة من JSON الحالي.
1.2. باستخدام التحديثية
للعمل مع التحديث التحديثي ، ستحتاج إلى الفئات الثلاثة التالية:
- فئة الطراز المستخدمة كنموذج JSON
- الواجهات التي تحدد عمليات HTTP المحتملة
- إن فئة Retrofit.Builder هي مثيل يستخدم واجهة Builder وواجهة برمجة التطبيقات (API) لتحديد تعريف نقطة نهاية عنوان URL لعمليات HTTP.
تمثل كل طريقة واجهة واحدة من مكالمات API المحتملة. يجب أن يحتوي على تعليق توضيحي HTTP (GET و POST وما إلى ذلك) للإشارة إلى نوع الطلب وعنوان URL النسبي. تكمل القيمة المرتجعة الاستجابة في عنصر الاستدعاء بنوع النتيجة المتوقعة.
@GET("users") Call<List<User>> getUsers();
يمكنك استخدام الكتل البديلة ومعلمات الاستعلام لتكوين عنوان URL. يتم إلحاق كتلة الاستبدال بعنوان URL النسبي باستخدام {}. باستخدام التعليق التوضيحي @ Path لمعلمة أسلوب ، فإن قيمة هذه المعلمة مرتبطة بكتلة استبدال معينة.
@GET("users/{name}/commits") Call<List<Commit>> getCommitsByName(@Path("name") String name);
تتم إضافة معلمات الاستعلام باستخدام التعليق التوضيحي @ Query إلى معلمة الأسلوب. تتم إضافتها تلقائيًا في نهاية عنوان URL.
@GET("users") Call<User> getUserById(@Query("id") Integer id);
يُشير التعليق التوضيحي @ Body إلى معلمة الأسلوب Retrofit إلى استخدام الكائن كنص طلب للاتصال به.
@POST("users") Call<User> postUser(@Body User user)
2. المتطلبات الأساسية
تستخدم الأمثلة التالية Eclipse IDE مع نظام بناء Gradle.
يفترض هذا التمرين أنك معتاد على
Gradle واستخدام Gradle مع Eclipse .
تفعل بيئات التطوير الأخرى ، مثل Visual Studio Code أو IntelliJ ، نفس الشيء ، حتى تتمكن من استخدام أداتك المفضلة.
3. التمرين: أول عميل التحديثية
ستقوم في هذا التمرين بإنشاء عميل REST مستقل. يتم إنشاء الردود بواسطة خادم Mock.
3.1. إنشاء وإنشاء مشروع
إنشاء مشروع Gradle جديد يسمى com.vogella.retrofitgerrit. أضف حزمة جديدة إلى src / main / java باسم com.vogella.retrofitgerrit.
أضف التبعيات التالية إلى ملف build.gradle.
// retrofit implementation 'com.squareup.retrofit2:retrofit:2.1.0' implementation 'com.squareup.retrofit2:converter-gson:2.1.0' // Junit testImplementation("org.junit.jupiter:junit-jupiter-api:5.0.0") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0") // to run JUnit 3/4 tests: testImplementation("junit:junit:4.12") testRuntime("org.junit.vintage:junit-vintage-engine:4.12.0")
3.2. تحديد واجهة برمجة التطبيقات ومحول التحديث التحديثي
في رد جيريت JSON ، نحن مهتمون فقط بمسألة التغييرات. لذلك ، قم بإنشاء فئة البيانات التالية في الحزمة الافتراضية المضافة مسبقًا.
package com.vogella.java.retrofitgerrit; public class Change { String subject; public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } }
حدد REST API لـ Retrofit من خلال الواجهة التالية.
package com.vogella.java.retrofitgerrit; import java.util.List; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; public interface GerritAPI { @GET("changes/") Call<List<Change>> loadChanges(@Query("q") String status); }
قم بإنشاء فئة وحدة التحكم التالية. تقوم هذه الفئة بإنشاء عميل Retrofit ، واستدعاء Gerrit API ومعالجة النتيجة (طباعة نتيجة المكالمة إلى وحدة التحكم).
package com.vogella.java.retrofitgerrit; import java.util.List; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class Controller implements Callback<List<Change>> { static final String BASE_URL = "https://git.eclipse.org/r/"; public void start() { Gson gson = new GsonBuilder() .setLenient() .create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); GerritAPI gerritAPI = retrofit.create(GerritAPI.class); Call<List<Change>> call = gerritAPI.loadChanges("status:open"); call.enqueue(this); } @Override public void onResponse(Call<List<Change>> call, Response<List<Change>> response) { if(response.isSuccessful()) { List<Change> changesList = response.body(); changesList.forEach(change -> System.out.println(change.subject)); } else { System.out.println(response.errorBody()); } } @Override public void onFailure(Call<List<Change>> call, Throwable t) { t.printStackTrace(); } }
قم بإنشاء فصل دراسي باستخدام طريقة رئيسية لبدء وحدة التحكم.
package com.vogella.java.retrofitgerrit; public class Main { public static void main(String[] args) { Controller controller = new Controller(); controller.start(); } }
4. المحولات التحديثية والمحولات
4.1. محولات التحديثية
يمكن تكوين التحديثية لاستخدام محول معين. يعالج هذا المحول (de) تسلسل البيانات. العديد من المحولات متاحة بالفعل لمختلف تنسيقات التسلسل.
- للتحويل إلى JSON والعكس بالعكس:
- Gson: com.squareup.retrofit: محول- gson
- جاكسون: com.squareup.retrofit: محول جاكسون
- موشي: com.squareup.retrofit: محول موشي
- للتحويل إلى Buffers Protocol والعكس بالعكس:
- Protobuf: com.squareup.retrofit: محول-protobuf
- الأسلاك: com.squareup.retrofit: محول الأسلاك
- للتحويل إلى XML والعكس بالعكس:
- XML بسيط: com.squareup.retrofit: محول - simplexml
بالإضافة إلى المحولات المدرجة ، يمكنك أيضًا إنشاء بروتوكولك الخاص لمعالجة البروتوكولات الأخرى عن طريق توسيع فئة Converter.Factory.
4.2. محولات التعديل التحديثي
يمكن أيضًا توسيع التحديثية باستخدام محولات للتفاعل مع مكتبات أخرى مثل RxJava 2.x و Java 8 و Guava.
يمكن العثور على نظرة عامة على المحولات المتوفرة في
مربع Github
/ التحديثية / التعديل التحديثي / .
على سبيل المثال ، يمكن توصيل محول RxJava 2.x باستخدام Gradle:
compile 'com.squareup.retrofit2:adapter-rxjava2:latest.version'
أو باستخدام Apache Maven:
<dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>adapter-rxjava2</artifactId> <version>latest.version</version> </dependency>
لإضافة محول ، يجب عليك استخدام طريقة retrofit2.Retrofit.Builder.addCallAdapterFactory (Factory).
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.example.com/") .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build();
باستخدام هذا المحول ، يمكن لواجهات التعديل التحديثي أن تُرجع أنواع RxJava 2.x ، على سبيل المثال ، يمكن ملاحظته أو تدفقه أو مفرد ، إلخ.
@GET("users") Observable<List<User>> getUsers();
5. المصادقة التحديثية
التعديل التحديثي يدعم مكالمات API التي تتطلب المصادقة. يمكن إجراء المصادقة باستخدام اسم مستخدم وكلمة مرور (مصادقة Http Basic) أو رمز API مميز.
هناك طريقتان لإدارة المصادقة. الطريقة الأولى هي إدارة رأس الطلب باستخدام التعليقات التوضيحية. طريقة أخرى هي استخدام اعتراض OkHttp لهذا الغرض.
5.1. المصادقة مع التعليقات التوضيحية
افترض أنك تريد طلب معلومات حول مستخدم مطلوب مصادقته. يمكنك القيام بذلك عن طريق إضافة معلمة جديدة إلى تعريف API ، على سبيل المثال:
@GET("user") Call<UserDetails> getUserDetails(@Header("Authorization") String credentials)
باستخدام التعليق التوضيحي @ Header ("التفويض") ، فإنك تطلب من Retrofit إضافة رأس التفويض إلى الطلب بالقيمة التي تمررها.
لإنشاء بيانات اعتماد للمصادقة الأساسية ، يمكنك استخدام فئة OkHttps Credentials مع أسلوبها الأساسي (سلسلة ، سلسلة). تقبل الطريقة اسم مستخدم وكلمة مرور وتعيد بيانات اعتماد المصادقة لنظام Http Basic.
Credentials.basic("ausername","apassword");
إذا كنت تريد استخدام الرمز المميز لواجهة برمجة التطبيقات ولا تستخدم المخطط الأساسي ، فما عليك سوى استدعاء طريقة getUserDetails (السلسلة) باستخدام الرمز المميز الخاص بك.
5.2. المصادقة مع اعتراضات OkHttp.
لا تضيف الطريقة أعلاه بيانات الاعتماد إلا إذا طلبت بيانات المستخدم. إذا كان لديك المزيد من المكالمات التي تتطلب المصادقة ، يمكنك استخدام اعتراض للقيام بذلك. يتم استخدام المعترض لتعديل كل طلب قبل تنفيذه وتعيين رأس الطلب. الميزة هي أنك لا تحتاج إلى إضافة @ Header ("التفويض") إلى كل تعريف لطريقة API.
لإضافة اعتراض ، يجب عليك استخدام أسلوب okhttp3.OkHttpClient.Builder.addInterceptor (اعتراض) في OkHttp Builder.
OkHttpClient okHttpClient = new OkHttpClient().newBuilder().addInterceptor(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request.Builder builder = originalRequest.newBuilder().header("Authorization", Credentials.basic("aUsername", "aPassword")); Request newRequest = builder.build(); return chain.proceed(newRequest); } }).build();
يجب إضافة العميل الذي تم إنشاؤه بواسطة OkHttp إلى عميل Retrofit باستخدام طريقة retrofit2.Retrofit.Builder.client (OkHttpClient).
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.example.com/") .client(okHttpClient) .build();
كما لاحظت ، يتم استخدام فئة بيانات الاعتماد هنا للحصول على التفويض الأساسي.
مرة أخرى ، إذا كنت تريد استخدام الرمز المميز لواجهة برمجة التطبيقات ، فما عليك سوى استخدام الرمز المميز بدلاً من ذلك.
6. تمرين: استخدام التعديل التحديثي لطلب Gerrit في Java
يصف القسم التالي كيفية إنشاء تطبيق Java صغير يستخدم Retrofit لاسترداد كائنات التغيير المفتوحة من Gerrit API. تتم طباعة النتائج في وحدة التحكم.
6.1. إنشاء وإنشاء مشروع
يفترض هذا التمرين أنك معتاد على
Gradle و
Buildship for Eclipse .
إنشاء مشروع Gradle جديد يسمى com.vogella.java.retrofitgerrit. أضف حزمة جديدة إلى src / main / java باسم com.vogella.java.retrofitgerrit.
أضف التبعيات التالية إلى ملف build.gradle.
// retrofit implementation 'com.squareup.retrofit2:retrofit:2.1.0' implementation 'com.squareup.retrofit2:converter-gson:2.1.0' // Junit testImplementation("org.junit.jupiter:junit-jupiter-api:5.0.0") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0") // to run JUnit 3/4 tests: testImplementation("junit:junit:4.12") testRuntime("org.junit.vintage:junit-vintage-engine:4.12.0")
6.2. تحديد واجهة برمجة التطبيقات ومحول التحديث التحديثي
في رد جيريت JSON ، نحن مهتمون فقط بمسألة التغييرات. لذلك ، قم بإنشاء فئة البيانات التالية في الحزمة الافتراضية المضافة مسبقًا.
package com.vogella.java.retrofitgerrit; public class Change { String subject; public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } }
حدد REST API لـ Retrofit باستخدام الواجهة التالية.
package com.vogella.java.retrofitgerrit; import java.util.List; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; public interface GerritAPI { @GET("changes/") Call<List<Change>> loadChanges(@Query("q") String status); }
قم بإنشاء فئة وحدة التحكم التالية. تقوم هذه الفئة بإنشاء عميل Retrofit ، واستدعاء Gerrit API ، ومعالجة النتيجة (طباعة نتيجة المكالمة إلى وحدة التحكم).
package com.vogella.java.retrofitgerrit; import java.util.List; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class Controller implements Callback<List<Change>> { static final String BASE_URL = "https://git.eclipse.org/r/"; public void start() { Gson gson = new GsonBuilder() .setLenient() .create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); GerritAPI gerritAPI = retrofit.create(GerritAPI.class); Call<List<Change>> call = gerritAPI.loadChanges("status:open"); call.enqueue(this); } @Override public void onResponse(Call<List<Change>> call, Response<List<Change>> response) { if(response.isSuccessful()) { List<Change> changesList = response.body(); changesList.forEach(change -> System.out.println(change.subject)); } else { System.out.println(response.errorBody()); } } @Override public void onFailure(Call<List<Change>> call, Throwable t) { t.printStackTrace(); } }
قم بإنشاء فصل دراسي باستخدام طريقة رئيسية لبدء وحدة التحكم.
package com.vogella.java.retrofitgerrit; public class Main { public static void main(String[] args) { Controller controller = new Controller(); controller.start(); } }
7. التمرين: استخدام التعديل التحديثي لتحويل استجابة XML من موجز RSS
يصف هذا القسم كيفية استخدام التحديث التحديثي لتحويل استجابة XML باستخدام SimpleXMLConverter.
يتم إنشاء تطبيق Java صغير يطلب خلاصة Vogella RSS (
http://vogella.com/article.rss ) ويطبع اسم القناة وروابط العنوان والمقال.
7.1. إنشاء وإنشاء مشروع
يفترض هذا التمرين أنك معتاد على
Gradle و
Buildship for Eclipse .
أنشئ مشروع Gradle جديدًا باسم com.vogella.java.retrofitxml. أضف حزمة جديدة إلى src / main / java باسم com.vogella.java.retrofitxml.
أضف التبعيات التالية إلى ملف build.gradle.
implementation 'com.squareup.retrofit2:retrofit:2.1.0' implementation 'com.squareup.retrofit2:converter-simplexml:2.1.0'
7.2. تعريف طريقة عرض XML
موجز RSS كما يلي:
<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0"> <channel> <title>Eclipse and Android Information</title> <link>http://www.vogella.com</link> <description>Eclipse and Android Information</description> <language>en</language> <copyright>Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Germany (CC BY-NC-SA 3.0)</copyright> <pubDate>Tue, 03 May 2016 11:46:11 +0200</pubDate> <item> <title>Android user interface testing with Espresso - Tutorial</title> <description> This tutorial describes how to test Android applications with the Android Espresso testing framework.</description> <link>http://www.vogella.com/tutorials/AndroidTestingEspresso/article.html</link> <author>lars.vogel@vogella.com (Lars Vogel)</author> <guid>http://www.vogella.com/tutorials/AndroidTestingEspresso/article.html</guid> </item> <item> <title>Using the Gradle build system in the Eclipse IDE - Tutorial</title> <description>This article describes how to use the Gradle tooling in Eclipse</description> <link>http://www.vogella.com/tutorials/EclipseGradle/article.html</link> <author>lars.vogel@vogella.com (Lars Vogel)</author> <guid>http://www.vogella.com/tutorials/EclipseGradle/article.html</guid> </item> <item> <title>Unit tests with Mockito - Tutorial</title> <description>This tutorial explains testing with the Mockito framework for writting software tests.</description> <link>http://www.vogella.com/tutorials/Mockito/article.html</link> <author>lars.vogel@vogella.com (Lars Vogel)</author> <guid>http://www.vogella.com/tutorials/Mockito/article.html</guid> </item> </channel> </rss>
بالإضافة إلى رأس XML ، يتكون هذا الملف من عناصر XML مختلفة. يحتوي عنصر RSS على عنصر قناة يحتوي على عناصر أخرى (على سبيل المثال ، العنوان والوصف و pubDate) وعناصر عناصر متعددة (مقالات).
قم بإنشاء فئتي البيانات التاليتين: RSSFeed و Article.
package com.vogella.java.retrofitxml; import org.simpleframework.xml.Element; import org.simpleframework.xml.Root; @Root(name = "item", strict = false) public class Article { @Element(name = "title") private String title; @Element(name = "link") private String link; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getLink() { return link; } public void setLink(String link) { this.link = link; } }
package com.vogella.java.retrofitxml; import java.util.List; import org.simpleframework.xml.Element; import org.simpleframework.xml.ElementList; import org.simpleframework.xml.Path; import org.simpleframework.xml.Root; @Root(name="rss", strict=false) public class RSSFeed { @Element(name="title") @Path("channel") private String channelTitle; @ElementList(name="item", inline=true) @Path("channel") private List<Article> articleList; public String getChannelTitle() { return channelTitle; } public void setChannelTitle(String channelTitle) { this.channelTitle = channelTitle; } public List<Article> getArticleList() { return articleList; } public void setArticleList(List<Article> articleList) { this.articleList = articleList; } }
تمثل فئة المقالة مقالًا واحدًا وتحتفظ فقط بالعنوان والارتباط بالمقال. هذه هي المجالات الوحيدة التي تهمنا.
يضع التعليق التوضيحي @ Root علامة على الفصل الدراسي على أنه يخضع للتسلسل (de). بشكل اختياري ، يمكنك تحديد اسم في التعليق التوضيحي @ Root يطابق اسم عنصر XML. إذا لم يتم تحديد اسم ، فسيتم استخدام اسم الفئة كاسم عنصر XML. نظرًا لأن اسم الفئة (RSSFeed) يختلف عن اسم عنصر XML (rss) ، نحتاج إلى تحديد الاسم.
عند ضبط صارم على خطأ ، يتم تعطيل التحليل الصارم. يخبر هذا المحلل اللغوي بعدم مقاطعة أو طرح استثناء إذا تم العثور على عنصر XML أو سمة لا يتم توفير تعيين لها. نظرًا لأن عنصر rss له سمة إصدار لا يوجد حقل مقابل لها ، فسينشئ التطبيق خطأ إذا لم يتم تعيين المعلمة الصارمة على خطأ.
باستخدام التعليق التوضيحي @ Element ، يتم تمثيل عنصر XML. إذا لزم الأمر ، يمكنك تحديد اسم XML للعنصر الذي يمثله هذا الحقل. إذا لم يتم تحديد اسم ، فسيتم استخدام اسم الحقل.
تم وضع علامة على حقل articleList بـ @ ElementList. يوضح هذا أن هذا الحقل يستخدم لتخزين مجموعة (في حالتنا: قائمة) من عناصر XML التي تحمل نفس الاسم. عندما يتم تعيين inline على true ، هذا يعني أن العناصر الموجودة في المجموعة يتم سردها واحدة تلو الأخرى مباشرة داخل العنصر المحدد وليس لها أصل وسيط.
باستخدام التعليق التوضيحي @ Path ، يمكنك تحديد المسار إلى عنصر XML داخل شجرة XML. يفيد ذلك إذا كنت لا تريد تصميم شجرة XML كاملة باستخدام كائنات Java. بالنسبة لاسم القناة وعناصر العناصر المتعددة ، يمكننا الإشارة مباشرة إلى عناصر محددة في عنصر القناة.
7.3. تحديد واجهة برمجة التطبيقات ومحول التحديث
حدد REST API لـ Retrofit من خلال الواجهة التالية.
package com.vogella.java.retrofitxml; import retrofit2.Call; import retrofit2.http.GET; public interface VogellaAPI { @GET("article.rss") Call<RSSFeed> loadRSSFeed(); }
قم بإنشاء فئة وحدة التحكم التالية. تقوم هذه الفئة بإنشاء عميل Retrofit ، واستدعاء Vogella API ، ومعالجة النتيجة.
package com.vogella.java.retrofitxml; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.simplexml.SimpleXmlConverterFactory; public class Controller implements Callback<RSSFeed> { static final String BASE_URL = "http://vogella.com/"; public void start() { Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL) .addConverterFactory(SimpleXmlConverterFactory.create()).build(); VogellaAPI vogellaAPI = retrofit.create(VogellaAPI.class); Call<RSSFeed> call = vogellaAPI.loadRSSFeed(); call.enqueue(this); } @Override public void onResponse(Call<RSSFeed> call, Response<RSSFeed> response) { if (response.isSuccessful()) { RSSFeed rss = response.body(); System.out.println("Channel title: " + rss.getChannelTitle()); rss.getArticleList().forEach( article -> System.out.println("Title: " + article.getTitle() + " Link: " + article.getLink())); } else { System.out.println(response.errorBody()); } } @Override public void onFailure(Call<RSSFeed> call, Throwable t) { t.printStackTrace(); } }
الخطوة الأخيرة هي إنشاء فئة باستخدام طريقة رئيسية لبدء وحدة التحكم.
package com.vogella.java.retrofitxml; public class Application { public static void main(String[] args) { Controller ctrl = new Controller(); ctrl.start(); } }
8. التمرين: إنشاء تطبيق لطلب StackOverflow
StackOverflow هو موقع شائع للمشكلات المتعلقة بالبرمجة. كما يوفر أيضًا واجهة برمجة تطبيقات REST ، موثقة جيدًا في
صفحة Stackoverflow API .
في هذا التمرين ، ستستخدم مكتبة REST Retrofit. ستستخدمه للاستعلام عن أسئلة علامة StackOverflow وإجاباتها.
في مثالنا ، نستخدم عنوان URL للطلب التالي. افتح عنوان URL هذا في متصفح وانظر إلى الإجابة.
https://api.stackexchange.com/2.2/search?order=desc&sort=votes&tagged=android&site=stackoverflow
8.1. إنشاء وإنشاء مشروع
قم بإنشاء تطبيق Android يسمى com.vogella.android.stackoverflow. استخدم com.vogella.android.stackoverflow كاسم حزمة المستوى الأعلى.
أضف التبعيات التالية إلى ملف build.gradle.
compile "com.android.support:recyclerview-v7:25.3.1" compile 'com.google.code.gson:gson:2.8.1'
8.2. إنشاء نموذج بيانات
نحن مهتمون بأسئلة وأجوبة من Stackoverflow. لهذا الغرض ، قم بإنشاء فئتي البيانات التاليتين.
package com.vogella.android.stackoverflow; import com.google.gson.annotations.SerializedName; public class Question { public String title; public String body; @SerializedName("question_id") public String questionId; @Override public String toString() { return(title); } }
package com.vogella.android.stackoverflow; import com.google.gson.annotations.SerializedName; public class Answer { @SerializedName("answer_id") public int answerId; @SerializedName("is_accepted") public boolean accepted; public int score; @Override public String toString() { return answerId + " - Score: " + score + " - Accepted: " + (accepted ? "Yes" : "No"); } }
8.3. إنشاء النشاط والتخطيط
قم بتعيين Activity_main.xml لنشاطك.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="8dp" android:orientation="vertical" tools:context="com.vogella.android.stackoverflow.MainActivity"> <Spinner android:id="@+id/questions_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" /> <android.support.v7.widget.RecyclerView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <Button android:id="@+id/authenticate_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="onClick" android:text="Authenticate" /> </LinearLayout>
قم بإضافة فئة عرض إعادة تدوير المحول المسمى RecyclerViewAdapter إلى مشروعك.
تنفيذ واحد ممكن على النحو التالي.
package com.vogella.android.stackoverflow; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.List; public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> { private List<Answer> data; public class ViewHolder extends RecyclerView.ViewHolder { public TextView text; public ViewHolder(View v) { super(v); text = (TextView) v.findViewById(android.R.id.text1); } } public RecyclerViewAdapter(List<Answer> data) { this.data = data; } @Override public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v; v = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_selectable_list_item, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(RecyclerViewAdapter.ViewHolder holder, int position) { Answer answer = ((Answer) data.get(position)); holder.text.setText(answer.toString()); holder.itemView.setTag(answer.answerId); } @Override public int getItemCount() { return data.size(); } }
تعديل فئة MainActivity على النحو التالي:
package com.vogella.android.stackoverflow; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.Toast; import java.util.ArrayList; import java.util.List; public class MainActivity extends Activity implements View.OnClickListener { private String token; private Button authenticateButton; private Spinner questionsSpinner; private RecyclerView recyclerView; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); questionsSpinner = (Spinner) findViewById(R.id.questions_spinner); questionsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "Spinner item selected", Toast.LENGTH_LONG).show(); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); authenticateButton = (Button) findViewById(R.id.authenticate_button); recyclerView = (RecyclerView) findViewById(R.id.list); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this)); } @Override protected void onResume() { super.onResume(); if (token != null) { authenticateButton.setEnabled(false); } } @Override public void onClick(View v) { switch (v.getId()) { case android.R.id.text1: if (token != null) {
8.4. استخدام مزود بيانات مزيف
قم بإنشاء مزود بيانات مزيف واملأ الدوار بالأسئلة المزيفة ونظرة عامة على إعادة التدوير بإجابات وهمية (بعد تغيير التحديد في الدوار).
package com.vogella.android.stackoverflow; import java.util.ArrayList; import java.util.List; public class FakeDataProvider { public static List<Question> getQuestions(){ List<Question> questions = new ArrayList<>(); for (int i = 0; i<10; i++) { Question question = new Question(); question.questionId = String.valueOf(i); question.title = String.valueOf(i); question.body = String.valueOf(i) + "Body"; questions.add(question); } return questions; } public static List<Answer> getAnswers(){ List<Answer> answers = new ArrayList<>(); for (int i = 0; i<10; i++) { Answer answer = new Answer(); answer.answerId = i; answer.accepted = false; answer.score = i; answers.add(answer); } return answers; } }
الآن تكوين الدوار والنظرة العامة لاستخدام هذه البيانات المزيفة.
package com.vogella.android.stackoverflow; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.Toast; import java.util.List; public class MainActivity extends Activity implements View.OnClickListener { private String token; private Button authenticateButton; private Spinner questionsSpinner; private RecyclerView recyclerView; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); questionsSpinner = (Spinner) findViewById(R.id.questions_spinner); questionsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "Spinner item selected", Toast.LENGTH_LONG).show(); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); List<Question> questions = FakeDataProvider.getQuestions(); ArrayAdapter<Question> arrayAdapter = new ArrayAdapter<Question>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, questions); questionsSpinner.setAdapter(arrayAdapter); authenticateButton = (Button) findViewById(R.id.authenticate_button); recyclerView = (RecyclerView) findViewById(R.id.list); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this)); List<Answer> answers = FakeDataProvider.getAnswers(); RecyclerViewAdapter adapter = new RecyclerViewAdapter(answers); recyclerView.setAdapter(adapter); } @Override protected void onResume() { super.onResume(); if (token != null) { authenticateButton.setEnabled(false); } } @Override public void onClick(View v) { switch (v.getId()) { case android.R.id.text1: if (token != null) {
8.5. إضافة تبعيات وأذونات Gradle
أضف التبعيات التالية إلى ملف build.gradle. implementation 'com.squareup.retrofit2:retrofit:2.1.0' implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
إضافة إذن للوصول إلى الإنترنت في البيان. <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.vogella.android.stackoverflow"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
8.6. تحديد واجهة برمجة التطبيقات ومحول التحديث
تلتف Stackoverflow API بالإجابات أو الأسئلة في عنصر JSON المسمى العناصر. للتعامل مع هذا ، قم بإنشاء فئة البيانات التالية المسماة ListWrapper. يعد ذلك ضروريًا لمعالجة مجمّع عناصر Stackoverflow. تأخذ هذه الفئة معلمة نوع وتحزم ببساطة قائمة بالكائنات من هذا النوع. package com.vogella.android.stackoverflow; import java.util.List; public class ListWrapper<T> { List<T> items; }
حدد REST API لـ Retrofit من خلال الواجهة التالية. package com.vogella.android.stackoverflow; import java.util.List; import okhttp3.ResponseBody; import retrofit2.http.Field; import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.Call; public interface StackOverflowAPI { String BASE_URL = "https://api.stackexchange.com/"; @GET("/2.2/questions?order=desc&sort=votes&site=stackoverflow&tagged=android&filter=withbody") Call<ListWrapper<Question>> getQuestions(); @GET("/2.2/questions/{id}/answers?order=desc&sort=votes&site=stackoverflow") Call<ListWrapper<Answer>> getAnswersForQuestion(@Path("id") String questionId); }
8.7. إعداد النشاط
قم بتغيير رمز MainActivity كما يلي. package com.vogella.android.stackoverflow; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.util.ArrayList; import java.util.List; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class MainActivity extends Activity implements View.OnClickListener { private StackOverflowAPI stackoverflowAPI; private String token; private Button authenticateButton; private Spinner questionsSpinner; private RecyclerView recyclerView; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); questionsSpinner = (Spinner) findViewById(R.id.questions_spinner); questionsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { Question question = (Question) parent.getAdapter().getItem(position); stackoverflowAPI.getAnswersForQuestion(question.questionId).enqueue(answersCallback); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); authenticateButton = (Button) findViewById(R.id.authenticate_button); recyclerView = (RecyclerView) findViewById(R.id.list); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this)); createStackoverflowAPI(); stackoverflowAPI.getQuestions().enqueue(questionsCallback); } @Override protected void onResume() { super.onResume(); if (token != null) { authenticateButton.setEnabled(false); } } private void createStackoverflowAPI() { Gson gson = new GsonBuilder() .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(StackOverflowAPI.BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); stackoverflowAPI = retrofit.create(StackOverflowAPI.class); } @Override public void onClick(View v) { switch (v.getId()) { case android.R.id.text1: if (token != null) {
8.8. اختياري: الحصول على صورة ملف تعريف المستخدم
قم بتغيير تخطيط الخطوط في عرض إعادة التدوير لعرض صورة ملف تعريف المستخدم أيضًا. قم بتوسيع نموذج البيانات الخاص بك للحصول على صورة الملف الشخصي للمستخدم الذي أجاب على السؤال. أضف ImageView إلى خطوط التخطيط واستخدم مكتبة Glide لتحميل الصورة.8.9. اختياري: استخدم تخطيطات مختلفة للخطوط الزوجية والفردية
قم بتغيير تنفيذ المحول لاستخدام تخطيطات مختلفة للخطوط الزوجية والفردية.يتطلب هذا إنشاء تخطيطات مختلفة بناءً على نوع البيانات. استخدم getItemViewType () في المحول.8.10. اختياري: معالجة أخطاء الشبكة
إذا واجهت فشلًا في الشبكة ، فأظهر زر إعادة الطلب بدلاً من واجهة المستخدم الرئيسية.9. التمرين: استخدام التحديث التحديثي للوصول إلى واجهة برمجة تطبيقات GitHub على Android
يصف هذا التمرين كيفية سرد جميع مستودعات GitHub لمستخدم في تطبيق Android باستخدام Retrofit. يمكنك تحديد المستودع من القائمة المنسدلة وتحديد المناقشات المتعلقة بالمستخدم للمستودع المحدد.ثم يمكنك تحديد مناقشة من الحقل المنسدل الإضافي ونشر تعليق عليها. سيتم استخدام DialogFragment لإدخال بيانات الاعتماد للمصادقة.تأكد من أن لديك حساب Github ، لأن هذا ضروري لهذا التمرين. نظرًا لأنه سيتم استخدام التعديل التحديثي مع RxJava2 خلال هذا التمرين ، انتبه أيضًا إلى دروس RxJava2 .9.1. إعداد المشروع
قم بإنشاء تطبيق Android يسمى Retrofit Github. استخدم com.vogella.android.retrofitgithub كاسم حزمة المستوى الأعلى واستخدم نموذجًا فارغًا. تأكد من تحديد علامة "التوافق مع الإصدارات السابقة".لاستخدام Retrofit و RxJava2 CallAdapter ، أضف التبعيات التالية إلى ملف build.gradle implementation 'com.squareup.retrofit2:retrofit:2.3.0' implementation 'com.squareup.retrofit2:converter-gson:2.3.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
إضافة إذن للوصول إلى الإنترنت في البيان. <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.vogella.android.retrofitgithub"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.vogella.android.retrofitgithub.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
9.2. تعريف API
قم بإنشاء فئتي البيانات التاليتين: GithubIssue و GithubRepo. package com.vogella.android.retrofitgithub; import com.google.gson.annotations.SerializedName; public class GithubIssue { String id; String title; String comments_url; @SerializedName("body") String comment; @Override public String toString() { return id + " - " + title; } }
package com.vogella.android.retrofitgithub; public class GithubRepo { String name; String owner; String url; @Override public String toString() { return(name + " " + url); } }
من معلومات المستودع ، سيتم عرض اسم المستودع وعنوان URL فقط في القائمة المنسدلة. كما نضيف المالك إلى فئة البيانات ، حيث يلزم اسم المالك لطلب المناقشات لاحقًا.نعرض فقط معرف وعنوان المناقشة في الحقل المنسدل ، لذلك نقوم بإنشاء حقل لكل واحد منهم. بالإضافة إلى ذلك ، تحتوي استجابة Github على عنوان URL لنشر تعليق ، والذي يتم تخزينه في حقل comments_url. لنشر تعليق جديد على واجهة برمجة تطبيقات Github لاحقًا ، أضف حقلاً يسمى التعليق. جيثب أبييشير إلى أن محتويات التعليق يجب أن تكون مرتبطة بحقل يسمى نصًا في طلب JSON. نظرًا لأن Retrofit (de) يقوم بتسلسل جميع الحقول استنادًا إلى اسمها ، وبما أننا لا نريد استخدام النص كاسم الحقل في فئة GithubIssue ، فإننا نستخدم التعليق التوضيحيSerializedName. باستخدام هذا التعليق التوضيحي ، يمكننا تغيير الاسم الذي يتم ترتيب الحقل (de) به إلى JSON.لسوء الحظ ، فإن فئة GithubRepo ليست كافية لطلب جميع المعلومات الضرورية حول المستودع. كما ترون هنا، مالك المستودع هو كائن JSON منفصل في استجابة المستودع ، وبالتالي يحتاج عادةً إلى فئة Java المناسبة (de) لإجراء تسلسل. لحسن الحظ ، يتيح لك التحديث التحديثي إضافة JSONDeserializer الخاص بك المكتوب للتحكم في إلغاء تسلسل نوع معين. في كل مرة يحتاج الأمر إلى إلغاء تسلسل كائن من نوع معين ، يتم استخدام أداة إلغاء التسلسل المخصصة هذه.للقيام بذلك ، قم بإضافة فئة GithubRepoDeserialzer التالية. package com.vogella.android.retrofitgithub; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import java.lang.reflect.Type; public class GithubRepoDeserializer implements JsonDeserializer<GithubRepo> { @Override public GithubRepo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { GithubRepo githubRepo = new GithubRepo(); JsonObject repoJsonObject = json.getAsJsonObject(); githubRepo.name = repoJsonObject.get("name").getAsString(); githubRepo.url = repoJsonObject.get("url").getAsString(); JsonElement ownerJsonElement = repoJsonObject.get("owner"); JsonObject ownerJsonObject = ownerJsonElement.getAsJsonObject(); githubRepo.owner = ownerJsonObject.get("login").getAsString(); return githubRepo; } }
حدد REST API لـ Retrofit من خلال الواجهة التالية: package com.vogella.android.retrofitgithub; import java.util.List; import io.reactivex.Single; import okhttp3.ResponseBody; import retrofit2.http.Body; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.http.Url; public interface GithubAPI { String ENDPOINT = "https://api.github.com"; @GET("user/repos?per_page=100") Single<List<GithubRepo>> getRepos(); @GET("/repos/{owner}/{repo}/issues") Single<List<GithubIssue>> getIssues(@Path("owner") String owner, @Path("repo") String repository); @POST Single<ResponseBody> postComment(@Url String url, @Body GithubIssue issue); }
قد يكون لديك سؤال حول التعليق التوضيحي @ Url. باستخدام هذا التعليق التوضيحي ، يمكننا تحديد عنوان URL لهذا الطلب. يسمح لنا هذا بتغيير عنوان URL لكل طلب ديناميكيًا. نحتاج هذا في مجال comments_url لفئة GithubIssue.تربط التعليقات التوضيحية للمسار @ قيمة المعلمة بالمتغير المقابل (الأقواس المتعرجة) في عنوان URL للطلب. يعد ذلك ضروريًا للإشارة إلى مالك واسم المستودع الذي يجب طلب المناقشات بشأنه.9.3. إنشاء مربع حوار بيانات الاعتماد
لمنح المستخدم القدرة على تخزين بيانات الاعتماد الخاصة به في التطبيق ، يتم استخدام DialogFragment. لذا قم بإنشاء الفئة التالية المسماة CredentialsDialog ، وأضف أيضًا ملف تخطيط يسمى الحوار_المؤهلات. xml إلى مجلد تخطيط الموارد.يجب أن تبدو النتيجة شيئًا مثل لقطة الشاشة التالية.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginTop="16dp" android:text="Fill you credentials here" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.3" android:text="Username:" /> <EditText android:id="@+id/username_edittext" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.7" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.3" android:text="Password" /> <EditText android:id="@+id/password_edittext" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.7" android:inputType="textPassword" /> </LinearLayout> </LinearLayout>
package com.vogella.android.retrofitgithub; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.view.View; import android.widget.EditText; import okhttp3.Credentials; public class CredentialsDialog extends DialogFragment { EditText usernameEditText; EditText passwordEditText; ICredentialsDialogListener listener; public interface ICredentialsDialogListener { void onDialogPositiveClick(String username, String password); } @Override public void onAttach(Context context) { super.onAttach(context); if (getActivity() instanceof ICredentialsDialogListener) { listener = (ICredentialsDialogListener) getActivity(); } } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_credentials, null); usernameEditText = (EditText) view.findViewById(R.id.username_edittext); passwordEditText = (EditText) view.findViewById(R.id.password_edittext); usernameEditText.setText(getArguments().getString("username")); passwordEditText.setText(getArguments().getString("password")); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) .setView(view) .setTitle("Credentials") .setNegativeButton("Cancel", null) .setPositiveButton("Continue", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (listener != null) { listener.onDialogPositiveClick(usernameEditText.getText().toString(), passwordEditText.getText().toString()); } } }); return builder.create(); } }
9.4. إنشاء نشاط
قم بتعديل Activity_main.xml كما يلي. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.vogella.android.retrofitgithub.MainActivity"> <android.support.v7.widget.Toolbar android:id="@+id/my_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:elevation="4dp" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> <Spinner android:id="@+id/repositories_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Spinner android:id="@+id/issues_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/repositories_spinner" /> <EditText android:id="@+id/comment_edittext" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/issues_spinner" android:enabled="false" android:hint="Your comment" android:imeOptions="actionDone" android:inputType="text" android:maxLines="1" /> <Button android:id="@+id/loadRepos_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:enabled="false" android:gravity="center" android:onClick="onClick" android:text="Load user repositories" /> <Button android:id="@+id/send_comment_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@id/loadRepos_button" android:enabled="false" android:onClick="onClick" android:text="Send comment" /> </RelativeLayout> </LinearLayout>
زرين (لتنزيل المستودعات وإرسال التعليقات) ، وزرين Spinner (حقل منسدل لعرض المستودعات والمناقشات) و EditText (لكتابة التعليقات). لتشغيل CredentialsDialog ، استخدم القائمة الموجودة على شريط أدوات Android. لإنشائه ، أضف ملف القائمة xml المسمى menu_main.xml إلى مجلد موارد القائمة (قم بإنشاء مجلد إذا لم يكن موجودًا). <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_credentials" android:title="Credentials"/> </menu>
نظرًا لأننا نستخدم أداة شريط الأدوات ، فأنت بحاجة إلى تعطيل شريط الإجراءات بشكل افتراضي. للقيام بذلك ، قم بتعديل ملف نمط xml كما هو موضح أدناه. <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> </resources>
قم بتغيير رمز نشاطك إلى ما يلي. package com.vogella.android.retrofitgithub; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.Spinner; import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.io.IOException; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.observers.DisposableSingleObserver; import io.reactivex.schedulers.Schedulers; import okhttp3.Credentials; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; public class MainActivity extends AppCompatActivity implements CredentialsDialog.ICredentialsDialogListener { GithubAPI githubAPI; String username; String password; Spinner repositoriesSpinner; Spinner issuesSpinner; EditText commentEditText; Button sendButton; Button loadReposButtons; private CompositeDisposable compositeDisposable = new CompositeDisposable(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar); setSupportActionBar(toolbar); sendButton = (Button) findViewById(R.id.send_comment_button); repositoriesSpinner = (Spinner) findViewById(R.id.repositories_spinner); repositoriesSpinner.setEnabled(false); repositoriesSpinner.setAdapter(new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"No repositories available"})); repositoriesSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (parent.getSelectedItem() instanceof GithubRepo) { GithubRepo githubRepo = (GithubRepo) parent.getSelectedItem(); compositeDisposable.add(githubAPI.getIssues(githubRepo.owner, githubRepo.name) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(getIssuesObserver())); } } @Override public void onNothingSelected(AdapterView<?> parent) { } }); issuesSpinner = (Spinner) findViewById(R.id.issues_spinner); issuesSpinner.setEnabled(false); issuesSpinner.setAdapter(new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"Please select repository"})); commentEditText = (EditText) findViewById(R.id.comment_edittext); loadReposButtons = (Button) findViewById(R.id.loadRepos_button); createGithubAPI(); } @Override protected void onStop() { super.onStop(); if (compositeDisposable != null && !compositeDisposable.isDisposed()) { compositeDisposable.dispose(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_credentials: showCredentialsDialog(); return true; } return super.onOptionsItemSelected(item); } private void showCredentialsDialog() { CredentialsDialog dialog = new CredentialsDialog(); Bundle arguments = new Bundle(); arguments.putString("username", username); arguments.putString("password", password); dialog.setArguments(arguments); dialog.show(getSupportFragmentManager(), "credentialsDialog"); } private void createGithubAPI() { Gson gson = new GsonBuilder() .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .registerTypeAdapter(GithubRepo.class, new GithubRepoDeserializer()) .create(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .addInterceptor(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request.Builder builder = originalRequest.newBuilder().header("Authorization", Credentials.basic(username, password)); Request newRequest = builder.build(); return chain.proceed(newRequest); } }).build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(GithubAPI.ENDPOINT) .client(okHttpClient) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); githubAPI = retrofit.create(GithubAPI.class); } public void onClick(View view) { switch (view.getId()) { case R.id.loadRepos_button: compositeDisposable.add(githubAPI.getRepos() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(getRepositoriesObserver())); break; case R.id.send_comment_button: String newComment = commentEditText.getText().toString(); if (!newComment.isEmpty()) { GithubIssue selectedItem = (GithubIssue) issuesSpinner.getSelectedItem(); selectedItem.comment = newComment; compositeDisposable.add(githubAPI.postComment(selectedItem.comments_url, selectedItem) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(getCommentObserver())); } else { Toast.makeText(MainActivity.this, "Please enter a comment", Toast.LENGTH_LONG).show(); } break; } } private DisposableSingleObserver<List<GithubRepo>> getRepositoriesObserver() { return new DisposableSingleObserver<List<GithubRepo>>() { @Override public void onSuccess(List<GithubRepo> value) { if (!value.isEmpty()) { ArrayAdapter<GithubRepo> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, value); repositoriesSpinner.setAdapter(spinnerAdapter); repositoriesSpinner.setEnabled(true); } else { ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"User has no repositories"}); repositoriesSpinner.setAdapter(spinnerAdapter); repositoriesSpinner.setEnabled(false); } } @Override public void onError(Throwable e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Can not load repositories", Toast.LENGTH_SHORT).show(); } }; } private DisposableSingleObserver<List<GithubIssue>> getIssuesObserver() { return new DisposableSingleObserver<List<GithubIssue>>() { @Override public void onSuccess(List<GithubIssue> value) { if (!value.isEmpty()) { ArrayAdapter<GithubIssue> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, value); issuesSpinner.setEnabled(true); commentEditText.setEnabled(true); sendButton.setEnabled(true); issuesSpinner.setAdapter(spinnerAdapter); } else { ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"Repository has no issues"}); issuesSpinner.setEnabled(false); commentEditText.setEnabled(false); sendButton.setEnabled(false); issuesSpinner.setAdapter(spinnerAdapter); } } @Override public void onError(Throwable e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Can not load issues", Toast.LENGTH_SHORT).show(); } }; } private DisposableSingleObserver<ResponseBody> getCommentObserver() { return new DisposableSingleObserver<ResponseBody>() { @Override public void onSuccess(ResponseBody value) { commentEditText.setText(""); Toast.makeText(MainActivity.this, "Comment created", Toast.LENGTH_LONG).show(); } @Override public void onError(Throwable e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Can not create comment", Toast.LENGTH_SHORT).show(); } }; } @Override public void onDialogPositiveClick(String username, String password) { this.username = username; this.password = password; loadReposButtons.setEnabled(true); } }
أضفنا هنا GithubRepoDeserializer الذي تم إنشاؤه سابقًا باعتباره TypeAdapter في GsonBuilder. لمعالجة المصادقة لكل مكالمة ، تمت إضافة اعتراض إلى OkHttpClient. لكي تتمكن أساليب واجهة برمجة التطبيقات من إرجاع أنواع RxJava2 ، أضفنا RXJava2 CallAdapter إلى عملائنا.10. التمرين: استخدام التحديث التحديثي مع OAuth لطلب معلومات المستخدم من Twitter على Android
يصف هذا التمرين كيفية الوصول إلى Twitter باستخدام Retrofit على Android. سنكتب تطبيقًا يمكنه طلب بيانات المستخدم وعرضها لاسم المستخدم المقدم. في هذا التمرين ، نستخدم تطبيق مصادقة Twitter فقط مع OAuth 2 للتفويض. للقيام بهذا التمرين ، يجب أن يكون لديك حساب Twitter. بالإضافة إلى ذلك ، تحتاج إلى الانتقال إلى تطبيقات Twitter وإنشاء تطبيق جديد للحصول على مفتاح العميل وسر العميل. سنحتاج إلى ذلك لاحقًا لطلب رمز OAuth المميز.10.1. إعداد المشروع
قم بإنشاء تطبيق Android يسمى Retrofit Twitter. استخدم com.vogella.android.retrofittwitter كاسم حزمة المستوى الأعلى.لاستخدام التحديث التحديثي ، أضف الأسطر التالية إلى ملف build.gradle implementation 'com.squareup.retrofit2:retrofit:2.1.0' implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
إضافة إذن للوصول إلى الإنترنت في البيان. <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.vogella.android.retrofittwitter"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
10.2. تعريف API
أنشئ فئتي البيانات التاليتين ، اللتين تسمى OAuthToken و UserDetails. package com.vogella.android.retrofittwitter; import com.google.gson.annotations.SerializedName; public class OAuthToken { @SerializedName("access_token") private String accessToken; @SerializedName("token_type") private String tokenType; public String getAccessToken() { return accessToken; } public String getTokenType() { return tokenType; } public String getAuthorization() { return getTokenType() + " " + getAccessToken(); } }
package com.vogella.android.retrofittwitter; public class UserDetails { private String name; private String location; private String description; private String url; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } }
يتم استخدام فئة OAuthToken لتخزين الرمز المميز للحامل الذي نطلبه من Twitter ، باستخدام مفتاحنا وسرنا. نستخدم التعليق التوضيحي @ SerializedName لتعيين اسم التحديث إلى (de) إجراء تسلسل للحقول.تحفظ فئة UserDetails ببساطة عدة حقول من استجابة Twitter عند طلب معلومات المستخدم. نحن لا نعرض جميع بيانات المستخدم التي تم تضمينها في الاستجابة ، فقط الاسم والموقع وعنوان URL والوصف.حدد REST API لـ Retrofit من خلال الواجهة التالية: package com.vogella.android.retrofittwitter; import retrofit2.Call; import retrofit2.http.Field; import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Query; public interface TwitterApi { String BASE_URL = "https://api.twitter.com/"; @FormUrlEncoded @POST("oauth2/token") Call<OAuthToken> postCredentials(@Field("grant_type") String grantType); @GET("/1.1/users/show.json") Call<UserDetails> getUserDetails(@Query("screen_name") String name); }
10.3. إنشاء نشاط
قم بتعديل ملف Activity_main.xml وفئة MainActivity المقابلة كما يلي: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.vogella.android.retrofittwitter.MainActivity"> <LinearLayout android:id="@+id/username_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:orientation="horizontal"> <TextView android:id="@+id/username_textview" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:enabled="false" android:gravity="center_vertical" android:text="Username:" /> <EditText android:id="@+id/username_edittext" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:enabled="false" android:gravity="center" android:imeOptions="actionDone" android:inputType="text" android:maxLines="1" /> </LinearLayout> <Button android:id="@+id/request_token_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:onClick="onClick" android:text="Request token" /> <Button android:id="@+id/request_user_details_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@id/request_token_button" android:enabled="false" android:onClick="onClick" android:text="Request user details" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@id/request_user_details_button" android:layout_below="@id/username_container" android:gravity="center" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="50dp"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="Name:" /> <TextView android:id="@+id/name_textview" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="---" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="50dp"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="Location:" /> <TextView android:id="@+id/location_textview" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="---" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="50dp"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="Url:" /> <TextView android:id="@+id/url_textview" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="---" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="50dp"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="Description:" /> <TextView android:id="@+id/description_textview" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center_vertical" android:text="---" /> </LinearLayout> </LinearLayout> </RelativeLayout>
package com.vogella.android.retrofittwitter; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import java.io.IOException; import okhttp3.Credentials; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class MainActivity extends AppCompatActivity { private String credentials = Credentials.basic("aConsumerKey", "aSecret"); Button requestTokenButton; Button requestUserDetailsButton; EditText usernameEditText; TextView usernameTextView; TextView nameTextView; TextView locationTextView; TextView urlTextView; TextView descriptionTextView; TwitterApi twitterApi; OAuthToken token; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); requestTokenButton = (Button) findViewById(R.id.request_token_button); requestUserDetailsButton = (Button) findViewById(R.id.request_user_details_button); usernameEditText = (EditText) findViewById(R.id.username_edittext); usernameTextView = (TextView) findViewById(R.id.username_textview); nameTextView = (TextView) findViewById(R.id.name_textview); locationTextView = (TextView) findViewById(R.id.location_textview); urlTextView = (TextView) findViewById(R.id.url_textview); descriptionTextView = (TextView) findViewById(R.id.description_textview); createTwitterApi(); } private void createTwitterApi() { OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request.Builder builder = originalRequest.newBuilder().header("Authorization", token != null ? token.getAuthorization() : credentials); Request newRequest = builder.build(); return chain.proceed(newRequest); } }).build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(TwitterApi.BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build(); twitterApi = retrofit.create(TwitterApi.class); } public void onClick(View view) { switch (view.getId()) { case R.id.request_token_button: twitterApi.postCredentials("client_credentials").enqueue(tokenCallback); break; case R.id.request_user_details_button: String editTextInput = usernameEditText.getText().toString(); if (!editTextInput.isEmpty()) { twitterApi.getUserDetails(editTextInput).enqueue(userDetailsCallback); } else { Toast.makeText(this, "Please provide a username", Toast.LENGTH_LONG).show(); } break; } } Callback<OAuthToken> tokenCallback = new Callback<OAuthToken>() { @Override public void onResponse(Call<OAuthToken> call, Response<OAuthToken> response) { if (response.isSuccessful()) { requestTokenButton.setEnabled(false); requestUserDetailsButton.setEnabled(true); usernameTextView.setEnabled(true); usernameEditText.setEnabled(true); token = response.body(); } else { Toast.makeText(MainActivity.this, "Failure while requesting token", Toast.LENGTH_LONG).show(); Log.d("RequestTokenCallback", "Code: " + response.code() + "Message: " + response.message()); } } @Override public void onFailure(Call<OAuthToken> call, Throwable t) { t.printStackTrace(); } }; Callback<UserDetails> userDetailsCallback = new Callback<UserDetails>() { @Override public void onResponse(Call<UserDetails> call, Response<UserDetails> response) { if (response.isSuccessful()) { UserDetails userDetails = response.body(); nameTextView.setText(userDetails.getName() == null ? "no value" : userDetails.getName()); locationTextView.setText(userDetails.getLocation() == null ? "no value" : userDetails.getLocation()); urlTextView.setText(userDetails.getUrl() == null ? "no value" : userDetails.getUrl()); descriptionTextView.setText(userDetails.getDescription().isEmpty() ? "no value" : userDetails.getDescription()); } else { Toast.makeText(MainActivity.this, "Failure while requesting user details", Toast.LENGTH_LONG).show(); Log.d("UserDetailsCallback", "Code: " + response.code() + "Message: " + response.message()); } } @Override public void onFailure(Call<UserDetails> call, Throwable t) { t.printStackTrace(); } }; }
استبدل aConsumerKey و aSecret بمفتاح العميل والسر المستلمين من Twitter.ألقِ نظرة أيضًا على المعترض الذي نضيفه إلى عميلنا التحديثي. نظرًا لأننا نستخدم OAuth ، تختلف بيانات الاعتماد الخاصة بنا لكل مكالمة. يجب أن تنشر طريقة postCredentials بيانات الاعتماد (مفتاح العميل والسر) في مخطط Twitter الأساسي. نتيجةً لذلك ، ترجع هذه المكالمة رمز حامل ، والذي يتم إلغاء تعديله في فئة OAuthToken ، والتي يتم تخزينها بعد ذلك في حقل الرمز المميز. يمكن لأي طلب آخر (ويجب) الآن استخدام هذا الرمز المميز كأوراق اعتماد للتفويض. معلومات المستخدم مطلوبة أيضا.11. الموارد التحديثية
استهلاك واجهات برمجة التطبيقات مع البرنامج التعليمي للتحديث
في سلسلة مدونة حول التعديل التحديثي استخدام
واجهات برمجة التطبيقات مع التحديث