Paging 2 লাইব্রেরি ওভারভিউ   Android Jetpack-এর অংশ।

পেজিং লাইব্রেরি আপনাকে একবারে ডেটার ছোট চাঙ্ক লোড এবং ডিসপ্লে করতে সাহায্য করে। চাহিদা অনুযায়ী আংশিক ডেটা লোড করলে নেটওয়ার্ক ব্যান্ডউইথ ও সিস্টেম রিসোর্সের ব্যবহার কমে।

এই নির্দেশিকায় লাইব্রেরির বেশ কিছু ধারণাগত উদাহরণ দেওয়া হয়েছে এবং এটি কীভাবে কাজ করে তার ওভারভিউও দেওয়া হয়েছে। এই লাইব্রেরি কীভাবে কাজ করে তার সম্পূর্ণ উদাহরণ দেখতে, অতিরিক্ত রিসোর্স বিভাগ থেকে কোডল্যাব এবং স্যাম্পেল ব্যবহার করে দেখুন।

সেটআপ

আপনার Android অ্যাপে পেজিং কম্পোনেন্ট ইমপোর্ট করতে, আপনার অ্যাপের build.gradle ফাইলে নিম্নলিখিত ডিপেন্ডেন্সি যোগ করুন:

গ্ৰুভি

dependencies {
  def paging_version = "2.1.2"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  implementation("androidx.paging:paging-runtime:$paging_version") // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation("androidx.paging:paging-common:$paging_version") // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation("androidx.paging:paging-rxjava2:$paging_version") // For Kotlin use paging-rxjava2-ktx
}

লাইব্রেরি আর্কিটেকচার

এই বিভাগে পেজিং লাইব্রেরির প্রধান কম্পোনেন্টগুলি বর্ণনা করা হয়েছে এবং দেখানো হয়েছে।

PagedList

পেজিং লাইব্রেরির মূল কম্পোনেন্ট হল PagedList ক্লাস, যা আপনার অ্যাপের ডেটার চাঙ্ক বা পৃষ্ঠা লোড করে। আরও ডেটা প্রয়োজন, তাই এটি আগে থেকে থাকা PagedList অবজেক্টে পেজ করা হয়েছে। লোড করা কোনও ডেটা পরিবর্তিত হলে, PagedList-এর একটি নতুন ইনস্ট্যান্স LiveData বা RxJava2-ভিত্তিক অবজেক্ট থেকে পর্যবেক্ষণযোগ্য ডেটা হোল্ডারে নির্গত হয়। PagedList অবজেক্ট তৈরি হওয়ার সাথে সাথে, আপনার অ্যাপের UI সেগুলির কন্টেন্ট দেখায়, এই সবকিছু আপনার UI কন্ট্রোলারের লাইফসাইকেল মেনে চলে।

নিম্নলিখিত কোড স্নিপেট থেকে আপনি জানতে পারবেন যে PagedList অবজেক্টের LiveData হোল্ডার ব্যবহার করে ডেটা লোড ও উপস্থাপন করার জন্য আপনার অ্যাপের ভিউ মডেল কীভাবে কনফিগার করবেন:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

জাভা

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    // Creates a PagedList object with 50 items per page.
    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), 50).build();
    }
}

ডেটা

PagedList-এর প্রতিটি ইনস্ট্যান্স আপনার অ্যাপের ডেটার আপ-টু-ডেট স্ন্যাপশট সংশ্লিষ্ট DataSource অবজেক্ট থেকে লোড করে। আপনার অ্যাপের ব্যাকএন্ড বা ডেটাবেস থেকে PagedList অবজেক্টে ডেটা ফ্লো হয়।

আপনার অ্যাপের ডেটা সাজাতে নিম্নলিখিত উদাহরণে রুম পারসিসটেন্স লাইব্রেরি ব্যবহার করা হয়েছে, তবে আপনি অন্য কোনও উপায়ে ডেটা স্টোর করতে চাইলে, আপনার নিজস্ব ডেটা সোর্স ফ্যাক্টরিও প্রদান করতে পারেন।

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

জাভা

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a
    // PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

