রচনা-ভিত্তিক UI দিয়ে শুরু করা, রচনা-ভিত্তিক UI দিয়ে শুরু করা

নির্ভরতা যোগ করুন

Media3 লাইব্রেরিতে একটি Jetpack Compose-ভিত্তিক UI মডিউল রয়েছে। এটি ব্যবহার করতে, নিম্নলিখিত নির্ভরতা যোগ করুন:

কোটলিন

implementation("androidx.media3:media3-ui-compose:1.6.0")

গ্রোভি

implementation "androidx.media3:media3-ui-compose:1.6.0"

আমরা আপনাকে আপনার অ্যাপটিকে একটি রচনা-প্রথম ফ্যাশনে বিকাশ করতে বা ভিউ ব্যবহার করা থেকে স্থানান্তর করতে উত্সাহিত করি৷

সম্পূর্ণরূপে কম্পোজ ডেমো অ্যাপ

যদিও media3-ui-compose লাইব্রেরিতে বাক্সের বাইরের কম্পোজেবল (যেমন বোতাম, সূচক, ছবি বা ডায়ালগ) অন্তর্ভুক্ত নেই, আপনি কম্পোজে সম্পূর্ণরূপে লেখা একটি ডেমো অ্যাপ খুঁজে পেতে পারেন যা AndroidViewPlayerView র‌্যাপ করার মতো যেকোন আন্তঃঅপারেবিলিটি সমাধান এড়িয়ে যায়। ডেমো অ্যাপটি media3-ui-compose মডিউল থেকে UI স্টেট হোল্ডার ক্লাস ব্যবহার করে এবং Compose Material3 লাইব্রেরি ব্যবহার করে।

UI স্টেট হোল্ডার

কম্পোজেবল বনাম ইউআই স্টেট হোল্ডারদের নমনীয়তা কীভাবে ব্যবহার করতে পারেন তা আরও ভালভাবে বোঝার জন্য, কম্পোজ কীভাবে স্টেট পরিচালনা করে তা পড়ুন।

বোতাম রাষ্ট্র ধারক

কিছু UI রাজ্যের জন্য, আমরা অনুমান করি যে সেগুলি সম্ভবত বোতামের মতো কম্পোজেবল দ্বারা গ্রাস করা হবে।

রাজ্য মনে রাখবেন *রাষ্ট্র টাইপ
PlayPauseButtonState rememberPlayPauseButtonState 2-টগল করুন
PreviousButtonState rememberPreviousButtonState ধ্রুবক
NextButtonState rememberNextButtonState ধ্রুবক
RepeatButtonState rememberRepeatButtonState 3-টগল করুন
ShuffleButtonState rememberShuffleButtonState 2-টগল করুন
PlaybackSpeedState rememberPlaybackSpeedState মেনু বা N-টগল

PlayPauseButtonState এর ব্যবহার উদাহরণ:

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  val state = rememberPlayPauseButtonState(player)

  IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
    Icon(
      imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
      contentDescription =
        if (state.showPlay) stringResource(R.string.playpause_button_play)
        else stringResource(R.string.playpause_button_pause),
    )
  }
}

দ্রষ্টব্য কিভাবে state কাছে কোনো থিমিং তথ্য নেই, যেমন আইকন খেলা বা বিরতির জন্য ব্যবহার করতে হবে। Player UI অবস্থায় রূপান্তর করাই এর একমাত্র দায়িত্ব।

তারপরে আপনি আপনার পছন্দের লেআউটে বোতামগুলিকে মিশ্রিত করতে এবং মেলাতে পারেন:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

ভিজ্যুয়াল আউটপুট স্টেট হোল্ডার

