مدیریت چرخه‌های حیات با کامپوننت‌های آگاه از چرخه حیات (Views)

مفاهیم و پیاده‌سازی Jetpack Compose

کامپوننت‌های آگاه از چرخه حیات، در پاسخ به تغییر در وضعیت چرخه حیات کامپوننت دیگری، مانند اکتیویتی‌ها و فرگمنت‌ها، اقداماتی را انجام می‌دهند. این کامپوننت‌ها به شما کمک می‌کنند تا کدی با سازماندهی بهتر و اغلب سبک‌تر تولید کنید که نگهداری آن آسان‌تر است.

یک الگوی رایج، پیاده‌سازی اقدامات اجزای وابسته در متدهای چرخه حیات فعالیت‌ها و فرگمنت‌ها است. با این حال، این الگو منجر به سازماندهی ضعیف کد و افزایش خطاها می‌شود. با استفاده از کامپوننت‌های آگاه از چرخه حیات، می‌توانید کد اجزای وابسته را از متدهای چرخه حیات خارج کرده و به خود کامپوننت‌ها منتقل کنید.

پکیج androidx.lifecycle کلاس‌ها و رابط‌هایی را ارائه می‌دهد که به شما امکان می‌دهند کامپوننت‌های آگاه از چرخه حیات بسازید - کامپوننت‌هایی که می‌توانند به طور خودکار رفتار خود را بر اساس وضعیت چرخه حیات فعلی یک اکتیویتی یا فرگمنت تنظیم کنند.

بیشتر کامپوننت‌های برنامه که در چارچوب اندروید تعریف شده‌اند، چرخه‌های حیات (lifecycles) دارند که به آنها متصل هستند. چرخه‌های حیات توسط سیستم عامل یا کد چارچوب که در فرآیند شما اجرا می‌شود، مدیریت می‌شوند. آنها هسته اصلی نحوه کار اندروید هستند و برنامه شما باید به آنها احترام بگذارد. عدم رعایت این موارد ممکن است باعث نشت حافظه یا حتی خرابی برنامه شود.

تصور کنید که ما یک activity داریم که مکان دستگاه را روی صفحه نمایش می‌دهد. یک پیاده‌سازی رایج می‌تواند به شکل زیر باشد:

کاتلین

internal class MyLocationListener(
        private val context: Context,
        private val callback: (Location) -> Unit
) {

    fun start() {
        // connect to system location service
    }

    fun stop() {
        // disconnect from system location service
    }
}

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        myLocationListener.start()
        // manage other components that need to respond
        // to the activity lifecycle
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

جاوا

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

اگرچه این نمونه خوب به نظر می‌رسد، اما در یک برنامه واقعی، در نهایت با فراخوانی‌های زیادی مواجه خواهید شد که رابط کاربری و سایر کامپوننت‌ها را در پاسخ به وضعیت فعلی چرخه حیات مدیریت می‌کنند. مدیریت چندین کامپوننت، مقدار قابل توجهی کد را در متدهای چرخه حیات، مانند onStart() و onStop ، قرار می‌دهد که نگهداری آنها را دشوار می‌کند.

علاوه بر این، هیچ تضمینی وجود ندارد که کامپوننت قبل از توقف اکتیویتی یا فرگمنت شروع به کار کند. این امر به ویژه در صورتی صادق است که نیاز به انجام یک عملیات طولانی مدت، مانند بررسی برخی پیکربندی‌ها در onStart داشته باشیم. این می‌تواند باعث ایجاد شرایط رقابتی شود که در آن متد onStop() قبل از onStart به پایان می‌رسد و کامپوننت را بیش از حد مورد نیاز فعال نگه می‌دارد.

کاتلین

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        Util.checkUserStatus { result ->
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start()
            }
        }
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
    }

}

جاوا

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

پکیج androidx.lifecycle کلاس‌ها و رابط‌هایی را ارائه می‌دهد که به شما کمک می‌کنند تا این مشکلات را به روشی انعطاف‌پذیر و ایزوله برطرف کنید.

