I am trying to learn compose and retrofit and for that I am developing a very easy app, fetching jokes from a public API and showing them in a lazy list. But it is not working and I am not able to see any jokes. I am new to Kotlin and Jetpack compose. Please help me debug this.
I have a joke class
data class Joke(
val id: Int,.
val punchline: String,
val setup: String,
val type: String
)
This is the API I am GETing from:
https://official-joke-api.appspot.com/jokes/:id
This is the response:
{"type":"general","setup":"What did the fish say when it hit the wall?","punchline":"Dam.","id":1}
This is the retrofit api service:
const val BASE_URL = "https://official-joke-api.appspot.com/"
interface JokeRepository {
#GET("jokes/{id}")
suspend fun getJoke(#Path("id") id: String ) : Joke
companion object {
var apiService: JokeRepository? = null
fun getInstance(): JokeRepository {
if (apiService == null) {
apiService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build().create(JokeRepository::class.java)
}
return apiService!!
}
}
}
This is the Jokes view model:
class JokeViewModel : ViewModel() {
private val _jokeList = mutableListOf<Joke>()
var errorMessage by mutableStateOf("")
val jokeList: List<Joke> get() = _jokeList
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
_jokeList.clear()
// for(i in 1..100) {
// var jokeWithId = apiService.getJoke(i.toString())
// _jokeList.add(jokeWithId)
// Log.d("DEBUGGG", jokeWithId.setup)
// }
var joke = apiService.getJoke("1")
_jokeList.add(joke)
}
catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
This is the Main Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val jokeViewModel = JokeViewModel()
super.onCreate(savedInstanceState)
setContent {
HasyamTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
JokeView(jvm = jokeViewModel)
}
}
}
}
}
This is the Joke Component and view
#Composable
fun JokeView(jvm: JokeViewModel) {
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn() {
items(jvm.jokeList) {
joke -> JokeComponent(joke)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun JokeComponent(joke: Joke) {
var opened by remember { mutableStateOf(false)}
Column(
modifier = Modifier.padding(15.dp)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { },
elevation = CardDefaults.cardElevation(
defaultElevation = 5.dp
),
onClick = { opened = !opened}
) {
Text(modifier = Modifier.padding(15.dp), text = joke.setup)
}
if (opened) {
Text(modifier = Modifier.padding(15.dp), text = joke.punchline)
}
}
}
Thank you so much
The issue here is that you are not using stateFlow. The screen is not recomposed so your LazyColumn is not recreated with the updated values.
ViewModel
class JokeViewModel : ViewModel() {
var errorMessage by mutableStateOf("")
private val _jokes = MutableStateFlow(emptyList<Joke>())
val jokes = _jokes.asStateFlow()
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
var jokes = apiService.getJoke("1")
_jokes.update { jokes }
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
Joke View
#Composable
fun JokeView(jvm: JokeViewModel) {
val jokes by jvm.jokes.collectAsState()
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn {
items(jokes) {
joke -> JokeComponent(joke)
}
}
}
You should read the following documentation about states : https://developer.android.com/jetpack/compose/state
Related
I'm trying to save access token data to jetpack DataStore from http response with Retrofit2.
But function of DataStore that save data is asynchronous task so this function execute before token data arrive and save empty data.
this is my UI Code:
#Composable
fun LoginBtn(
navigateToHome: () -> Unit,
authenticate: () -> Unit,
loginScreenViewModel: LoginScreenViewModel
){
val context = LocalContext.current
val scope = rememberCoroutineScope()
val tokenStore = TokenStore(context)
Button(
onClick = {
scope.launch {
val token = loginScreenViewModel.requestLogin()
tokenStore.saveAccessToken(token.access_token)
tokenStore.saveRefreshToken(token.refresh_token)
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Carrot
),
shape = MaterialTheme.shapes.extraSmall,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
enabled = (loginScreenViewModel.email.value != "" && loginScreenViewModel.password.value != "")
) {
Text(text = "login", fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
}
this is loginViewModel that call http request:
class LoginScreenViewModel(): ViewModel() {
private val _email = mutableStateOf("")
val email = _email
private val _password = mutableStateOf("")
val password = _password
fun setEmail(text: String) {
_email.value = text
}
fun setPassword(text: String) {
_password.value = text
}
fun requestLogin(): Token {
var token = Token("", "")
apiService.login(username = email.value, password = password.value)
.enqueue(object : Callback<LogInResponse> {
override fun onResponse(
call: Call<LogInResponse>,
response: Response<LogInResponse>
) {
Log.i("LOGIN RESPONSE", "access_token : ${response.body()?.access_token}")
token = Token(
access_token = response.body()?.access_token!!,
refresh_token = response.body()?.refresh_token!!
)
}
override fun onFailure(call: Call<LogInResponse>, t: Throwable) {
t.printStackTrace()
}
})
return token
}
}
this is DataStore code:
class TokenStore(
private val context: Context,
) {
companion object {
private val Context.datastore: DataStore<Preferences> by preferencesDataStore("token")
val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token")
val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
}
val getAccessToken: Flow<String> = context.datastore.data
.map { preference ->
preference[ACCESS_TOKEN_KEY] ?: ""
}
suspend fun saveAccessToken(access_token: String){
context.datastore.edit { preference ->
preference[ACCESS_TOKEN_KEY] = access_token
}
}
val getRefreshToken: Flow<String?> = context.datastore.data
.map { preference ->
preference[REFRESH_TOKEN_KEY] ?: ""
}
suspend fun saveRefreshToken(refresh_token: String){
context.datastore.edit { preference ->
preference[REFRESH_TOKEN_KEY] = refresh_token
}
}
}
I have tried by my self, but im getting for close and no error message detected in my ANDROID STUDIO IDE, please help me to solve this. i really appreciate who help me for this. thank you. sorry i am not experience in android dev
MyDataStoreRepoInterface
interface DataStoreRepo {
suspend fun putString(key:String,value:String)
suspend fun putBoolean(key:String,value:Boolean)
suspend fun getString(key: String):String?
suspend fun clearPReferences(key: String)
}
**MyDataStoreRepoImpl**
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name=DATASTORE_NAME)
class DataStoreRepoImpl #Inject constructor(
private val context: Context
): DataStoreRepo {
override suspend fun putString(key: String, value: String) {
val prefereneKay = stringPreferencesKey(key)
context.dataStore.edit {
it[prefereneKay] = value
}
}
override suspend fun putBoolean(key: String, value: Boolean) {
val prefernceKey = booleanPreferencesKey(key)
context.dataStore.edit {
it[prefernceKey] = value
}
}
override suspend fun getString(key: String): String? {
return try {
val preferenceKey = stringPreferencesKey(key)
val preference = context.dataStore.data.first()
preference[preferenceKey]
}catch (e:Exception){
e.printStackTrace()
null
}
}
override suspend fun clearPReferences(key: String) {
val preferenceKey = stringPreferencesKey(key)
context.dataStore.edit {
if (it.contains(preferenceKey)){
it.remove(preferenceKey)
}
}
}
}
My Dependency Module
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Provides
#Singleton
fun provideEpodApi(): ApiClient {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiClient::class.java)
}
#Provides
#Singleton
fun providesDatstoreRepo(
#ApplicationContext context: Context
): DataStoreRepo = DataStoreRepoImpl(context)
#Provides
#Singleton
fun provideCoinRepository(api: ApiClient): LoginRepository {
return LoginRepositoryImpl(api)
}
}
LoginViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val useCase: LoginUseCase,
private val dataStoreRepository:DataStoreRepo
) : ViewModel() {
var state by mutableStateOf(LoginState())
val email = mutableStateOf("")
val password = mutableStateOf("")
fun login(username: String, password: String) = viewModelScope.launch {
state = state.copy(isLoading = true)
val loginDeferred =
async { useCase.execute(loginRequest = LoginRequest(username, password)) }
when (val result = loginDeferred.await()) {
is Resource.Success -> {
state = state.copy(
login = result.data,
error = null,
isLoading = false
)
}
is Resource.Error ->
state = state.copy(
isLoading = false,
error = result.message,
login = null
)
else -> Unit
}
}
fun storeUserName(value:String) = runBlocking {
dataStoreRepository.putString(USER_NAME,value)
}
fun getUserName():String = runBlocking {
dataStoreRepository.getString(USER_NAME)!!
}
fun clearPreferences(key:String) = runBlocking {
dataStoreRepository.clearPReferences(key)
}
LoginScreen
Button(onClick = {
viewModel.login(username, password)
viewModel.storeUserName(username)
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
enabled = isFormValid,
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Blue500)
) {
Text(
text = "Login",
fontFamily = Poppins,
fontWeight = FontWeight.Bold,
color = Color.White,
fontSize = 16.sp
)
}
SplashScreen
#Composable
fun SplashScreen(
navController: NavController,
viwmodel:LoginViewModel = hiltViewModel()
) {
var startAnimation by remember { mutableStateOf(false) }
val scaleAnimation by animateFloatAsState(
targetValue = if (startAnimation) 0.7f else 0f,
animationSpec = tween(
durationMillis = 800,
easing = {
OvershootInterpolator(4f).getInterpolation(it)
})
)
val loginState = viwmodel.getUserName()
when(loginState){
"USER_NAME" ->{
navController.popBackStack()
navController.navigate("login_screen")
}
else ->{
navController.popBackStack()
navController.navigate("onboarding_screen")
}
LaunchedEffect(key1 = loginState ){
startAnimation = true
delay(3000L)
}
}
Splash(scaleAnimation)
}
In my application i'm using Navigation Compose. After implementing it, my viewModel's function getExercises() won't fetch the data. Why is that?
MainActivity:
class MainActivity : ComponentActivity() {
lateinit var navController: NavHostController
lateinit var viewModel: ExerciseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GymEncV2Theme {
// A surface container using the 'background' color from the theme
navController = rememberNavController()
viewModel = ExerciseViewModel()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
SetupNavGraph(navController = navController, viewModel = viewModel)
}
}
}
}
}
NavGraph:
#Composable
fun SetupNavGraph(navController: NavHostController, viewModel: ExerciseViewModel) {
NavHost(navController = navController, startDestination = Screen.Home.route) {
composable(
route = Screen.Home.route
) {
HomeScreen(navController = navController)
}
composable(
route = Screen.SampleExercise.route,
arguments = listOf(navArgument(SAMPLE_EXERCISE_SCREEN_KEY) {
type = NavType.StringType
})
) {
SampleExerciseScreen(navController = navController, viewModel = viewModel, muscleGroup = it.arguments?.getString(
SAMPLE_EXERCISE_SCREEN_KEY).toString())
}
}
}
ExerciseApi built with Retrofit:
interface ExerciseApi {
#GET("GymEnc_v2.JSON")
suspend fun getExercises() : List<Exercise>
companion object {
private var exerciseApi: ExerciseApi? = null
fun getInstance() : ExerciseApi {
if (exerciseApi == null) {
exerciseApi = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ExerciseApi::class.java)
}
return exerciseApi!!
}
}
}
Repository Implementation:
class ExerciseRepositoryImpl : ExerciseRepository {
private val api = ExerciseApi.getInstance()
override suspend fun getExercises(): List<Exercise> {
return api.getExercises()
}
}
And finally the viewModel:
class ExerciseViewModel : ViewModel() {
private val repository = ExerciseRepositoryImpl()
var exerciseListResponse : List<Exercise> by mutableStateOf(arrayListOf())
var errorMessage: String by mutableStateOf("")
fun getExercises(): List<Exercise>{
viewModelScope.launch {
try {
val exerciseList = repository.getExercises()
exerciseListResponse = exerciseList
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
return exerciseListResponse
}
}
SampleExercisesScreen:
#Composable
fun SampleExerciseScreen(navController: NavController, muscleGroup: String, viewModel: ExerciseViewModel) {
val exerciseList = viewModel.getExercises().filter { it.muscle == muscleGroup }
Surface(modifier = Modifier.fillMaxSize(),
color = AppColors.mBackground) {
Column {
MyTopBar(navController = navController)
Text(text = muscleGroup,
style = MaterialTheme.typography.h3,
color = AppColors.mDetails,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center)
UserExercisesButton()
}
}
}
After using viewModel.getExercises() in SampleExercisesScreen, they simply won't load, leaving me with an empty list.
My some view that starts from an activity shows an alert dialog when pressing the back button using the BackHandler.
#Composable
fun PostEditContent(
title: String,
uiState: BasePostEditUiState
) {
var enabledBackHandler by remember { mutableStateOf(true) }
var enabledAlertDialog by remember { mutableStateOf(false) }
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val navigateToBack = { onBackPressedDispatcher?.onBackPressed() }
val forceNavigateToBack = {
coroutineScope.launch {
enabledBackHandler = false
awaitFrame()
navigateToBack()
}
}
if (enabledAlertDialog) {
PostEditAlertDialog(
onDismissRequest = { enabledAlertDialog = false },
onOkClick = { forceNavigateToBack() }
)
}
BackHandler(enabledBackHandler) {
enabledAlertDialog = true
}
...
It's working fine. But in the testing, it sometimes failed because the awaitFrame() is not working properly.
#Suppress("TestFunctionName")
#Composable
private fun PostEditScreenWithHomeBackStack(uiState: PostEditUiState) {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable(route = "home") {
Text(HOME_STACK_TEXT)
}
composable(route = "edit") {
PostEditScreen(uiState = uiState)
}
}
SideEffect {
navController.navigate("edit")
}
}
...
#Test
fun navigateToUp_WhenSuccessSaving() {
composeTestRule.apply {
setContent {
PostEditScreenWithHomeBackStack(emptyUiState.copy(title = "a", content = "a"))
}
// The save button calls the `forceNavigateToBack()` that in the upper code block
onNodeWithContentDescription("save").performClick()
onNodeWithText(HOME_STACK_TEXT).assertIsDisplayed()
}
}
Is there a better way to show the alert dialog or fix the flaky test? Thanks.
I want to parse XML with XmlPullParser from a URL and then show the results on the UI (at the moment learning to work with Compose).
I got the basics of XmlPullParser but i cant understand how to access the URL in another thread and getting the values back.
I am struggling to understand coroutines and failing.
MainActivity
import ....
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
MyScreenContent(test())
}
}
}
//I want this to run async and return the parsed results
//................................................................................
fun test():List<String> {
val names = mutableListOf<String>()
val user = "User"
val url = URL("https://www.boardgamegeek.com/xmlapi/collection/$user?own=1")
val http: HttpURLConnection = url.openConnection() as HttpURLConnection
http.doInput = true
http.connect()
Log.d("bgg", "first" )
var games: List<Game>? = null
try {
val parser = XmlPullParserHandler()
val istream = http.inputStream
games = parser.parse(istream)
} catch (e: IOException) {
e.printStackTrace()
}
games?.forEach { it ->
it.name?.let { it1 -> names.add(it1) }
Log.d("bgg", "game" + it.name)
}
Log.d("bgg", "second" + names.toString())
return names
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
SecontComposeTutorialTheme {
Surface(color = Color.Yellow) {
content()
}
}
}
#Composable
fun Greeting(name: String) {
Text(text = name, modifier = Modifier.padding(all = 24.dp))
}
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name ->
Greeting(name = name)
Divider(color = Color.Black)
}
}
}
#Composable
fun MyScreenContent(names: List<String>) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
NameList(names = names, Modifier.weight(1f))
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = {
updateCount(count + 1)
getBggGame()
},
colors = ButtonDefaults.buttonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text(text = "I've beek clicked on $count times")
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyApp {
// MyScreenContent()
}
}
}
XmlPullParserHandler (working)
import android.util.Log
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
class XmlPullParserHandler {
private val collection = ArrayList<Game>()
private var game: Game? = null
private var text: String? = null
fun parse(inputStream: InputStream): List<Game> {
try {
val factory = XmlPullParserFactory.newInstance()
factory.isNamespaceAware = true
val parser = factory.newPullParser()
parser.setInput(inputStream, null)
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
val tagname = parser.name
when (eventType) {
XmlPullParser.START_TAG -> if (tagname.equals("item", ignoreCase = true)) {
// create a new instance of game
game = Game()
}
XmlPullParser.TEXT -> text = parser.text
XmlPullParser.END_TAG -> if (tagname.equals("item", ignoreCase = true)) {
// add game object to list
game?.let { collection.add(it) }
} else if (tagname.equals("id", ignoreCase = true)) {
game!!.id = Integer.parseInt(text)
} else if (tagname.equals("name", ignoreCase = true)) {
game!!.name = text
}
else -> {
}
}
eventType = parser.next()
}
} catch (e: XmlPullParserException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return collection
}
}
UPDATE.............................................
If i use the following, i get "android.os.NetworkOnMainThreadException"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
setContent {
MyApp {
MyScreenContent(test())
}
}
}
}
fun test():List<String> {
val names = mutableListOf<String>()
val user = "Uset"
val url = URL("https://www.boardgamegeek.com/xmlapi/collection/$user?own=1")
val http: HttpURLConnection = url.openConnection() as HttpURLConnection
http.doInput = true
http.connect()
Log.d("bgg", "first" )
var games: List<Game>? = null
try {
val parser = XmlPullParserHandler()
val istream = http.inputStream
games = parser.parse(istream)
} catch (e: IOException) {
e.printStackTrace()
}
games?.forEach { it ->
it.name?.let { it1 -> names.add(it1) }
Log.d("bgg", "game" + it.name)
}
Log.d("bgg", "second" + names.toString())
return names
}
....
If i mark the test function with suspend it tells me that its reduntand, and that i cant call it from an non suspend or coroutine function
You have to change to suspend and call it inside a coroutine scope
suspend fun test():List<String> {...
setContent {
MyApp {
lifeCycleScope.launch {
val list = test()
MyScreenContent(list)
}
}
}
You are using Jetpack compose so I'm not sure if you can use the lifeCycleScope there but try to move it around:
lifeCycleScope.launch {
setContent {...
}
Otherwise the problem could be a design error.
You can change test to run using coroutine io thread as
fun test()= lifecycleScope.launch(Dispatchers.IO) {
val names = mutableListOf<String>()
val user = "User"
val url = URL("https://www.boardgamegeek.com/xmlapi/collection/$user?own=1")
val http: HttpURLConnection = url.openConnection() as HttpURLConnection
http.doInput = true
http.connect()
Log.d("bgg", "first" )
var games: List<Game>? = null
try {
val parser = XmlPullParserHandler()
val istream = http.inputStream
games = parser.parse(istream)
} catch (e: IOException) {
e.printStackTrace()
}
games?.forEach { it ->
it.name?.let { it1 -> names.add(it1) }
Log.d("bgg", "game" + it.name)
}
withContext(Dispatchers.Main){
// Update UI with names
}
}