Android Webview unable to load a lazy loading website

I am loading a website URL using Jetpack Compose Webview.

Here is what I expected to see.

enter image description here

But I only get to see the white screen and it never shows the loading and loads the rest. Here is my code:

AndroidView(
    modifier = Modifier
      .fillMaxSize(),
    factory = { context ->
      WebView(context).apply {

        webViewClient = object : WebViewClient() {
        }

        settings.javaScriptEnabled = true
        settings.mediaPlaybackRequiresUserGesture = false
        settings.allowContentAccess = true
        settings.loadWithOverviewMode = true
        settings.useWideViewPort = true
        settings.setSupportZoom(true)
        overScrollMode = WebView.OVER_SCROLL_NEVER
        loadUrl(url)
      }
    }
  )

I really appreciate for any of your suggestion. Thank you!

5

I just fond the solution for this.

Maybe there are some edge case for this so we must use the custom webview.
Here is what I got from accompanist.
Hope that it could help you

import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle
import android.view.ViewGroup.LayoutParams
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.liquidity.feature.rain.mock_screens.rain_verification.LoadingState.Finished
import com.liquidity.feature.rain.mock_screens.rain_verification.LoadingState.Loading
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 * A wrapper around the Android View WebView to provide a basic WebView composable.
 *
 * If you require more customisation you are most likely better rolling your own and using this
 * wrapper as an example.
 *
 * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it
 * is incorrectly sizing, use the layoutParams composable function instead.
 *
 * @param state The webview state holder where the Uri to load is defined.
 * @param modifier A compose modifier
 * @param captureBackPresses Set to true to have this Composable capture back presses and navigate
 * the WebView back.
 * @param navigator An optional navigator object that can be used to control the WebView's
 * navigation from outside the composable.
 * @param onCreated Called when the WebView is first created, this can be used to set additional
 * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be
 * subsequently overwritten after this lambda is called.
 * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved
 * if you need to save and restore state in this WebView.
 * @param client Provides access to WebViewClient via subclassing
 * @param chromeClient Provides access to WebChromeClient via subclassing
 * @param factory An optional WebView factory for using a custom subclass of WebView
 */

@Composable
fun WebView(
  state: WebViewState,
  modifier: Modifier = Modifier,
  captureBackPresses: Boolean = true,
  navigator: WebViewNavigator = rememberWebViewNavigator(),
  onCreated: (WebView) -> Unit = {},
  onDispose: (WebView) -> Unit = {},
  client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
  chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
  factory: ((Context) -> WebView)? = null
) {
  BoxWithConstraints(modifier) {
    // WebView changes it's layout strategy based on
    // it's layoutParams. We convert from Compose Modifier to
    // layout params here.
    val width =
      if (constraints.hasFixedWidth) {
        LayoutParams.MATCH_PARENT
      } else {
        LayoutParams.WRAP_CONTENT
      }
    val height =
      if (constraints.hasFixedHeight) {
        LayoutParams.MATCH_PARENT
      } else {
        LayoutParams.WRAP_CONTENT
      }

    val layoutParams = FrameLayout.LayoutParams(
      width,
      height
    )

    WebView(
      state,
      layoutParams,
      Modifier,
      captureBackPresses,
      navigator,
      onCreated,
      onDispose,
      client,
      chromeClient,
      factory
    )
  }
}

/**
 * A wrapper around the Android View WebView to provide a basic WebView composable.
 *
 * If you require more customisation you are most likely better rolling your own and using this
 * wrapper as an example.
 *
 * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it
 * is incorrectly sizing, use the layoutParams composable function instead.
 *
 * @param state The webview state holder where the Uri to load is defined.
 * @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView.
 * @param modifier A compose modifier
 * @param captureBackPresses Set to true to have this Composable capture back presses and navigate
 * the WebView back.
 * @param navigator An optional navigator object that can be used to control the WebView's
 * navigation from outside the composable.
 * @param onCreated Called when the WebView is first created, this can be used to set additional
 * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be
 * subsequently overwritten after this lambda is called.
 * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved
 * if you need to save and restore state in this WebView.
 * @param client Provides access to WebViewClient via subclassing
 * @param chromeClient Provides access to WebChromeClient via subclassing
 * @param factory An optional WebView factory for using a custom subclass of WebView
 */

