Android-এ অফলাইন ক্ষমতা চালু করা হচ্ছে

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

ডিস্ক পারসিসটেন্স

Firebase অ্যাপ সাময়িক নেটওয়ার্ক বিঘ্নিত হওয়ার সমস্যা অটোমেটিক ম্যানেজ করে। অফলাইন থাকাকালীন ক্যাশে করা ডেটা উপলভ্য থাকে এবং নেটওয়ার্ক কানেক্টিভিটি রিস্টোর করা হলে Firebase যেকোনও রাইট আবার পাঠায়।

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

আপনি মাত্র এক লাইন কোড লিখে ডিস্ক পারসিস্টেন্স চালু করতে পারবেন।

Kotlin

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

পারসিস্টেন্স বিহেভিওর

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

আপনার অ্যাপ অফলাইন থাকাকালীন যেসব রাইট অপারেশন করা হয়, Firebase Realtime Database ক্লায়েন্ট অটোমেটিক সেগুলির একটি সারি তৈরি করে রাখে। পারসিস্টেন্স চালু করা থাকলে, এই সারি ডিস্কেও পারসিস্ট করা হয় যাতে ব্যবহারকারী বা অপারেটিং সিস্টেম অ্যাপ রিস্টার্ট করলে আপনার সব রাইট উপলভ্য থাকে। অ্যাপ আবার কানেক্ট হলে, সব অপারেশন Firebase Realtime Database সার্ভারে পাঠানো হয়।

আপনার অ্যাপ Firebase Authentication ব্যবহার করলে, Firebase Realtime Database ক্লায়েন্ট অ্যাপ রিস্টার্ট করার সময় ব্যবহারকারীর যাচাইকরণ টোকেন বজায় রাখে। আপনার অ্যাপ অফলাইন থাকাকালীন যদি অথেন্টিকেশন টোকেনের মেয়াদ শেষ হয়ে যায়, তাহলে আপনার অ্যাপ ব্যবহারকারীকে আবার যাচাই না করা পর্যন্ত ক্লায়েন্ট লেখার অপারেশন পজ করে দেয়, অন্যথায় নিরাপত্তা সংক্রান্ত নিয়মের কারণে লেখার অপারেশন ব্যর্থ হতে পারে।

ডেটা আপডেট রাখা

Firebase Realtime Database অ্যাক্টিভ শ্রোতাদের জন্য ডেটার একটি লোকাল কপি সিঙ্ক ও স্টোর করে। এছাড়াও, আপনি নির্দিষ্ট লোকেশন সিঙ্ক করে রাখতে পারবেন।

Kotlin

val scoresRef = Firebase.database.getReference("scores")
scoresRef.keepSynced(true)

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);

Firebase Realtime Database ক্লায়েন্ট অটোমেটিক এইসব লোকেশনে ডেটা ডাউনলোড করে এবং রেফারেন্সে কোনও অ্যাক্টিভ শ্রোতা না থাকলেও এটি সিঙ্ক করে রাখে। আপনি নিম্নলিখিত কোডের লাইন ব্যবহার করে সিঙ্ক্রোনাইজেশন বন্ধ করতে পারবেন।

Kotlin

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

ডিফল্ট হিসেবে, আগে সিঙ্ক করা ডেটার ১০ এমবি ক্যাশে করা হয়। বেশিরভাগ অ্যাপ্লিকেশনের জন্য এটি যথেষ্ট হওয়া উচিত। ক্যাশে তার কনফিগার করা সাইজের থেকে বড় হয়ে গেলে, Firebase Realtime Database সেই ডেটা মুছে দেয় যা সম্প্রতি সবচেয়ে কম ব্যবহার করা হয়েছে। সিঙ্ক করে রাখা ডেটা ক্যাশে থেকে মুছে ফেলা হয় না।

অফলাইনে ডেটা কোয়েরি করা

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

যেমন, এই কোডটি স্কোরের Firebase Realtime Database-এ শেষ চারটি আইটেমের জন্য কোয়েরি করে

Kotlin

val scoresRef = Firebase.database.getReference("scores")
scoresRef.orderByValue().limitToLast(4).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.orderByValue().limitToLast(4).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

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

Kotlin

scoresRef.orderByValue().limitToLast(2).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

scoresRef.orderByValue().limitToLast(2).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

