Chapters

Hide chapters

Android Fundamentals by Tutorials

First Edition · Android 14 · Kotlin 1.9 · Android Studio Hedgehog (2023.1.1)

7. Advanced Architecture
Written by Fuad Kamal

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the last chapter, you improved the chat app by implementing ViewModel and Flows and making some UI improvements. In this chapter, you’ll learn about another important architecture pattern in mobile development: the repository pattern. You’ll also learn how to use a revolutionary new P2P mesh technology via the Ditto SDK to finish the Kodeco Chat app and enable communication between devices without cloud service.

Ditto SDK

Now, you’re able to add real chat messages in your app. But what’s the use of a chat app if only one device or person can chat? That’s only a conversation if you enjoy talking to yourself!

So you need a way for devices or people to chat with each other. This is where the Ditto SDK is handy. Ditto is a cross-platform P2P SDK that lets two devices communicate using any type of wireless transport — Bluetooth, Wi-Fi, etc. — without an internet connection or cloud back end.

Registering Your App With Ditto

You’ll need to create a developer account with Ditto and build a personal app on its website. This gives you access to the authentication keys needed for the SDK. In a web browser, navigate to Ditto.live and click the Get Started button. Then, follow the steps to create an account. The simplest way is to sign up with your existing Google account. Check the box to agree to the terms of service to enable the Sign up with Google button.

Setting Up Ditto SDK in Your App

You need to add the Gradle dependency for Ditto in your app, so open the starter project of this chapter. Open libs.versions.toml and, in the [versions] section, add:

ditto = "4.5.0"
ditto = { module = "live.ditto:ditto", version.ref = "ditto" }
implementation(libs.ditto)
private fun checkPermissions() {
  val missing = DittoSyncPermissions(this).missingPermissions()
  if (missing.isNotEmpty()) {
    this.requestPermissions(missing, 0)
  }
}
import live.ditto.transports.DittoSyncPermissions

val keysPropertiesFile: File = rootProject.file("keys.properties")
val keysProperties = Properties()
keysProperties.load(FileInputStream(keysPropertiesFile))
DITTO_APP_ID = "replace with your app ID"
DITTO_TOKEN = "replace with your token"
debug {
  buildConfigField("String", "DITTO_APP_ID", keysProperties["DITTO_APP_ID"] as String)
  buildConfigField("String", "DITTO_TOKEN", keysProperties["DITTO_TOKEN"] as String)
}
buildConfig = true
private fun setupDitto() {
  val androidDependencies = DefaultAndroidDittoDependencies(applicationContext)
  DittoLogger.minimumLogLevel = DittoLogLevel.DEBUG
  ditto = Ditto(
    androidDependencies,
      DittoIdentity.OnlinePlayground(
        androidDependencies,
        appId = BuildConfig.DITTO_APP_ID,
        token = BuildConfig.DITTO_TOKEN
      )
  )
  ditto.startSync()
}
import com.kodeco.chat.DittoHandler.Companion.ditto
import live.ditto.Ditto
import live.ditto.DittoIdentity
import live.ditto.DittoLogLevel
import live.ditto.DittoLogger
import live.ditto.android.DefaultAndroidDittoDependencies

Repository Pattern

Sometimes, you want to store and fetch data from a local database in your app. Other times, you might want to fetch data from the internet or a cloud service using a networking API. And sometimes, you want to fetch data from the network. But if the network isn’t available, you’ll want to fetch the same data from a local cache you created the last time the network was available. In other words, you might want to combine both network and local data.

private fun getAllUsersFromDitto() {
  ditto.let { ditto: Ditto ->
    // 1
    usersCollection = ditto.store.collection(usersKey)
    // 2
    usersSubscription = usersCollection.findAll().subscribe()
    usersLiveQuery = usersCollection.findAll().observeLocal { docs, _ ->
      this.usersDocs = docs
      // 3
      allUsers.value = docs.map { User(it) }
    }
  }
}
class MainViewModel : ViewModel() {
  // 1
  private val userId = UUID.randomUUID().toString()
  var currentUserId = MutableStateFlow(userId)
  private var firstName: String = ""
  private var lastName: String = ""
  // 2
  private val repository = RepositoryImpl.getInstance()
  private val emptyChatRoom = ChatRoom(
    id = "public",
    name = "Android Apprentice",
    createdOn = Clock.System.now(),
    messagesCollectionId = DEFAULT_PUBLIC_ROOM_MESSAGES_COLLECTION_ID,
    isPrivate = false,
    collectionID = "public",
    createdBy = "Kodeco User"
  )

  private val _currentChatRoom = MutableStateFlow(emptyChatRoom)
  val currentRoom = _currentChatRoom.asStateFlow()

