ওয়েবে ডেটা পড়ুন এবং লিখুন

(ঐচ্ছিক) Firebase Local Emulator Suite দিয়ে প্রোটোটাইপ এবং পরীক্ষা করুন

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

একটি Realtime Database এমুলেটর হল Local Emulator Suite অংশ, যা আপনার অ্যাপকে আপনার এমুলেটেড ডাটাবেস কন্টেন্ট এবং কনফিগারেশনের সাথে ইন্টারঅ্যাক্ট করতে সক্ষম করে, সেইসাথে ঐচ্ছিকভাবে আপনার এমুলেটেড প্রজেক্ট রিসোর্স (ফাংশন, অন্যান্য ডাটাবেস এবং নিরাপত্তা নিয়ম)।

Realtime Database এমুলেটর ব্যবহার করতে মাত্র কয়েকটি ধাপ জড়িত:

  1. এমুলেটরের সাথে সংযোগ স্থাপনের জন্য আপনার অ্যাপের টেস্ট কনফিগারেশনে কোডের একটি লাইন যোগ করা হচ্ছে।
  2. আপনার স্থানীয় প্রকল্প ডিরেক্টরির রুট থেকে, firebase emulators:start চালান।
  3. আপনার অ্যাপের প্রোটোটাইপ কোড থেকে যথারীতি Realtime Database প্ল্যাটফর্ম SDK ব্যবহার করে অথবা Realtime Database REST API ব্যবহার করে কল করা।

Realtime Database এবং Cloud Functions সম্পর্কে একটি বিস্তারিত ওয়াকথ্রু উপলব্ধ। আপনার Local Emulator Suite ভূমিকাটিও দেখা উচিত।

একটি ডাটাবেস রেফারেন্স পান

ডাটাবেস থেকে ডেটা পড়তে বা লিখতে, আপনার firebase.database.Reference এর একটি উদাহরণ প্রয়োজন:

Web

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web

var database = firebase.database();

তথ্য লিখুন

এই ডকুমেন্টে ডেটা পুনরুদ্ধারের মূল বিষয়গুলি এবং ফায়ারবেস ডেটা কীভাবে অর্ডার এবং ফিল্টার করতে হয় তা কভার করা হয়েছে।

firebase.database.Reference এর সাথে একটি অ্যাসিঙ্ক্রোনাস লিসেনার সংযুক্ত করে Firebase ডেটা পুনরুদ্ধার করা হয়। লিসেনারটি একবার ডেটার প্রাথমিক অবস্থার জন্য এবং আবার যেকোনো সময় ডেটা পরিবর্তনের জন্য ট্রিগার করা হয়।

মৌলিক লেখার ক্রিয়াকলাপ

মৌলিক লেখার ক্রিয়াকলাপের জন্য, আপনি set() ব্যবহার করে একটি নির্দিষ্ট রেফারেন্সে ডেটা সংরক্ষণ করতে পারেন, সেই পথে বিদ্যমান যেকোনো ডেটা প্রতিস্থাপন করতে পারেন। উদাহরণস্বরূপ, একটি সোশ্যাল ব্লগিং অ্যাপ্লিকেশন নিম্নলিখিতভাবে set() সহ একজন ব্যবহারকারী যুক্ত করতে পারে:

Web

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

Web

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

set() ব্যবহার করলে নির্দিষ্ট স্থানে থাকা ডেটা ওভাররাইট হয়, যার মধ্যে যেকোনো চাইল্ড নোডও অন্তর্ভুক্ত।

তথ্য পড়ুন

মূল্যবান ঘটনাগুলি শুনুন

কোনও পাথে ডেটা পড়তে এবং পরিবর্তনগুলি শুনতে, onValue() ব্যবহার করে ইভেন্টগুলি পর্যবেক্ষণ করুন। আপনি এই ইভেন্টটি ব্যবহার করে কোনও নির্দিষ্ট পাথে থাকা বিষয়বস্তুর স্ট্যাটিক স্ন্যাপশটগুলি পড়তে পারেন, যেমনটি ইভেন্টের সময় বিদ্যমান ছিল। শ্রোতা সংযুক্ত করার সময় এই পদ্ধতিটি একবার ট্রিগার করা হয় এবং প্রতিবার যখন ডেটা, শিশু সহ, পরিবর্তিত হয়। ইভেন্ট কলব্যাকটি সেই অবস্থানের সমস্ত ডেটা ধারণকারী একটি স্ন্যাপশট পাস করে, যার মধ্যে শিশু ডেটাও অন্তর্ভুক্ত থাকে। যদি কোনও ডেটা না থাকে, তাহলে exists() কল করলে স্ন্যাপশটটি false এবং val() কল করলে null ফিরে আসবে।

