I have made a simple Jetpack compose app coupled with a server. When I try to communicate with the server from the app, the app crashes without any error messages.
The app code:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.Text
import androidx.compose.runtime.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.Socket
fun getServerMessage(onServerMessage: (msg: String)-> Unit) {
val client = Socket("127.0.0.1", 9999)
val output = PrintWriter(client.getOutputStream(), true)
val input = BufferedReader(InputStreamReader(client.inputStream))
output.println("msg")
onServerMessage(input.readLine())
}
#Composable
fun GreetingFromServer(name: String) {
var text by remember { mutableStateOf("waiting for message") }
thread { getServerMessage { msg -> text = msg } }
Text(text)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GreetingFromServer("Android")
}
}
}
The server code:
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.ServerSocket
fun main(args: Array<String>) {
val server = ServerSocket(9999)
println("printing")
while (true) {
val client = server.accept()
println("a client connected")
val output = PrintWriter(client.getOutputStream(), true)
val input = BufferedReader(InputStreamReader(client.inputStream))
output.println("${input.readLine()} back!")
}
}
Why does the app crash when initiating the client object and when running the thread? And how should I go about fixing that?
(The app still crashes when removing the "thread {}" wrapping.)
Ignoring the backend code, since the question was on Jetpack compose Android aspects, a simplified version of a network call would look something like this.
API call
suspend fun getServerMessage(): String {
delay(5000)
return "Success"
}
ViewModel
class GreetingFromServerViewModel : ViewModel() {
var text by mutableStateOf("waiting for message")
init {
getMessage()
}
private fun getMessage() {
viewModelScope.launch(Dispatchers.IO) {
text = getServerMessage()
}
}
}
View Composable
#Composable
fun GreetingFromServer(
viewmodel: GreetingFromServerViewModel = viewModel(),
) {
Text(viewmodel.text)
}
Actual product apps would have a lot more things like using Repositories to manage data fetching, Retrofit if using REST APIs, etc.
Refer: ViewModel, Coroutines
Related
Hello developers i have a problem i calling an api from viewmodel but is a coroutine and i do not how to wait to api response to make login .... i have this method in viewmodel:
fun createUser(context: Context){
viewModelScope.launch {
val jsonObject= JSONObject()
jsonObject.put("correo", register.value.correo)
jsonObject.put("password", register.value.password)
jsonObject.put("nombre", register.value.nombre)
jsonObject.put("apellido", register.value.apellido)
jsonObject.put("direccion",register.value.direccion)
jsonObject.put("telefono", register.value.telefono)
var bodyrequest=jsonObject.toString().toRequestBody()
val result: RegisterResponse = registerService.register(bodyrequest)
response.value=result
Log.d("response",result.message)
}
}
and this is onClick event from button in a #Composable function:
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser(context)
Log.d("result from compose","${vm.response.value.message}")
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}else{
showPopUp = !showPopUp
}
}
The big problem is that when the navigation is triggered, the coroutine has not finished yet and therefore I cannot find out if the api response was successful or not. How could I do this?
You can let createUser return the Job, and then join it in onClick. Like this:
fun createUser(context: Context) = viewModelScope.launch {...}
...
onClick = {
scope.launch {
viewModel.createUser(context).join()
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}
}
But this makes the code difficult to maintain. I suggest having the ViewModel layer send an Event when createUser is done and the View layer collects it and navigate to a new screen. Like this:
interface Event
abstract class BaseViewModel<E : Event> : ViewModel() {
private val _event = Channel<E>()
val event = _event.receiveAsFlow().shareIn(viewModelScope, SharingStarted.Lazily)
protected suspend fun sendEvent(event: E) = _event.send(event)
protected fun sendEventSync(event: E) = viewModelScope.launch { _event.send(event) }
}
#Composable
fun <E : Event> OnEvent(event: Flow<E>, onEvent: (E) -> Unit) {
LaunchedEffect(Unit) {
event.collect(onEvent)
}
}
sealed interface LoginEvent : Event {
object LoginSuccess : LoginEvent
data class LoginFailure(val exception: Throwable) : LoginEvent
}
class LoginViewModel() : BaseViewModel<LoginEvent>() {
fun createUser(context: Context){
viewModelScope.launch {
repository.loign()
.onSuccess { sendEvent(LoginEvent.LoginSuccess) }
.onFailure { sendEvent(LoginEvent.LoginFailure(it)) }
}
}
}
#Composable
fun Screen() {
...
OnEvent(viewModel.event) {
when (it) {
LoginEvent.LoginSuccess ->
navController.navToXXXScreen()
is LoginEvent.LoginFailure ->
context.toast(it.exception.localizedMessage ?: "")
}
}
}
I am currently using vaadin v23.
I want to navigate to (change view / page) to "Dashboard"
Default - login page view code looks like this:
package com.fd.jvmbackend.views.adminPanel.login
import com.fd.jvmbackend.extensions.isNull
import com.fd.jvmbackend.views.AdminPanelRoute
import com.fd.jvmbackend.views.BaseView
import com.fd.jvmbackend.views.Extras
import com.vaadin.flow.component.AttachEvent
import com.vaadin.flow.component.DetachEvent
import com.vaadin.flow.component.Text
import com.vaadin.flow.component.Unit
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.html.Label
import com.vaadin.flow.component.notification.Notification
import com.vaadin.flow.router.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.apache.commons.lang3.RandomStringUtils
import java.util.concurrent.TimeUnit
#Route(value = AdminPanelRoute.LOGIN)
#PageTitle("Login | FD CMS")
class LoginView() : BaseView() {
private val TAG = "LoginView"
private var viewModel: LoginViewModel? = null
override fun onAttach(attachEvent: AttachEvent?) {
super.onAttach(attachEvent)
viewModel = LoginViewModel()
val label = Label("Welcome.")
val loginField = getLoginTextField("Login", "ex: mike", true, true)
val passwordField = getPasswordField("Password", "ex. myLongPassword", true, true, true)
val button = Button("Log in with credentials")
button.setWidth(15F, Unit.PERCENTAGE)
button.addClickListener { event ->
viewModel?.onLoginClicked(loginField.value, passwordField.value)
}
add(label)
add(loginField)
add(passwordField)
add(button)
collectorsJob = lifecycleScope.launch {
launch {
viewModel?.getPageTitle()?.collect { value ->
println("$TAG -> getPageTitle -> ${value}")
ui.get().access {
ui.get().page.setTitle(value)
}
}
}
launch {
viewModel?.getErrorText()?.collect { value ->
if(value.isNull()){
return#collect
}
ui.get().access {
notification?.close()
notification = Notification.show(value,TimeUnit.SECONDS.toMillis(5).toInt(),Notification.Position.BOTTOM_CENTER)
}
}
}
launch {
viewModel?.getIsLoading()?.collect { value ->
ui.get().access {
if (value) {
progressDialog = getIndeterminateProgressDialog("Loading", "please wait")
progressDialog?.open()
} else {
progressDialog?.close()
progressDialog = null
}
}
ui.get().access {
loginField.isEnabled = !value
}
ui.get().access {
passwordField.isEnabled = !value
}
ui.get().access {
button.isEnabled = !value
}
}
}
launch {
viewModel?.getNavigationRouterLink()?.collect { value ->
if(value.isNull()){
return#collect
}
ui.get().access {
ui.get().navigate(
DashboardView::class.java,
RouteParameters(Extras.USER_ID, RandomStringUtils.randomAlphabetic(10))
)
}
}
}
}
}
override fun onDetach(detachEvent: DetachEvent?) {
viewModel?.onCleared()
viewModel = null
super.onDetach(detachEvent)
}
}
AdminPanelRoute.LOGIN = "login",
AdminPanelRoute.DASHBOARD = "dashboard"
Code which handles navigating to another page/view looks like this:
ui.get().navigate(DashboardView::class.java, RouteParameters(Extras.USER_ID, RandomStringUtils.randomAlphabetic(10)))
After execution this is what I get:
Caused by: com.vaadin.flow.router.NotFoundException: No route found
for the given navigation target
'com.fd.jvmbackend.views.adminPanel.login.DashboardView' and
parameters '{extra_user_id=ElKbspkysb}'
DashboardView.kt contents:
package com.fd.jvmbackend.views.adminPanel.login
import com.fd.jvmbackend.util.ResourceHandler
import com.fd.jvmbackend.views.AdminPanelRoute
import com.vaadin.flow.component.AttachEvent
import com.vaadin.flow.component.DetachEvent
import com.vaadin.flow.component.applayout.AppLayout
import com.vaadin.flow.component.applayout.DrawerToggle
import com.vaadin.flow.component.html.Image
import com.vaadin.flow.component.html.Label
import com.vaadin.flow.router.PageTitle
import com.vaadin.flow.router.Route
#Route(value = AdminPanelRoute.DASHBOARD)
#PageTitle("Dashboard | FD CMS")
class DashboardView:AppLayout() {
private val TAG = "DashboardView"
val label = Label("Secret message -> ")
override fun onAttach(attachEvent: AttachEvent?) {
super.onAttach(attachEvent)
}
override fun onDetach(detachEvent: DetachEvent?) {
super.onDetach(detachEvent)
}
}
Please tell me where is the mistake. What am I doing wrong ? The view -> DashboardView is registered with #Route annotation.
Looks like DashboardView::class.java is interpreted as the literal Java String "com.fd.jvmbackend.views.adminPanel.login.DashboardView", not the class object, and the wrong overload of ui.navigate is called. Try passing the Route String (AdminPanelRoute.DASHBOARD) instead.
EDIT: I note you're also passing route parameters with the second parameter of navigate. DashboardView doesn't accept route parameters, so you should remove those.
The navigation is paramater sensitiv. That means if you navigate to the dashboard Route with Parameters it will not be found. Remove the argument from your UI.navigate expression or declare your #Route with a value like "dashboard/:id". See the URL Template Docu.
I am quite new in Android Flow and JetPack compose,
I am trying to update my UI when mutable state is being changed , but this is not calling our composable , here is my code
#Composable
fun Grid() {
val mainViewModel by viewModels<DashBoardViewModel>()
mainViewModel.getData()
when (val result = mainViewModel.mutableState.value) {
is Resource.Success -> {
LazyVerticalGrid(
cells = GridCells.Adaptive(100.dp)
) {
items(result.device.items.first().devices.count()) {
EachItem(it)
}
}
}
is Resource.Error -> { Text(text = result.message) }
Resource.Loading -> { CircularProgressIndicator() }
Resource.Empty -> {}
else -> { CircularProgressIndicator() }
}
}
ViewModel:
#HiltViewModel
class DashBoardViewModel #Inject constructor(
private val dashBoardRepository: DashBoardRepository
) : ViewModel() {
val mutableState = MutableLiveData<Resource>()
fun getData() = viewModelScope.launch {
flow {
emit(Resource.Loading)
try {
val mResponse = dashBoardRepository.getDevice()
emit(Resource.Success(mResponse))
} catch (e: Exception) {
e.printStackTrace()
emit(Resource.Error("Error"))
}
}.flowOn(Dispatchers.IO).collect {
mutableState.value = it
}
}
}
There are two problems in your code:
mainViewModel.mutableState.value gets only the current value from your mutable state. Your composable will not be notified when this value changes and thus it cannot reflect the change. If you want to use LiveData in your viewmodel, you have to use observeAsState() extension function which converts LivaData to State that can be automatically observed by composable function. Other option is to have (Mutable)State directly in your viewmodel. See this state explanation.
Your mainViewModel.getData() function will be called every time your Grid() function recomposes, which will be every time your mainViewModel.mutableState changes (once you observe it correctly). You definitely don't want that. Better solution would be to call getData() from your viewModel's init block, or, if you really need to call it from your composable function, use LaunchedEffect.
And, as a side note, the way you are creating flow and then collecting it into LiveData is really odd and unnecessary. You can do something like this instead:
fun getData() = viewModelScope.launch {
mutableState.value = Resource.Loading
try {
val mResponse = dashBoardRepository.getDevice()
mutableState.value = Resource.Success(mResponse)
} catch (e: Exception) {
e.printStackTrace()
mutableState.value = Resource.Error("Error")
}
}
I can't figure out how to pass my app object to my TypeGrapQL resolvers.
I created my types and resolvers and setup a graphql server using express-graphql. I was able to run the graph, but no luck in passing the app object to use the registered services.
My graphql.service.ts looks like this:
import { ServiceAddons } from '#feathersjs/feathers'
import { graphqlHTTP } from 'express-graphql'
import 'reflect-metadata'
import { buildSchemaSync } from 'type-graphql'
import { Container } from 'typedi'
import { Application } from '../../declarations'
import { Graphql } from './graphql.class'
import { ArticleResolver } from './resolvers/article.resolver'
// Add this service to the service type index
declare module '../../declarations' {
interface ServiceTypes {
graphql: Graphql & ServiceAddons<any>
}
}
export default async function (app: Application): Promise<void> {
const schema = buildSchemaSync({
resolvers: [__dirname + '/resolvers/*.resolver.ts'],
container: Container,
})
app.use(
'/graphql',
graphqlHTTP({
schema: schema,
graphiql: true,
})
)
}
and here's one of my resolver classes article.resolver.ts
import { Arg, Query, Resolver } from 'type-graphql'
import { Service } from 'typedi'
import { Application } from '../../../declarations'
import { Category } from '../types/category.type'
#Service()
#Resolver(Category)
export class CategoryResolver {
constructor(private readonly app: Application) {}
#Query((returns) => [Category])
async categories() {
try {
const result = await this.app.service('category').find()
return (result as any).data // TODO: Refactor to return result with pagination details
} catch (err) {
console.log('Categories resolver error', err)
return []
}
}
}
I can't do this.app.service() as this.app is undefined
Im a little confused on how dependency injection works in TypeGrapQL, any help is appreciated.
Thanks
I managed to make it work, here's my solution if anyone has the same problem:
I created a Graphql class decorated with #Service from typedi that takes in an app object as such
import { Service } from 'typedi'
import { Application } from '../../declarations'
#Service()
export class Graphql {
app: Application
//eslint-disable-next-line #typescript-eslint/no-unused-vars
constructor(app: Application) {
this.app = app
}
}
In my graphql.service.ts I initiated the class and passed down the instance to the typedi container
import { buildSchemaSync } from 'type-graphql'
import { Container } from 'typedi'
import { Application } from '../../declarations'
import { Graphql } from './graphql.class'
export default async function (app: Application): Promise<void> {
const graphql = new Graphql(app)
Container.set('graphql', graphql)
const schema = buildSchemaSync({
resolvers: [__dirname + '/resolvers/category.resolver.ts'],
container: Container, // Pass the container to the resolvers
})
// Initialize our express graphql server
}
And Finally in my resolvers I am decorating the resolver with #Service and injecting the graphql instance to the constructor:
import { Application } from '../../../declarations'
import { Graphql } from '../graphql.class'
import { Inject, Service } from 'typedi'
#Service()
#Resolver(Category)
export class CategoryResolver {
app: Application
constructor(#Inject('graphql') private readonly graphql: Graphql) {
this.app = this.graphql.app
}
// Queries and Mutations
}
This solved it to me, hope it comes with any help to you 😊
I followed the official guide to create viewModel instance and it works perfectly. However, when there is any viewModel in the #composable, Android Studio isn't able to render the preview and with the error code ViewModels creation is not supported in Preview. Anyone got any solution?
P.S. using compose version 1.0.0-alpha06
You could use an approach that looks like this which will show up in the recommended video bellow:
#Composable
fun TestView(
action: MainActions,
viewModel: OnboardViewModel = getViewModel()
) {
TestUI(onClick = viewModel.clickMethod())
}
#Composable
fun TestUI(onClick: () -> Unit) {}
#Preview
#Composable
fun TestUIPreview() {
MaterialTheme() {
TestUI(onClick = {})
}
}
There is a recommendation from google in this video at the selected time: https://youtu.be/0z_dwBGQQWQ?t=573
I had exactly the same problem.
The solution was: Extend the ViewModel with an interface
ComposeView:
#Composable
fun MyScreen(myVm: IMyViewModel = MyViewModel()) {
Text(text = myVm.getTextA())
}
#Preview()
#Composable
fun MyScreenPreview() {
MyScreen(myVm = MyViewModelPreview())
}
ViewModel:
abstract class IMyViewModel : ViewModel(){
abstract val dynamicValue: StateFlow<String>
abstract fun getTextA() : String
}
class MyViewModel : IMyViewModel() {
private val _dynamicValue: MutableStateFlow<String> = MutableStateFlow("")
override val dynamicValue: StateFlow<String> = _dynamicValue
init {
}
override fun getTextA(): String {
return "Details: ${EntityDb.getAllEntities().lastOrNull()?.details}"
}
}
class MyViewModelPreview(override val dynamicValue: StateFlow<String> = MutableStateFlow("no data")) : IMyViewModel() {
override fun getTextA(): String {
return ""
}
}
The #Preview annotated composable class should be agnostic from viewmodel.
i use this solution adding an extra class with models as input
#Composable
fun TabScreen(
viewModel: DetailsViewModel = viewModel()
) {
val details = viewModel.details.observeAsState()
Tabs(
details.value
)
}
#Composable
fun Tabs(
details: Details?
) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
}
#Composable
#Preview(showBackground = true)
fun TabsPreview(
#PreviewParameter(DetailsPreview::class)
details: Details?
) {
AppTheme {
Tabs(details)
}
}
You could use interfaces and hilt.
interface IMyViewModel {
fun getTextA() : String
}
#HiltViewModel
class MyViewModel() : ViewModel(), IMyViewModel {
fun getTextA() : String {
//do some cool stuff
}
}
class MyViewModelPreview() : IMyViewModel {
fun getTextA() : String {
//do some mock stuff
}
}
#Composable
fun MyScreen(myVm = hiltViewModel<MyViewModel>()) {
Text(text = myVm.getTextA())
}
#Preview()
#Composable
fun MyScreenPreview() {
MyScreen(myVm = MyViewModelPreview())
}
In this point MyViewModel is an implementation of IMyViewModel annotated with #HiltViewModel and hiltViewModel make all the required wired for you, In preview you could use any other simple mock implementation.
If you need to provide some dependency to your view model use injected constructor with dagger injection(already supported by hilt). Obviously this dependency should be paced on your actual viewmodel and your preview implementations need to be just a wrapper class with no other dependency since they function is just satisfy arguments
#HiltViewModel
class MyViewModel #Inject constructor(
private val myDependencyRepositoryOne: MyDependencyRepositoryOne,
private val myDependencyRepositoryTwo: MyDependencyRepositoryTwo)
: ViewModel(), IMyViewModel {
fun getTextA() : String {
//do some cool stuff
}
}
Here is another useful resource related to viewmodel injection in compose
what I am using:
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyMVIApp1Theme {
val myViewModel = remember { AppViewModel() }
Greeting(viewModel = myViewModel,"Android")
}
}