@Composable
fun WebView(
  state: WebViewState,
  layoutParams: FrameLayout.LayoutParams,
  modifier: Modifier = Modifier,
  captureBackPresses: Boolean = true,
  navigator: WebViewNavigator = rememberWebViewNavigator(),
  onCreated: (WebView) -> Unit = {},
  onDispose: (WebView) -> Unit = {},
  client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
  chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
  factory: ((Context) -> WebView)? = null
) {
  val webView = state.webView

  BackHandler(captureBackPresses && navigator.canGoBack) {
    webView?.goBack()
  }

  webView?.let { wv ->
    LaunchedEffect(wv, navigator) {
      with(navigator) {
        wv.handleNavigationEvents()
      }
    }

    LaunchedEffect(wv, state) {
      snapshotFlow { state.content }.collect { content ->
        when (content) {
          is WebContent.Url -> {
            wv.loadUrl(content.url, content.additionalHttpHeaders)
          }

          is WebContent.Data -> {
            wv.loadDataWithBaseURL(
              content.baseUrl,
              content.data,
              content.mimeType,
              content.encoding,
              content.historyUrl
            )
          }

          is WebContent.Post -> {
            wv.postUrl(
              content.url,
              content.postData
            )
          }

          is WebContent.NavigatorOnly -> {
            // NO-OP
          }
        }
      }
    }
  }

  // Set the state of the client and chrome client
  // This is done internally to ensure they always are the same instance as the
  // parent Web composable
  client.state = state
  client.navigator = navigator
  chromeClient.state = state

  AndroidView(
    factory = { context ->
      (factory?.invoke(context) ?: WebView(context)).apply {
        onCreated(this)

        this.layoutParams = layoutParams

        state.viewState?.let {
          this.restoreState(it)
        }

        webChromeClient = chromeClient
        webViewClient = client
      }.also { state.webView = it }
    },
    modifier = modifier,
    onRelease = {
      onDispose(it)
    }
  )
}

/**
 * AccompanistWebViewClient
 *
 * A parent class implementation of WebViewClient that can be subclassed to add custom behaviour.
 *
 * As Accompanist Web needs to set its own web client to function, it provides this intermediary
 * class that can be overriden if further custom behaviour is required.
 */

open class AccompanistWebViewClient : WebViewClient() {
  open lateinit var state: WebViewState
    internal set
  open lateinit var navigator: WebViewNavigator
    internal set

  override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
    super.onPageStarted(view, url, favicon)
    state.loadingState = Loading(0.0f)
    state.errorsForCurrentRequest.clear()
    state.pageTitle = null
    state.pageIcon = null

    state.lastLoadedUrl = url
  }

  override fun onPageFinished(view: WebView, url: String?) {
    super.onPageFinished(view, url)
    state.loadingState = Finished
  }

  override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) {
    super.doUpdateVisitedHistory(view, url, isReload)

    navigator.canGoBack = view.canGoBack()
    navigator.canGoForward = view.canGoForward()
  }

  override fun onReceivedError(
    view: WebView,
    request: WebResourceRequest?,
    error: WebResourceError?
  ) {
    super.onReceivedError(view, request, error)

    if (error != null) {
      state.errorsForCurrentRequest.add(WebViewError(request, error))
    }
  }
}

/**
 * AccompanistWebChromeClient
 *
 * A parent class implementation of WebChromeClient that can be subclassed to add custom behaviour.
 *
 * As Accompanist Web needs to set its own web client to function, it provides this intermediary
 * class that can be overriden if further custom behaviour is required.
 */

open class AccompanistWebChromeClient : WebChromeClient() {
  open lateinit var state: WebViewState
    internal set

  override fun onReceivedTitle(view: WebView, title: String?) {
    super.onReceivedTitle(view, title)
    state.pageTitle = title
  }

  override fun onReceivedIcon(view: WebView, icon: Bitmap?) {
    super.onReceivedIcon(view, icon)
    state.pageIcon = icon
  }