নিচের উদাহরণটি একটি সোশ্যাল ব্লগিং অ্যাপ্লিকেশন দেখায় যা ডাটাবেস থেকে একটি পোস্টের তারকা সংখ্যা পুনরুদ্ধার করে:

Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

Web

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

শ্রোতা একটি snapshot পায় যাতে ইভেন্টের সময় ডাটাবেসের নির্দিষ্ট স্থানে থাকা ডেটা থাকে। আপনি val() পদ্ধতি ব্যবহার করে snapshot ডেটা পুনরুদ্ধার করতে পারেন।

একবার তথ্য পড়ুন

get() দিয়ে একবার ডেটা পড়ুন।

আপনার অ্যাপ অনলাইন বা অফলাইন যাই হোক না কেন, ডাটাবেস সার্ভারের সাথে মিথস্ক্রিয়া পরিচালনা করার জন্য SDK ডিজাইন করা হয়েছে।

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

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

get() এর অপ্রয়োজনীয় ব্যবহার ব্যান্ডউইথের ব্যবহার বাড়িয়ে দিতে পারে এবং কর্মক্ষমতা হ্রাস করতে পারে, যা উপরে দেখানো রিয়েলটাইম লিসেনার ব্যবহার করে প্রতিরোধ করা যেতে পারে।

Web

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Web

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

একজন পর্যবেক্ষকের সাথে একবার তথ্য পড়ুন

কিছু ক্ষেত্রে আপনি সার্ভারে আপডেট করা মান পরীক্ষা করার পরিবর্তে স্থানীয় ক্যাশে থেকে মানটি অবিলম্বে ফেরত পেতে চাইতে পারেন। এই ক্ষেত্রে আপনি স্থানীয় ডিস্ক ক্যাশে থেকে তাৎক্ষণিকভাবে ডেটা পেতে once() ব্যবহার করতে পারেন।

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

Web

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

Web

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

ডেটা আপডেট করা বা মুছে ফেলা

নির্দিষ্ট ক্ষেত্রগুলি আপডেট করুন

অন্য চাইল্ড নোডগুলিকে ওভাররাইট না করে একটি নোডের নির্দিষ্ট চাইল্ড্রেনগুলিতে একই সাথে লেখার জন্য, update() পদ্ধতিটি ব্যবহার করুন।

update() কল করার সময়, আপনি কী-এর জন্য একটি পাথ নির্দিষ্ট করে নিম্ন-স্তরের চাইল্ড মান আপডেট করতে পারেন। যদি ডেটা আরও ভালোভাবে স্কেল করার জন্য একাধিক স্থানে সংরক্ষণ করা হয়, তাহলে আপনি data fan-out ব্যবহার করে সেই ডেটার সমস্ত উদাহরণ আপডেট করতে পারেন।

উদাহরণস্বরূপ, একটি সোশ্যাল ব্লগিং অ্যাপ একটি পোস্ট তৈরি করতে পারে এবং একই সাথে সাম্প্রতিক অ্যাক্টিভিটি ফিড এবং পোস্টকারী ব্যবহারকারীর অ্যাক্টিভিটি ফিডে এই কোড ব্যবহার করে আপডেট করতে পারে:

Web

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

Web

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

এই উদাহরণে push() ব্যবহার করে /posts/$postid এ সকল ব্যবহারকারীর জন্য পোস্ট সম্বলিত নোডে একটি পোস্ট তৈরি করা হয় এবং একই সাথে কীটি পুনরুদ্ধার করা হয়। এরপর /user-posts/$userid/$postid এ ব্যবহারকারীর পোস্টে দ্বিতীয় এন্ট্রি তৈরি করতে কীটি ব্যবহার করা যেতে পারে।