PlayerSurface ভিডিও আউটপুট কখন দেখানো হতে পারে বা একটি স্থানধারক UI উপাদান দ্বারা কভার করা উচিত তার জন্য PresentationState তথ্য রাখে।

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised in
  // the process. If this composable is guarded by some condition, it might never become visible
  // because the Player won't emit the relevant event, e.g. the first frame being ready.
  PlayerSurface(
    player = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

এখানে, আমরা সারফেসকে কাঙ্খিত আকৃতির অনুপাতের স্কেল করতে presentationState.videoSizeDp উভয়ই ব্যবহার করতে পারি (আরও ধরণের জন্য ContentScale ডক্স দেখুন) এবং presentationState.coverSurface কখন সারফেস দেখানোর সময় সঠিক নয় তা জানতে। এই ক্ষেত্রে, আপনি পৃষ্ঠের উপরে একটি অস্বচ্ছ শাটার স্থাপন করতে পারেন, যা পৃষ্ঠটি প্রস্তুত হয়ে গেলে অদৃশ্য হয়ে যাবে।

প্রবাহ কোথায়?

অনেক অ্যান্ড্রয়েড ডেভেলপার সবসময় পরিবর্তনশীল UI ডেটা সংগ্রহ করতে Kotlin Flow অবজেক্ট ব্যবহার করার সাথে পরিচিত। উদাহরণস্বরূপ, আপনি Player.isPlaying প্রবাহের সন্ধানে থাকতে পারেন যা আপনি জীবনচক্র-সচেতন পদ্ধতিতে collect করতে পারেন। অথবা Player.eventsFlow এর মতো কিছু আপনাকে একটি Flow<Player.Events> প্রদান করতে যা আপনি আপনার ইচ্ছামত filter করতে পারেন।

যাইহোক, Player UI স্টেটের জন্য ফ্লো ব্যবহার করার কিছু ত্রুটি রয়েছে। প্রধান উদ্বেগের মধ্যে একটি হল ডেটা স্থানান্তরের অ্যাসিঙ্ক্রোনাস প্রকৃতি। আমরা Player.Event মধ্যে যতটা সম্ভব কম লেটেন্সি নিশ্চিত করতে চাই। ইভেন্ট এবং UI সাইডে এর ব্যবহার, Player সাথে সিঙ্কের বাইরে থাকা UI উপাদানগুলি দেখানো এড়িয়ে চলুন।

অন্যান্য পয়েন্ট অন্তর্ভুক্ত:

  • সমস্ত Player.Events সাথে একটি প্রবাহ। ইভেন্টগুলি একটি একক দায়িত্ব নীতি মেনে চলে না, প্রতিটি গ্রাহককে প্রাসঙ্গিক ইভেন্টগুলি ফিল্টার করতে হবে৷
  • প্রতিটি Player.Event জন্য একটি ফ্লো তৈরি করা। ইভেন্টের জন্য আপনাকে প্রতিটি UI উপাদানের জন্য সেগুলিকে combine করতে হবে। একটি Player.Event এবং একটি UI উপাদান পরিবর্তনের মধ্যে একটি বহু-থেকে-অনেক ম্যাপিং আছে৷ combine ব্যবহার করার ফলে UI কে সম্ভাব্য অবৈধ অবস্থায় নিয়ে যেতে পারে।

কাস্টম UI স্টেট তৈরি করুন

আপনি কাস্টম UI রাজ্য যোগ করতে পারেন যদি বিদ্যমানগুলি আপনার চাহিদা পূরণ না করে। প্যাটার্নটি অনুলিপি করতে বিদ্যমান রাজ্যের উত্স কোডটি দেখুন। একটি সাধারণ UI স্টেট হোল্ডার ক্লাস নিম্নলিখিতগুলি করে:

  1. একজন Player নেয়।
  2. coroutines ব্যবহার করে Player সদস্যতা. আরও বিস্তারিত জানার জন্য Player.listen দেখুন।
  3. এর অভ্যন্তরীণ অবস্থা আপডেট করে নির্দিষ্ট Player.Events প্রতি সাড়া দেয়।
  4. ব্যবসা-লজিক কমান্ডগুলি গ্রহণ করুন যা একটি উপযুক্ত Player আপডেটে রূপান্তরিত হবে।
  5. UI ট্রি জুড়ে একাধিক জায়গায় তৈরি করা যেতে পারে এবং সর্বদা প্লেয়ারের অবস্থার একটি সামঞ্জস্যপূর্ণ দৃশ্য বজায় রাখবে।
  6. কম্পোজ State ক্ষেত্রগুলিকে প্রকাশ করে যা পরিবর্তনের জন্য গতিশীলভাবে প্রতিক্রিয়া জানাতে কম্পোজেবল দ্বারা গ্রাস করা যেতে পারে।
  7. কম্পোজিশনের মধ্যে উদাহরণ মনে রাখার জন্য একটি remember*State ফাংশন সহ আসে।

পর্দার আড়ালে যা ঘটে:

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_ACTION_A))
    private set

  var someField by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe() =
    player.listen { events ->
      if (
        events.containsAny(
          Player.EVENT_B_CHANGED,
          Player.EVENT_C_CHANGED,
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
        )
      ) {
        someField = this.someField
        isEnabled = this.isCommandAvailable(Player.COMMAND_ACTION_A)
      }
    }
}

আপনার নিজের Player.Events এ প্রতিক্রিয়া জানাতে, আপনি Player.listen ব্যবহার করে সেগুলি ধরতে পারেন যা একটি suspend fun যা আপনাকে coroutine জগতে প্রবেশ করতে এবং অনির্দিষ্টকালের জন্য Player.Events শুনতে দেয়৷ বিভিন্ন UI অবস্থার Media3 বাস্তবায়ন শেষ ডেভেলপারকে Player.Events সম্পর্কে শেখার বিষয়ে নিজেদের উদ্বিগ্ন না হতে সাহায্য করে।