چرخه حیات

Lifecycle کلاسی است که اطلاعات مربوط به وضعیت چرخه حیات یک کامپوننت (مانند یک اکتیویتی یا یک فرگمنت) را در خود نگه می‌دارد و به سایر اشیاء اجازه می‌دهد تا این وضعیت را مشاهده کنند.

Lifecycle از دو شمارش اصلی برای ردیابی وضعیت چرخه حیات کامپوننت مرتبط با آن استفاده می‌کند:

رویداد

رویدادهای چرخه حیات که از چارچوب و کلاس Lifecycle ارسال می‌شوند. این رویدادها به رویدادهای فراخوانی در فعالیت‌ها و فرگمنت‌ها نگاشت می‌شوند.

ایالت

وضعیت فعلی کامپوننت که توسط شیء Lifecycle ردیابی می‌شود.

شکل 1. حالت‌ها و رویدادهای چرخه حیات فعالیت اندروید.

حالت‌ها را به عنوان گره‌های یک گراف و رویدادها را به عنوان یال‌های بین این گره‌ها در نظر بگیرید.

یک کلاس می‌تواند با پیاده‌سازی DefaultLifecycleObserver و بازنویسی متدهای مربوطه مانند onCreate, onStart و غیره، وضعیت چرخه حیات کامپوننت را رصد کند. سپس می‌توانید با فراخوانی متد addObserver() از کلاس Lifecycle و ارسال یک نمونه از observer خود، همانطور که در مثال زیر نشان داده شده است، یک observer اضافه کنید:

کاتلین

class MyObserver : DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
        connect()
    }

    override fun onPause(owner: LifecycleOwner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(MyObserver())

جاوا

public class MyObserver implements DefaultLifecycleObserver {
    @Override
    public void onResume(LifecycleOwner owner) {
        connect()
    }

    @Override
    public void onPause(LifecycleOwner owner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

در مثال بالا، شیء myLifecycleOwner رابط LifecycleOwner را پیاده‌سازی می‌کند که در بخش بعدی توضیح داده خواهد شد.

مالک چرخه حیات

LifecycleOwner یک رابط تک‌متدی است که نشان می‌دهد کلاس دارای یک Lifecycle است. این رابط یک متد به نام getLifecycle دارد که باید توسط کلاس پیاده‌سازی شود. اگر می‌خواهید چرخه حیات کل فرآیند برنامه را مدیریت کنید، به ProcessLifecycleOwner مراجعه کنید.

این رابط، مالکیت یک Lifecycle را از کلاس‌های منفرد، مانند Fragment و AppCompatActivity ، جدا می‌کند و امکان نوشتن کامپوننت‌هایی را فراهم می‌کند که با آنها کار می‌کنند. هر کلاس برنامه سفارشی می‌تواند رابط LifecycleOwner را پیاده‌سازی کند.

کامپوننت‌هایی که DefaultLifecycleObserver پیاده‌سازی می‌کنند، به طور یکپارچه با کامپوننت‌هایی که LifecycleOwner پیاده‌سازی می‌کنند، کار می‌کنند، زیرا یک مالک می‌تواند یک چرخه حیات ارائه دهد که یک ناظر می‌تواند آن را برای مشاهده ثبت کند.

برای مثال ردیابی موقعیت مکانی، می‌توانیم کلاس MyLocationListener را طوری پیاده‌سازی کنیم که DefaultLifecycleObserver پیاده‌سازی کند و سپس آن را با Lifecycle مربوط به activity در متد onCreate() مقداردهی اولیه کند. این کار به کلاس MyLocationListener اجازه می‌دهد تا خودکفا باشد، به این معنی که منطق واکنش به تغییرات در وضعیت lifecycle به جای activity در MyLocationListener تعریف می‌شود. داشتن کامپوننت‌های جداگانه که منطق خود را ذخیره می‌کنند، مدیریت activityها و fragmentها را آسان‌تر می‌کند.

کاتلین

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this, lifecycle) { location ->
            // update UI
        }
        Util.checkUserStatus { result ->
            if (result) {
                myLocationListener.enable()
            }
        }
    }
}

