العمل غير المتزامن مع سلاسل محادثات Java

تستخدم جميع تطبيقات Android سلسلة محادثات رئيسية لمعالجة عمليات واجهة المستخدم. قد يؤدي استدعاء العمليات طويلة المدى من سلسلة التعليمات الرئيسية هذه إلى التوقف عن العمل وعدم الاستجابة. على سبيل المثال، إذا أرسل تطبيقك طلب الشبكة من سلسلة التعليمات الرئيسية، سيتم تجميد واجهة مستخدم التطبيق إلى أن يتلقّى استجابة الشبكة. إذا كنت تستخدم Java، يمكنك إنشاء سلاسل محادثات في الخلفية إضافية لمعالجة العمليات طويلة المدى مع استمرار سلسلة التعليمات الرئيسية في معالجة تحديثات واجهة المستخدم.

يوضِّح هذا الدليل كيف يمكن لمطوّري البرامج الذين يستخدمون لغة برمجة Java استخدام مجموعة سلاسل المحادثات لإعداد سلاسل محادثات متعددة واستخدامها في تطبيق على Android. كما يوضّح هذا الدليل كيفية تحديد الرمز البرمجي لتشغيله على سلسلة محادثات وكيفية التواصل بين إحدى سلاسل المحادثات هذه وسلسلة المحادثات الرئيسية.

مكتبات التزامن

من المهم فهم أساسيات سلسلة التعليمات وآلياتها الأساسية. ومع ذلك، هناك العديد من المكتبات الشائعة التي تقدم تجريدات عالية المستوى حول هذه المفاهيم وأدوات مساعدة جاهزة للاستخدام لتمرير البيانات بين سلاسل التعليمات. وتتضمّن هذه المكتبات Guava وRxJava لمستخدمي لغة البرمجة Java وCoroutines اللتين ننصح باستخدامهما لمستخدمي لغة Kotlin.

من الناحية العملية، ينبغي عليك اختيار الشكل الذي يناسب تطبيقك وفريق التطوير لديك، على الرغم من أن قواعد سلسلة المحادثات تظل كما هي.

نظرة عامة على الأمثلة

استنادًا إلى دليل بنية التطبيق، تقدّم الأمثلة في هذا الموضوع طلب شبكة وتعرض النتيجة إلى سلسلة التعليمات الرئيسية، حيث قد يعرض التطبيق النتيجة على الشاشة.

على وجه التحديد، تستدعي ViewModel طبقة البيانات في سلسلة التعليمات الرئيسية لتفعيل طلب الشبكة. طبقة البيانات هي المسؤولة عن نقل تنفيذ طلب الشبكة من سلسلة التعليمات الرئيسية ونشر النتيجة مرة أخرى على سلسلة التعليمات الرئيسية باستخدام معاودة الاتصال.

لنقل تنفيذ طلب الشبكة خارج سلسلة التعليمات الرئيسية، نحتاج إلى إنشاء سلاسل محادثات أخرى في تطبيقنا.

إنشاء سلاسل محادثات متعددة

مجموعة سلاسل المحادثات هي مجموعة مُدارة من سلاسل المحادثات التي تشغِّل المهام بشكل متوازٍ من قائمة انتظار. يتم تنفيذ المهام الجديدة على السلاسل الحالية حيث تصبح هذه السلاسل خاملة. لإرسال مهمة إلى مجموعة سلاسل محادثات، استخدِم واجهة ExecutorService. تجدر الإشارة إلى أنّ ExecutorService ليس له علاقة بـ الخدمات، وهي مكوّن تطبيق Android.

يُعد إنشاء سلاسل المحادثات أمرًا مكلفًا، لذا عليك إنشاء مجموعة سلاسل محادثات مرة واحدة فقط عند بدء تطبيقك. تأكَّد من حفظ مثيل ExecutorService إما في فئة Application أو في حاوية حقن التبعية. ينشئ المثال التالي مجموعة سلاسل محادثات من أربعة سلاسل محادثات يمكننا استخدامها لتشغيل مهام الخلفية.

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

هناك طرق أخرى يمكنك من خلالها ضبط مجموعة سلاسل محادثات بناءً على عبء العمل المتوقع. راجِع ضبط مجموعة سلاسل محادثات للحصول على مزيد من المعلومات.