  override fun onProgressChanged(view: WebView, newProgress: Int) {
    super.onProgressChanged(view, newProgress)
    if (state.loadingState is Finished) return
    state.loadingState = Loading(newProgress / 100.0f)
  }
}

sealed class WebContent {
  data class Url(
    val url: String,
    val additionalHttpHeaders: Map<String, String> = emptyMap()
  ) : WebContent()

  data class Data(
    val data: String,
    val baseUrl: String? = null,
    val encoding: String = "utf-8",
    val mimeType: String? = null,
    val historyUrl: String? = null
  ) : WebContent()

  data class Post(
    val url: String,
    val postData: ByteArray
  ) : WebContent() {
    override fun equals(other: Any?): Boolean {
      if (this === other) return true
      if (javaClass != other?.javaClass) return false

      other as Post

      if (url != other.url) return false
      if (!postData.contentEquals(other.postData)) return false

      return true
    }

    override fun hashCode(): Int {
      var result = url.hashCode()
      result = 31 * result + postData.contentHashCode()
      return result
    }
  }

  @Deprecated("Use state.lastLoadedUrl instead", replaceWith = ReplaceWith("state.lastLoadedUrl"))
  fun getCurrentUrl(): String? {
    return when (this) {
      is Url -> url
      is Data -> baseUrl
      is Post -> url
      is NavigatorOnly -> throw IllegalStateException("Unsupported")
    }
  }

  data object NavigatorOnly : WebContent()
}

internal fun WebContent.withUrl(url: String) = when (this) {
  is WebContent.Url -> copy(url = url)
  else -> WebContent.Url(url)
}

/**
 * Sealed class for constraining possible loading states.
 * See [Loading] and [Finished].
 */

sealed class LoadingState {
  /**
   * Describes a WebView that has not yet loaded for the first time.
   */
  data object Initializing : LoadingState()

  /**
   * Describes a webview between `onPageStarted` and `onPageFinished` events, contains a
   * [progress] property which is updated by the webview.
   */
  data class Loading(val progress: Float) : LoadingState()

  /**
   * Describes a webview that has finished loading content.
   */
  data object Finished : LoadingState()
}

/**
 * A state holder to hold the state for the WebView. In most cases this will be remembered
 * using the rememberWebViewState(uri) function.
 */

@Stable
class WebViewState(webContent: WebContent) {
  var lastLoadedUrl: String? by mutableStateOf(null)
    internal set

  /**
   *  The content being loaded by the WebView
   */
  var content: WebContent by mutableStateOf(webContent)

  /**
   * Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with
   * progress) or the data loading has [LoadingState.Finished]. See [LoadingState]
   */
  var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)
    internal set

  /**
   * Whether the webview is currently loading data in its main frame
   */
  val isLoading: Boolean
    get() = loadingState !is Finished

  /**
   * The title received from the loaded content of the current page
   */
  var pageTitle: String? by mutableStateOf(null)
    internal set

  /**
   * the favicon received from the loaded content of the current page
   */
  var pageIcon: Bitmap? by mutableStateOf(null)
    internal set

  /**
   * A list for errors captured in the last load. Reset when a new page is loaded.
   * Errors could be from any resource (iframe, image, etc.), not just for the main page.
   * For more fine grained control use the OnError callback of the WebView.
   */
  val errorsForCurrentRequest: SnapshotStateList<WebViewError> = mutableStateListOf()

  /**
   * The saved view state from when the view was destroyed last. To restore state,
   * use the navigator and only call loadUrl if the bundle is null.
   * See WebViewSaveStateSample.
   */
  var viewState: Bundle? = null
    internal set

  // We need access to this in the state saver. An internal DisposableEffect or AndroidView
  // onDestroy is called after the state saver and so can't be used.
  internal var webView by mutableStateOf<WebView?>(null)
}

/**
 * Allows control over the navigation of a WebView from outside the composable. E.g. for performing
 * a back navigation in response to the user clicking the "up" button in a TopAppBar.
 *
 * @see [rememberWebViewNavigator]
 */
