অ্যাপ আর্কিটেকচার: ডেটা লেয়ার - ডেটাস্টোর - অ্যান্ড্রয়েড বিকাশকারী

প্রকল্প: /architecture/_project.yaml বই: /architecture/_book.yaml কীওয়ার্ড: ডেটাস্টোর, আর্কিটেকচার, api:JetpackDataStore বর্ণনা: পছন্দ ডেটাস্টোর এবং প্রোটো ডেটাস্টোর, সেটআপ এবং আরও অনেক কিছু সম্পর্কে জানতে ডেটা লেয়ার লাইব্রেরি সম্পর্কে এই অ্যাপ আর্কিটেকচার গাইডটি অন্বেষণ করুন। hide_page_heading: true

ডেটাস্টোর অ্যান্ড্রয়েড জেটপ্যাকের অংশ।

কোটলিন মাল্টিপ্ল্যাটফর্ম দিয়ে চেষ্টা করুন
কোটলিন মাল্টিপ্ল্যাটফর্ম অন্যান্য প্ল্যাটফর্মের সাথে ডেটা লেয়ার শেয়ার করার সুযোগ দেয়। KMP তে ডেটাস্টোর কীভাবে সেট আপ করবেন এবং কীভাবে কাজ করবেন তা শিখুন।

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

যদি আপনি ডেটা সংরক্ষণের জন্য SharedPreferences ব্যবহার করেন, তাহলে DataStore-এ মাইগ্রেট করার কথা বিবেচনা করুন।

পছন্দসমূহ ডেটাস্টোর এবং প্রোটো ডেটাস্টোর

ডেটাস্টোর দুটি ভিন্ন বাস্তবায়ন প্রদান করে: প্রেফারেন্সেস ডেটাস্টোর এবং প্রোটো ডেটাস্টোর।

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

ডেটাস্টোর সঠিকভাবে ব্যবহার করুন

ডেটাস্টোর সঠিকভাবে ব্যবহার করার জন্য সর্বদা নিম্নলিখিত নিয়মগুলি মনে রাখবেন:

  1. একই প্রক্রিয়ায় একটি নির্দিষ্ট ফাইলের জন্য DataStore এর একাধিক উদাহরণ তৈরি করবেন না। এটি করলে DataStore-এর সমস্ত কার্যকারিতা ব্যাহত হতে পারে। একই প্রক্রিয়ায় যদি একটি নির্দিষ্ট ফাইলের জন্য একাধিক DataStores সক্রিয় থাকে, তাহলে DataStore ডেটা পড়ার বা আপডেট করার সময় IllegalStateException নিক্ষেপ করবে।

  2. DataStore<T> এর জেনেরিক টাইপ অবশ্যই অপরিবর্তনীয় হতে হবে। DataStore-এ ব্যবহৃত টাইপ পরিবর্তন করলে DataStore-এর প্রদত্ত ধারাবাহিকতা বাতিল হয়ে যায় এবং সম্ভাব্য গুরুতর, ধরা কঠিন বাগ তৈরি হয়। আমরা আপনাকে প্রোটোকল বাফার ব্যবহার করার পরামর্শ দিচ্ছি, যা অপরিবর্তনীয়তা, একটি স্পষ্ট API এবং দক্ষ সিরিয়ালাইজেশন নিশ্চিত করতে সহায়তা করে।

  3. একই ফাইলের জন্য SingleProcessDataStore এবং MultiProcessDataStore এর ব্যবহার একত্রিত করবেন না । যদি আপনি একাধিক প্রক্রিয়া থেকে DataStore অ্যাক্সেস করতে চান, তাহলে আপনাকে অবশ্যই MultiProcessDataStore ব্যবহার করতে হবে।

সেটআপ

আপনার অ্যাপে Jetpack DataStore ব্যবহার করতে, আপনি কোন বাস্তবায়ন ব্যবহার করতে চান তার উপর নির্ভর করে আপনার Gradle ফাইলে নিম্নলিখিতগুলি যোগ করুন:

পছন্দসমূহ ডেটাস্টোর

খাঁজকাটা

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.7"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.7"
    }
    

কোটলিন

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.7")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.7")
    }
    

প্রোটো ডেটাস্টোর

খাঁজকাটা

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.7"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.7"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.7"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.7"
    }
    

কোটলিন

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.7")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.7")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.7")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.7")
    }
    

Preferences DataStore-এর সাথে কী-মান জোড়া সংরক্ষণ করুন

প্রেফারেন্সেস ডেটাস্টোর বাস্তবায়ন ডিস্কে কী-মান জোড়া ধরে রাখার জন্য DataStore এবং Preferences ক্লাস ব্যবহার করে।

একটি পছন্দসই ডেটাস্টোর তৈরি করুন

DataStore<Preferences> এর একটি ইনস্ট্যান্স তৈরি করতে preferencesDataStore দ্বারা তৈরি প্রপার্টি ডেলিগেট ব্যবহার করুন। আপনার Kotlin ফাইলের উপরের স্তরে একবার এটি কল করুন এবং আপনার বাকি অ্যাপ্লিকেশন জুড়ে এই প্রপার্টির মাধ্যমে এটি অ্যাক্সেস করুন। এটি আপনার DataStore একটি সিঙ্গেলটন হিসেবে রাখা সহজ করে তোলে। বিকল্পভাবে, যদি আপনি RxJava ব্যবহার করেন তবে RxPreferenceDataStoreBuilder ব্যবহার করুন। বাধ্যতামূলক name প্যারামিটার হল Preferences DataStore এর নাম।

কোটলিন

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

জাভা

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, "settings").build();

একটি পছন্দসই ডেটাস্টোর থেকে পড়ুন

যেহেতু Preferences DataStore পূর্বনির্ধারিত স্কিমা ব্যবহার করে না, তাই DataStore<Preferences> ইনস্ট্যান্সে আপনার প্রয়োজনীয় প্রতিটি মানের জন্য একটি কী সংজ্ঞায়িত করতে আপনাকে সংশ্লিষ্ট কী টাইপ ফাংশন ব্যবহার করতে হবে। উদাহরণস্বরূপ, একটি int মানের জন্য একটি কী সংজ্ঞায়িত করতে, intPreferencesKey() ব্যবহার করুন। তারপর, Flow ব্যবহার করে উপযুক্ত সঞ্চিত মান প্রকাশ করতে DataStore.data বৈশিষ্ট্য ব্যবহার করুন।

কোটলিন

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> =
  context.dataStore.data.map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

জাভা

Preferences.Key<Integer> EXAMPLE_COUNTER =
  PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

একটি পছন্দ ডেটাস্টোরে লিখুন

Preferences DataStore একটি edit() ফাংশন প্রদান করে যা DataStore এর ডেটা লেনদেনের মাধ্যমে আপডেট করে। ফাংশনের transform প্যারামিটার কোডের একটি ব্লক গ্রহণ করে যেখানে আপনি প্রয়োজন অনুসারে মান আপডেট করতে পারেন। transform ব্লকের সমস্ত কোডকে একটি একক লেনদেন হিসাবে বিবেচনা করা হয়।

কোটলিন

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

জাভা

Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
  MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
  Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
  return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

প্রোটো ডেটাস্টোর দিয়ে টাইপ করা বস্তু সংরক্ষণ করুন

প্রোটো ডেটাস্টোর বাস্তবায়ন ডেটাস্টোর এবং প্রোটোকল বাফার ব্যবহার করে টাইপ করা বস্তুগুলিকে ডিস্কে ধরে রাখে।

একটি স্কিমা সংজ্ঞায়িত করুন

প্রোটো ডেটাস্টোরের জন্য app/src/main/proto/ ডিরেক্টরির একটি প্রোটো ফাইলে একটি পূর্বনির্ধারিত স্কিমা প্রয়োজন। এই স্কিমা আপনার প্রোটো ডেটাস্টোরে থাকা বস্তুর ধরণ নির্ধারণ করে। প্রোটো স্কিমা সংজ্ঞায়িত করার বিষয়ে আরও জানতে, প্রোটোবফ ভাষা নির্দেশিকাটি দেখুন।

syntax = "proto3";

option java_package = "com.example.application.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

একটি প্রোটো ডেটাস্টোর তৈরি করুন

আপনার টাইপ করা বস্তু সংরক্ষণের জন্য একটি প্রোটো ডেটাস্টোর তৈরিতে দুটি ধাপ জড়িত:

  1. Serializer<T> প্রয়োগ করে এমন একটি ক্লাস নির্ধারণ করুন, যেখানে T হল প্রোটো ফাইলে সংজ্ঞায়িত টাইপ। এই সিরিয়ালাইজার ক্লাসটি DataStore কে আপনার ডেটা টাইপ কীভাবে পড়তে এবং লিখতে হয় তা বলে। যদি এখনও কোনও ফাইল তৈরি না করা হয় তবে সিরিয়ালাইজার ব্যবহারের জন্য একটি ডিফল্ট মান অন্তর্ভুক্ত করুন।
  2. dataStore দ্বারা তৈরি প্রপার্টি ডেলিগেট ব্যবহার করে DataStore<T> এর একটি ইনস্ট্যান্স তৈরি করুন, যেখানে T হল প্রোটো ফাইলে সংজ্ঞায়িত প্রকার। আপনার কোটলিন ফাইলের উপরের স্তরে একবার এটি কল করুন এবং আপনার অ্যাপের বাকি অংশ জুড়ে এই প্রপার্টি ডেলিগেটের মাধ্যমে এটি অ্যাক্সেস করুন। filename প্যারামিটার DataStore কে বলে যে কোন ফাইলটি ডেটা সংরক্ষণ করতে ব্যবহার করতে হবে, এবং serializer প্যারামিটার ধাপ 1 এ সংজ্ঞায়িত serializer ক্লাসের নাম নির্দিষ্ট করে।

কোটলিন

object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override suspend fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

  override suspend fun writeTo(
    t: Settings,
    output: OutputStream) = t.writeTo(output)
}

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

জাভা

private static class SettingsSerializer implements Serializer<Settings> {
  @Override
  public Settings getDefaultValue() {
    return Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (InvalidProtocolBufferException exception) {
      throw CorruptionException("Cannot read proto.", exception);
    }
  }

  @Override
  public void writeTo(Settings t, @NotNull OutputStream output) {
    t.writeTo(output);
  }
}

RxDataStore<Byte> dataStore =
  new RxDataStoreBuilder<Byte>(
    context,
    /* fileName= */ "settings.pb",
    new SettingsSerializer()
  ).build();

প্রোটো ডেটাস্টোর থেকে পড়ুন

আপনার সঞ্চিত বস্তু থেকে উপযুক্ত সম্পত্তির একটি Flow প্রকাশ করতে DataStore.data ব্যবহার করুন।

কোটলিন

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

জাভা

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

একটি প্রোটো ডেটাস্টোরে লিখুন

প্রোটো ডেটাস্টোর একটি updateData() ফাংশন প্রদান করে যা লেনদেনের মাধ্যমে একটি সঞ্চিত বস্তু আপডেট করে। updateData() আপনার ডেটা ধরণের উদাহরণ হিসাবে ডেটার বর্তমান অবস্থা দেয় এবং একটি পারমাণবিক রিড-রাইট-মডিফাই অপারেশনে লেনদেনের মাধ্যমে ডেটা আপডেট করে।

কোটলিন

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

জাভা

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

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

DataStore এর প্রধান সুবিধাগুলির মধ্যে একটি হল অ্যাসিঙ্ক্রোনাস API, তবে আপনার চারপাশের কোডটিকে অ্যাসিঙ্ক্রোনাস করা সবসময় সম্ভব নাও হতে পারে। এটি এমন ক্ষেত্রে হতে পারে যদি আপনি এমন একটি বিদ্যমান কোডবেসের সাথে কাজ করেন যা সিঙ্ক্রোনাস ডিস্ক I/O ব্যবহার করে অথবা যদি আপনার এমন একটি নির্ভরতা থাকে যা অ্যাসিঙ্ক্রোনাস API প্রদান করে না।

কোটলিন কোরোটিনগুলি সিঙ্ক্রোনাস এবং অ্যাসিঙ্ক্রোনাস কোডের মধ্যে ব্যবধান পূরণ করতে সাহায্য করার জন্য runBlocking() কোরোটিন বিল্ডার প্রদান করে। আপনি DataStore থেকে সিঙ্ক্রোনাসভাবে ডেটা পড়ার জন্য runBlocking() ব্যবহার করতে পারেন। RxJava Flowable এ ব্লকিং পদ্ধতি অফার করে। DataStore ডেটা ফেরত না দেওয়া পর্যন্ত নিম্নলিখিত কোড কলিং থ্রেডকে ব্লক করে:

কোটলিন

val exampleData = runBlocking { context.dataStore.data.first() }

জাভা

Settings settings = dataStore.data().blockingFirst();

UI থ্রেডে সিঙ্ক্রোনাস I/O অপারেশন করলে ANR বা প্রতিক্রিয়াহীন UI হতে পারে। DataStore থেকে ডেটা অ্যাসিঙ্ক্রোনাসভাবে প্রিলোড করে আপনি এই সমস্যাগুলি কমাতে পারেন:

কোটলিন

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

জাভা

dataStore.data().first().subscribe();

এইভাবে, DataStore অ্যাসিঙ্ক্রোনাসভাবে ডেটা পড়ে এবং মেমোরিতে ক্যাশে করে। runBlocking() ব্যবহার করে পরবর্তীতে সিঙ্ক্রোনাস রিড দ্রুততর হতে পারে অথবা প্রাথমিক রিড সম্পন্ন হলে ডিস্ক I/O অপারেশন সম্পূর্ণরূপে এড়াতে পারে।

মাল্টি-প্রসেস কোডে ডেটাস্টোর ব্যবহার করুন

আপনি DataStore কে বিভিন্ন প্রক্রিয়ায় একই ডেটা অ্যাক্সেস করার জন্য কনফিগার করতে পারেন, যার ডেটা কনসিস্টেন্সি প্রোপার্টি একই, যা একটি একক প্রক্রিয়ার মধ্যে থেকে পাওয়া যায়। বিশেষ করে, DataStore নিম্নলিখিত সুবিধা প্রদান করে:

  • রিড শুধুমাত্র ডিস্কে সংরক্ষিত ডেটা ফেরত দেয়।
  • পড়ার পর লেখার ধারাবাহিকতা।
  • লেখাগুলো ধারাবাহিকভাবে সাজানো হয়েছে।
  • লেখার মাধ্যমে পঠন কখনও অবরুদ্ধ হয় না।

একটি পরিষেবা এবং কার্যকলাপ সহ একটি নমুনা আবেদন বিবেচনা করুন:

  1. পরিষেবাটি একটি পৃথক প্রক্রিয়ায় চলছে এবং পর্যায়ক্রমে ডেটাস্টোর আপডেট করে

    <service
      android:name=".MyService"
      android:process=":my_process_id" />
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope.launch {
              while(isActive) {
                  dataStore.updateData {
                      Settings(lastUpdate = System.currentTimeMillis())
                  }
                  delay(1000)
              }
          }
    }
    
  2. অ্যাপটি সেই পরিবর্তনগুলি সংগ্রহ করবে এবং তার UI আপডেট করবে

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

বিভিন্ন প্রক্রিয়া জুড়ে DataStore ব্যবহার করতে সক্ষম হতে, আপনাকে MultiProcessDataStoreFactory ব্যবহার করে DataStore অবজেক্ট তৈরি করতে হবে।

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

serializer DataStore কে আপনার ডেটা টাইপ কিভাবে পড়তে এবং লিখতে হয় তা বলে। যদি এখনও কোনও ফাইল তৈরি না হয়, তাহলে serializer ব্যবহারের জন্য একটি ডিফল্ট মান অন্তর্ভুক্ত করুন। kotlinx.serialization ব্যবহার করে বাস্তবায়নের একটি উদাহরণ নিচে দেওয়া হল:

@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Settings =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

আপনি Hilt dependence injection ব্যবহার করতে পারেন যাতে আপনার DataStore ইনস্ট্যান্স প্রতিটি প্রক্রিয়ার জন্য অনন্য হয়:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

ফাইল দুর্নীতি পরিচালনা করুন

DataStore-এর স্থায়ী অন-ডিস্ক ফাইলটি দূষিত হওয়ার সম্ভাবনা খুব কমই থাকে। ডিফল্টরূপে, DataStore স্বয়ংক্রিয়ভাবে দূষিত অবস্থা থেকে পুনরুদ্ধার করে না এবং এটি থেকে পড়ার চেষ্টা করলে সিস্টেমটি CorruptionException নিক্ষেপ করবে।

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

এই হ্যান্ডলারটি সেট আপ করার জন্য, by dataStore() অথবা DataStoreFactory factory পদ্ধতিতে DataStore ইনস্ট্যান্স তৈরি করার সময় একটি corruptionHandler প্রদান করুন:

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

মতামত প্রদান করুন

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

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

অতিরিক্ত সম্পদ

জেটপ্যাক ডেটাস্টোর সম্পর্কে আরও জানতে, নিম্নলিখিত অতিরিক্ত সংস্থানগুলি দেখুন:

নমুনা

ব্লগ

কোডল্যাব

{% অক্ষরে অক্ষরে %} {% এন্ডভারব্যাটিম %} {% অক্ষরে অক্ষরে %} {% এন্ডভারব্যাটিম %}