Wait for result from Coroutine and then use it in Composable function - kotlin

I am creating a video scraper, and it has the following function which scrapes the video source from a URL that has been given as the parameter:
fun scrapeVideoSrcFromUrl(url: String): String? {
val document = Jsoup.connect(url).get()
for (element in document.getElementsByTag("script")) {
if (element.attr("type") == "application/ld+json") {
val content = element.data()
val array = JsonParser.parseString(content).asJsonArray
val embedUrl = Gson().fromJson(array.get(0).asJsonObject.get("embedUrl"), String::class.java)
var embedId = ""
for (char in embedUrl.dropLast(1).reversed()) {
if (char != '/') {
embedId += char
} else {
break
}
}
val doc = Jsoup.connect("$RUMBLE_API_URL${embedId.reversed()}").ignoreContentType(true).get()
val jsonData = doc.getElementsByTag("body").first()?.text()
val mp4 = JsonParser.parseString(jsonData).asJsonObject.get("u").asJsonObject.get("mp4").asJsonObject.get("url").toString()
return mp4.replace("\"", "")
}
}
return null
}
I want to show this in a dialog for a certain link using ExoPlayer, so I did the following:
#Composable
fun VideoPlayer(videoSrc: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(
MediaItem.fromUri(
videoSrc
)
)
prepare()
playWhenReady = true
}
}
Box(modifier = Modifier.fillMaxSize()) {
DisposableEffect(key1 = Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
)
}
}
Then, in the main Composable:
if (openDialog) {
AlertDialog(
onDismissRequest = {
openDialog = false
},
title = {
Column {
Text(
text = viewModel.currentRumbleSearchResult?.title ?: ""
)
Spacer(
Modifier.height(8.dp)
)
Text(
text = "By ${viewModel.currentRumbleSearchResult?.channel?.name ?: ""}",
style = MaterialTheme.typography.titleSmall
)
}
},
text = {
VideoPlayer(RumbleScraper.create().scrapeVideoSrcFromUrl("https://rumble.com/v1m9oki-our-first-automatic-afk-farms-locals-minecraft-server-smp-ep3-live-stream.html")!!)
},
confirmButton = {
TextButton(
onClick = {
openDialog = false
}
) {
Text("Exit")
}
}
)
}
After running that code I keep getting NetworkOnMainThread exceptions, and I tried many things to fix it but nothing worked.
So I am unsure what to do as to how I can go around fixing this. I was wondering how I would go around waiting in the background for a result and then show it in the Compose function when it returns the value?

You can do something like this:
var videoSrc by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
videoSrc = RumbleScraper.create().scrapeVideoSrcFromUrl("")
}
}
text = { VideoPlayer(videoSrc) }
You can also call the scrapeVideoSrcFromUrl inside your viewModel and update some state that you will use in UI.
If you want to run it in response to some event like item click, you will be better of with something like this:
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
withContext(Dispatchers.IO) { ... }
}
}
)

Related

Loading state adapter is always loading