جاوا

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

یک مورد استفاده رایج، اجتناب از فراخوانی برخی از فراخوانی‌های برگشتی در صورتی است که Lifecycle در حال حاضر در وضعیت خوبی نباشد. برای مثال، اگر فراخوانی برگشتی پس از ذخیره وضعیت فعالیت، یک تراکنش قطعه‌ای را اجرا کند، باعث خرابی می‌شود، بنابراین ما هرگز نمی‌خواهیم آن فراخوانی برگشتی را فراخوانی کنیم.

برای آسان‌تر کردن این مورد استفاده، کلاس Lifecycle به سایر اشیاء اجازه می‌دهد تا وضعیت فعلی را بررسی کنند.

کاتلین

internal class MyLocationListener(
        private val context: Context,
        private val lifecycle: Lifecycle,
        private val callback: (Location) -> Unit
): DefaultLifecycleObserver {

    private var enabled = false

    override fun onStart(owner: LifecycleOwner) {
        if (enabled) {
            // connect
        }
    }

    fun enable() {
        enabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // connect if not connected
        }
    }

    override fun onStop(owner: LifecycleOwner) {
        // disconnect if connected
    }
}

جاوا

class MyLocationListener implements DefaultLifecycleObserver {
    private boolean enabled = false;
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
       ...
    }

    @Override
    public void onStart(LifecycleOwner owner) {
        if (enabled) {
           // connect
        }
    }

    public void enable() {
        enabled = true;
        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }

    @Override
    public void onStop(LifecycleOwner owner) {
        // disconnect if connected
    }
}

با این پیاده‌سازی، کلاس LocationListener ما کاملاً از چرخه حیات آگاه است. اگر نیاز به استفاده از LocationListener خود از یک activity یا fragment دیگر داشته باشیم، فقط باید آن را مقداردهی اولیه کنیم. تمام عملیات راه‌اندازی و حذف توسط خود کلاس مدیریت می‌شود.

اگر کتابخانه‌ای کلاس‌هایی ارائه می‌دهد که نیاز به کار با چرخه حیات اندروید دارند، توصیه می‌کنیم از کامپوننت‌های آگاه از چرخه حیات استفاده کنید. کلاینت‌های کتابخانه شما می‌توانند به راحتی آن کامپوننت‌ها را بدون مدیریت دستی چرخه حیات در سمت کلاینت ادغام کنند.

پیاده‌سازی یک LifecycleOwner سفارشی

قطعات و فعالیت‌های موجود در کتابخانه پشتیبانی ۲۶.۱.۰ و بالاتر، رابط LifecycleOwner را پیاده‌سازی کرده‌اند.

اگر کلاس سفارشی دارید که می‌خواهید آن را به عنوان LifecycleOwner تعریف کنید، می‌توانید از کلاس LifecycleRegistry استفاده کنید، اما باید رویدادها را به آن کلاس ارسال کنید، همانطور که در مثال کد زیر نشان داده شده است:

کاتلین

class MyActivity : Activity(), LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }

    public override fun onStart() {
        super.onStart()
        lifecycleRegistry.markState(Lifecycle.State.STARTED)
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

جاوا

public class MyActivity extends Activity implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;

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

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }

    @Override
    public void onStart() {
        super.onStart();
        lifecycleRegistry.markState(Lifecycle.State.STARTED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

بهترین شیوه‌ها برای کامپوننت‌های آگاه از چرخه حیات

  • کنترلرهای رابط کاربری (اکتیویتی‌ها و فرگمنت‌ها) خود را تا حد امکان سبک نگه دارید. آن‌ها نباید سعی کنند داده‌های خود را به دست آورند؛ در عوض، از یک ViewModel برای انجام این کار استفاده کنید و یک شیء LiveData را مشاهده کنید تا تغییرات را به نماها منعکس کند.
  • سعی کنید رابط‌های کاربری داده‌محور بنویسید که در آن‌ها مسئولیت کنترلر رابط کاربری به‌روزرسانی نماها با تغییر داده‌ها یا ارسال اعلان‌های مربوط به اقدامات کاربر به ViewModel باشد.
  • منطق داده‌های خود را در کلاس ViewModel قرار دهید. ViewModel باید به عنوان رابط بین کنترلر رابط کاربری و بقیه برنامه شما عمل کند. البته مراقب باشید، ViewModel وظیفه دریافت داده‌ها (مثلاً از یک شبکه) را ندارد. در عوض، ViewModel باید کامپوننت مناسب را برای دریافت داده‌ها فراخوانی کند، سپس نتیجه را به کنترلر رابط کاربری ارائه دهد.
  • از Data Binding برای حفظ یک رابط کاربری تمیز بین viewها و کنترلر UI استفاده کنید. این به شما امکان می‌دهد viewهای خود را اعلانی‌تر کنید و کد به‌روزرسانی مورد نیاز برای نوشتن در activityها و fragmentها را به حداقل برسانید. اگر ترجیح می‌دهید این کار را با زبان برنامه‌نویسی جاوا انجام دهید، از کتابخانه‌ای مانند Butter Knife استفاده کنید تا از کد تکراری جلوگیری کنید و انتزاع بهتری داشته باشید.
  • اگر رابط کاربری شما پیچیده است، ایجاد یک کلاس presenter برای مدیریت تغییرات رابط کاربری را در نظر بگیرید. این ممکن است یک کار پر زحمت باشد، اما می‌تواند تست اجزای رابط کاربری شما را آسان‌تر کند.
  • از ارجاع به یک زمینه View یا Activity در ViewModel خود خودداری کنید. اگر ViewModel از activity بیشتر عمر کند (در صورت تغییرات پیکربندی)، activity شما دچار نشت داده می‌شود و به درستی توسط garbage collector دفع نمی‌شود.
  • از کوروتین‌های کاتلین برای مدیریت وظایف طولانی‌مدت و سایر عملیاتی که می‌توانند به‌صورت غیرهمزمان اجرا شوند، استفاده کنید.

موارد استفاده برای کامپوننت‌های آگاه از چرخه عمر

کامپوننت‌های آگاه از چرخه حیات می‌توانند مدیریت چرخه‌های حیات را در موارد مختلف برای شما بسیار آسان‌تر کنند. چند نمونه از آنها عبارتند از:

  • جابجایی بین به‌روزرسانی‌های مکان دقیق و جزئی. از کامپوننت‌های آگاه از چرخه حیات برای فعال کردن به‌روزرسانی‌های مکان دقیق در حالی که برنامه مکان شما قابل مشاهده است استفاده کنید و وقتی برنامه در پس‌زمینه است، به به‌روزرسانی‌های جزئی تغییر دهید. LiveData ، یک کامپوننت آگاه از چرخه حیات، به برنامه شما اجازه می‌دهد تا وقتی کاربر مکان خود را تغییر می‌دهد، رابط کاربری را به‌طور خودکار به‌روزرسانی کند.
  • توقف و شروع بافرینگ ویدیو. از کامپوننت‌های آگاه از چرخه حیات برای شروع بافرینگ ویدیو در اسرع وقت استفاده کنید، اما پخش را تا زمان شروع کامل برنامه به تعویق بیندازید. همچنین می‌توانید از کامپوننت‌های آگاه از چرخه حیات برای خاتمه بافرینگ در هنگام از بین رفتن برنامه خود استفاده کنید.
  • شروع و توقف اتصال شبکه. از اجزای آگاه از چرخه حیات برای فعال کردن به‌روزرسانی زنده (پخش) داده‌های شبکه در حالی که برنامه در پیش‌زمینه است و همچنین برای مکث خودکار هنگام رفتن برنامه به پس‌زمینه استفاده کنید.
  • مکث و از سرگیری نمایش‌های متحرک. از کامپوننت‌های آگاه از چرخه حیات برای مدیریت مکث نمایش‌های متحرک در زمانی که برنامه در پس‌زمینه است و از سرگیری نمایش‌های متحرک پس از قرار گرفتن برنامه در پیش‌زمینه استفاده کنید.

مدیریت رویدادهای توقف

وقتی یک Lifecycle متعلق به یک AppCompatActivity یا Fragment باشد، وضعیت Lifecycle به CREATED تغییر می‌کند و رویداد ON_STOP زمانی ارسال می‌شود که onSaveInstanceState() مربوط به AppCompatActivity یا Fragment فراخوانی شود.

وقتی وضعیت یک Fragment یا AppCompatActivity با استفاده از onSaveInstanceState ذخیره می‌شود، رابط کاربری آن تا زمان فراخوانی ON_START تغییرناپذیر در نظر گرفته می‌شود. تلاش برای تغییر رابط کاربری پس از ذخیره وضعیت، احتمالاً باعث ایجاد ناهماهنگی در وضعیت ناوبری برنامه شما می‌شود، به همین دلیل است که اگر برنامه پس از ذخیره وضعیت، FragmentTransaction را اجرا کند، FragmentManager یک استثنا ایجاد می‌کند. برای جزئیات بیشتر به commit() مراجعه کنید.

LiveData از این حالت حاشیه‌ای به طور پیش‌فرض جلوگیری می‌کند، به این صورت که اگر Lifecycle ) مربوط به ناظر (observer) حداقل STARTED نشده باشد، از فراخوانی ناظر خود خودداری می‌کند. در پشت صحنه، قبل از تصمیم‌گیری برای فراخوانی ناظر خود، تابع isAtLeast() را فراخوانی می‌کند.

متأسفانه، متد onStop() در AppCompatActivity پس از onSaveInstanceState فراخوانی می‌شود، که باعث ایجاد شکافی می‌شود که در آن تغییرات وضعیت رابط کاربری مجاز نیست، اما Lifecycle هنوز به وضعیت CREATED منتقل نشده است.

برای جلوگیری از این مشکل، کلاس Lifecycle در نسخه beta2 و lower، وضعیت را بدون ارسال رویداد، به صورت CREATED علامت‌گذاری می‌کند تا هر کدی که وضعیت فعلی را بررسی می‌کند، مقدار واقعی را دریافت کند، حتی اگر رویداد تا زمان فراخوانی onStop() توسط سیستم ارسال نشده باشد.

متأسفانه، این راه‌حل دو مشکل اساسی دارد:

  • در API سطح ۲۳ و پایین‌تر، سیستم اندروید در واقع وضعیت یک اکتیویتی را ذخیره می‌کند، حتی اگر بخشی از آن توسط اکتیویتی دیگری پوشش داده شده باشد. به عبارت دیگر، سیستم اندروید تابع onSaveInstanceState() را فراخوانی می‌کند، اما لزوماً onStop فراخوانی نمی‌کند. این یک بازه زمانی بالقوه طولانی ایجاد می‌کند که در آن ناظر هنوز فکر می‌کند که چرخه حیات فعال است، حتی اگر وضعیت رابط کاربری آن قابل تغییر نباشد.
  • هر کلاسی که بخواهد رفتاری مشابه کلاس LiveData را ارائه دهد، باید راهکار ارائه شده توسط Lifecycle نسخه beta 2 و پایین‌تر را پیاده‌سازی کند.

منابع اضافی

برای کسب اطلاعات بیشتر در مورد مدیریت چرخه‌های حیات با کامپوننت‌های آگاه از چرخه حیات، به منابع اضافی زیر مراجعه کنید.

نمونه‌ها

  • آفتابگردان ، یک برنامه آزمایشی که بهترین شیوه‌ها را با کامپوننت‌های معماری نشان می‌دهد

کدلبز

وبلاگ‌ها