งานพื้นหลังกล่องโต้ตอบความคืบหน้าการเปลี่ยนทิศทาง - มีวิธีแก้ปัญหาการทำงาน 100% หรือไม่


235

ฉันดาวน์โหลดข้อมูลบางส่วนจากอินเทอร์เน็ตในเธรดพื้นหลัง (ฉันใช้AsyncTask) และแสดงกล่องโต้ตอบความคืบหน้าขณะดาวน์โหลด การเปลี่ยนแปลงการวางแนวกิจกรรมจะเริ่มขึ้นใหม่จากนั้น AsyncTask ของฉันจะเสร็จสมบูรณ์ - ฉันต้องการยกเลิกกล่องข้อความ progess และเริ่มกิจกรรมใหม่ แต่บางครั้งการโทรเลิกใช้ Dialog ก็มีข้อยกเว้น (อาจเป็นเพราะกิจกรรมถูกทำลายและกิจกรรมใหม่ยังไม่ได้เริ่ม)

วิธีที่ดีที่สุดในการจัดการปัญหาประเภทนี้คืออะไร (การอัปเดต UI จากเธรดพื้นหลังที่ทำงานแม้ว่าผู้ใช้จะเปลี่ยนการวางแนว) มีใครบางคนจาก Google ให้ "วิธีแก้ปัญหาอย่างเป็นทางการ" หรือไม่?


4
บล็อกโพสต์ของฉันในหัวข้อนี้อาจช่วยได้ มันเกี่ยวกับการรักษางานที่ยาวนานในการเปลี่ยนแปลงการกำหนดค่า
Alex Lockwood

1
คำถามนี้มีความเกี่ยวข้องเช่นกัน
Alex Lockwood

FTR มีความลึกลับที่เกี่ยวข้องอยู่ที่นี่ .. stackoverflow.com/q/23742412/294884
Fattie

คำตอบ:


336

ขั้นตอนที่ 1: ตรวจของคุณชั้นซ้อนกันหรือแยกชั้นทั้งหมดก็ไม่ภายใน (ไม่คงที่ซ้อนกัน) ระดับAsyncTaskstatic

ขั้นตอนที่ # 2: มีการAsyncTaskถือActivityผ่านสมาชิกข้อมูลตั้งค่าผ่านตัวสร้างและหมา

ขั้นตอนที่ # 3: เมื่อสร้างAsyncTask, จัดหากระแสไฟฟ้าActivityให้กับตัวสร้าง

ขั้นตอนที่ # 4: ในonRetainNonConfigurationInstance()คืนค่าAsyncTaskหลังจากแยกกิจกรรมเดิมซึ่งกำลังออกไป

ขั้นตอนที่ # 5: onCreate()หากgetLastNonConfigurationInstance()ไม่ใช่nullให้ส่งไปยังAsyncTaskชั้นเรียนของคุณและโทรไปที่ผู้ตั้งค่าของคุณเพื่อเชื่อมโยงกิจกรรมใหม่ของคุณกับงาน

ขั้นตอนที่ 6: doInBackground()ไม่ต้องดูที่ข้อมูลสมาชิกกิจกรรมจาก

ถ้าคุณทำตามสูตรข้างต้นมันจะใช้ได้ทั้งหมด onProgressUpdate()และonPostExecute()ถูกระงับระหว่างจุดเริ่มต้นของและจุดสิ้นสุดของที่ตามมาonRetainNonConfigurationInstance()onCreate()

นี่เป็นโครงการตัวอย่างที่แสดงให้เห็นถึงเทคนิค

อีกวิธีหนึ่งคือการทิ้งและย้ายการทำงานของคุณให้เป็นAsyncTask IntentServiceสิ่งนี้มีประโยชน์อย่างยิ่งหากงานที่ต้องทำอาจยาวนานและควรดำเนินการต่อโดยไม่คำนึงถึงสิ่งที่ผู้ใช้ทำในแง่ของกิจกรรม (เช่นการดาวน์โหลดไฟล์ขนาดใหญ่) คุณสามารถใช้การออกอากาศที่สั่งซื้อIntentเพื่อให้กิจกรรมตอบสนองต่องานที่กำลังทำอยู่ (ถ้ายังอยู่ในเบื้องหน้า) หรือเพิ่ม a Notificationเพื่อแจ้งให้ผู้ใช้ทราบว่างานนั้นเสร็จสิ้นแล้ว นี่คือโพสต์บล็อกที่มีเพิ่มเติมเกี่ยวกับรูปแบบนี้


8
ขอบคุณมากสำหรับคำตอบที่ยอดเยี่ยมสำหรับปัญหาทั่วไปนี้! เพื่อให้ละเอียดคุณอาจเพิ่มขั้นตอน # 4 ที่เราต้องแยกออก (ตั้งค่าเป็น null) กิจกรรมใน AsyncTask นี่เป็นตัวอย่างที่ดีในโครงการตัวอย่าง
Kevin Gaudin

3
แต่ถ้าฉันต้องมีสิทธิ์เข้าถึงสมาชิกของกิจกรรม
ยูจีน

3
@Andrew: สร้างคลาสภายในแบบคงที่หรือสิ่งที่เก็บไว้บนวัตถุหลาย ๆ อันแล้วส่งคืน
CommonsWare

11
onRetainNonConfigurationInstance()เลิกใช้แล้วและตัวเลือกอื่นที่แนะนำคือการใช้setRetainInstance()แต่จะไม่ส่งคืนวัตถุ เป็นไปได้หรือไม่ที่จะจัดการasyncTaskกับการเปลี่ยนแปลงการกำหนดค่าด้วยsetRetainInstance()?
Indrek Kõue

10
@SYLARRR: แน่นอน มีไว้Fragment AsyncTaskมีการFragmentเรียกร้องsetRetainInstance(true)ให้กับตัวเอง มีการพูดคุยเฉพาะกับAsyncTask Fragmentตอนนี้ในการเปลี่ยนแปลงการกำหนดค่าFragmentจะไม่ถูกทำลายและสร้างใหม่ (แม้ว่ากิจกรรมจะเป็น) และอื่น ๆAsyncTaskจะถูกเก็บไว้ในการเปลี่ยนแปลงการกำหนดค่า
CommonsWare

13

คำตอบที่ได้รับการยอมรับมีประโยชน์มาก แต่ไม่มีช่องโต้ตอบความคืบหน้า

โชคดีสำหรับคุณผู้อ่านที่ผมได้สร้างตัวอย่างที่ครอบคลุมมากและการทำงานของ AsyncTask กับโต้ตอบความคืบหน้า !

  1. การหมุนใช้งานได้และกล่องโต้ตอบยังมีชีวิตอยู่
  2. คุณสามารถยกเลิกงานและไดอะล็อกโดยกดปุ่มย้อนกลับ (ถ้าคุณต้องการพฤติกรรมนี้)
  3. มันใช้ชิ้นส่วน
  4. เลย์เอาต์ของชิ้นส่วนที่อยู่ใต้กิจกรรมจะเปลี่ยนแปลงอย่างเหมาะสมเมื่ออุปกรณ์หมุน

คำตอบที่ได้รับการยอมรับนั้นเกี่ยวกับคลาสแบบสแตติก (ไม่ใช่สมาชิก) และสิ่งเหล่านั้นจำเป็นต้องหลีกเลี่ยงว่า AsyncTask มีตัวชี้ (ซ่อน) ไปยังอินสแตนซ์ของคลาสภายนอกซึ่งกลายเป็นความจำรั่วในการทำลายกิจกรรม
Bananeweizen

ใช่ไม่แน่ใจว่าทำไมฉันถึงใส่สมาชิกแบบสแตติกเนื่องจากฉันใช้พวกเขาจริงๆ ... แปลก แก้ไขคำตอบแล้ว
Timmmm

คุณช่วยอัพเดทลิงค์ของคุณได้ไหม ฉันต้องการสิ่งนี้จริงๆ
Romain Pellerin

ขออภัยไม่ได้มีเวลาในการคืนค่าเว็บไซต์ของฉัน - ฉันจะทำมันในไม่ช้า! แต่ในขณะเดียวกันมันก็เหมือนกับโค้ดในคำตอบนี้: stackoverflow.com/questions/8417885/ …
Timmmm

1
ลิงค์นั้นปลอม เพียงแค่นำไปสู่ดัชนีไร้ประโยชน์โดยไม่มีการบ่งชี้ว่าโค้ดอยู่ที่ไหน
FractalBob

9

ฉันทำงานหนักเป็นเวลาหนึ่งสัปดาห์เพื่อหาทางออกให้กับภาวะที่กลืนไม่เข้าคายไม่ออกนี้โดยไม่ต้องหันไปแก้ไขไฟล์รายการ สมมติฐานสำหรับการแก้ปัญหานี้คือ:

  1. คุณจำเป็นต้องใช้กล่องโต้ตอบความคืบหน้าเสมอ
  2. ดำเนินการครั้งละหนึ่งงานเท่านั้น
  3. คุณต้องการให้งานคงอยู่เมื่อหมุนโทรศัพท์และไดอะล็อกความคืบหน้าจะถูกยกเลิกโดยอัตโนมัติ

การดำเนินงาน

คุณจะต้องคัดลอกสองไฟล์ที่ด้านล่างของโพสต์นี้ลงในพื้นที่ทำงานของคุณ ตรวจสอบให้แน่ใจว่า:

  1. ทั้งหมดของคุณActivityควรขยายBaseActivity

  2. ในonCreate(), super.onCreate()ควรจะเรียกว่าหลังจากที่คุณเริ่มต้นสมาชิกใด ๆ ที่จะต้องมีการเข้าถึงได้โดยคุณASyncTasks นอกจากนี้แทนที่getContentViewId()เพื่อให้รหัสรูปแบบ

  3. แทนที่onCreateDialog() เหมือนปกติเพื่อสร้างไดอะล็อกที่จัดการโดยกิจกรรม

  4. ดูโค้ดด้านล่างสำหรับคลาสภายในตัวอย่างแบบคงที่เพื่อสร้าง AsyncTasks ของคุณ คุณสามารถเก็บผลลัพธ์ไว้ใน mResult เพื่อเข้าถึงในภายหลัง


final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

และในที่สุดเมื่อต้องการเริ่มงานใหม่ของคุณ:

mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

แค่นั้นแหละ! ฉันหวังว่าโซลูชันที่มีประสิทธิภาพนี้จะช่วยให้ใครบางคน

BaseActivity.java (จัดการการนำเข้าด้วยตนเอง)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}

4

มีใครบางคนจาก Google ให้ "วิธีแก้ปัญหาอย่างเป็นทางการ" หรือไม่?

ใช่.

วิธีการแก้ปัญหามีมากขึ้นของข้อเสนอสถาปัตยกรรมสมัครมากกว่าที่เพียงบางรหัส

พวกเขาเสนอรูปแบบการออกแบบ 3 แบบที่อนุญาตให้แอปพลิเคชันทำงานแบบซิงค์กับเซิร์ฟเวอร์โดยไม่คำนึงถึงสถานะของแอปพลิเคชัน(มันจะทำงานแม้ว่าผู้ใช้จะเสร็จสิ้นแอปหน้าจอเปลี่ยนผู้ใช้แอปจะสิ้นสุดลง การดำเนินการข้อมูลพื้นหลังอาจถูก interrumpted ซึ่งครอบคลุมมัน)

มีการอธิบายข้อเสนอนี้ในการพูดแอปพลิเคชันไคลเอนต์ Android RESTระหว่างGoogle I / O 2010โดย Virgil Dobjanschi มันยาว 1 ชั่วโมง แต่มันก็คุ้มค่าที่จะดู

พื้นฐานของมันคือการแยกการดำเนินงานเครือข่ายให้เป็นServiceไปอย่างอิสระActivityในแอปพลิเคชัน หากคุณกำลังทำงานกับฐานข้อมูลการใช้ContentResolverและCursorจะให้รูปแบบผู้สังเกตการณ์ที่ไม่ทันสมัยซึ่งสะดวกในการอัปเดต UI โดยไม่ต้องใช้ตรรกะใด ๆ เพิ่มเติมเมื่อคุณอัปเดตฐานข้อมูลท้องถิ่นด้วยข้อมูลระยะไกลที่ดึงมา รหัส after-operation อื่นใดจะถูกเรียกใช้ผ่านการเรียกกลับที่ส่งไปยังService(ฉันใช้ResultReceiverคลาสย่อยสำหรับเรื่องนี้)

อย่างไรก็ตามคำอธิบายของฉันค่อนข้างคลุมเครือคุณควรดูคำพูดอย่างแน่นอน


2

ในขณะที่คำตอบของ Mark (CommonsWare) ใช้งานได้จริงสำหรับการเปลี่ยนแปลงการวางแนวมันจะล้มเหลวหากกิจกรรมถูกทำลายโดยตรง (เช่นในกรณีของโทรศัพท์)

คุณสามารถจัดการกับการเปลี่ยนแปลงการวางแนวและกิจกรรมกิจกรรมที่ถูกทำลายได้ยากโดยใช้แอปพลิเคชันวัตถุเพื่ออ้างอิง ASyncTask ของคุณ

มีคำอธิบายที่ยอดเยี่ยมเกี่ยวกับปัญหาและวิธีแก้ไขที่นี่ :

เครดิตไปที่ไรอันโดยสมบูรณ์เพื่อหาสิ่งนี้


1

หลังจาก 4 ปีที่ Google แก้ไขปัญหาเพียงแค่เรียกใช้ setRetainInstance (จริง) ใน Activity onCreate มันจะรักษาอินสแตนซ์กิจกรรมของคุณในระหว่างการหมุนอุปกรณ์ ฉันมีวิธีแก้ปัญหาง่ายๆสำหรับ Android รุ่นเก่า


1
ปัญหาที่ผู้สังเกตการณ์เกิดขึ้นเพราะ Android ทำลายคลาสกิจกรรมเมื่อหมุนส่วนขยายแป้นพิมพ์และกิจกรรมอื่น ๆ แต่งาน async ยังคงเก็บข้อมูลอ้างอิงสำหรับอินสแตนซ์ที่ถูกทำลายและพยายามใช้งานสำหรับการอัปเดต UI คุณสามารถสั่งให้ Android ไม่ทำลายกิจกรรมไม่ว่าจะเป็นแบบชัดแจ้งหรือในทางปฏิบัติ ในกรณีนี้การอ้างอิงงาน async ยังคงใช้ได้และไม่พบปัญหา เนื่องจากการหมุนเวียนอาจต้องใช้งานเพิ่มเติมในการโหลดมุมมองเป็นต้น Google ไม่แนะนำให้ทำกิจกรรมไว้ ดังนั้นคุณตัดสินใจ
Singagirl

ขอบคุณฉันรู้เกี่ยวกับสถานการณ์ แต่ไม่เกี่ยวกับ setRetainInstance () สิ่งที่ฉันไม่เข้าใจคือการอ้างสิทธิ์ของคุณที่ Google ใช้เพื่อแก้ไขปัญหาที่ถามในคำถาม คุณสามารถลิงค์แหล่งข้อมูลได้หรือไม่ ขอบคุณ
jj_


onRetainNonConfigurationInstance () ฟังก์ชั่นนี้เรียกว่าเป็นการเพิ่มประสิทธิภาพอย่างหมดจดและคุณต้องไม่พึ่งพามันถูกเรียกว่า <จากแหล่งข้อมูลเดียวกัน: developer.android.com/reference/android/app/…
Dhananjay M

0

คุณควรเรียกการกระทำของกิจกรรมทั้งหมดโดยใช้ตัวจัดการกิจกรรม ดังนั้นถ้าคุณอยู่ในเธรดคุณควรสร้าง Runnable และโพสต์โดยใช้ Activitie's Handler มิฉะนั้นแอปของคุณอาจมีปัญหาในบางครั้งโดยมีข้อยกเว้นร้ายแรง


0

นี่คือโซลูชันของฉัน: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

โดยทั่วไปขั้นตอนคือ:

  1. ฉันใช้onSaveInstanceStateเพื่อบันทึกงานหากยังดำเนินการอยู่
  2. ในonCreateฉันได้รับงานถ้ามันถูกบันทึกไว้
  3. ในonPauseฉันทิ้งProgressDialogถ้ามันจะปรากฏขึ้น
  4. ในonResumeฉันแสดงProgressDialogว่างานยังคงอยู่ในการประมวลผล
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.