My task is to load recyclerView and show pagination which I both implemented.
I also implemented LoadingState for adapter.
ApiCall:
#GET("top-headlines?sources=bbc-news,techcrunch&apiKey=${BuildConfig.API_KEY}")
suspend fun getTopHeadlinesArticles(
#Query("page") page:Int = 1,
#Query("q") query: String,
) : Response<ArticleListResponse>
I wont show paging because it is working so I will jump to repository:
fun getSearchResult(query: String) =
Pager(
config = PagingConfig(
pageSize = 1,
maxSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = { ArticlePaging(newsService, query)}
).flow
ViewModel:
#OptIn(ExperimentalCoroutinesApi::class)
val news = _currentQuery.flatMapLatest { query ->
articleRepository.getSearchResult(query).cachedIn(viewModelScope)
}
Fragment:
binding.recyclerViewTop.layoutManager = LinearLayoutManager(context)
binding.recyclerViewTop.adapter = adapter.withLoadStateHeaderAndFooter(
header = ArticleLoadStateAdapter { adapter.retry() },
footer = ArticleLoadStateAdapter { adapter.retry() }
)
lifecycleScope.launchWhenCreated {
viewModel.news.collect { articles ->
adapter.submitData(articles)
}
}
And LoadStateViewHolder in LoadStateAdapter:
init {
binding.buttonRetry.setOnClickListener {
retry.invoke()
}
}
fun bind(loadState: LoadState) {
binding.apply {
progressBar.isVisible = loadState is LoadState.Loading
buttonRetry.isVisible = loadState !is LoadState.Loading
textViewError.isVisible = loadState !is LoadState.Loading
}
}
I already predefined DEFAULT_QUERY so I only get 1 Article.
Problem is that progressBar loading is always visible and I have no more articles to show.
Edit: when i have at least 10 items to show this works fine
Edit 2nd: Add ArticlePagingSource if that can help
override fun getRefreshKey(state: PagingState<Int, ArticleResponse>): Int? {
return state.anchorPosition?.let {
val anchorPage = state.closestPageToPosition(it)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
try {
val currentPageList = params.key ?: 1
response = if(query != ""){
newsService.getTopHeadlinesArticles(currentPageList, query)
} else{
newsService.getTopHeadlinesArticles(currentPageList)
}
val responseList = mutableListOf<ArticleResponse>()
val data = response.body()?.articleResponses ?: emptyList()
responseList.addAll(data)
val prevKey = if (currentPageList == 1) null else currentPageList - 1
return LoadResult.Page(
responseList,
prevKey,
currentPageList.plus(1)
)

How can I make two windows on jetpack compose desktop and going from window to another?

How can I make two windows on jetpack compose desktop and going from window to another when I click button for example?
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Products Manager",
state = rememberWindowState(width = 700.dp, height = 600.dp)
) {
val count = remember { mutableStateOf(0) }
MaterialTheme {
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value++
}) {
Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!")
}
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value = 0
}) {
Text("Reset")
}
}
}
}
}
To create multiple windows, you simply need to have multiple Window composables. Check out Open and close multiple windows documentation section for example.
To switch between windows programmatically, you can use window.toFront() on the window that should become topmost: window is property available in FrameWindowScope inside Window.content.
Here's an example how it can be done with two window "types". You can replace type with any other identifier.
enum class WindowTypes {
First,
Second,
}
fun main() = application {
val windowFocusRequestSharedFlow = remember { MutableSharedFlow<WindowTypes>() }
WindowTypes.values().forEach { windowType ->
key(windowType) {
Window(
title = windowType.toString(),
onCloseRequest = ::exitApplication,
) {
LaunchedEffect(Unit) {
windowFocusRequestSharedFlow
.filter { it == windowType }
.collect {
window.toFront()
}
}
val scope = rememberCoroutineScope()
Button({
scope.launch {
val windowTypeToFocus = WindowTypes.values().run {
get((indexOf(windowType) + 1) % count())
}
windowFocusRequestSharedFlow.emit(windowTypeToFocus)
}
}) {
Text("next window")
}
}
}
}
}

LazyColumn doing infinite number of recompositions (Jetpack Compose)