PagedList অবজেক্টে কীভাবে ডেটা লোড করবেন সেই সম্পর্কে আরও জানতে, পেজ করা ডেটা লোড করা সংক্রান্ত নির্দেশিকা দেখুন।

UI

PagedList ক্লাস PagedListAdapter-এর সাথে কাজ করে RecyclerView-এ আইটেম লোড করে। এই ক্লাসগুলি একসাথে কাজ করে কন্টেন্ট লোড হওয়ার সাথে সাথে তা ফেচ করে এবং দেখায়, ভিউয়ের বাইরে থাকা কন্টেন্ট প্রিফেচ করে এবং কন্টেন্টের পরিবর্তন অ্যানিমেট করে।

আরও জানতে, কীভাবে পৃষ্ঠা অনুযায়ী তালিকা দেখানো যায় সেই সংক্রান্ত নির্দেশিকা দেখুন।

বিভিন্ন ডেটা আর্কিটেকচার সাপোর্ট করা

পেজিং লাইব্রেরি নিম্নলিখিত ডেটা আর্কিটেকচার সাপোর্ট করে:

  • শুধুমাত্র ব্যাকএন্ড সার্ভার থেকে পরিবেশন করা হয়।
  • শুধুমাত্র অন-ডিভাইস ডেটাবেসে স্টোর করা হয়।
  • অন্যান্য সোর্সের কম্বিনেশন, ক্যাশে হিসেবে অন-ডিভাইস ডেটাবেস ব্যবহার করে।

এইসব আর্কিটেকচার সিনারিওর প্রতিটিতে ডেটা কীভাবে ফ্লো করে তা ১ নম্বর ছবিতে দেখানো হয়েছে। শুধুমাত্র নেটওয়ার্ক বা শুধুমাত্র ডেটাবেস সলিউশনের ক্ষেত্রে, ডেটা সরাসরি আপনার অ্যাপের UI মডেলে ফ্লো করে। আপনি যদি একটি সম্মিলিত পদ্ধতি ব্যবহার করেন, তাহলে আপনার ব্যাকএন্ড সার্ভার থেকে ডেটা, একটি অন-ডিভাইস ডেটাবেসে এবং তারপর আপনার অ্যাপের UI মডেলে প্রবাহিত হয়। মাঝে মাঝে, প্রতিটি ডেটা ফ্লোর এন্ডপয়েন্টে লোড করার মতো ডেটা শেষ হয়ে যায়, সেই সময় এটি ডেটা প্রদানকারী কম্পোনেন্টের কাছে আরও ডেটার জন্য অনুরোধ করে। যেমন, অন-ডিভাইস ডেটাবেসে ডেটা শেষ হয়ে গেলে, এটি সার্ভার থেকে আরও ডেটার জন্য অনুরোধ করে।

ডেটা ফ্লোর ডায়াগ্রাম
ছবি ১. পেজিং লাইব্রেরি সাপোর্ট করে এমন প্রতিটি আর্কিটেকচারের মাধ্যমে ডেটা কীভাবে প্রবাহিত হয়

এই বিভাগের বাকি অংশে প্রতিটি ডেটা ফ্লো ব্যবহারের কেস কনফিগার করার জন্য সাজেশন দেওয়া হয়েছে।

শুধুমাত্র নেটওয়ার্ক

ব্যাকএন্ড সার্ভার থেকে ডেটা দেখাতে, Retrofit API-এর সিঙ্ক্রোনাস ভার্সন ব্যবহার করে আপনার নিজস্ব কাস্টম DataSource অবজেক্টে তথ্য লোড করুন।

শুধু ডেটাবেস

লোকাল স্টোরেজ পর্যবেক্ষণ করতে আপনার RecyclerView সেট-আপ করুন, রুম পারসিসটেন্স লাইব্রেরি ব্যবহার করলে ভালো হয়। এইভাবে, আপনার অ্যাপের ডেটাবেসে যখনই ডেটা যোগ বা পরিবর্তন করা হয়, এই পরিবর্তনগুলি অটোমেটিক RecyclerView-এ প্রতিফলিত হয় যা এই ডেটা দেখায়।