এই পাথগুলি ব্যবহার করে, আপনি JSON ট্রিতে একাধিক অবস্থানে একযোগে আপডেট করতে পারেন update() একটি একক কলের মাধ্যমে, যেমন এই উদাহরণটি কীভাবে উভয় অবস্থানে নতুন পোস্ট তৈরি করে। এইভাবে করা একযোগে আপডেটগুলি পারমাণবিক: হয় সমস্ত আপডেট সফল হয় অথবা সমস্ত আপডেট ব্যর্থ হয়।

একটি সমাপ্তি কলব্যাক যোগ করুন

আপনার ডেটা কখন কমিট করা হয়েছে তা জানতে চাইলে, আপনি একটি কমপ্লিশন কলব্যাক যোগ করতে পারেন। set() এবং update() উভয়ই একটি ঐচ্ছিক কমপ্লিশন কলব্যাক নেয় যা ডাটাবেসে লেখা কমিট করা হলে কল করা হয়। যদি কলটি ব্যর্থ হয়, তাহলে কলব্যাকটি একটি ত্রুটি বস্তু পাস করে যা নির্দেশ করে যে ব্যর্থতা কেন ঘটেছে।

Web

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

Web

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

ডেটা মুছুন

ডেটা মুছে ফেলার সবচেয়ে সহজ উপায় হল সেই ডেটার অবস্থানের রেফারেন্সে remove() কল করা।

আপনি set() অথবা update() এর মতো অন্য লেখার অপারেশনের জন্য null মান নির্দিষ্ট করেও মুছে ফেলতে পারেন। আপনি update() এর সাহায্যে এই কৌশলটি ব্যবহার করে একটি API কলে একাধিক শিশু মুছে ফেলতে পারেন।

একটি Promise গ্রহণ করুন

আপনার ডেটা কখন Firebase Realtime Database সার্ভারে কমিট করা হয়েছে তা জানতে, আপনি একটি Promise ব্যবহার করতে পারেন। set() এবং update() উভয়ই একটি Promise ফেরত দিতে পারে যা আপনি কখন ডাটাবেসে কমিট করা হয়েছে তা জানতে ব্যবহার করতে পারেন।

শ্রোতাদের আলাদা করুন

আপনার Firebase ডাটাবেস রেফারেন্সে off() পদ্ধতিটি কল করে কলব্যাকগুলি সরানো হয়।

আপনি একটি একক শ্রোতাকে off() প্যারামিটার হিসেবে পাস করে অপসারণ করতে পারেন। কোনও আর্গুমেন্ট ছাড়াই লোকেশনে off() কল করলে সেই লোকেশনের সমস্ত শ্রোতা অপসারণ করা হয়।

প্যারেন্ট লিসেনারের off() কলিং তার চাইল্ড নোডে নিবন্ধিত লিসেনারের ক্ষেত্রে স্বয়ংক্রিয়ভাবে মুছে ফেলা হয় না; কলব্যাকটি সরাতে যেকোনো চাইল্ড লিসেনারের ক্ষেত্রেও off() কল করতে হবে।

লেনদেন হিসেবে ডেটা সংরক্ষণ করুন

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

উদাহরণস্বরূপ, উদাহরণ সোশ্যাল ব্লগিং অ্যাপে, আপনি ব্যবহারকারীদের পোস্টগুলিকে তারকাচিহ্নিত এবং তারকাচিহ্নিত করার অনুমতি দিতে পারেন এবং একটি পোস্ট কতগুলি তারকা পেয়েছে তা ট্র্যাক রাখতে পারেন:

Web

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Web

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

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

পারমাণবিক সার্ভার-সাইড বৃদ্ধি

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

Web

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

Web

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(updates);
}

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

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

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

যদি কোনও ক্লায়েন্ট তার নেটওয়ার্ক সংযোগ হারিয়ে ফেলে, তাহলে আপনার অ্যাপটি সঠিকভাবে কাজ করতে থাকবে।

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

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

একবার সংযোগ পুনঃস্থাপিত হয়ে গেলে, আপনার অ্যাপটি উপযুক্ত ইভেন্টের সেট গ্রহণ করে যাতে ক্লায়েন্টটি কোনও কাস্টম কোড না লিখেই বর্তমান সার্ভারের অবস্থার সাথে সিঙ্ক করে।

আমরা "অনলাইন এবং অফলাইন ক্ষমতা সম্পর্কে আরও জানুন" বিভাগে অফলাইন আচরণ সম্পর্কে আরও আলোচনা করব।

পরবর্তী পদক্ষেপ