I want to display a list of items using LazyColumn. I wrote the needed code but when I ran the app I observed that the UI is very laggy. I put some Logs to find out where the problem may be and I discovered that the LazyColumn inifinitely recomposes the items. I don't know why LazyColumn behaves like it did
list composable:
fun listOfReceivers(
receivers: List<ReceivedPingItem>,
) {
if (receivers.isNotEmpty()) {
val listState = rememberLazyListState()
val firstVisibleIndex = listState.firstVisibleItemIndex
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastIndex + listState.firstVisibleItemIndex
if (listState.isScrolledToTheEnd()) {
Log.d("*****", "Scrolled to the End")
viewModel.onRecyclerViewScrolledToLast()
}
//LazyColumn -> equivalent of the RecyclerView
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(receivers) { receiverItem ->
Log.d("*****", "ADDED $receiverItem")
ReceiversListItem(
receiver = receiverItem,
modifier = Modifier.fillMaxWidth()
)
}
}
// AppViewState
if (firstVisibleIndex == 0) {
appViewState.viewState = ViewState.ReceivedPingsViewState(firstVisibleIndex)
} else {
appViewState.viewState = ViewState.DefaultViewState
}
// Seen status check
if (firstVisibleIndex != -1 && lastVisibleItemIndex != -1) {
viewModel.onRecyclerViewScrolledSetViewed(
receivers.subList(
firstVisibleIndex,
lastVisibleItemIndex
)
)
}
}
else{
EmptyPingsScreen()
}
}
Due to this video - youtu.be/EOQB8PTLkpY?t=284 , i did smthn like this, and it works perfectly:
#Composable
fun ListOfReceivers(
receivers: List<ReceivedPingItem>,
) {
if (receivers.isNotEmpty()) {
val listState = rememberLazyListState()
val derivedListState by remember { derivedStateOf { listState.isScrolledToTheEnd() } }
if (derivedListState) {
viewModel.onRecyclerViewScrolledToLast()
}
// LazyColumn -> equivalent of the RecyclerView
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(receivers) { receiverItem ->
Log.d("*****", "ADDED $receiverItem")
ReceiversListItem(
receiver = receiverItem,
modifier = Modifier.fillMaxWidth()
)
}
}
// derivedStateOf - prevents infinite recomposition
val visibleItemsCount by remember { derivedStateOf { listState.layoutInfo.visibleItemsInfo.size } }
val firstVisibleItemIndex = listState.firstVisibleItemIndex
val lastVisibleItemIndex = firstVisibleItemIndex + visibleItemsCount - 1
// AppViewState
if (firstVisibleItemIndex == 0) {
appViewState.viewState = ViewState.ReceivedPingsViewState(firstVisibleItemIndex)
} else {
appViewState.viewState = ViewState.DefaultViewState
}

Why textfield not catch text inside and don't add to database

When I press button with onSendClicked is not adding text from textfield. I don't know where not catching text. I guess is somewhere mistake with viewmodel, becouse viewmodel don't get new value.
fun AddBar(
onSendClicked: () -> Unit
){
Row(Modifier.padding(5.dp)) {
var title by remember {
mutableStateOf("")
}
TextField(
value = title,
onValueChange = { title = it }
)
IconButton(onClick = {
onSendClicked()})
{
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Send Icon")
}
}
}
#Composable
fun MainScreen(
basketViewModel: BasketViewModel,
){
AddBar(onSendClicked = { basketViewModel.addToBasket() })
}
And viewModel
val id: MutableState<Int> = mutableStateOf(0)
val title: MutableState<String> = mutableStateOf("")
fun addToBasket(){
viewModelScope.launch(Dispatchers.IO) {
val basket = Basket(
title = title.value,
isChecked = false
)
repository.addToBasket(basket = basket)
}
}
Help....
You are never using the title state of the ViewModel. You are only updating the local title. For this to you have to stop using local title and just replace it with the title from viewModel. Something like that:
fun AddBar(
title: MutableState<String>,
onSendClicked: () -> Unit
){
Row(Modifier.padding(5.dp)) {
TextField(
value = title.value,
onValueChange = { title.value = it }
)
IconButton(onClick = {
onSendClicked()})
{
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Send Icon")
}
}
}
#Composable
fun MainScreen(
basketViewModel: BasketViewModel,
){
AddBar(
title = basketViewModel.title,
onSendClicked = { basketViewModel.addToBasket() }
)
}
You define the title both in view model and your main screen. Use the one in your view model.
fun AddBar(
title: String,
onValueChange: (String) -> Unit,
onSendClicked: () -> Unit
){
Row(Modifier.padding(5.dp)) {
TextField(
value = title,
onValueChange = { onValueChange(it) }
)
IconButton(
onClick = { onSendClicked() }
) {
Icon(
imageVector = Icons.Filled.ArrowForward,
contentDescription = "Send Icon"
)
}
}
}
#Composable
fun MainScreen(
basketViewModel: BasketViewModel,
){
AddBar(
title = basketViewModel.title,
onValueChange = { basketViewModel.changeTitle(it) }
onSendClicked = { basketViewModel.addToBasket() }
)
}
class BasketViewModel : ViewModel() {
var title by mutableStateOf("")
private set
fun changeTitle(value: String) {
title = value
}
fun addToBasket(){
viewModelScope.launch(Dispatchers.IO) {
val basket = Basket(
title = title.value,
isChecked = false
)
repository.addToBasket(basket = basket)
}
}
}

How can I successfully pass my LiveData from my Repository to my Compose UI using a ViewModel?

I am trying to pass live events from a Broadcast Receiver to the title of my Homepage.
I am passing a String from the Broadcast Receiver to my Repository successfully, but in the end my title is always null. What am I missing?
My Repository looks like this:
object Repository {
fun getAndSendData (query: String): String{
return query
}
}
Then in my ViewModel I have:
private val _data = MutableLiveData<String>()
val repoData = _data.switchMap {
liveData {
emit(Repository.getAndSendData(it))
}
}
And finally in my Composable I have:
val repoData = viewModel.repoData.observeAsState()
topBar = {
TopAppBar(
title = { Text(text = if (repoData.value == null)"null" else repoData.value!!, style = typography.body1) },
navigationIcon = {
IconButton(onClick = { scaffoldState.drawerState.open() }) {
Icon(Icons.Rounded.Menu)
}
}
)
},
I don't think we can use Live Data from the compose(jetpack) because it can run from the Main thread. I used onCommit() {} with interface from the compose.
onCommit() {
viewModel.testCountriesList(object : NetworkCallBack {
override fun test(response: GeneralResponse) {
if(response.code == 200) {
counties = response.response
responseState.value = true
} else {
responseState.value = false
}
}
})
}