Compose UI আর্কিটেকচার

Compose-এ UI অপরিবর্তনীয়—এটি ড্র করার পরে আপডেট করার কোনও উপায় নেই। আপনি যা নিয়ন্ত্রণ করতে পারবেন তা হল আপনার UI-এর অবস্থা। UI-এর স্টেট পরিবর্তন হলে, Compose UI ট্রি-এর সেইসব অংশ রিক্রিয়েট করে যেগুলি পরিবর্তিত হয়েছে। কম্পোজ করার উপযুক্ত ফাংশন স্টেট গ্রহণ করতে এবং ইভেন্ট এক্সপোজ করতে পারে—যেমন, a TextField একটি ভ্যালু গ্রহণ করে এবং একটি কলব্যাক onValueChange এক্সপোজ করে যা কলব্যাক হ্যান্ডলারকে ভ্যালু পরিবর্তন করার অনুরোধ করে।

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

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

একমুখী ডেটা ফ্লো

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

একমুখী ডেটা ফ্লো ব্যবহার করে এমন অ্যাপের জন্য UI আপডেট লুপটি এইরকম দেখায়:

  1. ইভেন্ট: UI-এর অংশ ইভেন্ট তৈরি করে এবং তা উপরের দিকে পাস করে, যেমন, হ্যান্ডেল করার জন্য ViewModel-এ পাস করা বোতামে ক্লিক করা; অথবা ব্যবহারকারীর সেশনের মেয়াদ শেষ হয়ে গেছে এমন ইঙ্গিত দিয়ে আপনার অ্যাপের অন্য লেয়ার থেকে ইভেন্ট পাস করা হয়।
  2. স্ট্যাটাস আপডেট করা: কোনও ইভেন্ট হ্যান্ডলার স্ট্যাটাস পরিবর্তন করতে পারে।
  3. ডিসপ্লে স্টেট: স্টেট হোল্ডার স্টেট পাস করে এবং UI সেটি দেখায়।
UI থেকে স্টেট হোল্ডারে ইভেন্ট ফ্লো হয় এবং স্টেট হোল্ডার থেকে UI-তে স্টেট ফ্লো হয়।
ছবি ১. একমুখী ডেটা ফ্লো।

Jetpack Compose ব্যবহার করার সময় এই প্যাটার্ন ফলো করলে অনেক সুবিধা পাওয়া যায়:

  • পরীক্ষাযোগ্যতা: UI থেকে স্টেটকে আলাদা করে দিলে, দু'টিকেই আলাদাভাবে পরীক্ষা করা সহজ হয়ে যায়।
  • স্টেট এনক্যাপসুলেশন: যেহেতু স্টেট শুধুমাত্র একটি জায়গায় আপডেট করা যায় এবং কম্পোজ করার মতো কোনও এলিমেন্টের স্টেটের জন্য শুধুমাত্র একটি সোর্স অফ ট্রুথ থাকে, তাই অসঙ্গতিপূর্ণ স্টেটের কারণে আপনার বাগ তৈরি হওয়ার সম্ভাবনা কম থাকে।
  • UI কনসিস্টেন্সি: StateFlow বা LiveData-এর মতো অবজার্ভেবল স্টেট হোল্ডার ব্যবহার করে UI-তে সব স্টেট আপডেট অবিলম্বে রিফ্লেক্ট করা হয়।

Jetpack Compose-এ একমুখী ডেটা ফ্লো

কম্পোজ করার উপযুক্ত আইটেম স্টেট ও ইভেন্টের উপর ভিত্তি করে কাজ করে। যেমন, কোনও TextField তখনই আপডেট করা হয় যখন এর value প্যারামিটার আপডেট করা হয় এবং এটি একটি onValueChange কলব্যাক প্রকাশ করে—এটি এমন একটি ইভেন্ট যা ভ্যালুকে নতুন ভ্যালুতে পরিবর্তন করার অনুরোধ করে। Compose State অবজেক্টকে ভ্যালু হোল্ডার হিসেবে সংজ্ঞায়িত করে এবং স্টেট ভ্যালুতে পরিবর্তন হলে রিকম্পোজিশন ট্রিগার হয়। আপনি কতক্ষণ ভ্যালু মনে রাখতে চান তার উপর নির্ভর করে, আপনি remember { mutableStateOf(value) } বা rememberSaveable { mutableStateOf(value) }-এ স্টেট হোল্ড করতে পারেন।

TextField কম্পোজ করার মতো ভ্যালুর ধরন হল String, তাই এটি যেকোনও জায়গা থেকে আসতে পারে—হার্ডকোড করা ভ্যালু, ViewModel থেকে অথবা প্যারেন্ট কম্পোজ করার মতো ভ্যালু থেকে পাস করা হতে পারে। আপনাকে এটি State অবজেক্টে হোল্ড করে রাখতে হবে না, তবে onValueChange কল করা হলে আপনাকে মান আপডেট করতে হবে।

  • mutableStateOf(value) একটি MutableState তৈরি করে, যা হল Compose-এ একটি পর্যবেক্ষণযোগ্য টাইপ । এর ভ্যালু শিডিউলে কোনও পরিবর্তন হলে, সেই ভ্যালু রিড করে এমন কোনও কম্পোজ করার উপযুক্ত ফাংশনের রিকম্পোজিশন হয়।
  • remember কম্পোজিশনে অবজেক্ট স্টোর করে এবং যে কম্পোজ করার উপযুক্ত আইটেম remember-কে কল করেছে সেটি কম্পোজিশন থেকে সরিয়ে দেওয়া হলে অবজেক্টটি ভুলে যায়।
  • rememberSaveable কনফিগারেশনে পরিবর্তন করা হলেও, Bundle-এ সেভ করে রাখার মাধ্যমে এর অবস্থা বজায় রাখে।

কম্পোজ করা যায় এমন প্যারামিটার নির্ধারণ করা

কোনও কম্পোজ করার মতো আইটেমের স্টেট প্যারামিটার নির্ধারণ করার সময়, নিম্নলিখিত প্রশ্নগুলি মনে রাখবেন:

  • কম্পোজ করার উপযুক্ত আইটেম কতটা রিইউজেবল বা ফ্লেক্সিবল?
  • স্টেট প্যারামিটার কীভাবে এই কম্পোজ করার উপযুক্ত আইটেমের পারফর্ম্যান্সকে প্রভাবিত করে?

ডিকাপেল করা ও আবার ব্যবহার করাকে উৎসাহিত করতে, প্রতিটি কম্পোজ করার উপযুক্ত আইটেমে যতটা সম্ভব কম তথ্য রাখা উচিত। যেমন, খবরের নিবন্ধের হেডার রাখার জন্য কম্পোজ করার উপযুক্ত আইটেম তৈরি করার সময়, পুরো খবরের নিবন্ধের পরিবর্তে, শুধুমাত্র সেই তথ্য পাস করুন যা দেখানো প্রয়োজন:

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

কখনও কখনও, আলাদা আলাদা প্যারামিটার ব্যবহার করলেও পারফর্ম্যান্স উন্নত হয়—যেমন, যদি News-এ শুধুমাত্র title ও subtitle-এর চেয়ে বেশি তথ্য থাকে, তাহলে যখনই Header(news)-এ News-এর নতুন ইনস্ট্যান্স পাস করা হয়, কম্পোজ করার উপযুক্ত আইটেমটি আবার কম্পোজ হবে, এমনকি title ও subtitle-এ কোনও পরিবর্তন না হলেও।

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

Compose-এ ইভেন্ট

আপনার অ্যাপে প্রতিটি ইনপুটকে একটি ইভেন্ট হিসেবে দেখাতে হবে: ট্যাপ, টেক্সট পরিবর্তন, এমনকি টাইমার বা অন্যান্য আপডেট। এইসব ইভেন্ট আপনার UI-এর স্টেট পরিবর্তন করে বলে, ViewModel-কে সেগুলি ম্যানেজ করতে হবে এবং UI স্টেট আপডেট করতে হবে।

UI লেয়ার কখনই ইভেন্ট হ্যান্ডলারের বাইরে স্টেট পরিবর্তন করবে না, কারণ এটি আপনার অ্যাপ্লিকেশনে অসঙ্গতি ও বাগ তৈরি করতে পারে।

স্টেট ও ইভেন্ট হ্যান্ডলার ল্যাম্বডার জন্য অপরিবর্তনীয় ভ্যালু পাস করা পছন্দ করি। এই পদ্ধতির নিম্নলিখিত সুবিধাগুলি রয়েছে:

  • আপনি আবার ব্যবহারযোগ্যতা উন্নত করেন।
  • আপনি যাচাই করে নিন যে আপনার UI সরাসরি স্টেটের ভ্যালু পরিবর্তন করে না।
  • আপনি কনকারেন্সি সংক্রান্ত সমস্যা এড়াতে পারেন কারণ আপনি নিশ্চিত করেন যে অন্য থ্রেড থেকে স্টেট পরিবর্তিত হয়নি।
  • প্রায়শই, আপনি কোডের জটিলতা কমান।

যেমন, একটি কম্পোজ করার উপযুক্ত আইটেম যা প্যারামিটার হিসেবে String ও ল্যাম্বডা নেয়, সেটি অনেক জায়গা থেকে কল করা যায় এবং সেটি অনেকবার ব্যবহার করা যায়। ধরে নিন যে আপনার অ্যাপের সবচেয়ে উপরের অ্যাপ বারে সবসময় টেক্সট দেখা যায় এবং একটি ব্যাক বোতাম থাকে। আপনি আরও জেনেরিক MyAppTopAppBar কম্পোজ করার উপযুক্ত ফাংশন ডিফাইন করতে পারেন যা প্যারামিটার হিসেবে টেক্সট ও 'ফিরে যান' বোতাম হ্যান্ডলার পায়:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModel, স্টেট ও ইভেন্ট: একটি উদাহরণ

ViewModel ও mutableStateOf ব্যবহার করে, আপনি অ্যাপে একমুখী ডেটা ফ্লোও যোগ করতে পারবেন, যদি নিম্নলিখিতগুলির মধ্যে কোনও একটি সত্যি হয়:

  • StateFlow বা LiveData-এর মতো অবজার্ভেবল স্টেট হোল্ডার ব্যবহার করে আপনার UI-এর স্টেট এক্সপোজ করা হয়।
  • ViewModel আপনার অ্যাপের UI বা অন্যান্য লেয়ার থেকে আসা ইভেন্ট ম্যানেজ করে এবং ইভেন্টের উপর ভিত্তি করে স্টেট হোল্ডার আপডেট করে।

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

স্ক্রিনের চারটি অবস্থা আছে:

  • সাইন-আউট করা: ব্যবহারকারী এখনও সাইন-ইন করেননি।
  • প্রসেস চলছে: আপনার অ্যাপ যখন কোনও নেটওয়ার্ক কল পারফর্ম করে ব্যবহারকারীকে সাইন-ইন করানোর চেষ্টা করছে।
  • সমস্যা: সাইন-ইন করার সময় কোনও সমস্যা হলে।
  • সাইন-ইন করা আছে: ব্যবহারকারী সাইন-ইন করে থাকলে।

আপনি এইসব স্টেটকে একটি সিল করা ক্লাস হিসেবে মডেল করতে পারেন। ViewModel, State হিসেবে স্টেটকে এক্সপোজ করে, প্রাথমিক স্টেট সেট করে এবং প্রয়োজন অনুযায়ী স্টেট আপডেট করে। ViewModel-এ onSignIn() পদ্ধতি ব্যবহার করে সাইন-ইন ইভেন্টও ম্যানেজ করা হয়।

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

mutableStateOf API ছাড়াও, Compose LiveData, Flow ও Observable-এর জন্য এক্সটেনশন প্রদান করে যাতে কোনও ভ্যালুকে স্টেট হিসেবে রেজিস্টার করা যায় এবং তা দেখানো যায়।

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

আরও জানুন

Jetpack Compose-এ আর্কিটেকচার সম্পর্কে আরও জানতে, নিম্নলিখিত রিসোর্স দেখুন:

নমুনা