,

নির্ভরতা যোগ করুন

Media3 লাইব্রেরিতে একটি Jetpack Compose-ভিত্তিক UI মডিউল রয়েছে। এটি ব্যবহার করতে, নিম্নলিখিত নির্ভরতা যোগ করুন:

কোটলিন

implementation("androidx.media3:media3-ui-compose:1.6.0")

গ্রোভি

implementation "androidx.media3:media3-ui-compose:1.6.0"

আমরা আপনাকে আপনার অ্যাপটিকে একটি রচনা-প্রথম ফ্যাশনে বিকাশ করতে বা ভিউ ব্যবহার করা থেকে স্থানান্তর করতে উত্সাহিত করি৷

সম্পূর্ণরূপে কম্পোজ ডেমো অ্যাপ

যদিও media3-ui-compose লাইব্রেরিতে বাক্সের বাইরের কম্পোজেবল (যেমন বোতাম, সূচক, ছবি বা ডায়ালগ) অন্তর্ভুক্ত নেই, আপনি কম্পোজে সম্পূর্ণরূপে লেখা একটি ডেমো অ্যাপ খুঁজে পেতে পারেন যা AndroidViewPlayerView র‌্যাপ করার মতো যেকোন আন্তঃঅপারেবিলিটি সমাধান এড়িয়ে যায়। ডেমো অ্যাপটি media3-ui-compose মডিউল থেকে UI স্টেট হোল্ডার ক্লাস ব্যবহার করে এবং Compose Material3 লাইব্রেরি ব্যবহার করে।

UI স্টেট হোল্ডার

কম্পোজেবল বনাম ইউআই স্টেট হোল্ডারদের নমনীয়তা কীভাবে ব্যবহার করতে পারেন তা আরও ভালভাবে বোঝার জন্য, কম্পোজ কীভাবে স্টেট পরিচালনা করে তা পড়ুন।

বোতাম রাষ্ট্র ধারক

কিছু UI রাজ্যের জন্য, আমরা অনুমান করি যে সেগুলি সম্ভবত বোতামের মতো কম্পোজেবল দ্বারা গ্রাস করা হবে।

রাজ্য মনে রাখবেন *রাষ্ট্র টাইপ
PlayPauseButtonState rememberPlayPauseButtonState 2-টগল করুন
PreviousButtonState rememberPreviousButtonState ধ্রুবক
NextButtonState rememberNextButtonState ধ্রুবক
RepeatButtonState rememberRepeatButtonState 3-টগল করুন
ShuffleButtonState rememberShuffleButtonState 2-টগল করুন
PlaybackSpeedState rememberPlaybackSpeedState মেনু বা N-টগল

PlayPauseButtonState এর ব্যবহার উদাহরণ:

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  val state = rememberPlayPauseButtonState(player)

  IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
    Icon(
      imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
      contentDescription =
        if (state.showPlay) stringResource(R.string.playpause_button_play)
        else stringResource(R.string.playpause_button_pause),
    )
  }
}

দ্রষ্টব্য কিভাবে state কাছে কোনো থিমিং তথ্য নেই, যেমন আইকন খেলা বা বিরতির জন্য ব্যবহার করতে হবে। Player UI অবস্থায় রূপান্তর করাই এর একমাত্র দায়িত্ব।

তারপরে আপনি আপনার পছন্দের লেআউটে বোতামগুলিকে মিশ্রিত করতে এবং মেলাতে পারেন:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

ভিজ্যুয়াল আউটপুট স্টেট হোল্ডার