আগের উদাহরণে, Firebase Realtime Database ক্লায়েন্ট সবচেয়ে বেশি স্কোর করা দুটি ডাইনোসরের জন্য 'চাইল্ড যোগ করা হয়েছে' ইভেন্ট তৈরি করে, সেভ করা ক্যাশে ব্যবহার করে। কিন্তু এটি 'ভ্যালু' ইভেন্ট তৈরি করবে না, কারণ অ্যাপটি অনলাইন থাকাকালীন কখনও সেই কোয়েরি এক্সিকিউট করেনি।

অ্যাপটি অফলাইনে থাকাকালীন শেষ ছয়টি আইটেমের জন্য অনুরোধ করলে, এটি সরাসরি চারটি ক্যাশে করা আইটেমের জন্য 'চাইল্ড যোগ করা হয়েছে' ইভেন্ট পাবে। ডিভাইস আবার অনলাইন হলে, Firebase Realtime Database ক্লায়েন্ট সার্ভারের সাথে সিঙ্ক করে এবং অ্যাপের জন্য শেষ দুটি 'চাইল্ড যোগ করা হয়েছে' এবং 'ভ্যালু' ইভেন্ট পায়।

অফলাইনে ট্রানজ্যাকশন ম্যানেজ করা

অ্যাপ অফলাইন থাকাকালীন কোনও ট্রানজ্যাকশন করা হলে, তা সারিতে রাখা হয়। অ্যাপটি নেটওয়ার্ক কানেক্টিভিটি ফিরে পেলে, ট্রানজ্যাকশনগুলি Realtime Database সার্ভারে পাঠানো হয়।

উপস্থিতি ম্যানেজ করা

রিয়েল-টাইম অ্যাপ্লিকেশনে ক্লায়েন্ট কখন কানেক্ট ও ডিসকানেক্ট করছে তা শনাক্ত করা প্রায়ই কাজে লাগে। যেমন, আপনি হয়ত কোনও ব্যবহারকারীর ক্লায়েন্ট ডিসকানেক্ট হয়ে গেলে তাকে 'অফলাইন' হিসেবে চিহ্নিত করতে চান।

Firebase ডেটাবেস ক্লায়েন্ট সহজ প্রিমিটিভ প্রদান করে যা আপনি Firebase ডেটাবেস সার্ভার থেকে ক্লায়েন্ট ডিসকানেক্ট হয়ে গেলে ডেটাবেসে লেখার জন্য ব্যবহার করতে পারেন। ক্লায়েন্ট সঠিকভাবে ডিসকানেক্ট করুক বা না করুক, এই আপডেটগুলি হয়, তাই কানেকশন ড্রপ হলে বা ক্লায়েন্ট ক্র্যাশ করলেও, ডেটা ক্লিন-আপ করার জন্য আপনি এগুলির উপর নির্ভর করতে পারেন। ডিসকানেক্ট করার পরে, সেট করা, আপডেট করা এবং সরিয়ে দেওয়া সহ সব রাইট অপারেশন করা যাবে।

onDisconnect প্রিমিটিভ ব্যবহার করে ডিসকানেক্ট করার সময় ডেটা লেখার একটি সহজ উদাহরণ এখানে দেওয়া হল:

Kotlin

val presenceRef = Firebase.database.getReference("disconnectmessage")
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!")

Java

DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!");

onDisconnect কীভাবে কাজ করে

আপনি কোনও onDisconnect() অপারেশন সেট-আপ করলে, অপারেশনটি Firebase Realtime Database সার্ভারে লাইভ থাকে। ব্যবহারকারী অনুরোধ করা রাইট ইভেন্টটি সম্পাদন করতে পারে কিনা তা নিশ্চিত করতে সার্ভার নিরাপত্তা পরীক্ষা করে এবং এটি অবৈধ হলে আপনার অ্যাপকে জানায়। তারপর সার্ভার কানেকশন মনিটর করে। কোনও সময় কানেকশন টাইম-আউট হয়ে গেলে অথবা Realtime Database ক্লায়েন্ট সক্রিয়ভাবে বন্ধ করে দিলে, সার্ভার দ্বিতীয়বার নিরাপত্তা চেক করে (অপারেশন এখনও বৈধ কিনা তা নিশ্চিত করতে) এবং তারপর ইভেন্ট ইনভোক করে।