@Stable
class WebViewNavigator(private val coroutineScope: CoroutineScope) {
  private sealed interface NavigationEvent {
    data object Back : NavigationEvent
    data object Forward : NavigationEvent
    data object Reload : NavigationEvent
    data object StopLoading : NavigationEvent

    data class LoadUrl(
      val url: String,
      val additionalHttpHeaders: Map<String, String> = emptyMap()
    ) : NavigationEvent

    data class LoadHtml(
      val html: String,
      val baseUrl: String? = null,
      val mimeType: String? = null,
      val encoding: String? = "utf-8",
      val historyUrl: String? = null
    ) : NavigationEvent

    data class PostUrl(
      val url: String,
      val postData: ByteArray
    ) : NavigationEvent {
      override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as PostUrl

        if (url != other.url) return false
        if (!postData.contentEquals(other.postData)) return false

        return true
      }

      override fun hashCode(): Int {
        var result = url.hashCode()
        result = 31 * result + postData.contentHashCode()
        return result
      }
    }
  }

  private val navigationEvents: MutableSharedFlow<NavigationEvent> = MutableSharedFlow(replay = 1)

  // Use Dispatchers.Main to ensure that the webview methods are called on UI thread
  internal suspend fun WebView.handleNavigationEvents(): Nothing = withContext(Dispatchers.Main) {
    navigationEvents.collect { event ->
      when (event) {
        is NavigationEvent.Back -> goBack()
        is NavigationEvent.Forward -> goForward()
        is NavigationEvent.Reload -> reload()
        is NavigationEvent.StopLoading -> stopLoading()
        is NavigationEvent.LoadHtml -> loadDataWithBaseURL(
          event.baseUrl,
          event.html,
          event.mimeType,
          event.encoding,
          event.historyUrl
        )

        is NavigationEvent.LoadUrl -> {
          loadUrl(event.url, event.additionalHttpHeaders)
        }

        is NavigationEvent.PostUrl -> {
          postUrl(event.url, event.postData)
        }
      }
    }
  }

  /**
   * True when the web view is able to navigate backwards, false otherwise.
   */
  var canGoBack: Boolean by mutableStateOf(false)
    internal set

  /**
   * True when the web view is able to navigate forwards, false otherwise.
   */
  var canGoForward: Boolean by mutableStateOf(false)
    internal set

  fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()) {
    coroutineScope.launch {
      navigationEvents.emit(
        NavigationEvent.LoadUrl(
          url,
          additionalHttpHeaders
        )
      )
    }
  }

  fun loadHtml(
    html: String,
    baseUrl: String? = null,
    mimeType: String? = null,
    encoding: String? = "utf-8",
    historyUrl: String? = null
  ) {
    coroutineScope.launch {
      navigationEvents.emit(
        NavigationEvent.LoadHtml(
          html,
          baseUrl,
          mimeType,
          encoding,
          historyUrl
        )
      )
    }
  }

  fun postUrl(
    url: String,
    postData: ByteArray
  ) {
    coroutineScope.launch {
      navigationEvents.emit(
        NavigationEvent.PostUrl(
          url,
          postData
        )
      )
    }
  }

  /**
   * Navigates the webview back to the previous page.
   */
  fun navigateBack() {
    coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) }
  }

  /**
   * Navigates the webview forward after going back from a page.
   */
  fun navigateForward() {
    coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) }
  }

  /**
   * Reloads the current page in the webview.
   */
  fun reload() {
    coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) }
  }

  /**
   * Stops the current page load (if one is loading).
   */
  fun stopLoading() {
    coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) }
  }
}

/**
 * Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided
 * override.
 */
@Composable
fun rememberWebViewNavigator(
  coroutineScope: CoroutineScope = rememberCoroutineScope()
): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) }

/**
 * A wrapper class to hold errors from the WebView.
 */
@Immutable
data class WebViewError(
  /**
   * The request the error came from.
   */
  val request: WebResourceRequest?,
  /**
   * The error that was reported.
   */
  val error: WebResourceError
)

/**
 * Creates a WebView state that is remembered across Compositions.
 *
 * @param url The url to load in the WebView
 * @param additionalHttpHeaders Optional, additional HTTP headers that are passed to [WebView.loadUrl].
 *                              Note that these headers are used for all subsequent requests of the WebView.
 */