PlayerSurface ভিডিও আউটপুট কখন দেখানো হতে পারে বা একটি স্থানধারক UI উপাদান দ্বারা কভার করা উচিত তার জন্য PresentationState তথ্য রাখে।

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised in
  // the process. If this composable is guarded by some condition, it might never become visible
  // because the Player won't emit the relevant event, e.g. the first frame being ready.
  PlayerSurface(
    player = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

এখানে, আমরা সারফেসকে কাঙ্খিত আকৃতির অনুপাতের স্কেল করতে presentationState.videoSizeDp উভয়ই ব্যবহার করতে পারি (আরও ধরণের জন্য ContentScale ডক্স দেখুন) এবং presentationState.coverSurface কখন সারফেস দেখানোর সময় সঠিক নয় তা জানতে। এই ক্ষেত্রে, আপনি পৃষ্ঠের উপরে একটি অস্বচ্ছ শাটার স্থাপন করতে পারেন, যা পৃষ্ঠটি প্রস্তুত হয়ে গেলে অদৃশ্য হয়ে যাবে।

প্রবাহ কোথায়?

অনেক অ্যান্ড্রয়েড ডেভেলপার সবসময় পরিবর্তনশীল UI ডেটা সংগ্রহ করতে Kotlin Flow অবজেক্ট ব্যবহার করার সাথে পরিচিত। উদাহরণস্বরূপ, আপনি Player.isPlaying প্রবাহের সন্ধানে থাকতে পারেন যা আপনি জীবনচক্র-সচেতন পদ্ধতিতে collect করতে পারেন। অথবা Player.eventsFlow এর মতো কিছু আপনাকে একটি Flow<Player.Events> প্রদান করতে যা আপনি আপনার ইচ্ছামত filter করতে পারেন।

যাইহোক, Player UI স্টেটের জন্য ফ্লো ব্যবহার করার কিছু ত্রুটি রয়েছে। প্রধান উদ্বেগের মধ্যে একটি হল ডেটা স্থানান্তরের অ্যাসিঙ্ক্রোনাস প্রকৃতি। আমরা Player.Event মধ্যে যতটা সম্ভব কম লেটেন্সি নিশ্চিত করতে চাই। ইভেন্ট এবং UI সাইডে এর ব্যবহার, Player সাথে সিঙ্কের বাইরে থাকা UI উপাদানগুলি দেখানো এড়িয়ে চলুন।

অন্যান্য পয়েন্ট অন্তর্ভুক্ত:

  • সমস্ত Player.Events সাথে একটি প্রবাহ। ইভেন্টগুলি একটি একক দায়িত্ব নীতি মেনে চলে না, প্রতিটি গ্রাহককে প্রাসঙ্গিক ইভেন্টগুলি ফিল্টার করতে হবে৷
  • প্রতিটি Player.Event জন্য একটি ফ্লো তৈরি করা। ইভেন্টের জন্য আপনাকে প্রতিটি UI উপাদানের জন্য সেগুলিকে combine করতে হবে। একটি Player.Event এবং একটি UI উপাদান পরিবর্তনের মধ্যে একটি বহু-থেকে-অনেক ম্যাপিং আছে৷ combine ব্যবহার করার ফলে UI কে সম্ভাব্য অবৈধ অবস্থায় নিয়ে যেতে পারে।

কাস্টম UI স্টেট তৈরি করুন

আপনি কাস্টম UI রাজ্য যোগ করতে পারেন যদি বিদ্যমানগুলি আপনার চাহিদা পূরণ না করে। প্যাটার্নটি অনুলিপি করতে বিদ্যমান রাজ্যের উত্স কোডটি দেখুন। একটি সাধারণ UI স্টেট হোল্ডার ক্লাস নিম্নলিখিতগুলি করে:

  1. একজন Player নেয়।
  2. coroutines ব্যবহার করে Player সদস্যতা. আরও বিস্তারিত জানার জন্য Player.listen দেখুন।
  3. এর অভ্যন্তরীণ অবস্থা আপডেট করে নির্দিষ্ট Player.Events প্রতি সাড়া দেয়।
  4. ব্যবসা-লজিক কমান্ডগুলি গ্রহণ করুন যা একটি উপযুক্ত Player আপডেটে রূপান্তরিত হবে।
  5. UI ট্রি জুড়ে একাধিক জায়গায় তৈরি করা যেতে পারে এবং সর্বদা প্লেয়ারের অবস্থার একটি সামঞ্জস্যপূর্ণ দৃশ্য বজায় রাখবে।
  6. কম্পোজ State ক্ষেত্রগুলিকে প্রকাশ করে যা পরিবর্তনের জন্য গতিশীলভাবে প্রতিক্রিয়া জানাতে কম্পোজেবল দ্বারা গ্রাস করা যেতে পারে।
  7. কম্পোজিশনের মধ্যে উদাহরণ মনে রাখার জন্য একটি remember*State ফাংশন সহ আসে।

পর্দার আড়ালে যা ঘটে:

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_ACTION_A))
    private set

  var someField by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe() =
    player.listen { events ->
      if (
        events.containsAny(
          Player.EVENT_B_CHANGED,
          Player.EVENT_C_CHANGED,
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
        )
      ) {
        someField = this.someField
        isEnabled = this.isCommandAvailable(Player.COMMAND_ACTION_A)
      }
    }
}

আপনার নিজের Player.Events এ প্রতিক্রিয়া জানাতে, আপনি Player.listen ব্যবহার করে সেগুলি ধরতে পারেন যা একটি suspend fun যা আপনাকে coroutine জগতে প্রবেশ করতে এবং অনির্দিষ্টকালের জন্য Player.Events শুনতে দেয়৷ বিভিন্ন UI অবস্থার Media3 বাস্তবায়ন শেষ ডেভেলপারকে Player.Events সম্পর্কে শেখার বিষয়ে নিজেদের উদ্বিগ্ন না হতে সাহায্য করে।