onDisconnect সঠিকভাবে অ্যাটাচ করা হয়েছে কিনা তা নিশ্চিত করতে আপনার অ্যাপ রাইট অপারেশনে কলব্যাক ব্যবহার করতে পারে:

Kotlin

presenceRef.onDisconnect().removeValue { error, reference ->
    error?.let {
        Log.d(TAG, "could not establish onDisconnect event: ${error.message}")
    }
}

Java

presenceRef.onDisconnect().removeValue(new DatabaseReference.CompletionListener() {
    @Override
    public void onComplete(DatabaseError error, @NonNull DatabaseReference reference) {
        if (error != null) {
            Log.d(TAG, "could not establish onDisconnect event:" + error.getMessage());
        }
    }
});

.cancel() নম্বরে কল করে onDisconnect ইভেন্ট বাতিল করা যেতে পারে:

Kotlin

val onDisconnectRef = presenceRef.onDisconnect()
onDisconnectRef.setValue("I disconnected")
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel()

Java

OnDisconnect onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.setValue("I disconnected");
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel();

কানেকশন স্ট্যাটাস শনাক্ত করা হচ্ছে

উপস্থিতি-সম্পর্কিত অনেক ফিচারের জন্য, আপনার অ্যাপ কখন অনলাইন বা অফলাইন থাকে তা জানা উপযোগী। Firebase Realtime Database /.info/connected-এ একটি বিশেষ লোকেশন প্রদান করে যা Firebase Realtime Database ক্লায়েন্টের কানেকশন স্টেট পরিবর্তন হওয়ার সময় আপডেট করা হয়। এখানে একটি উদাহরণ দেওয়া হল:

Kotlin

val connectedRef = Firebase.database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue(Boolean::class.java) ?: false
        if (connected) {
            Log.d(TAG, "connected")
        } else {
            Log.d(TAG, "not connected")
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            Log.d(TAG, "connected");
        } else {
            Log.d(TAG, "not connected");
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

/.info/connected হল একটি বুলিয়ান ভ্যালু যা Realtime Database ক্লায়েন্টের মধ্যে সিঙ্ক করা হয় না কারণ ভ্যালুটি ক্লায়েন্টের অবস্থার উপর নির্ভর করে। অন্যভাবে বলতে গেলে, একটি ক্লায়েন্ট /.info/connected-কে মিথ্যা হিসেবে পড়লে, অন্য ক্লায়েন্টও যে মিথ্যা হিসেবে পড়বে তার কোনও গ্যারেন্টি নেই।

Android-এ, Firebase ব্যান্ডউইথ ও ব্যাটারির ব্যবহার কমাতে কানেকশন স্ট্যাটাস অটোমেটিক ম্যানেজ করে। কোনও ক্লায়েন্টের অ্যাক্টিভ লিসেনার, পেন্ডিং রাইট বা onDisconnect অপারেশন না থাকলে এবং goOffline পদ্ধতির মাধ্যমে স্পষ্টভাবে ডিসকানেক্ট করা না হলে, ৬০ সেকেন্ড কোনও অ্যাক্টিভিটি না হওয়ার পরে Firebase কানেকশন বন্ধ করে দেয়।

লেটেন্সি ম্যানেজ করা

সার্ভার টাইমস্ট্যাম্প

Firebase Realtime Database সার্ভার, ডেটা হিসেবে সার্ভারে তৈরি করা টাইমস্ট্যাম্প যোগ করার একটি মেকানিজম প্রদান করে। এই ফিচার, onDisconnect-এর সাথে যুক্ত হয়ে, নির্ভরযোগ্যভাবে নোট করার একটি সহজ উপায় প্রদান করে যে সময়ে একজন Realtime Database ক্লায়েন্ট ডিসকানেক্ট হয়েছে:

Kotlin

val userLastOnlineRef = Firebase.database.getReference("users/joe/lastOnline")
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

Java

DatabaseReference userLastOnlineRef = FirebaseDatabase.getInstance().getReference("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

ক্লক স্কিউ

firebase.database.ServerValue.TIMESTAMP অনেক বেশি সঠিক এবং বেশিরভাগ রিড/রাইট অপারেশনের জন্য উপযুক্ত হলেও, Firebase Realtime Database-এর সার্ভারের সাপেক্ষে ক্লায়েন্টের ঘড়ির স্কিউ অনুমান করার জন্য এটি মাঝে মাঝে কাজে লাগতে পারে। সার্ভারের সময় অনুমান করতে, Firebase Realtime Database ক্লায়েন্ট স্থানীয়ভাবে রিপোর্ট করা সময়ে (মিলি সেকেন্ডে ইপক টাইম) যে ভ্যালু (মিলি সেকেন্ডে) যোগ করে, তা পেতে আপনি লোকেশন /.info/serverTimeOffset-এ কলব্যাক অ্যাটাচ করতে পারবেন। মনে রাখবেন যে এই অফসেটের নির্ভুলতা নেটওয়ার্কিং লেটেন্সির দ্বারা প্রভাবিত হতে পারে এবং তাই এটি প্রাথমিকভাবে ঘড়ির সময়ে বড় (> ১ সেকেন্ড) অসঙ্গতি খুঁজে বের করার জন্য উপযোগী।

Kotlin

val offsetRef = Firebase.database.getReference(".info/serverTimeOffset")
offsetRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val offset = snapshot.getValue(Double::class.java) ?: 0.0
        val estimatedServerTimeMs = System.currentTimeMillis() + offset
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference offsetRef = FirebaseDatabase.getInstance().getReference(".info/serverTimeOffset");
offsetRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        double offset = snapshot.getValue(Double.class);
        double estimatedServerTimeMs = System.currentTimeMillis() + offset;
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

Sample Presence অ্যাপ

কানেকশন স্টেট মনিটরিং ও সার্ভার টাইমস্ট্যাম্পের সাথে ডিসকানেক্ট অপারেশন একত্রিত করে, আপনি ব্যবহারকারীর উপস্থিতি সিস্টেম তৈরি করতে পারবেন। এই সিস্টেমে, প্রতিটি ব্যবহারকারী ডেটাবেসের কোনও লোকেশনে ডেটা স্টোর করেন যাতে বোঝা যায় যে Realtime Database ক্লায়েন্ট অনলাইন আছে কিনা। ক্লায়েন্টরা অনলাইন হলে এই লোকেশনকে সত্য হিসেবে সেট করে এবং ডিসকানেক্ট হলে টাইমস্ট্যাম্প সেট করে। এই টাইমস্ট্যাম্প নির্দেশ করে যে শেষবার কখন প্রদত্ত ব্যবহারকারী অনলাইন ছিলেন।

মনে রাখবেন, কোনও ব্যবহারকারীকে অনলাইন হিসেবে চিহ্নিত করার আগে আপনার অ্যাপের ডিসকানেক্ট অপারেশনগুলি সারিবদ্ধ করা উচিত, যাতে ক্লায়েন্টের নেটওয়ার্ক কানেকশন সার্ভারে দুটি কমান্ড পাঠানোর আগেই হারিয়ে যাওয়ার মতো কোনও রেস কন্ডিশন এড়ানো যায়।

এখানে একটি সহজ ব্যবহারকারীর উপস্থিতি সিস্টেম দেওয়া হল:

Kotlin

// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
val database = Firebase.database
val myConnectionsRef = database.getReference("users/joe/connections")

// Stores the timestamp of my last disconnect (the last time I was seen online)
val lastOnlineRef = database.getReference("/users/joe/lastOnline")

val connectedRef = database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue<Boolean>() ?: false
        if (connected) {
            val con = myConnectionsRef.push()

            // When this device disconnects, remove it
            con.onDisconnect().removeValue()

            // When I disconnect, update the last time I was seen online
            lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

            // Add this device to my connections list
            // this value could contain info about the device or a timestamp too
            con.setValue(java.lang.Boolean.TRUE)
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled at .info/connected")
    }
})

Java

// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = FirebaseDatabase.getInstance();
final DatabaseReference myConnectionsRef = database.getReference("users/joe/connections");

// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/users/joe/lastOnline");

final DatabaseReference connectedRef = database.getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            DatabaseReference con = myConnectionsRef.push();

            // When this device disconnects, remove it
            con.onDisconnect().removeValue();

            // When I disconnect, update the last time I was seen online
            lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

            // Add this device to my connections list
            // this value could contain info about the device or a timestamp too
            con.setValue(Boolean.TRUE);
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled at .info/connected");
    }
});