@Composable
fun rememberWebViewState(
  url: String,
  additionalHttpHeaders: Map<String, String> = emptyMap()
): WebViewState =
// Rather than using .apply {} here we will recreate the state, this prevents
  // a recomposition loop when the webview updates the url itself.
  remember {
    WebViewState(
      WebContent.Url(
        url = url,
        additionalHttpHeaders = additionalHttpHeaders
      )
    )
  }.apply {
    this.content = WebContent.Url(
      url = url,
      additionalHttpHeaders = additionalHttpHeaders
    )
  }

/**
 * Creates a WebView state that is remembered across Compositions.
 *
 * @param data The uri to load in the WebView
 */
@Composable
fun rememberWebViewStateWithHTMLData(
  data: String,
  baseUrl: String? = null,
  encoding: String = "utf-8",
  mimeType: String? = null,
  historyUrl: String? = null
): WebViewState =
  remember {
    WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl))
  }.apply {
    this.content = WebContent.Data(
      data,
      baseUrl,
      encoding,
      mimeType,
      historyUrl
    )
  }

/**
 * Creates a WebView state that is remembered across Compositions.
 *
 * @param url The url to load in the WebView
 * @param postData The data to be posted to the WebView with the url
 */
@Composable
fun rememberWebViewState(
  url: String,
  postData: ByteArray
): WebViewState =
// Rather than using .apply {} here we will recreate the state, this prevents
  // a recomposition loop when the webview updates the url itself.
  remember {
    WebViewState(
      WebContent.Post(
        url = url,
        postData = postData
      )
    )
  }.apply {
    this.content = WebContent.Post(
      url = url,
      postData = postData
    )
  }

/**
 * Creates a WebView state that is remembered across Compositions and saved
 * across activity recreation.
 * When using saved state, you cannot change the URL via recomposition. The only way to load
 * a URL is via a WebViewNavigator.
 */
@Composable
fun rememberSaveableWebViewState(): WebViewState =
  rememberSaveable(saver = WebStateSaver) {
    WebViewState(WebContent.NavigatorOnly)
  }

val WebStateSaver: Saver<WebViewState, Any> = run {
  val pageTitleKey = "pagetitle"
  val lastLoadedUrlKey = "lastloaded"
  val stateBundle = "bundle"

  mapSaver(
    save = {
      val viewState = Bundle().apply { it.webView?.saveState(this) }
      mapOf(
        pageTitleKey to it.pageTitle,
        lastLoadedUrlKey to it.lastLoadedUrl,
        stateBundle to viewState
      )
    },
    restore = {
      WebViewState(WebContent.NavigatorOnly).apply {
        this.pageTitle = it[pageTitleKey] as String?
        this.lastLoadedUrl = it[lastLoadedUrlKey] as String?
        this.viewState = it[stateBundle] as Bundle?
      }
    }
  )
}

Here is a simple way which you can follow to show CircularProgressIndicator while loading WebView.

You can override WebviewClient to show and hide CircularProgressIndicator.

import android.graphics.Bitmap
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView

@Composable
fun Stack036() {

    val isLoading = remember {
        mutableStateOf(true)
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState()),
        contentAlignment = Alignment.Center
    ) {

        AndroidView(
            modifier = Modifier.fillMaxSize(),
            factory = { context ->
                WebView(context).apply {
                    webViewClient = object : WebViewClient() {
                        override fun onPageFinished(view: WebView?, url: String?) {
                            super.onPageFinished(view, url)
                            isLoading.value = false
                        }

                        override fun onPageStarted(
                            view: WebView?,
                            url: String?,
                            favicon: Bitmap?
                        ) {
                            super.onPageStarted(view, url, favicon)
                            isLoading.value = true
                        }
                    }
                    settings.javaScriptEnabled = true
                    settings.loadWithOverviewMode = true
                    loadUrl("https://v6.mantine.dev/core/modal/")
                }
            }, update = {
            })

        if (isLoading.value) {
            CircularProgressIndicator()
        }

    }

}

Hope this works for you.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật