I've got some configuration in a json file stored at the asset folder of the app.
I need this configuration during my complete app so I thought a CompositionLocalProvider may be a good choice.
But now I realize I need the context for parsing that json file and that doesn't seem to be possible.
Might there be another way to achieve the goal I'm looking for?
This is my implementation so far:
val LocalAppConfiguration = compositionLocalOf {
Configuration.init(LocalContext.current) // <-- not possible
}
Where my Configuration is like:
object Configuration {
lateinit var branding: Branding
fun init(context: Context) {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
}
}
I would be very grateful if someone could help me further
compositionLocalOf is not a Composable function. So, LocalContext.current can not be used.
I believe you can achieve the similar goal if you move the initialization of branding outside of the default factory. You'd then do the initialization inside your actual composable where you have access to Context.
Here's a sample code to explain what I'm talking about.
val LocalAppConfiguration = compositionLocalOf {
Configuration
}
#Composable
fun RootApp(
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit
) {
val brandedConfiguration = Configuration.init(LocalContext.current)
MaterialTheme {
CompositionLocalProvider(LocalAppConfiguration provides brandedConfiguration) {
//your app screen composable here.
}
}
}
Note that you will also have to modify your init method slightly.
object Configuration {
lateinit var branding: Branding
fun init(context: Context) : Configuration {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
return this
}
}
Related
When I change ViewModel variable, Composable Doesn't Update the View and I'm not sure what to do.
This is my MainActivity:
class MainActivity : ComponentActivity() {
companion object {
val TAG: String = MainActivity::class.java.simpleName
}
private val auth by lazy {
Firebase.auth
}
var isAuthorised: MutableState<Boolean> = mutableStateOf(FirebaseAuth.getInstance().currentUser != null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val user = FirebaseAuth.getInstance().currentUser
setContent {
HeroTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
if (user != null) {
Menu(user)
} else {
AuthTools(auth, isAuthorised)
}
}
}
}
}
}
I have a a View Model:
class ProfileViewModel: ViewModel() {
val firestore = FirebaseFirestore.getInstance()
var profile: Profile? = null
val user = Firebase.auth.currentUser
init {
fetchProfile()
}
fun fetchProfile() {
GlobalScope.async {
getProfile()
}
}
suspend fun getProfile() {
user?.let {
val docRef = firestore.collection("Profiles")
.document(user.uid)
return suspendCoroutine { continuation ->
docRef.get()
.addOnSuccessListener { document ->
if (document != null) {
this.profile = getProfileFromDoc(document)
}
}
.addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
}
}
}
}
And a Composable View upon user autentication:
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel()
Column(
modifier = Modifier
.background(color = Color.White)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Signed in!");
ProfileVModel.profile?.let {
Text(it.username);
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = {
FirebaseAuth.getInstance().signOut()
context.startActivity(Intent(context, MainActivity::class.java))
}) {
Text(
color = Color.Black,
text = "Sign out?",
modifier = Modifier.padding(all = 8.dp)
)
}
}
}
}
When my Firestore method returns, I update the profile var, and "expect" it to be updated in the composable, here:
ProfileVModel.profile?.let {
Text(it.username);
}
However, nothing is changing?
When I was adding firebase functions from inside composable, I could just do:
context.startActivity(Intent(context, MainActivity::class.java))
And it would update the view. However, I'm not quite sure how to do this from inside a ViewModel, since "context" is a Composable-specific feature?
I've tried to look up Live Data, but every tutorial is either too confusing or differs from my code. I'm coming from SwiftUI MVVM so when I update something in a ViewModel, any view that's using the value updates. It doesn't seem to be the case here, any help is appreciated.
Thank you.
Part 1: Obtaining a ViewModel correctly
On the marked line below you are setting your view model to a new ProfileViewModel instance on every recomposition of your Menu composable, which means your view model (and any state tracked by it) will reset on every recomposition. That prevents your view model to act as a view state holder.
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel() // <-- view model resets on every recomposition
// ...
}
You can fix this by always obtaining your ViewModels from the ViewModelStore. In that way the ViewModel will have the correct owner (correct lifecycle owner) and thus the correct lifecycle.
Compose has a helper for obtaining ViewModels with the viewModel() call.
This is how you would use the call in your code
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
// or this way, if you prefer
// val ProfileVModel = viewModel<ProfileViewModel>()
// ...
}
See also ViewModels in Compose that outlines the fundamentals related to ViewModels in Compose.
Note: if you are using a DI (dependency injection) library (such as Hilt, Koin...) then you would use the helpers provided by the DI library to obtain ViewModels.
Part 2: Avoid GlobalScope (unless you know exactly why you need it) and watch out for exceptions
As described in Avoid Global Scope you should avoid using GlobalScope whenever possible. Android ViewModels come with their own coroutine scope accessible through viewModelScope. You should also watch out for exceptions.
Example for your code
class ProfileViewModel: ViewModel() {
// ...
fun fetchProfile() {
// Use .launch instead of .async if you are not using
// the returned Deferred result anyway
viewModelScope.launch {
// handle exceptions
try {
getProfile()
} catch (error: Throwable) {
// TODO: Log the failed attempt and/or notify the user
}
}
}
// make it private, in most cases you want to expose
// non-suspending functions from VMs that then call other
// suspend factions inside the viewModelScope like fetchProfile does
private suspend fun getProfile() {
// ...
}
// ...
}
More coroutine best practices are covered in Best practices for coroutines in Android.
Part 3: Managing state in Compose
Compose tracks state through State<T>. If you want to manage state you can create MutableState<T> instances with mutableStateOf<T>(value: T), where the value parameter is the value you want to initialize the state with.
You could keep the state in your view model like this
// This VM now depends on androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class ProfileViewModel: ViewModel() {
var profile: Profile? by mutableStateOf(null)
private set
// ...
}
then every time you would change the profile variable, composables that use it in some way (i.e. read it) would recompose.
However, if you don't want your view model ProfileViewModel to depend on the Compose runtime then there are other options to track state changes while not depending on the Compose runtime. From the documentation section Compose and other libraries
Compose comes with extensions for Android's most popular stream-based
solutions. Each of these extensions is provided by a different
artifact:
Flow.collectAsState() doesn't require extra dependencies. (because it is part of kotlinx-coroutines-core)
LiveData.observeAsState() included in the androidx.compose.runtime:runtime-livedata:$composeVersion artifact.
Observable.subscribeAsState() included in the androidx.compose.runtime:runtime-rxjava2:$composeVersion or
> androidx.compose.runtime:runtime-rxjava3:$composeVersion artifact.
These artifacts register as a listener and represent the values as a
State. Whenever a new value is emitted, Compose recomposes those parts
of the UI where that state.value is used.
This means that you could also use a MutableStateFlow<T> to track changes inside the ViewModel and expose it outside your view model as a StateFlow<T>.
// This VM does not depend on androidx.compose.runtime.* anymore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class ProfileViewModel : ViewModel() {
private val _profileFlow = MutableStateFlow<Profile?>(null)
val profileFlow = _profileFlow.asStateFlow()
private suspend fun getProfile() {
_profileFlow.value = getProfileFromDoc(document)
}
}
And then use StateFlow<T>.collectAsState() inside your composable to get the State<T> that is needed by Compose.
A general Flow<T> can also be collected as State<T> with Flow<T : R>.collectAsState(initial: R), where the initial value has to be provided.
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
val profile by ProfileVModel.profileFlow.collectAsState()
Column(
// ...
) {
// ...
profile?.let {
Text(it.username);
}
// ...
}
}
To learn more about working with state in Compose see the documentation section on Managing State. This is fundamental information to be able to work with state in Compose and trigger recompositions efficiently. It also covers the fundamentals of state hoisting. If you prefer a coding tutorial here is the code lab for State in Jetpack Compose.
An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.
Profile in view model should be State<*>
private val _viewState: MutableState<Profile?> = mutableStateOf(null)
val viewState: State<Profile?> = _viewState
In composable
ProfileVModel.profile.value?.let {
Text(it.username);
}
I recommend using MutableStateFlow.
a simple sample is described in this Medium article :
https://farhan-tanvir.medium.com/stateflow-with-jetpack-compose-7d9c9711c286
Me and my colleague are having a debate as to where would be the right place to map our entity objects or remote dto objects to plain simple domain objects.
Our structure looks like this.
source(includes dao) > repo(includes source) > usecase(includes repo)
My colleague thinks that mapping to domain should be done inside the source so that the domain object could be passed on to the next layers as so
class SomeSourceImpl(private val dao: Dao) : SomeSource {
override fun get(): Observable<DomainModel> {
return dao.getResponse().map { it.mapToDomain() }
}
}
My colleagues argues that according to Uncle Bob this is due to the dependency rule.
This rule says that source code dependencies can only point inwards.
Nothing in an inner circle can know anything at all about something in
an outer circle. In particular, the name of something declared in an
outer circle must not be mentioned by the code in the an inner circle.
That includes, functions, classes. variables, or any other named
software entity.
I very much disagree with the approach of mapping to domain directly inside the source because then the repositories become anaemic and we are consequently adopting the anti-pattern of anaemic repositories being useless and all they do is to blindly propagating everything that comes from the source. (Now you may say that sources are also anaemic and we could simply remove them and include the dao object directly into the repo but this is out of the question in our case).
Instead I propose that sources would return the raw database entity (or remote entity if we are into rest calls) as it makes sense for a source to return the raw data for later processing. It's the job of the repo to get the result from the source then map it to domain and lastly propagate this domain object to use cases something like so.
class SomeRepoImpl(private val someSource: SomeSource) : SomeRepo {
override fun get(haId: String): Observable<DomainModel> {
return otherAssetSource.get().map { it.mapToDomain() }
}
I also came across some samples on github where they map to domain inside their repos rather than the sources
Here
Here
Here
Here is one for iOS too
What would be the strict rule in clean architecture principles regarding the place one can map an entity into a domain object?
Quoting the rule
source code dependencies can only point inwards
That would depend on the architecture I guess. Let me explain this with an example:
Architecture:
DOMAIN <- DATA <- PRESENTATION
Where:
DATA -> LOCAL
|
v
REMOTE
NOTE: DOMAIN represents the innermost circle and PRESENTATION represents the outmost circle.
Now DOMAIN is a pure Kotlin module and does not have any Android dependencies. Let's define a repository:
interface ProfileRepository {
fun getProfile(): Profile?
fun updateProfile(profile: Profile): Profile
}
We implement this in the DATA layer(which is an Android library):
class ProfileRepositoryImpl(
private val networkManager: NetworkManager,
private val remoteDataSource: ProfileRemoteDataSource,
private val localDataSource: ProfileLocalDataSource
): ProfileRepository {
override fun getProfile(): Profile? {
return if(networkManager.isNetworkAvailable) {
localDataSource.insert(remoteDataSource.get())
} else {
localDataSource.get()
}
}
override fun updateProfile(profile: Profile): Profile {
val updatedProfile = remoteDataSource.update(profile)
return localDataSource.insert(updatedProfile)
}
}
class ProfileRemoteDataSource(
private val api: ProfileApi,
private val mapper: Mapper<ProfileDto, Profile>
) {
fun get(): Profile {
return mapper.toModel(api.getProfile())
}
fun update(profile: Profile): Profile {
val dto = api.updateProfile(
mapper.fromModel(profile)
)
return mapper.toModel(dto)
}
}
class ProfileLocalDataSource(
private val dao: ProfileDao,
private val mapper: Mapper<ProfileEntity, Profile>
) {
fun insert(profile: Profile): Profile {
dao.insert(mapper.fromModel(profile))
return requireNotNull(get())
}
fun get(): Profile? {
return dao.get()?.let(mapper::toModel)
}
}
interface Mapper<T : Any, Model : Any> {
fun toModel(value: T): Model
fun fromModel(value: Model): T
}
The LOCAL module is an Android library independent of any dependencies and exposes the DAO and Entity objects:
interface ProfileDao {
fun insert(profile: ProfileEntity)
fun get(): ProfileEntity?
}
Similarly, for the REMOTE module:
interface ProfileApi {
fun get(): ProfileDto
fun update(profile: ProfileDto): ProfileDto
}
So, it doesn't make sense for me to have the Source classes return DTO and Entity objects. The repo class would look something like this:
class ProfileRepositoryImpl(
private val networkManager: NetworkManager,
private val remoteDataSource: ProfileRemoteDataSource,
private val remoteDataMapper: Mapper<ProfileDto, Profile>,
private val localDataSource: ProfileLocalDataSource,
private val localDataMapper: Mapper<ProfileEntity, Profile>
) : ProfileRepository {
override fun getProfile(): Profile? {
if (networkManager.isNetworkAvailable) {
val dto = remoteDataSource.get()
val profile = remoteDataMapper.toModel(dto)
val entity = localDataMapper.fromModel(profile)
localDataSource.insert(entity)
}
return localDataSource.get()?.let(localDataMapper::toModel)
}
override fun updateProfile(profile: Profile): Profile {
val request = remoteDataMapper.fromModel(profile)
val dto = remoteDataSource.update(request)
val updatedProfile = remoteDataMapper.toModel(dto)
val entity = localDataMapper.fromModel(updatedProfile)
localDataSource.insert(entity)
return localDataMapper.toModel(
requireNotNull(localDataSource.get())
)
}
}
In your example, you have taken only the GET operation into consideration. Here, for the UPDATE operation we need to map the DOMAIN object as well. So as we add more functionalities the Repo class would become very messy if the mapping of objects is done in the Repo class.
I believe it would depend on the overall architecture of the system.
I am learning the subject, but my approach is on Clean Architecture which I use in most projects.
The Domain Layer is the innermost layer, therefore, it does not depend on other layers. Thus, the first decision is to make mappers stay in the data and presentation layers, or whichever layers I use for the two purposes.
Then, I have an interface that defines how I define my mappers
interface EntityMapper<M : Model, ME : ModelEntity> {
fun mapToDomain(entity: ME): M
fun mapToEntity(model: M): ME
}
Then, I have classes, in the data layer which map from data models to domain model. An example is
class ItemEntityMapper #Inject constructor(
private val ownerEntityMapper: OwnerEntityMapper
) : EntityMapper<Item, ItemEntity> {
override fun mapToDomain(entity: ItemEntity) = Item(
id = entity.id,
name = entity.name,
fullName = entity.fullName,
description = entity.description,
url = entity.url,
stars = entity.stars,
owner = entity.ownerEntity?.let { ownerEntityMapper.mapToDomain(it) }
)
override fun mapToEntity(model: Item) = ItemEntity(
id = model.id,
name = model.name,
fullName = model.fullName,
description = model.description,
url = model.url,
stars = model.stars,
ownerEntity = model.owner?.let { ownerEntityMapper.mapToEntity(it) }
)
}
I prefer OOP classes over functional Kotlin to ease with DI
I'am using Compose now with passing parameters to functions, but I feel, those parameters are getting longer with each new data to be passed down the tree.
I am wondering if there is a way to pass values to other composables down the tree.
Other than using function arguments for Composables (the recommended way),
you can use CompositionLocal:
Avoid CompositionLocal for concepts that aren't thought as tree-scoped or sub-hierarchy scoped.
First, define variable of compositionLocalOf globally (e.g., in LocalCompositions.kt):
import androidx.compose.runtime.compositionLocalOf
data class Post(val title: String)
val LocalPost = compositionLocalOf { Post(title = "Ahmed") }
and then define it inside #Compose function to be used by other composables down the tree:
#Composable
fun MainView() {
...
val post = Post(title = "Shamil")
CompositionLocalProvider(LocalPost provides post) {
PostContentView()
}
...
}
Now, retrieve the value inside a #Composable:
// composable down the nodes tree
#Composable
fun PostContentView() {
...
val post = LocalPost.current
...
}
Observe variable changes
If you want to achieve the above solution but with watching changes to the variable and update the composable (node) accordingly, you can use MutableState variable.
Redefining the previous example:
LocalCompositions.kt:
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
data class Post(val title: MutableState<String> = mutableStateOf(""))
val LocalPost = compositionLocalOf { Post(title = mutableStateOf("Ahmed")) }
and then define it inside #Compose function to be used by other composables down the tree:
#Composable
fun MainView() {
...
val post = Post(title = remember { mutableStateOf("Shamil") })
CompositionLocalProvider(LocalPost provides post) {
PostContentView()
}
...
post.title.value = "Ali"
...
}
Now, retrieve the value inside a #Composable:
// composable down the nodes tree
#Composable
fun PostContentView() {
...
val post = LocalPost.current
// now this composable will be updated whenever post.title value is updated
if (post.title.value == "Jamal") {
...
}
...
}
So with the new alpha07 version, Android ditched the private val dataStore = context.createDataStore(name = "settings_pref"), however the new way they use datastore doesn't work for me.
Since upgrading from "androidx.datastore:datastore-core:1.0.0-alpha06" to alpha07, I can't seem to make my datastore syntax work without getting red-colored code (the error comes when i add context.dataStore.edit). Also downgrading back to alpha06, code that previously worked is now not working anymore (with createDataStore).
What I'm using is their example on the main page but going anywhere else they still haven't updated their examples besides this one.
#Singleton
class PreferencesManager #Inject constructor(#ApplicationContext context: Context) {
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}
suspend fun incrementCounter() {
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
}
If someone knows the problem (or my error), I would appreciate it.
This was throwing me too, but I figured it out (aka, guessed until it worked):
// Note: This is at the top level of the file, outside of any classes.
private val Context.dataStore by preferencesDataStore("user_preferences")
class UserPreferencesManager(context: Context) {
private val dataStore = context.dataStore
// ...
}
This is for a DataStore<Preferences>, but if you need a custom serializer, you can do the following (same parameters as the old method):
// Still top level!
private val Context.dataStore by dataStore(
fileName = "user_preferences",
serializer = MyCustomSerializer,
)
I had the same problem and found a mistake: Context should be as a member of class to use it in any method:
private val Context.dataStore by preferencesDataStore("preferences")
class Preferenses(val context: Context) { get, set, etc}
I'm trying to implement the new typed DataStore API in Java and I'm having some issues. All the documentation seems to be in Kotlin only and trying to create a new data store is not as straight forward from the Java side it seems.
Calling DataStoreFactoryKt.createDataStore() from Java requires me to provide all the arguments including the ones with default values in the Kotlin implementation. There doesnt seem to be any #JvmOverloads annotation for that function, resulting in my predicament.
fun <T> Context.createDataStore(
fileName: String,
serializer: Serializer<T>,
corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
migrations: List<DataMigration<T>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<T> =
DataStoreFactory.create(
produceFile = { File(this.filesDir, "datastore/$fileName") },
serializer = serializer,
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
)
What's the better way around this, if there is any? Or is the Data Store api simple designed to be used with Kotlin only? I have no idea how I would go about providing a CoroutineScope argument from Java.
After updating dataStore dependency to '1.0.0-alpha08' as below.
// DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha08"
You can have preferences implementation as follow:
private val Context.dataStore by preferencesDataStore("app_preferences")
After that if you like create some preference key:
private object Keys {
val HIDE_VISITED = booleanPreferencesKey("hide_visited")
}
other options can be stringPreferencesKey, intPreferencesKey, etc.
Saving value example:
context.dataStore.edit { prefs -> prefs[Keys.HIDE_VISITED] = hideVisited }
Reading saved value example:
val hideVisited = preferences[Keys.HIDE_VISITED] ?: false
You need to add to your Grade build file the dependency for DataStore preferences:
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha04"
and not the one for Types, that way you will be able to resolve the androidx.datastore.preferences.Context.createDataStore method that you are expecting:
public fun Context.createDataStore(
name: String,
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
migrations: List<DataMigration<Preferences>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
) {
File(this.filesDir, "datastore/$name.preferences_pb")
}
If you need to use proto dataStore from version 1.0.0-beta01, you can:
implementation("androidx.datastore:datastore-core:1.0.0-beta01")
initialize with Data Store Factory
val data: DataStore<SomeMessage> = DataStoreFactory.create(
serializer = SessionSerializer, // your Serializer
corruptionHandler = null,
migrations = emptyList(),
scope = CoroutineScope(Dispatchers.IO + Job())
And continue as before
data.updateData {
it.toBuilder().setAddress("address").build()
}
data.collect { ChargingSession ->
ChargingSession.address
}
This is only valid for dependency version 1.0.0-alpha08 and above
#Ercan approach is correct but requires context every time we need access to the dataStore.
Below is a better approach for the same.
private val Context._dataStore: DataStore<Preferences> by preferencesDataStore(APP_PREFERENCES)
private val dataStore : DataStore<Preferences> = context._dataStore
companion object {
const val APP_PREFERENCES = "app_preferences"
}
reference: https://issuetracker.google.com/issues/173726702