  // 3
  val roomMessagesWithUsersFlow: Flow<List<MessageUiModel>> = combine(
    repository.getAllUsers(),
    repository.getAllMessagesForRoom(currentRoom.value)
  ) { users: List<User>, messages:List<Message> ->

    messages.map {
      MessageUiModel.invoke(
        message = it,
        users = users
      )
    }
  }
  // 4
  init {
      // user initialization - we use the device name for the user's name
      val firstName = "My"
      val lastName = android.os.Build.MODEL
      updateUserInfo(firstName, lastName)
  }

  fun updateUserInfo(firstName: String = this.firstName, lastName: String = this.lastName) {
    viewModelScope.launch {
      repository.saveCurrentUser(userId, firstName, lastName)
    }
  }
  // 5
  fun onCreateNewMessageClick(messageText: String, photoUri: Uri?, attachmentToken: DittoAttachmentToken?) {
    val currentMoment: Instant = Clock.System.now()
    val message = Message(
      UUID.randomUUID().toString(),
      currentMoment,
      currentRoom.value.id,
      messageText,
      userId,
      attachmentToken,
      photoUri
    )

    if (message.photoUri == null) {
      viewModelScope.launch(Dispatchers.Default) {
        repository.createMessageForRoom(userId, message, currentRoom.value, null)
      }
    }
  }
}
import com.kodeco.chat.data.repository.RepositoryImpl
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import live.ditto.DittoAttachmentToken
val messagesWithUsers: List<MessageUiModel> by viewModel
  .roomMessagesWithUsersFlow
  .collectAsStateWithLifecycle(initialValue = emptyList())
val currentUiState =
  ConversationUiState(
    channelName = "Android Apprentice",
    initialMessages = messagesWithUsers.asReversed(),
    viewModel = viewModel
)
class ConversationUiState(
  val channelName: String,
  initialMessages: List<MessageUiModel>,
  val viewModel: MainViewModel
) {
  private val _messages: MutableList<MessageUiModel> = initialMessages.toMutableStateList()

  val messages: List<MessageUiModel> = _messages

  //author ID is set to the user ID - it's used to tell if the message is sent from this user (self) when rendering the UI
  val authorId: MutableStateFlow<String> = viewModel.currentUserId

  fun addMessage(msg: String, photoUri: Uri?) {
    viewModel.onCreateNewMessageClick(msg, photoUri, null)
  }
}

@Immutable
data class Message(
  val _id: String = UUID.randomUUID().toString(),
  val createdOn: Instant? = Clock.System.now(),
  val roomId: String = "public", // "public" is the roomID for the default public chat room
  val text: String = "test",
  val userId: String = UUID.randomUUID().toString(),
  val attachmentToken: DittoAttachmentToken?,
  val photoUri: Uri? = null,
  val authorImage: Int = if (userId == "me") R.drawable.profile_photo_android_developer else R.drawable.someone_else
){
  constructor(document: DittoDocument) : this(
    document[dbIdKey].stringValue,
    document[createdOnKey].stringValue.toInstant(),
    document[roomIdKey].stringValue,
    document[textKey].stringValue,
    document[userIdKey].stringValue,
    document[thumbnailKey].attachmentToken
  )
}
import com.kodeco.chat.data.createdOnKey
import com.kodeco.chat.data.dbIdKey
import com.kodeco.chat.data.model.toInstant
import com.kodeco.chat.data.roomIdKey
import com.kodeco.chat.data.textKey
import com.kodeco.chat.data.thumbnailKey
import com.kodeco.chat.data.userIdKey
import live.ditto.DittoAttachmentToken
import live.ditto.DittoDocument
constructor(document: DittoDocument) : this(
  document[dbIdKey].stringValue,
  document[nameKey].stringValue,
  document[createdOnKey].stringValue.toInstant(),
  document[messagesIdKey].stringValue,
  document[isPrivateKey].booleanValue,
  document[collectionIdKey].stringValue,
  document[createdByKey].stringValue,
)
import com.kodeco.chat.data.collectionIdKey
import com.kodeco.chat.data.createdByKey
import com.kodeco.chat.data.createdOnKey
import com.kodeco.chat.data.dbIdKey
import com.kodeco.chat.data.isPrivateKey
import com.kodeco.chat.data.messagesIdKey
import com.kodeco.chat.data.nameKey
import live.ditto.DittoDocument
constructor(document: DittoDocument) : this(
  document[dbIdKey].stringValue,
  document[firstNameKey].stringValue,
  document[lastNameKey].stringValue
)
import com.kodeco.chat.data.dbIdKey
import com.kodeco.chat.data.firstNameKey
import com.kodeco.chat.data.lastNameKey
import live.ditto.DittoDocument

Key Points

Well done! In this chapter, you’ve covered so much ground and created a real-world, P2P Android chat app! To recap, you learned:

Where to Go From Here?

For a more in-depth look at app architecture and design patterns, check out the book Advanced Android App Architecture.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now