নেটওয়ার্ক ও ডেটাবেস

আপনি ডেটাবেস পর্যবেক্ষণ করা শুরু করার পরে, ডেটাবেসে ডেটা শেষ হয়ে গেলে আপনি PagedList.BoundaryCallback ব্যবহার করে শুনতে পারবেন। তারপর আপনি নিজের নেটওয়ার্ক থেকে আরও আইটেম নিয়ে এসে ডেটাবেসে যোগ করতে পারবেন। আপনার UI যদি ডেটাবেস পর্যবেক্ষণ করে, তাহলে আপনাকে আর কিছু করতে হবে না।

নেটওয়ার্ক সংক্রান্ত সমস্যা ম্যানেজ করা

পেজিং লাইব্রেরি ব্যবহার করে আপনি যে ডেটা দেখাচ্ছেন তা ফেচ বা পেজ করার জন্য নেটওয়ার্ক ব্যবহার করার সময়, নেটওয়ার্ককে সবসময় "উপলভ্য" বা "অনুপলভ্য" হিসেবে বিবেচনা না করা গুরুত্বপূর্ণ, কারণ অনেক কানেকশনই মাঝে মাঝে কাজ করে বা দুর্বল হয়:

  • কোনও নির্দিষ্ট সার্ভার নেটওয়ার্ক অনুরোধে সাড়া নাও দিতে পারে।
  • ডিভাইসটি এমন কোনও নেটওয়ার্কের সাথে কানেক্ট করা থাকতে পারে যা স্লো বা দুর্বল।

পরিবর্তে, আপনার অ্যাপের প্রতিটি অনুরোধে ব্যর্থতা চেক করা উচিত এবং নেটওয়ার্ক উপলভ্য না থাকলে যতটা সম্ভব সহজে রিকভার করা উচিত। যেমন, ডেটা রিফ্রেশ করার ধাপ কাজ না করলে, ব্যবহারকারীদের বেছে নেওয়ার জন্য আপনি "আবার চেষ্টা করুন" বোতাম দিতে পারেন। ডেটা পেজিংয়ের সময় কোনও সমস্যা হলে, পেজিংয়ের অনুরোধ অটোমেটিক আবার চেষ্টা করা সবচেয়ে ভালো।

আপনার আগে থেকে থাকা অ্যাপ আপডেট করুন

আপনার অ্যাপ যদি আগে থেকেই কোনও ডেটাবেস বা ব্যাকএন্ড সোর্স থেকে ডেটা ব্যবহার করে, তাহলে পেজিং লাইব্রেরি প্রদান করে এমন কার্যকারিতায় সরাসরি আপগ্রেড করা সম্ভব। এই বিভাগ থেকে জানতে পারবেন যে আগে থেকে থাকা সাধারণ ডিজাইন সহ কোনও অ্যাপ কীভাবে আপগ্রেড করতে হয়।

কাস্টম পেজিং সমাধান

আপনার অ্যাপের ডেটা সোর্স থেকে ডেটার ছোট সাবসেট লোড করার জন্য আপনি কাস্টম কার্যকারিতা ব্যবহার করলে, আপনি এই লজিকটি PagedList ক্লাস থেকে পরিবর্তন করতে পারেন। PagedList-এর ইনস্ট্যান্স সাধারণ ডেটা সোর্সের সাথে বিল্ট-ইন কানেকশন অফার করে। এইসব ইনস্ট্যান্স RecyclerView অবজেক্টের জন্য অ্যাডাপ্টারও প্রদান করে যা আপনি নিজের অ্যাপের UI-তে অন্তর্ভুক্ত করতে পারেন।

পৃষ্ঠার পরিবর্তে তালিকা ব্যবহার করে ডেটা লোড করা হয়েছে

আপনি যদি নিজের UI-এর অ্যাডাপ্টারের জন্য ব্যাকিং ডেটা স্ট্রাকচার হিসেবে ইন-মেমরি তালিকা ব্যবহার করেন, তাহলে PagedList ক্লাস ব্যবহার করে ডেটা আপডেট দেখার কথা বিবেচনা করুন, যদি তালিকায় আইটেমের সংখ্যা অনেক বেশি হয়। PagedList-এর ইনস্ট্যান্স, আপনার অ্যাপের UI-তে ডেটা আপডেট পাস করার জন্য LiveData<PagedList> অথবা Observable<List> ব্যবহার করতে পারে, যা লোড টাইম ও মেমরির ব্যবহার কম করে। আরও ভাল হয়, আপনার অ্যাপে List অবজেক্টের পরিবর্তে PagedList অবজেক্ট ব্যবহার করলে, আপনার অ্যাপের UI স্ট্রাকচার বা ডেটা আপডেট করার লজিকে কোনও পরিবর্তন করতে হয় না।

CursorAdapter ব্যবহার করে তালিকা দৃশ্যের সাথে ডেটা কার্সর যুক্ত করুন

আপনার অ্যাপ Cursor থেকে ListView-এ ডেটা অ্যাসোসিয়েট করতে CursorAdapter ব্যবহার করতে পারে। সেক্ষেত্রে, সাধারণত আপনাকে অ্যাপের তালিকা UI কন্টেনার হিসেবে ListView থেকে RecyclerView-এ মাইগ্রেট করতে হবে, তারপর Cursor কম্পোনেন্টকে Room অথবা PositionalDataSource দিয়ে বদল করতে হবে, যা নির্ভর করে Cursor-এর ইনস্ট্যান্স SQLite ডেটাবেস অ্যাক্সেস করে কিনা তার উপর।

কিছু পরিস্থিতিতে, যেমন Spinner-এর ইনস্ট্যান্সের সাথে কাজ করার সময়, আপনি শুধু অ্যাডাপ্টার প্রদান করেন। তারপর, একটি লাইব্রেরি সেই অ্যাডাপ্টারে লোড করা ডেটা নেয় এবং আপনার জন্য ডেটা দেখায়। এইসব পরিস্থিতিতে, আপনার অ্যাডাপ্টারের ডেটার ধরন পরিবর্তন করে LiveData<PagedList> করুন, তারপর এই তালিকাটি একটি ArrayAdapter অবজেক্টে র‌্যাপ করুন, যাতে কোনও লাইব্রেরি ক্লাস UI-তে এইসব আইটেম ইনফ্লেট করতে পারে।

AsyncListUtil ব্যবহার করে অ্যাসিঙ্ক্রোনাসভাবে কন্টেন্ট লোড করুন

আপনি যদি AsyncListUtil অবজেক্ট ব্যবহার করে অ্যাসিঙ্ক্রোনাসভাবে তথ্যের গ্রুপ লোড ও ডিসপ্লে করেন, তাহলে পেজিং লাইব্রেরি আপনাকে আরও সহজে ডেটা লোড করতে দেয়:

  • আপনার ডেটা পজিশনাল হওয়ার প্রয়োজন নেই। পেজিং লাইব্রেরি আপনাকে নেটওয়ার্কের প্রদান করা কী ব্যবহার করে সরাসরি আপনার ব্যাকএন্ড থেকে ডেটা লোড করতে দেয়।
  • আপনার ডেটা অগণিত হতে পারে। পেজিং লাইব্রেরি ব্যবহার করে, আপনি কোনও ডেটা বাকি না থাকা পর্যন্ত পৃষ্ঠায় ডেটা লোড করতে পারেন।
  • আপনি আরও সহজে আপনার ডেটা দেখতে পারবেন। পেজিং লাইব্রেরি আপনার ডেটা দেখাতে পারে যা আপনার অ্যাপের ViewModel একটি পর্যবেক্ষণযোগ্য ডেটা স্ট্রাকচারে ধারণ করে।

ডেটাবেসের উদাহরণ

নিচের কোড স্নিপেট থেকে জানা যাবে যে কীভাবে সবকটি অংশ একসাথে কাজ করতে পারে।

LiveData ব্যবহার করে পৃষ্ঠাভিত্তিক ডেটা পর্যবেক্ষণ করা