التنفيذ في سلسلة محادثات في الخلفية

يؤدي تقديم طلب للشبكة في سلسلة التعليمات الرئيسية إلى انتظار سلسلة التعليمات أو حظرها إلى أن تتلقّى ردًّا. نظرًا لحظر سلسلة المحادثات، لا يمكن لنظام التشغيل الاتصال بـ onDraw()، وسيتوقف تطبيقك، ما قد يؤدي إلى ظهور مربّع حوار "التطبيق لا يستجيب" (ANR). بدلاً من ذلك، لنقم بتشغيل هذه العملية على سلسلة محادثات في الخلفية.

تقديم الطلب

أولاً، لنلقِ نظرة على صف LoginRepository ونتعرف على طريقة إنشاء طلب الشبكة:

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

تطبيق makeLoginRequest() متزامن ويحظر سلسلة الاتصال. لنمذجة استجابة طلب الشبكة، لدينا فئة Result الخاصة بنا.

تشغيل الطلب

تؤدّي السمة ViewModel إلى تشغيل طلب الشبكة عندما ينقر المستخدم على أحد الأزرار مثلاً:

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

باستخدام الرمز السابق، تحظر LoginViewModel سلسلة التعليمات الرئيسية عند إجراء طلب الشبكة. يمكننا استخدام تجمع سلسلة التعليمات الذي أنشأناه لنقل عملية التنفيذ إلى مؤشر ترابط في الخلفية.

التعامل مع حقن التبعية

أولاً، باتّباع مبادئ إدخال التبعية، يأخذ LoginRepository مثيلاً من Executor وليس ExecutorService لأنه ينفّذ الترميز ولا يدير سلاسل المحادثات:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

تستخدم الطريقة execute() في التنفيذ Runnable. واجهة Runnable هي واجهة ذات أسلوب تجريدي واحد (SAM) بأسلوب run() يتم تنفيذه في سلسلة محادثات عند استدعائها.

التنفيذ في الخلفية

لننشئ دالة أخرى تسمى makeLoginRequest() تنقل التنفيذ إلى سلسلة التعليمات في الخلفية وتتجاهل الاستجابة في الوقت الحالي:

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

في الطريقة execute()، ننشئ عنصر Runnable جديدًا باستخدام كتلة الرموز التي نريد تنفيذها في سلسلة التعليمات في الخلفية، في حالتنا، طريقة طلب الشبكة المتزامنة. داخليًا، يدير ExecutorService Runnable وينفّذها في سلسلة محادثات متاحة.

الاعتبارات

يمكن أن تعمل أي سلسلة محادثات في تطبيقك بالتوازي مع سلاسل محادثات أخرى، بما في ذلك سلسلة التعليمات الرئيسية، لذا يجب أن تتأكد من أنّ الرمز البرمجي متوافق مع سلسلة المحادثات. لاحظ أننا في المثال الخاص بنا نتجنب الكتابة على المتغيرات المشتركة بين سلاسل، تمرير بيانات غير قابلة للتغيير بدلاً من ذلك. هذه ممارسة جيدة، لأن كل سلسلة محادثات تعمل مع مثيلاتها الخاصة من البيانات، ونتجنب تعقيد المزامنة.

إذا كنت بحاجة إلى مشاركة الحالة بين سلاسل المحادثات، يجب توخي الحذر لإدارة الوصول من سلاسل المحادثات باستخدام آليات المزامنة، مثل الأقفال. هذا خارج نطاق هذا الدليل. بشكل عام، يجب عليك تجنب مشاركة الحالة القابلة للتغيير بين السلاسل كلما أمكن ذلك.

التواصل مع سلسلة التعليمات الرئيسية

في الخطوة السابقة، تجاهلنا استجابة طلب الشبكة. لعرض النتيجة على الشاشة، يجب أن تعرف "LoginViewModel" ذلك. يمكننا إجراء ذلك باستخدام طلبات معاودة الاتصال.

يجب أن تستخدم الدالة makeLoginRequest() استدعاء كمَعلمة حتى تتمكّن من عرض قيمة بشكل غير متزامن. يتم استدعاء رد الاتصال الذي يحمل النتيجة كلما اكتمل طلب الشبكة أو حدث إخفاق. في لغة Kotlin، يمكننا استخدام دالة ذات ترتيب أعلى. ومع ذلك، في جافا، يجب علينا إنشاء واجهة استدعاء جديدة للحصول على الوظيفة ذاتها:

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

