Skip to content

Feature/My Dev Bytes app #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: starter-code
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,53 @@
*
*/

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs"
apply plugin: 'kotlin-android-extensions'
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'androidx.navigation.safeargs.kotlin'
}

android {
compileSdkVersion 30
defaultConfig {
applicationId "com.example.android.devbyteviewer"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true

javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true"
]
}
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
dataBinding true
}

packagingOptions {
exclude 'DebugProbesKt.bin'
exclude 'META-INF/atomicfu.kotlin_module'
}
}

dependencies {
Expand All @@ -51,15 +70,15 @@ dependencies {

// Support libraries
implementation "androidx.appcompat:appcompat:$version_appcompat"
implementation "androidx.fragment:fragment:$version_fragment"
implementation "androidx.fragment:fragment-ktx:$version_fragment"
implementation "androidx.constraintlayout:constraintlayout:$version_constraint_layout"

// Android KTX
implementation "androidx.core:core-ktx:$version_core"

// Navigation
implementation "android.arch.navigation:navigation-fragment-ktx:$version_navigation"
implementation "android.arch.navigation:navigation-ui-ktx:$version_navigation"
implementation "androidx.navigation:navigation-fragment-ktx:$version_navigation"
implementation "androidx.navigation:navigation-ui-ktx:$version_navigation"

// Coroutines for getting off the UI thread
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_kotlin_coroutines"
Expand All @@ -68,7 +87,6 @@ dependencies {
// Retrofit for networking
implementation "com.squareup.retrofit2:retrofit:$version_retrofit"
implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$version_retrofit_coroutines_adapter"

// Moshi for parsing the JSON format
implementation "com.squareup.moshi:moshi:$version_moshi"
Expand All @@ -95,5 +113,5 @@ dependencies {
implementation "androidx.room:room-ktx:$version_room"

// WorkManager
implementation "android.arch.work:work-runtime-ktx:$version_work"
implementation "androidx.work:work-runtime-ktx:$work_version"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "21c90e8223ebcb03b6bfb045f4a26142",
"entities": [
{
"tableName": "videos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `updated` TEXT NOT NULL, `thumbnail` TEXT NOT NULL, PRIMARY KEY(`url`))",
"fields": [
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"url"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '21c90e8223ebcb03b6bfb045f4a26142')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,22 @@
package com.example.android.devbyteviewer

import android.app.Application
import androidx.work.*
import androidx.work.impl.Schedulers
import com.example.android.devbyteviewer.work.RefreshDataWork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit

/**
* Override application to setup background work via WorkManager
*/
class DevByteApplication : Application() {

private val applicationScope = CoroutineScope(Dispatchers.Default)

/**
* onCreate is called before the first screen is shown to the user.
*
Expand All @@ -34,5 +43,31 @@ class DevByteApplication : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
delayedInit()
}

private fun delayedInit() {
applicationScope.launch { setupRecurringWork() }
}

private fun setupRecurringWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
.apply {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
setRequiresDeviceIdle(true)
}
}
.build()
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWork>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
RefreshDataWork.WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
repeatingRequest
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,29 @@

package com.example.android.devbyteviewer.database

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.example.android.devbyteviewer.domain.Video

@Entity(
tableName = "videos",
)
data class DatabaseVideo(
@PrimaryKey @ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "description") val description: String,
@ColumnInfo(name = "updated") val updated: String,
@ColumnInfo(name = "thumbnail") val thumbnail: String,
)

fun List<DatabaseVideo>.asDomainModel(): List<Video> =
map {
Video(
url = it.url,
title = it.url,
description = it.description,
updated = it.updated,
thumbnail = it.thumbnail,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,43 @@
*/

package com.example.android.devbyteviewer.database

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.room.*

@Dao
interface VideoDao {

@Query("SELECT * FROM videos")
fun getVideos(): LiveData<List<DatabaseVideo>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(vararg videos: DatabaseVideo): List<Long>
}

@Database(
entities = [DatabaseVideo::class],
version = 1,
exportSchema = true
)
abstract class DevBytesDatabase : RoomDatabase() {

abstract val videoDao: VideoDao
}

private var INSTANCE: DevBytesDatabase? = null

fun getDatabase(context: Context): DevBytesDatabase =
synchronized(DevBytesDatabase::class.java) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context,
DevBytesDatabase::class.java,
"dev-bytes.db"
).build()
INSTANCE = instance
}
instance
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package com.example.android.devbyteviewer.network

import com.example.android.devbyteviewer.database.DatabaseVideo
import com.example.android.devbyteviewer.domain.Video
import com.squareup.moshi.JsonClass

Expand Down Expand Up @@ -63,3 +64,14 @@ fun NetworkVideoContainer.asDomainModel(): List<Video> {
thumbnail = it.thumbnail)
}
}

fun NetworkVideoContainer.asDatabaseModel(): Array<DatabaseVideo> =
videos.map {
DatabaseVideo(
url = it.url,
title = it.url,
description = it.description,
updated = it.updated,
thumbnail = it.thumbnail,
)
}.toTypedArray()
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package com.example.android.devbyteviewer.network

import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Deferred
Expand All @@ -34,7 +33,7 @@ import retrofit2.http.GET
*/
interface DevbyteService {
@GET("devbytes.json")
fun getPlaylist(): Deferred<NetworkVideoContainer>
suspend fun getPlaylist(): NetworkVideoContainer
}

/**
Expand All @@ -53,7 +52,6 @@ object Network {
private val retrofit = Retrofit.Builder()
.baseUrl("https://devbytes.udacity.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()

val devbytes = retrofit.create(DevbyteService::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2018, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.example.android.devbyteviewer.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.example.android.devbyteviewer.database.VideoDao
import com.example.android.devbyteviewer.database.asDomainModel
import com.example.android.devbyteviewer.domain.Video
import com.example.android.devbyteviewer.network.Network
import com.example.android.devbyteviewer.network.asDatabaseModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber

/**
* Main entry to access [Video] objects wherever it comes from, this is the video repository.
*/
class VideoRepository(private val videoDao: VideoDao) {

val videos: LiveData<List<Video>> = Transformations.map(videoDao.getVideos()) {
it.asDomainModel()
}

suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
try {
Timber.i("Refreshing videos")
val playlist = Network.devbytes.getPlaylist()
Timber.i("Videos were fetched from internet successfully")
videoDao.insertAll(*playlist.asDatabaseModel())
Timber.i("Videos were saved to local data store successfully")
} catch (error: Exception) {
Timber.e(error, "Failed to refresh videos")
// TODO Log to Crashlytics/Bugsnag
// TODO Show visual feedback
}
}
}
}
Loading