নিম্নলিখিত কোড স্নিপেট থেকে বোঝা যাবে যে সব অংশ কীভাবে একসাথে কাজ করে। কনসার্ট ইভেন্ট ডেটাবেসে যোগ করা, সরানো বা পরিবর্তন করা হলে, RecyclerView-এর কন্টেন্ট অটোমেটিক ও দক্ষতার সাথে আপডেট করা হয়:

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

class ConcertActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.observe(this, PagedList(adapter::submitList))
        recyclerView.setAdapter(adapter)
    }
}

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
        val concert: Concert? = getItem(position)

        // Note that "concert" is a placeholder if it's null.
        holder.bindTo(concert)
    }

    companion object {
        private val DIFF_CALLBACK = object :
                DiffUtil.ItemCallback<Concert>() {
            // Concert details may have changed if reloaded from the database,
            // but ID is fixed.
            override fun areItemsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert.id == newConcert.id

            override fun areContentsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert == newConcert
        }
    }
}

জাভা

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
            concertDao.concertsByDate(), /* page size */ 50).build();
    }
}

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConcertViewModel viewModel =
                new ViewModelProvider(this).get(ConcertViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.concert_list);
        ConcertAdapter adapter = new ConcertAdapter();
        viewModel.concertList.observe(this, adapter::submitList);
        recyclerView.setAdapter(adapter);
    }
}

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.bindTo(concert);
        } else {
            // Null defines a placeholder item - PagedListAdapter automatically
            // invalidates this row when the actual object is loaded from the
            // database.
            holder.clear();
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
        // Concert details may have changed if reloaded from the database,
        // but ID is fixed.
        @Override
        public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
            return oldConcert.getId() == newConcert.getId();
        }

        @Override
        public boolean areContentsTheSame(Concert oldConcert,
                Concert newConcert) {
            return oldConcert.equals(newConcert);
        }
    };
}

RxJava2 ব্যবহার করে পৃষ্ঠাভিত্তিক ডেটা পর্যবেক্ষণ করা

আপনি যদি LiveData-এর পরিবর্তে RxJava2 ব্যবহার করতে চান, তাহলে Observable বা Flowable অবজেক্ট তৈরি করতে পারেন:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: Observable<PagedList<Concert>> =
            concertDao.concertsByDate().toObservable(pageSize = 50)
}

জাভা

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final Observable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50)
                        .buildObservable();
    }
}

তারপর, আপনি নিম্নলিখিত স্নিপেটে কোড ব্যবহার করে ডেটা পর্যবেক্ষণ করা শুরু ও বন্ধ করতে পারবেন:

Kotlin

class ConcertActivity : AppCompatActivity() {
    private val adapter = ConcertAdapter()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        recyclerView.setAdapter(adapter)
    }

    override fun onStart() {
        super.onStart()
        disposable.add(viewModel.concertList
                .subscribe(adapter::submitList)))
    }

    override fun onStop() {
        super.onStop()
        disposable.clear()
    }
}

জাভা

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = findViewById(R.id.concert_list);

        viewModel = new ViewModelProvider(this).get(ConcertViewModel.class);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        disposable.add(viewModel.concertList
                .subscribe(adapter.submitList(flowableList)
        ));
    }

    @Override
    protected void onStop() {
        super.onStop();
        disposable.clear();
    }
}

ConcertDao এবং ConcertAdapter-এর কোড RxJava2-ভিত্তিক সমাধানের জন্য একই, কারণ সেগুলি LiveData-ভিত্তিক সমাধানের জন্য।

মতামত জানান

এইসব রিসোর্সের মাধ্যমে আপনার মতামত ও আইডিয়া আমাদের সাথে শেয়ার করুন:

ইস্যু ট্র্যাকার
সমস্যার ব্যাপারে অভিযোগ জানান যাতে আমরা বাগ ঠিক করতে পারি।

অতিরিক্ত রিসোর্স

পেজিং লাইব্রেরি সম্পর্কে আরও জানতে, নিম্নলিখিত রিসোর্স পরামর্শ করুন।

নমুনা

কোডল্যাবস

ভিডিও