على "ViewModel" تنفيذ عملية معاودة الاتصال الآن. يمكن أن يؤدي منطقًا مختلفًا اعتمادًا على النتيجة:

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

في هذا المثال، يتم تنفيذ رد الاتصال في سلسلة الاتصال، وهي سلسلة في الخلفية. هذا يعني أنه لا يمكنك تعديل طبقة واجهة المستخدم أو الاتصال بها مباشرةً حتى تقوم بالتبديل مرة أخرى إلى سلسلة التعليمات الرئيسية.

استخدام المعالجات

يمكنك استخدام معالج لإدراج إجراء في قائمة الانتظار لتنفيذه على سلسلة محادثات مختلفة. لتحديد سلسلة المحادثات التي تريد تنفيذ الإجراء عليها، أنشِئ Handler باستخدام Looper لسلسلة المحادثات. علامة Looper هي كائن يشغّل حلقة الرسائل لسلسلة محادثات مرتبطة. بعد إنشاء Handler، يمكنك استخدام طريقة post(Runnable) لتشغيل مجموعة رموز في سلسلة التعليمات المقابلة.

يتضمن Looper دالة مساعد، getMainLooper()، والتي تسترد Looper من سلسلة التعليمات الرئيسية. يمكنك تنفيذ الرمز في سلسلة التعليمات الرئيسية باستخدام Looper هذا لإنشاء Handler. بما أنّك قد تنفّذ هذا الإجراء عادةً، يمكنك أيضًا حفظ مثيل من Handler في المكان نفسه الذي حفظت فيه السمة ExecutorService:

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

يُعد إدخال المعالج في المستودع ممارسة جيدة، إذ يمنحك المزيد من المرونة. على سبيل المثال، قد تحتاج في المستقبل إلى ضبط سمة Handler مختلفة لجدولة المهام في سلسلة محادثات منفصلة. إذا كنت تتواصل دائمًا مع السلسلة نفسها، يمكنك تمرير Handler إلى الدالة الإنشائية للمستودع، كما هو موضّح في المثال التالي.

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

بدلاً من ذلك، إذا أردت الحصول على مزيد من المرونة، يمكنك إدخال Handler لكل دالة:

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

في هذا المثال، يتم تنفيذ استدعاء الاتصال الذي تم تمريره إلى استدعاء makeLoginRequest في المستودع على سلسلة التعليمات الرئيسية. هذا يعني أنّه يمكنك تعديل واجهة المستخدم مباشرةً من خلال معاودة الاتصال أو استخدام LiveData.setValue() للتواصل مع واجهة المستخدم.

إعداد مجموعة سلاسل محادثات

يمكنك إنشاء مجموعة سلاسل محادثات باستخدام إحدى وظائف مساعد التنفيذ مع الإعدادات المحددة مسبقًا، كما هو موضّح في المثال السابق للرمز. أما إذا أردت تخصيص تفاصيل مجموعة سلاسل المحادثات، فيمكنك إنشاء مثيل باستخدام ThreadPoolExecutor مباشرةً. يمكنك ضبط التفاصيل التالية:

  • الحجم المبدئي والحد الأقصى لمجموعة البيانات.
  • الحفاظ على الوقت الحيوية والوحدة الزمنية. وقت البقاء على قيد الحياة هو الحد الأقصى للمدة التي يمكن أن تظل فيها سلسلة التعليمات غير نشطة قبل أن يتم إيقافها.
  • قائمة انتظار إدخال تتضمن Runnable مهام. يجب أن تنفذ قائمة الانتظار هذه واجهة blockQueue لتلبية متطلبات تطبيقك، يمكنك الاختيار من بين عمليات تنفيذ قائمة الانتظار المتاحة. لمعرفة المزيد من المعلومات، راجع النظرة العامة للصف ThreadPoolExecutor.

في ما يلي مثال يحدد حجم مجموعة سلاسل التعليمات بناءً على إجمالي عدد نوى المعالج ومدة الاحتفاظ بالبيانات بمقدار ثانية واحدة وقائمة انتظار الإدخال.

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}